为什么我们最终选了MCP
文章目录 · 12 个章节
为什么我们最终选了MCP-Agent数据底座的三次倒重来
这里是”意图驱动的增长中台”系列的第七篇(今天不🐦了)。
前面几篇里,我讲过系统怎么从操作面板演进到逻辑网关、模型怎么分层选型、素材引擎怎么搭、投放Agent怎么落地。这些讲的都是动作侧——Agent怎么做决策、怎么执行、怎么协同。
但Agent要做决策,前提是它得先看得到数据、看得懂数据、并且看的时候不把自己的上下文打爆。
这一篇就讲这件事。讲我从2026年2月底开始,这一个多月怎么一步步走到”数据MCP”这个方案的——中间走过弯路、踩过坑、推倒重来过,到今天也还有没解决完的问题。
如果说逻辑网关是动作的协议化,那数据MCP就是数据的协议化。两个加起来,才是Agent能真正干活的基础设施。严格意义上讲我已经是练习时长两月半的Agent开发了(代码都是Claude写的),所以有一些自己的感悟,话不多说直接启动:
一、Agent能做决策,但喂不进数据
2026年3月底,我们第一个MCP开始真正上线跑业务。一个经营数据MCP,但不是单一视角——它要覆盖渠道、项目、广告、素材四个维度,每个维度的查询逻辑、关注指标、异常判断都不一样。
第一周就乱了。

不是Agent的决策能力出问题,而是四个维度各自接数据通道的时候,很快就铺不动了。
最开始我们让Agent直接调原来给投放后台用的API。渠道维度调了一套、项目维度调了一套、广告维度调了一套、素材维度又调了一套。表面上四个维度都能查到数,底下是四套完全不同的接口、四种字段口径、四份权限逻辑。口径不一致是小事,真正要命的是——每多接一个维度,就要重新铺一次数据管道。
更麻烦的是每个管道都有自己的毛病:API适配不好、字段语义不清、返回粒度不对。Agent的能力上限,被底下这几条破管道死死压着。
那阵子我明显感觉到:“Agent能做决策,但喂不进数据。”
这篇文章就是这个问题的解题过程。
二、四道弯:我们怎么走到MCP这条路的
“喂数据”这件事,这一个多月里我们试过四种思路。前三种都没走通,第四种才是现在线上的方案。
弯一:直连原有的数据API
最直觉的做法——我们本来就有一套给投放后台用的数据API,Web端在用、看板在用,Agent照着调不就行了?
不行。
原有API是给人用的。人用的接口,前端知道要拿什么字段、怎么组装展示、哪些字段是装饰哪些是关键指标——这些隐式知识全在写前端的工程师脑子里。Agent拿到这些API,第一反应是”这几十个字段我该看哪个”,然后开始花大量上下文去猜。
更头疼的是返回粒度。给人看的API通常一次返回一大坨,因为前端要铺满一整屏。Agent只需要其中三个字段,但整个响应体都进了上下文,一次查询几千token就没了。
还有参数设计。人用的API参数经常是”约定俗成”的——比如某个type=1表示账户维度、type=2表示计划维度,这种魔法数字前端开发者背得滚瓜烂熟,Agent看到就一脸懵。
看下来就是:人用的东西,AI用着水土不服。
弯二:干脆让AI写SQL
API不行,那就让AI直接写SQL查数据库?理论上最灵活——AI想查什么自己组装,字段自由挑、条件自由拼。
我们在测试环境试了一下,很快就收手了。
AI把不住查询性能。
让它查”过去30天每个计划的每日消耗明细”,它老老实实写了个SELECT * FROM xxx WHERE ...,跑了个全表扫描。我们的投放数据表是大宽表,单表体量不小,这一下测试库直接卡住。
这不是AI的错,是这件事本来就不应该让AI做。SQL写得好不好,取决于你对表结构、索引分布、分区策略、数据量的理解。这些是纯工程知识,AI没法从自然语言语义里推出来。你可以在prompt里把”记得加分区”、“记得限制时间范围”、“记得用索引字段”这些规则一条条告诉它,但一旦表结构变化、或者查询场景复杂一点,它照样翻车。
更深的问题是权限。SQL直连意味着Agent拿到了数据库的直接访问权,哪些表能读、哪些字段能看、哪些账户的数据能跨租户查——这些边界全得在SQL层面做,一旦失守就是数据事故。
结论:把”怎么查”完全交给AI,它hold不住工程约束。
弯三:做成CLI给Agent用?
这条弯我们没真走,只是脑子里过了一下。
社区里有些项目把工具做成CLI给Agent调用——Agent按需组合命令,输出是文本流,天然适合LLM处理。听上去也行。
但想了一下就放下了。
CLI的核心优势是”人机交互的灵活性”——人可以在终端里随意拼接命令、用管道组合、拿tab补全探索。这些特性对面向Agent的场景几乎没用。
我们的Agent不是拿着CLI在explore,它是被编排好流程、在特定场景下精准调用。CLI那套灵活性对Agent来说是噪声,反而增加了实现复杂度——你要做参数解析、输出解析、错误码处理、stdin/stdout协议适配,工程量不小,收益却不明显。
更关键的是协议标准化这件事。CLI没有统一协议,每个工具都自己定义一套。而Agent-to-Tool的交互,需要一个标准化的、机器友好的协议。
结论:CLI的复杂度不划算,Agent场景用不上它的人机灵活性。没深入做poc就pass掉了。
弯四:MCP
排除完前三条,MCP这条路几乎是被推着走上去的。
Anthropic开源MCP协议那阵子,我们刚好在纠结”Agent-to-数据”这层要怎么设计。看完协议发现,它解决的几个点正好是我们的痛点:
-
面向Agent原生设计——不是把人用的东西包一层,而是从”LLM怎么消费工具”这个角度重新定义交互
-
协议标准化——输入输出、错误处理、流式响应、状态管理都有规范
-
可共享——一套MCP Server可以同时服务多个Agent,不用每个Agent自己接一遍
最后定下来:数据侧全部走MCP。Agent不直接碰API、不直接碰SQL、不走CLI,统一通过MCP拿数据。
四条路的横向对比
把这四条路的差异摆在一起看,选择逻辑就很清楚了:
| 维度 | 直连API | SQL直连 | CLI | MCP |
|---|---|---|---|---|
| 设计初衷 | 面向人(前端) | 面向人(DBA/分析师) | 面向人/Agent | 面向Agent |
| AI适配度 | 差(字段语义缺失) | 差(hold不住性能) | 好 | 好(原生支持) |
| 工程约束处理 | 靠API内部 | AI承担(危险) | 靠CLI内部 | 靠Server内部 |
| 权限收口 | 分散在各API | 几乎无法收口 | 分统一收口 | 统一收口 |
| 多Agent共享 | 需重复建设 | 需重复建设 | 随地安装 | 天然共享 |
| 协议标准化 | 无 | 无 | 无 | 有 |
这张表不是我们事后画的,是中间那几周一边试一边画出来的。每多走一条弯路,就多一行标注。最后看到MCP那一列全是加粗,决定就不用再纠结了。
三、第一版MCP为什么也炸了:工具加载即上下文爆炸
选定MCP不等于就万事大吉了。真正折腾的部分,从这里才开始。

第一版MCP,我们按最直觉的思路设计——每个数据看板做成一个tool。
投放侧我们有多少看板?账户大盘、计划明细、创意效果、时段分布、人群画像、转化漏斗、异常监测、归因报表……随便一数就是几十个。每个看板背后还有自己的参数、时间粒度、维度组合。
第一版MCP搭起来做压测,我们就发现问题了——Agent还没开始干活,光加载工具列表就把上下文吃掉了一大半。
具体什么场景:盯盘Agent启动时,MCP把所有可用工具的描述一次性推给它。每个工具的描述要讲清楚用途、参数、返回字段、使用示例,几十个工具堆在一起,光tools schema就是上万token。
Agent看完这份”菜单”,才开始看真正的任务需求。等它分析完数据再做决策,上下文预算已经花掉七八成了。多轮对话下去,很快就撑爆。
这里有个很反直觉的点:工具描述本身就是context成本。我们做API设计的时候,OpenAPI文档写得越详细越好——反正是给人看的,不占运行时开销。但给Agent用的工具描述,每一个字都在烧token。
更糟糕的是组合爆炸。我们的数据是典型的大宽表结构,一个接口可能有几十上百个字段,再乘以多个维度、多个时间粒度。如果把”查X看板 + 按Y维度 + 看Z指标”每一种组合都做成一个tool,工具数量直接爆炸;如果做成一个大而全的tool让Agent自己拼参数,单个工具的描述又长得吓人。
左也不是,右也不是。
这个时候我们意识到一件事:第一版MCP的设计,本质上是把”给人看的API”包了一层MCP协议,并没有真的按”Agent怎么消费”这个角度重新设计。我们只是把包装纸换了,里面的东西没变。
得推倒重来。
四、三步拆分:意图理解 + 字段筛选 + 接口调用
推倒重来的时候,我们先想清楚一件事——Agent查一次数,到底在干哪几件事?
拆开看,其实是三步:
-
理解自己要查什么(意图→场景)
-
决定要看哪些字段(场景→字段清单)
-
实际去拿数据(字段清单→查询结果)
第一版的问题在于,我们把这三步压到一个tool里,期望Agent一口气完成。结果就是:工具描述既要讲意图识别的场景、又要讲字段菜单、又要讲参数组合——上下文能不炸吗?
既然这三步本质上是不同性质的任务,那就拆成三个tool,每个tool只解决一件事。
Step 1:意图理解
-
输入:自然语言(Agent当前的任务描述)
-
输出:
{场景ID, 接口号, 指标列表} -
内部做的事:把模糊意图收敛到具体场景
这一步解决的问题是:Agent不应该自己去”猜”该调哪个接口。它只需要用自然语言说清楚自己要干什么,意图理解工具内部通过RAG检索(这个后面专门讲),定位到对应的业务场景和接口。
举个例子,Agent收到任务”看一下昨天渠道A广告消耗异常的TOP10”,它把这句话直接扔给意图理解工具,拿回来的是:
{
"sceneId": "QUERY_AD_DATA",
"apiNo": "xxx",
"metrics": ["cost", "costDayOverDay", "roi", "adStatus"]
}Agent不用认识几十个接口,也不用记住每个接口叫什么。它只需要表达意图。
Step 2:字段筛选
-
输入:场景ID + 接口号 + 当前任务上下文
-
输出:从几百个字段里挑出来的N个字段清单
-
内部做的事:在大宽表的字段海里,挑出”当前任务真正需要的那几个”
这一步是第一版MCP踩坑最深的地方。我们的数据接口背后是大宽表,单个接口就有几百个字段——账户信息、计划信息、消耗指标、转化指标、归因指标、人群标签、素材关联……全量返回的话,一次查询几千token就没了。
字段筛选工具做的事是:根据当前场景和任务,从字段海里精选出相关字段。Agent不用自己从几百个字段里挑,也不用把全部字段描述塞进上下文。
拿上面那个”异常广告TOP10”的例子,字段筛选工具返回的可能是:
["adId", "adName", "costYesterday", "costTheDayBefore",
"costChangeRate", "roiYesterday", "adStatus", "alertFlag"]够用就行,不多给。
Step 3:接口调用
-
输入:前两步的产出(场景 + 接口 + 指标 + 字段)
-
输出:实际查询结果
-
内部做的事:打到封装后的查询引擎,拿数据
这一步本身很薄——它不是一个”智能”的工具,就是一个执行层。它把前两步收敛好的查询意图,翻译成对查询引擎的调用,拿回数据返回给Agent。
查询引擎自己长什么样,我们后面专门讲。这里只需要知道:Agent在这一步不碰SQL、不碰原生API,只跟MCP的接口调用工具打交道。

完整调用链走一遍
把三步串起来,Agent查”昨天渠道A广告消耗异常的TOP10”这个场景,实际发生的事是这样的:
[用户/上游] → 经营数据Agent收到任务
Step 1: intentRecognition("帮我看昨天渠道A广告消耗异常的TOP10")
↓
返回 {sceneId: "QUERY_AD_DATA", apiNo: "xxx",
metrics: ["cost", "costDod", "roi", "adStatus"]}
Step 2: fieldSelection(sceneId, apiNo, taskContext)
↓
返回 ["adId", "adName", "costYesterday",
"costChangeRate", "roi", "adStatus", "alertFlag"]
Step 3: apiCall(apiNo, fields, filters={channel: "A", date: "yesterday"})
↓
返回查询结果(结构化数据 + 必要的元信息)
→ Agent拿到数据,做决策每一步都很薄,每一步都只解决一类问题。Agent的上下文里,永远只有当前这一步必须的信息,不会被前一步的中间产物污染(这个”中间产物怎么不污染上下文”的问题,我们用session机制解决,第八章讲)。
为什么是三步,不是两步或者四步?
有人可能会问——能不能把意图理解和字段筛选合成一步?或者拆得更细?
合成一步的问题是:这两步的输入空间不一样。意图理解的输入是自由文本,靠RAG做检索;字段筛选的输入是结构化的场景ID + 任务上下文,做的是字段级别的精选。把两件事塞进一个tool,工具描述会回到”既要又要”的老问题。
拆得更细的问题是:Agent的调用成本会放大。每多一步MCP调用,就多一次网络往返、多一次上下文打断。三步是我们跑下来发现职责清晰 + 调用链可控的平衡点。
这不是拍脑袋定的,是踩过”一步到位”的坑之后,倒推出来的。
五、RAG替代Tools列表:工具不进上下文,靠检索召回

三步拆分解决了”单次查询要填多少上下文”的问题。但还有一个更底层的问题没解决——Agent怎么知道都有哪些工具可用?
MCP协议原生的做法是:Server启动时,把所有工具的schema一次性推给Client(也就是Agent侧)。Agent看完这份”菜单”,才知道自己有哪些手段。
这个设计在工具数量少的时候没问题。但我们的场景是——四个维度各自有自己的意图理解、字段筛选、接口调用,光第一步的场景就有几十上百种。如果把所有场景的完整描述都铺在tools schema里让Agent加载,第一版MCP那个”工具加载即爆炸”的坑,换个马甲又回来了。
得换思路。
工具本身也是数据
我们换了个角度看这件事——Agent认识工具这件事,本质上也是一次”查询”。
Agent当前在干什么任务 → 这个任务需要什么能力 → 系统里有没有匹配的工具。这个过程跟”Agent要查某个维度的数据”在认知结构上是一样的。既然是查询,那就可以用检索的方式做,而不是把全部候选一次性塞给它。
于是有了现在的设计:工具能力维护在一个RAG知识库里,Agent用到的时候按语义检索召回,召回结果才进上下文。
知识库的组织形式
RAG知识库的设计有几个抉择点,我们是这么做的:
一条记录按”场景”为单位。为什么是场景而不是接口,因为一个接口下可能对应多个业务场景(同一个API,在”异常排查”和”日常复盘”两个场景下,关注的指标和维度是不一样的)。按场景组织,检索回来的结果更贴近Agent当前要干的事。
Embedding粒度是拆片的。每条场景记录里有多个信息块:
-
用途:这个场景在解决什么问题(例:排查某渠道下广告异常消耗)
-
参数:有哪些必填参数、可选参数(例:时间范围、渠道ID、异常类型)
-
字段:这个场景常用的字段清单和语义(例:costYesterday表示昨日消耗、alertFlag表示告警标记)
这三个信息块分别embed,而不是拼成一大段文本整条embed。
这个选择很关键。我们一开始是整条embed的,跑下来发现问题——长文本embed出来的向量,语义会被稀释。同一个场景,“用途”部分讲的是”异常排查”,而”参数”部分讲的是”时间范围和渠道ID”,这两部分的语义其实差别很大。如果整条embed,向量会变成这些语义的平均,召回时反而不够准。
拆片之后,同一个场景对应多个向量,每个向量有自己聚焦的语义。Agent的query来的时候,可能是冲着”用途”匹配的、也可能是冲着”字段”匹配的(比如Agent知道自己要看某个具体指标,反查哪个场景能提供),两类匹配都能被精准召回。
混合召回,不是纯向量
纯向量检索在这个场景下不够用。
举个例子,Agent要查”渠道维度的消耗”和”广告维度的消耗”,纯语义上这两句话的向量非常接近——都在讲消耗。但对应的场景是两个完全不同的(QUERY_CHANNEL_DATA vs QUERY_AD_DATA),指标、字段、接口号都不一样。向量检索召回的top-K里,两个场景会混在一起,甚至顺序还错。
所以我们走的是混合召回:
-
向量召回:处理语义相似性,负责”意思接近”的匹配
-
关键词召回:处理精确匹配,负责”就是要这个词”的情况(比如query里明确出现”渠道”、“广告”这种维度关键词)
-
标签过滤:处理硬约束,负责”在某个维度内找”(比如当前Agent任务已经限定在”广告维度”,那只在
dimension=ad的候选集里找)
三路召回的结果合并之后,进入下一步——重排。
Top-K重排
混合召回能保证召得回,但不能保证排得准。向量召回、关键词召回、标签过滤三路的打分逻辑不一样,直接合并的话排序是乱的。
重排这一步做的事是:在召回的top-K候选集里,用一个更精细的模型做二次打分。打分依据包括:
-
语义相关性(query和场景描述的细粒度匹配程度)
-
维度匹配度(query提到的维度跟场景所属维度是否一致)
-
字段覆盖度(候选场景的字段,对当前query意图的覆盖程度)
重排之后,Agent拿到的是一个已经按相关性排好序的候选列表,它只需要取top-1或top-2就能用。

这套机制换来的东西
这么一套RAG + 混合召回 + 重排的组合,换来的是一件非常关键的事:
Agent启动时,上下文里只需要三个入口工具的描述,不需要具体场景的schema。
它只需要知道三件事——“有一个叫intentRecognition的入口负责把意图转成场景,一个叫fieldSelection的入口负责选字段,一个叫apiCall的入口负责拿数据”。具体能查什么维度、有哪些场景、每个场景的参数和字段,全部在调用的时候按需检索出来。
这跟第一版”加载即爆炸”的对比非常直接:
| 维度 | 第一版(原生Tools) | 现在(RAG召回) |
|---|---|---|
| 常驻工具schema | 几十个,全量加载 | 3个入口工具 |
| 上下文占用 | 随场景数量线性增长 | 常数级 |
| 新增场景成本 | 所有Agent都要重载schema | 更新知识库(人工维护) |
| 工具描述详细度 | 必须压缩(怕炸) | 可以写详细(不进上下文) |
最后一行其实特别关键。RAG机制让我们可以”放心地把工具描述写详细”——用途、参数、边界条件、使用示例、注意事项,想写多少写多少,反正只有召回命中的那几条才会进Agent的上下文。这反过来又提升了召回质量——知识库里的描述越详细,检索匹配得越准。
一个正向循环就建起来了。
顺便说一句:RAG在我看来是过渡方案
这套 RAG + 混合召回 + 重排跑得不错,但我不觉得它是终态。
我们的工具元数据其实就三块——接口文档、字段 schema、使用说明,量级不大。选 RAG 主要因为现成的组件多——向量模型、向量库都是成熟件,召回搞个 Top-K 也好搞,当下性价比最高。但”人肉运营一个知识库”这个模式,第十一章会专门讲它长不起来。
长期看这事的终态大概率不在 RAG 这条路上。已经有 notebooklm 这种产品在示范——把一堆文档扔进去,模型自己消化、自己组织、自己按需取用,不需要外挂一层 embedding 流水线。等底层模型的长上下文和元数据处理能力再涨一截,工具发现这件事可能就退化成”维护一套 markdown 文档”——或者按 skills 的方式拆成粒度合适的能力包,由 Agent 运行时自己加载。
到那个时候,我们这一层 RAG 基础设施就可以拆了。
所以现在维护知识库的每一行代码、每一条召回规则,心态上是”用够三到六个月的东西”,不是”用十年的东西”。这个预期会反过来影响我们在它身上投多少精力——踩在基本够用的线上就行,不过度打磨。
六、查询引擎:AI不碰SQL,只表达意图
回到三步里的最后一步——接口调用。
这一步的本质是:Agent把”我要什么场景、要哪些字段、带哪些过滤条件”收敛好之后,打给一个查询引擎,引擎返回数据。
这里有个看起来很简单但非常关键的设计判断——查询引擎不暴露给Agent。Agent看到的只是MCP的apiCall工具,至于这个工具底下连的是什么存储、怎么做路由、怎么走缓存,Agent完全不知道也不需要知道。
为什么要藏起来?因为查询引擎自己也是个复杂的东西。

三层存储架构
我们现在线上的查询引擎是三层架构:
-
ADB(阿里云AnalyticDB):承接大规模分析型查询——跨维度聚合、时间序列、大范围扫描这类计算密集型请求
-
PolarDB:承接事务型和明细查询——精确到单条记录、或者小范围的明细拉取
-
Redis:缓存层——高频查询结果、热点数据、短期窗口内重复请求的结果
这三层的分工不是平均的。ADB处理”大而慢”的查询,PolarDB处理”小而准”的查询,Redis处理”重复”的查询。引擎内部根据查询特征自动路由,Agent不参与这个决策。
为什么这些不能交给AI?
可能有人会说——让Agent自己判断这次查询该打ADB还是PolarDB、要不要走缓存,不也挺好?灵活。
这是误解。存储路由、查询优化、缓存策略,这些是工程约束问题,不是智能问题。
-
路由决策依赖的是查询特征和存储特性的匹配——哪类查询用哪个存储更快,这个映射关系是确定的,没有”智能”的空间
-
查询优化依赖的是索引分布、分区策略、执行计划——这些在每次表结构变化时都会变,Agent没法实时感知
-
缓存策略依赖的是数据的时效性和一致性要求——什么数据能缓存多久、什么时候必须穿透,这是业务规则,不是模型判断
把这些东西塞给Agent,不仅它做不好,而且一旦做错后果很严重(查错库、缓存脏数据、索引没走)。
所以我们的边界划得很清楚:
Agent做智能决策,引擎做工程保障。
Agent决定”要查什么”——这是语义和业务判断,需要智能。引擎决定”怎么查”——这是性能和一致性保障,需要工程。两者各司其职,中间通过接口调用工具的协议传递意图,不传递执行细节。
权限也收口在引擎
字段级别的数据权限,我们也放在查询引擎这一层做,不在MCP层做。
为什么?因为权限是数据本身的属性,不是Agent调用路径的属性。
如果权限放在MCP层,意味着”Agent通过MCP访问数据要过权限,如果绕过MCP就不用过权限”——这种设计是不稳的,一旦未来新增一个非Agent的数据消费场景(比如内部BI、或者人工查询),权限校验就得再写一遍,还可能跟MCP层的逻辑不一致。
放在查询引擎层意味着:所有访问数据的路径,都必须过同一道权限门。MCP是这道门之上的调用者之一,不是守门人自己。
这个设计有个隐含的好处——未来我们想让人也能走这套引擎做临时查询,只要过同一道权限,数据的访问边界就是安全的。权限的唯一性不会随着调用方数量的增加而被稀释。
引擎封装带来的另一个好处:AI视角的稳定性
数据底层的变更是常态——今天多一张表,明天加一个索引,后天把某个查询从PolarDB迁到ADB。这些变更在过去会让所有调用方都跟着改。
现在引擎封装之后,底层怎么变,对Agent透明。Agent调用的还是同一个apiCall、传的还是同一组参数。引擎内部路由变了也好、索引结构调了也好、缓存策略调了也好,Agent侧完全感知不到。
这在Agent时代特别关键。因为Agent的行为是基于对工具稳定性的假设来编排的——今天用这个工具能查到某个字段,明天还应该能查到。如果底层一变Agent的行为就漂移,这套系统就没法长期运行。
引擎封装的本质,是给Agent提供一个时间维度上稳定的数据视图。
七、大返回怎么让AI判断:不是看数据,是看数据的形状
查询引擎返回的数据量,经常是Agent上下文容不下的。
举个例子,Agent查”某渠道下所有广告过去7天的每日消耗明细”,一个渠道可能挂几百上千个广告,乘以7天,就是几千行记录。每行记录又有十几个字段。全量返回的话,单次查询几十上百K token起步,一两次就把Agent的预算打完了。
这件事不能指望”让Agent自己少查一点”——它事前根本不知道会返回多少数据。得在协议层解决。
原则:AI表达需要什么,引擎就给什么
我们没有走”先返回摘要、Agent按需深挖明细”这种复杂路径。真实做法更朴素,主要靠两个机制:
一是查询引擎自带分页。任何一次查询都必须带分页参数——pageSize默认比较小,Agent需要更多数据的时候显式请求下一页。这样即使一个查询背后有几千行,单次进入Agent上下文的也就几十行。
二是按需加载字段——这就和Step 2字段筛选呼应上了。
Step 2为什么重要,在这里体现得最充分。同样一次查询,如果默认把几百个字段全返回,光一行记录就要占大量上下文;如果通过字段筛选精选到七八个必要字段,单行的token消耗能降一个数量级。分页 × 字段精选,两个乘起来,上下文压力才真的控得住。
为什么没有做”摘要 + 明细ID”
其实一开始我们是考虑过更花哨的做法——比如第一次返回只给摘要(总行数、关键聚合指标、异常点标记),然后给一批明细ID,Agent判断需要哪些就按ID去拉具体明细。
想了一下没做。
原因是:这种设计的复杂度收益不成正比。摘要要设计、明细ID的有效期要管理、Agent的多轮调用链要编排、错误场景要处理——工程量不小,但换来的好处,在我们当前场景下没比”分页 + 字段精选”明显多少。
而且有一个更本质的问题——让Agent主动做”要不要深挖”的判断,反而增加了它的认知负担。Agent本来是为了解决业务问题来查数的,不是为了和数据交互方式博弈的。分页和字段筛选是更自然的心智模型:你告诉我要什么维度和字段,我按页给你。简单、可预测、可解释。
不是所有场景都需要用最酷的技术方案。
关键心智:给AI看数据的形状,不是数据本身
这章说到底,是一个设计哲学:
不要让AI在”理解海量原始数据”上浪费上下文。
Agent的优势是决策判断,不是信息摘取。让它吞下一整张表然后告诉你哪里异常,是拿它的短处去打别人的长处。更合理的做法是:通过字段筛选和分页,让Agent每次只看到一小块”形状清晰”的数据——这块数据是面向它当前任务精选过的,结构紧凑、语义明确、没有冗余。
Agent的决策质量反而会在这种”少而精”的数据形状下变好。
八、Session机制:把中间产物挪出Agent上下文
回头看三步拆分,其实有一个隐患一直没处理——三步之间的中间产物怎么传递?
最直觉的做法是让Agent自己带着。Step 1返回sceneId、apiNo、metrics,Agent拿到后,在Step 2的调用参数里把这些带进去;Step 2返回字段清单,Agent拿到后,在Step 3的调用参数里再把前两步的结果都带进去。
一套流程走下来,Agent的上下文里堆着一堆”不是我业务关心、但为了让MCP继续干活我必须记住”的状态。而且Agent如果在中间做点别的(比如调别的工具、或者跟用户多轮交互),这些状态还可能丢失或者漂移。
我们后来引入了Session机制,把这个问题收回MCP侧处理。

机制很简单
Agent第一次调用intentRecognition的时候,MCP分配一个sessionId返回给它。之后Agent调Step 2、Step 3的时候,只需要带sessionId,不需要带任何中间产物。前一步的结果,MCP内部缓存着,下一步自动读取。
Agent视角下,调用链变成了这样:
Step 1: intentRecognition(query) → 返回 sessionId
Step 2: fieldSelection(sessionId) → 返回字段清单
Step 3: apiCall(sessionId, filters) → 返回查询结果Agent的上下文里只留下一个sessionId这个”指针”,所有中间产物都在MCP侧的session缓存里。上下文从”累加模式”变成了”指针模式”。
为什么这件事值得单独说
这个机制带来的好处不止是省上下文,更关键的是降低了Agent的认知负担。
Agent不用再记”我上一步是不是该把场景ID也带过来”、“字段清单是上一步返回的还是我要自己推”——这些东西全由MCP管。Agent只需要关心两件事:当前这一步的业务意图是什么,以及上一步的sessionId在哪。
还有一个附带好处:调用链可观测。所有相关的调用都挂在同一个sessionId下,出问题的时候,回放这一次完整的查询过程就是一次session trace的事。这对于排查Agent行为异常非常关键——你很难从一堆散落的tool调用里拼出”Agent当时到底在干什么”,但有sessionId就能一眼看到。
session什么时候过期?
我们现在的策略是比较简单的:session有一个TTL(几分钟量级),Agent如果长时间不操作就自动过期。过期之后Agent如果还想继续,重新发起Step 1拿新的sessionId就行。
短TTL的好处是:不用担心session状态被长时间持有带来的一致性问题。数据是会变的,场景定义也是会变的,session缓存了几分钟内的中间态,过期就作废,简单可靠。
九、一个Agent,四个维度:场景库的治理
前面讲过我们现在只有一个经营数据Agent,但它要覆盖渠道、项目、广告、素材四个维度。这四个维度的查询需求非常不同,场景库怎么组织才既能服务好每个维度、又不互相干扰?

场景库是共用的,但维度是打标的
我们没有给每个维度单独搞一份场景库——那样会造成同类逻辑的重复维护。现在的做法是一个统一的场景库,但每个场景带明确的维度标签:
-
QUERY_CHANNEL_DATA→ 维度标签:channel -
QUERY_PROJECT_DATA→ 维度标签:project -
QUERY_AD_DATA→ 维度标签:ad -
QUERY_MATERIAL_DATA→ 维度标签:material
Agent当前在处理哪个维度的任务,检索时带上对应的维度标签作为过滤条件——这就回到了第五章讲的”混合召回”里标签过滤的那一路。
这个设计的好处是:同一个Agent在不同维度之间切换时,不需要加载不同的工具集。它只需要在query里明确当前关注的维度(或者由上游任务编排层带入),检索自动收敛到对应维度的场景。
跨维度的场景怎么处理?
有些分析场景是天然跨维度的——比如”某渠道下表现好的素材类型”,这既涉及渠道维度也涉及素材维度。
我们的做法是显式标注这种跨维度场景,维度标签可以是多值的(比如[channel, material])。检索的时候,query如果同时命中了多个维度的关键词,跨维度场景会被优先召回。
这里有个小坑:跨维度场景的字段组合比单维度复杂得多。Step 2字段筛选在这种场景下要干的活更重,我们目前是在知识库里把这类场景的常用字段组合预先配好,而不是让AI临场去组合——临场组合的准确性暂时还不够稳。
为什么字段权限不放MCP层,放在查询引擎层
第六章提过这个选择,这里再深入一点。
如果权限放在MCP层,每个维度的字段权限要在MCP的工具定义里维护;一旦权限策略变了,MCP的代码要跟着改。而且MCP只是数据消费路径之一,如果未来有别的路径(比如人工查询、报表系统、其他非Agent消费方),权限逻辑就得在每个入口都写一遍。
放在查询引擎层就统一了——不管上游是谁来查,都过同一道权限门。MCP、人工、报表系统,所有数据访问都在引擎层被校验。Agent侧的权限边界,跟人用同一套系统时的权限边界,严格一致。
这一点对我们未来做Agent和人的混合协作很重要——Agent查不到的数据,人也查不到;人能查到的,Agent在相应身份下也能查到。边界是一套,不是两套。
十、工具内部实现的几个窍门
前面讲的都是架构层面的抉择。落到具体工具实现上,还有一些看起来不起眼但对效果影响很大的细节。
工具描述:每个字都在烧token
入口工具(intentRecognition、fieldSelection、apiCall)是常驻在Agent上下文里的,它们的schema描述是真真正正的每次对话都要消耗的token。
所以入口工具的描述,我们的原则是”能一句话讲清楚的绝不两句话”:
intentRecognition: 把自然语言任务转成场景ID和指标列表,Agent表达"我要查什么",这里返回"对应哪个场景、有哪些可选指标"。不堆术语、不重复解释、不写冗余的使用示例。入口工具的目的是让Agent知道”什么时候该调我”,而不是教会Agent”我的所有细节”。细节全都在RAG知识库里,召回时再给。
相反地,知识库里的场景描述可以写得很详细——反正只有召回命中才进上下文。这个详略分配是我们踩了几次坑才摸清楚的。
错误信息:要让AI能自己纠错

Agent调用工具出错的时候,错误信息是它决定”下一步怎么办”的唯一依据。错误信息写得好不好,直接决定了Agent能不能自救。
反面教材:
{"error": "invalid parameter"}Agent看到这个只能懵——哪个参数不对?怎么改才对?它只能重试或者放弃。
我们现在的错误返回是这样的:
{
"error": "invalid parameter",
"field": "dateRange",
"reason": "dateRange超过最大允许范围(30天),实际传入90天",
"suggestion": "将dateRange缩短到30天以内,或者分多次查询"
}field指明错误在哪个参数、reason讲清楚为什么错、suggestion直接给出下一步动作建议。Agent看到这样的错误,能自己判断要不要重试、以及怎么重试。
这里面的关键心智是:错误信息不是给工程师debug用的,是给Agent做决策用的。两种受众对错误信息的需求完全不同——工程师想看堆栈,Agent想看”下一步该干什么”。
幂等性:同一个请求多次调不能出事
Agent在真实运行中,有时候会因为各种原因重复发起调用——超时重试、上下文回滚、多Agent并发。如果工具不是幂等的,同一个查询请求调两次结果不一样,Agent就会陷入混乱。
我们的接口调用工具是幂等的——同样的sessionId + 同样的参数,无论调多少次,返回的结果一致(命中缓存的话直接返回缓存、没命中则执行后写入缓存)。这件事听起来理所当然,但在早期版本里,我们有过几次”同一个查询Agent连续调了三次,三次结果因为缓存抖动不一样”的灵异事件。
幂等不是只靠”接口设计良好”就能得到的,它是整个调用链一起保证的一致性——引擎层的缓存策略、MCP层的session缓存、工具层的参数归一化,每一层都要配合。
参数归一化:小处省大事
相关的一个细节——参数在进入引擎之前一定要归一化。
比如Agent传的date字段,可能是”昨天”、“yesterday”、“2026-04-16”、“04/16”这么多种写法。如果不归一化,缓存命中率会很低(同样的意图,字符串不同,缓存miss),而且错误率高(引擎可能只认一种格式)。
我们在MCP层统一做归一化:时间全部转成ISO格式、维度名全部转小写、数值参数校验范围。这层薄薄的归一化逻辑,解决了一大堆看起来神神秘秘的”调用行为不稳定”的问题。
十一、还没解决的问题
前面讲的都是跑通的部分。但这套系统离”完备”还远,有几个问题我们到今天也没好好解决,诚实讲一下:
Schema变更靠手动维护
RAG知识库里的场景描述、字段清单、接口号映射,目前全是人肉维护的。
数据侧每新增一个字段、改一个字段语义、或者业务上线一个新场景,都需要有人手动去更新知识库。这件事在项目早期还能扛——整个系统也没几个场景,加一条记录几分钟的事。但我们已经能预见到这个做法不可持续:
-
场景数量一旦到几百个级别,人工维护的成本会急剧上升
-
更可怕的是漏更新——数据接口加了个新字段,没人记得去更新知识库,Agent就永远查不到这个字段
-
字段语义变化(比如
cost的口径从”日消耗”改成”累计消耗”)如果没同步到知识库,Agent的判断会悄悄错,而且很难发现
正在想的方向是:schema感知自动化——让知识库直接订阅数据侧的元数据变更事件,字段新增、字段语义标注、接口上下线,自动同步进知识库。但这件事依赖上游数据平台的元数据管理成熟度,不是我们单方面能推的。
没有版本兼容机制
跟schema变更相关的另一个问题——变更发生时,之前的调用链没有兼容保护。
比如某个场景的默认字段清单变了,Agent之前基于老字段清单编排的后续逻辑,可能就不work了。我们现在没有”场景版本”的概念,改了就是改了,没有回滚空间,也没有”老版本继续可用、新版本逐步切流”的能力。
短期内这个问题靠”变更前周知所有Agent编排方”扛着。但这本质上是个人肉协调的脆弱机制,哪天漏掉一次就会出事。
长期得加版本号,让每个场景有version标签,Agent可以指定版本调用,变更时老版本保留一段时间再下线。这件事不复杂,只是当前优先级还没排上来。
场景库膨胀后的治理
现在场景几十个,还管得过来。如果未来业务扩展,场景数量冲到几百上千,知识库本身的治理就成了一个新问题——重复场景怎么识别、废弃场景怎么清理、相似场景怎么合并。
这个问题现在没有答案。估计到时候得引入一套”场景生命周期管理”的机制,但那是下一个阶段的事。
十二、结语:MCP不是”给AI包个接口”,是重新设计数据消费的方式
这篇写了很长,因为这一个多月踩的坑和想明白的事都不少。回头看这套系统,有四个判断是我现在特别确信的:
第一,工具不进上下文,靠检索召回。Agent时代,工具本身也是数据,能力发现这件事应该用检索的方式做,而不是把全部候选塞给Agent。这是我们从”第一版工具加载即爆炸”里最疼的一个教训。
第二,AI不碰SQL,只表达意图。性能、索引、缓存、权限这些是工程约束,不是智能问题。把它们塞给AI不是”灵活”,是拿智能的短处去打工程的长处。引擎封装带来的稳定性,是Agent长期运行的基础。
第三,数据消费单一收口。MCP是Agent访问数据的唯一合法路径,权限则进一步下沉到查询引擎,实现”不管上游是Agent还是人,数据边界一致”。单一收口不是为了控制Agent,是为了让边界在哪儿、一眼能看清。
第四,三步拆分的本质,是把AI做不好的事都挪出去。意图理解靠RAG、字段筛选靠知识库、查询执行靠引擎——每一步都是在替AI承担一件它做不好的工作。留给AI的,是它真正擅长的:表达意图、做决策。

这一篇跟上一篇《操作面板到逻辑网关》是姐妹篇。
逻辑网关讲的是动作的协议化:Agent决策出一个动作,怎么安全地、标准化地传导到系统里执行。
数据MCP讲的是数据的协议化:Agent要做决策,怎么从繁杂的数据底座里精准地拿到它需要的那一小块。
两者加起来,才是Agent能真正干活的基础设施。再智能的模型,如果底下没有这套协议化的动作层和数据层,它的决策只能停在”说说而已”。
回到标题——为什么我们最终选了MCP?
不是我们先有一个优雅架构想法,然后去实现它。是API直连、SQL直连、CLI这些”看起来更简单、外边人说起来更好”的方案都撞过墙之后,才不得不走到这里。即便选定MCP,第一版也立刻炸了,又在三步拆分、RAG替代、session机制上反复推翻重来。
三次倒重来,换来的不是一个最优解,是试错之后幸存下来的解。
然后接着试错,接着演进。
本文首发于微信公众号 kelovp.ai,原始发布时间为 2026年4月17日。查看公众号原文。