第 5 章 / 共 11 章
工具就是上下文
5.1 四十个工具定义,和三个功能重叠的订单查询
shopfront 的迁移做到一半,团队觉得手上工具不够用,于是又接了两个 MCP 服务器。原本只有一个连内部订单系统的,现在加上了 Sentry 和数据库。加上 Claude Code 内置的读写文件、执行命令、搜索这一套,会话一启动,四十多个工具定义就常驻在上下文里,每一轮都在。
问题很快显形。查一笔订单在 v1 和 v2 下的返回差异,agent 有三条路可走:
search_orders(内部订单系统 MCP,按条件搜订单)query_orders_db(数据库 MCP,直接跑 SQL)get_order_detail(内部订单系统 MCP,按 ID 取详情)
它先用 search_orders 拿到一批订单号,再用 query_orders_db 查同一批数据,发现字段名对不上(一个是驼峰一个是下划线),又回头用 get_order_detail 核对。三次调用查的是同一件事。更糟的是,query_orders_db 拿到的是数据库原始行,没有经过 v2 API 的序列化,agent 把它当成 v2 的返回结构写进了测试断言——用错了,而且不自知。
你事后去看,会发现自己也说不清这三个工具该用哪个。这就是判据。
5.2 工具定义是常驻成本,不是一次性开销
旧的直觉是这样的:工具装着不用也不亏,反正用不到就不调用。这个直觉错在把工具当成了功能开关,而它实际上是上下文里的一段固定文本。
每一个工具定义——名字、描述、参数 schema——都要序列化进每一轮请求。四十个工具,按每个 150 到 400 token 算,就是六千到一万六千 token 常驻。这笔钱你在第 1 轮付,在第 50 轮还在付。第 3 章那张盘点表里,工具定义那一栏往往是最容易被低估的一项,因为它不像对话历史那样肉眼可见地变长。
但真正的代价不是 token,是决策点变模糊。模型每一轮都要在这四十个选项里挑一个。选项之间职责重叠的时候,挑选这个动作本身就在消耗模型的推理能力,而且没有正确答案可挑——因为确实有三个工具都能沾边。
记住这一句:
如果人类工程师都说不清该用哪个工具,不能指望 agent 做得更好。
这句话是可执行的。拿你的工具清单,随便挑两个名字相近的,问自己「什么情况下用 A 不用 B」。如果你要想超过十秒,或者答案是「都行」,那这两个工具就是你 agent 混乱的来源。这也正是第 3 章说的**上下文混淆(context confusion)**在工具层面的具体形态:无关或重叠的内容影响了模型的选择。

图 5.1:左边三个工具的职责边界互相压盖,模型每次都要在重叠区里赌一把;右边合并成一个入口后,决策点变成一条直线。看两侧从「意图」到「调用」之间连线的数量差。
5.3 自洽、抗错、用途极其清晰
好工具有三个属性,逐个说什么意思。
自洽:一个工具的名字、描述、参数、返回值讲的是同一件事,不需要读文档才能对上。search_orders 的返回里如果只有 ID 没有状态,那它就不该叫 search,该叫 list_order_ids。名字撒的谎,模型会信。
用途极其清晰:description 的第一句就要说清「什么时候用我」,而不是「我能做什么」。对比一下:
- 差:
查询订单数据。支持多种条件组合和分页。 - 好:
按用户 ID 或订单号查订单摘要(状态、金额、创建时间)。需要完整字段时改用 get_order_detail。
第二种写法把边界写进了描述本身,顺手替模型排除了一个错误选项。
抗错:这个词最容易被当成「不崩溃」,其实说的是出错时返回可行动的信息,而不是一个栈。
同一个失败,两种返回:
# 抗错差:把异常直接抛回上下文
Traceback (most recent call last):
File "/app/mcp/orders.py", line 142, in search_orders
resp = self._client.get(f"/v1/orders", params=params)
File "/app/vendor/httpx/_client.py", line 1041, in get
return self.request("GET", url, ...)
...
httpx.HTTPStatusError: Client error '422 Unprocessable Entity'
# 抗错好:告诉它错在哪、下一步怎么办
参数 status="paid" 无效。v2 的合法状态值为:
pending / authorized / captured / refunded。
v1 的 "paid" 在 v2 中对应 "captured"。
上面那段栈大概两百多 token,信息量接近零,而且会一直留在历史里被反复看到——它是**上下文投毒(context poisoning)**的常见入口:模型读到一堆文件路径和库名,接下来几轮开始琢磨去修 httpx。下面那段五十来 token,直接推进了任务,还顺手教会了 agent 一条 v1/v2 的映射事实。
保留失败信息是对的(Manus 的经验里明确说要把失败的动作和报错留给模型自己纠偏),但留的应该是结论型的失败,不是原始噪声。
5.4 返回值比定义危险得多
一个工具定义占 200 token,一次调用返回 800 行日志占八千 token。后者才是真正吃掉预算的东西,而且它不出现在任何「工具数量」的统计里。
shopfront 里最典型的三个:
| 工具 | 最坏情况返回 | 后果 |
|---|---|---|
query_orders_db | 无 LIMIT 的 SELECT,几千行 | 一次调用填掉四分之一窗口 |
Sentry 的 get_issue_events | 一个高频错误的全部事件,含堆栈 | 大量语义相似的重复内容,正是干扰项 |
Bash 跑 npm test | 全量测试输出 | 有用的失败信息埋在中间,最容易被忽略的位置 |
对策是让工具自己截断,并明确说明被截断了。截断本身不难,难的是不能悄悄截断——模型如果不知道内容被砍了,就会把看到的当成全部,然后基于不完整的数据下结论。
一个可用的返回格式:
共匹配 1284 行,以下为前 20 行(按 created_at 倒序)。
[...20 行数据...]
已截断 1264 行。需要更多请加条件后重新查询:
- 按状态过滤:status=captured
- 按时间窗口:created_after=2026-08-01
三个要素都在:给了数据、说了砍掉多少、告诉了怎么拿到剩下的。第三条最重要,它把「信息不全」从一个死胡同变成了一个下一步。
MCP 服务器不是你写的时候,你改不了它的返回。那就别让 agent 直接调它跑大范围查询——在 CLAUDE.md 里写一条约束(「查订单必须带 limit 和时间窗口」),或者干脆把这类调用交给子代理去做,只让它回传结论,这是第 9 章的事。
5.5 工具集瘦身清单
对每一个工具逐条过一遍。这张表可以直接抄进你自己的项目文档。
| 检查项 | 通过标准 | 不通过时的动作 |
|---|---|---|
| 这个工具本月用过吗? | 至少被真实任务调用过一次 | 从当前项目配置里摘掉,需要时再开 |
| 它和谁职责重叠? | 能说清与最相近工具的边界,一句话 | 合并两者,或在 description 里写明互斥条件 |
| description 能让人在 5 秒内判断该不该用吗? | 第一句就是「什么时候用」 | 重写第一句,把适用场景提到最前 |
| 它最坏情况返回多少内容? | 有硬上限,且超限时会明说被截断 | 加 limit 参数或包一层带截断说明的封装 |
| 出错时返回的是可行动信息还是栈? | 错误里包含合法取值或下一步建议 | 捕获异常,改写成结论型消息 |
| 它能不能按需加载而不是常驻? | 只在特定阶段需要 | 拆成技能或子代理,用到再进上下文(第 7 章) |
跑完这张表,shopfront 的四十多个工具通常能砍到二十上下。query_orders_db 是第一个该走的——迁移期间需要的是 API 语义,不是数据库行;它提供的是另一个抽象层的事实,天然会和 API 返回打架,也就是上下文冲突(context clash)。
5.6 禁用工具的正确姿势:掩码 logits,不是删定义
有一个反直觉的经验来自 Manus 团队。他们在生产里也需要临时禁用某些工具(比如任务某个阶段不该让 agent 碰写操作),但他们不把工具定义从请求里删掉,而是在解码时掩码 logits,让那些工具的 token 选不出来。
原因是 KV-cache。工具定义在提示的最前段,改动它会让整段缓存前缀失效,后面所有内容都得重算。命中和未命中的输入 token 单价可以差到大约十倍(这是他们撰文时的定价,各家会变,你要按当下价目核对)。为了省几百 token 的定义,付出整个前缀重算的代价,账是反的。这也是为什么 KV-cache 命中率被他们当成生产 agent 最重要的单项指标——在一个平均五十次工具调用、输入输出比约 100
的任务里,前缀被反复读取的次数远超你的直觉。这个做法依赖你能控制推理栈。用 Claude Code 或 Cursor 的话你掩码不了 logits,但对应的原则可以照搬:
按项目切换 MCP 配置,而不是在一次会话里反复增删工具。
具体到 shopfront:迁移期开一套配置(订单系统 MCP + 文件工具),排查线上问题时切另一套(Sentry + 日志)。切换发生在会话之间,一次会话内的提示前缀保持稳定。会话中途接一个新 MCP,代价不只是多出来的定义,还有从那一刻起缓存全线失效。
顺带说清 MCP 的定位:它是连接 AI 应用和外部系统的开放协议,官方比喻是 AI 应用的 USB-C 口。这个比喻讲的是接口统一,不是「插上就没成本」。每插一个服务器,它的全部工具定义就进了你每一轮的预算。能力是真的,占用也是真的,不是白拿。什么时候该用 MCP、什么时候该用技能或子代理,站内《MCP、Skills、Subagent 到底该用哪个》有一份选型对照,可以配合这一章看。
5.7 动手:把 shopfront 的工具集砍到二十以内
- 让 agent 列出它当前可用的全部工具名和 description 第一句。数一数总数,估一下 token(每个按 250 token 粗算)。
- 用 5.5 那张表逐条过。重点找职责重叠的组:把名字里含同一个名词(order、file、search)的工具挑出来放一起,逐对回答「什么情况下用 A 不用 B」。答不上来的记下来。
- 挑一个你最常用的工具,故意用错误参数调一次,看它返回的是栈还是可行动信息。如果是栈,而这个工具是你自己写的,改掉它。