RSS

第 5 章 / 共 11 章

工具就是上下文

约 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章:工具职责重叠让决策点变模糊

图 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一个高频错误的全部事件,含堆栈大量语义相似的重复内容,正是干扰项
Bashnpm 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 的工具集砍到二十以内

  1. 让 agent 列出它当前可用的全部工具名和 description 第一句。数一数总数,估一下 token(每个按 250 token 粗算)。
  2. 用 5.5 那张表逐条过。重点找职责重叠的组:把名字里含同一个名词(order、file、search)的工具挑出来放一起,逐对回答「什么情况下用 A 不用 B」。答不上来的记下来。
  3. 挑一个你最常用的工具,故意用错误参数调一次,看它返回的是栈还是可行动信息。如果是栈,而这个工具是你自己写的,改掉它。