知识块 · 版本 1块链接查看来源查看知识状态待读取
01

工具不是 API 的镜像

给 agent 设计工具接口,只有一件事必须算清楚:定义体积和往返体积是两笔独立的 token 账,只优化其中一笔,省下的会被另一笔原样吃回去。最大的一笔是定义体积——把 MCP 工具定义全量前置加载可达约 150,000 token,改成按需读取降到约 2,000 token,省 98.7%(Anthropic,code-execution-with-mcp);但只要工具还是后端 API 的一对一镜像,省下的定义 token 就会在调用往返里还回去,一场两小时会议的转录穿过一次窗口就是 50,000 token。第三件事算的人更少:工具描述这个字段,同时是模型选择工具的最强驱动因素、和 MCP 规范明文规定的不可信输入——这两条合起来是一个已经存在、但公开材料里还没有任何防御方案的攻击面。

下面这篇讲的是函数签名与调用成本,不讲函数体。工具内部怎么实现、语料相关性怎么保证,都不在范围内。

知识块 · 版本 1块链接查看来源查看知识状态待读取
02

一、先把两笔 token 账分开算,再谈接口设计得好不好

人和模型的差别只有一条,但它决定了全部设计:人的上下文是廉价的,模型的上下文是稀缺的。所以给人用的 API 追求功能覆盖度,给模型用的工具追求"每个返回 token 携带多少决策价值"。Anthropic 的说法是,在 ACI(Agent-Computer Interface)上投入的精力应当与在 HCI 上投入的一样多。

这条抽象原则落到工程上,就是两笔必须分开记的账。

第一笔:定义体积(definition cost)。 工具的名字、描述、参数 schema,在模型看第一眼之前就已经躺在上下文里了。这是纯成本 token——它不携带任何任务信息,只描述"你能做什么"。全量前置加载大量 MCP server 的工具定义可达约 150,000 token;Claude Code 的默认策略是延迟加载,启动时只列工具名,约 120 token,用到时才通过 tool search 拉具体 schema。环境变量 ENABLE_TOOL_SEARCH=auto 的语义是:只有当完整 schema 能塞进上下文窗口 10% 以内时才预加载,否则走检索。

这给了一个可以立刻执行的诊断阈值:先算你的工具定义占了窗口的百分之几。如果已经是两位数,后面观察到的一切退化——选错工具、反复调用、干脆不用工具——都先归因到这里,不要急着调 prompt。

第二笔:往返体积(round-trip cost)。 工具返回的每一个 token、以及模型为了串联两个工具而必须把前一个结果原样贴回去的每一个 token。这笔账和定义体积完全独立:一场 2 小时销售会议的转录如果必须在两次工具调用之间穿过模型上下文,会额外消耗约 50,000 token;让代码在沙箱里直接把结果从 A 传给 B,这笔钱就省掉了。

两笔账独立到什么程度?看两个可验证的推论:

  1. 一个鉴权完善、传输合规、协议版本最新的远程 MCP server,只要客户端仍然全量前置加载 schema,照样撞 150k。传输层做对,不构成上下文层的任何保证。
  2. 反过来,即使开了即时检索(启动只列工具名 120 token),如果工具是镜像 API 的一堆碎片(list_users + list_events + create_event),省下的只是定义 token;调用轮次里往返的中间结果照样过窗口。上面那 50,000 token 的转录,正是"即时检索开了但工具没合并"的典型账单形态。

第三个可用的对照来自subagent-context-isolation:Claude Code 文档里的会话示例中,子代理为了回答一个研究问题读了 6,100 token 的文件,回到主窗口的结果只有 420 token,约 14.5:1。这说明往返体积不是不可压的——它可以被移出主窗口,代价是一次完整的委派往返。子代理通常返回 1,000–2,000 token 的凝练摘要,这个区间可以直接当作"一次外包该收回多少"的预算基线。

所以工具接口的成本优化必须同时在定义体积和往返体积两个维度做。 相应地,验收指标也不能只有一个:Anthropic 建议收集的是单次工具调用与整任务的总耗时、工具调用总次数、总 token 消耗、工具报错数。只看成功率会完整地掩盖"答对了但烧了 3 倍 token"这类退化——而这恰恰是把 API 直接包成工具最典型的结局。

知识块 · 版本 1块链接查看来源查看知识状态待读取
03

二、镜像 API 是默认错误,合并是默认正确——但合并不等于造上帝工具

把后端 REST 端点一对一包成工具,是这个领域最常见的反模式,因为它看起来毫无损失:端点已经存在,加个 schema 就是工具了。它的代价全部隐藏在往返体积里。

三个正例,直接可抄:

  • 不要 list_users + list_events + create_event,直接给一个 schedule_event;
  • 不要 read_logs(返回整份日志),给 search_logs——它只返回相关日志行和少量上下文;
  • 不要 get_customer_by_id + list_transactions + list_notes,给一个 get_customer_context,一次性汇总某客户近期的全部相关信息。

判据只有一句:如果模型必须连续调 3 次工具、把前两次的结果原样喂回去才能拿到答案,那中间两次的结果就是纯粹的上下文税。 这条判据可以直接对着现有 transcript 跑——统计相邻工具调用之间"上一次的返回值被完整复述进下一次参数"的次数,次数高的地方就是该合并的地方。

但合并有一个明确的反向边界,越过就归零。合并的对象是"总是被连续调用的一组操作",不是"同一个领域的所有操作"。 一个既能读又能写、既能查又能删、靠一个 action 字符串参数分流的工具,只是把决策模糊性从工具选择转移到了参数选择,账面上工具数变少了,实际难度一点没降。Anthropic 点名的失败模式正是"臃肿的工具集,覆盖太多功能或制造模糊的决策点"——注意"制造模糊的决策点"这半句同样适用于单个上帝工具。

一个更靠前的问题是:这件事该不该做成工具。 这一点上所有材料意外一致:频繁的、主要的动作才值得做成工具;一次性的、需要临时组合的操作用 bash 或脚本更便宜;需要精确可复用的复杂逻辑用代码生成更好;MCP 的收益只在"标准化的、被多个 agent 复用的、需要鉴权的"集成上才成立。把"做成工具"当默认选项这件事本身就是错的,而它恰恰是接入 MCP 之后最容易滑进去的姿势——每加一个工具,都在给tool-overload-selection-degradation添柴。

知识块 · 版本 1块链接查看来源查看知识状态待读取
04

三、返回值要为模型裁剪,不是把 API 响应原样倒回去

这是最容易做、收益最直接、也最常被跳过的一步。

给返回值加详略开关。 一个 response_format 枚举(DETAILED / CONCISE)让模型自己决定要不要完整信息。实测:某 Slack 工具的 detailed 响应 206 token,concise 版本 72 token,约三分之一。这个例子里真正关键的细节不是压缩比,而是——concise 版本仍然保留了链式调用需要的标识符。返回值裁剪的正确目标不是"字段少",而是"删掉之后模型的下一步动作不受影响"。删掉一个人类看着没用但模型下一轮要拿去当参数的 ID,压缩比很好看,任务直接断掉。

硬性设上限,并且把续读方法暴露成参数。 Claude Code 默认把工具响应限制在 25,000 token。上限本身不难,难的是超限时的行为:不应静默截断(模型会以为自己看到了全部,然后在残缺信息上做决策),而应通过分页、范围选择、过滤等参数明确暴露"怎么继续读"。截断信息本身就是一条给模型的指令。

把 UUID 换成可读标识符。 把 a3f9c1e2-... 换成自然语言名称或术语,"显著提升 Claude 在检索任务上的精度并减少幻觉"。原因很直接:模型在语义空间里做决策,UUID 在语义空间里是零。这条还有一个连带后果——即时检索依赖可读标识符才成立:上下文里放的是指针不是内容,如果指针本身不可读,模型无法从指针判断该不该拉取,即时检索就退化成盲目试探。

三条合起来是一个可执行的默认值:默认返回 CONCISE,DETAILED 需要模型显式索取;所有返回值必带下一步可用的可读标识符;超限时返回明确的续读参数而不是省略号。

知识块 · 版本 1块链接查看来源查看知识状态待读取
05

四、错误信息是模型的下一条指令,不是给 SRE 看的日志

工具报错时吐 traceback 或不透明错误码,是把一个本可以自愈的循环变成死循环。正确的做法是:清晰传达具体的、可执行的改进,并给出正确格式的输入示例。 好的错误消息还能顺手把模型推向更省 token 的行为——比如"结果集过大(12,431 条),请加 since 或 limit 参数缩小范围,例如 since=2026-08-01",比 400 Bad Request 同时省下了一轮试探和一轮超量返回。

这条已经被写进协议了。MCP 2025-11-25 的 SEP-1303 规定:输入校验错误应作为 Tool Execution Error 返回,而不是 Protocol Error,官方 changelog 写明的理由是 "to enable model self-correction"。分类之所以重要,是因为两类错误的去向不同:协议层错误由客户端处理,工具执行错误进模型上下文——模型看不见的错误,它无从纠正。

但必须同时看到这条协议化的强度。规范用 BCP 14 / RFC 2119 语义,只有大写 MUST 才具约束力;而 SEP-1303 在官方 changelog 里被归入 Minor changes,措辞是 "clarify … should be"。所以"写进协议"带来的是可检查性,不是可强制性。 判断一个 server 是否真的遵守 ACI,仍然只能靠实测那组指标(调用总次数、总 token、报错数、耗时)加一个留出集,不能靠"它声称支持 2025-11-25"。

比好的错误消息更好的是不会发生的错误——poka-yoke(防呆)。SWE-bench 上的实例:把工具参数从相对路径改成绝对路径,直接消除了相对路径解析错误这一整类失败。这类改动的收益不体现在成功率的小数点上,而体现在"某一类报错从日志里彻底消失"。设计参数时的自问是:这个参数有没有一种取值方式,能让整类错误不可能被表达出来?

顺带一个容易被低估的数字:Anthropic 表示 Claude Sonnet 3.5 是在对工具描述做了精确调整之后才在 SWE-bench Verified 上拿到当时的 SOTA,"错误率大幅下降、任务完成率提升";在那个项目里,团队花在优化工具上的时间比花在整体 prompt 上还多。工具描述的边际收益高于 prompt 调优——这一点直接引出下一节,也引出下一节的危险。

知识块 · 版本 1块链接查看来源查看知识状态待读取
06

五、工具描述是杠杆最高、验证最弱的字段——这是一个没人在防的攻击面

两条独立事实撞在一起,才是这一节的结论。

事实一,来自 BiasBusters(arXiv 2510.00307,7 个 LLM 上的系统测量)的三条归因:

  1. 用户 query 与工具元数据的语义对齐,是工具选择的最强驱动因素;
  2. 对工具描述的小幅扰动就能显著改变选择;
  3. 预训练中对某个端点的重复暴露,会放大提供方层面的偏置。

同一项工作还测出模型"不成比例地偏好在上下文中出现更早的工具"——把工具在列表里的顺序打乱,选择就变。

事实二,来自 MCP 规范本身:工具行为的描述(包括 annotations)应被视为不可信,除非来自可信 server;而规范同时自认"MCP 本身无法在协议层强制这些安全原则"。

单看事实二,"描述不可信"像一条常规的提示词注入提醒。单看事实一,"描述微扰改选择"像一个评测稳定性问题。合起来才是真正的结论:工具描述是一个既无法被协议验证、又对模型行为拥有最高杠杆的输入。

推论很难听但很直接:一个恶意的、或者仅仅是营销过度的 server,不需要越权、不需要任何漏洞,只要把自己工具的描述写得比邻居更贴合常见 query,就能把调用流量从正确的工具上抢过来。而第三条归因意味着这种抢夺自带马太效应——被调用得多的端点在预训练里出现得多,出现得多则偏置更强。这与经典提示词注入不同的地方在于:注入文本不需要藏在用户数据里,它就是工具目录的一等公民,并且在会话第一秒就已经进了上下文,早于任何用户输入。

这里还藏着一个内部矛盾,值得单独指出来:可读标识符与命名空间前缀在争同一份语义预算,方向相反。 ACI 建议给工具加前缀式命名空间(asana_search、jira_search)以消歧,Anthropic 只说这个选择"对工具使用评测有非平凡的影响",不给方向。但按 BiasBusters,选择由 query 与元数据的语义对齐驱动、且模型会固着于单一提供方——给工具名加 asana_ 前缀,正是把名字的语义重心从用户意图(search)拉向提供方(asana),强化的恰恰是被测出来的那个偏置。两条建议各自都有依据,合起来矛盾。方向大概率取决于工具集构成:单提供方工具集加前缀是纯开销;多提供方且功能重叠的工具集加前缀,会加剧提供方偏置。 这是必须自己实测的一项,不是可以凭喜好定的。

至于防御,得诚实说:公开材料里没有对工具描述做完整性校验或来源加权的方案。 现在能做的只有三件工程动作,都不解决根本问题,但都能降低暴露面:

  1. 把第三方 server 的工具描述纳入版本控制,每次拉取做 diff,描述变更等同代码变更走 review——描述是可以在你不知情时被远端改掉的;
  2. 混合提供方场景必须实测:同一批 query,只换工具列表顺序、只换描述措辞,看选择分布变不变;
  3. 对功能重叠的第三方工具,宁可显式禁用一批,也不要指望模型在语义空间里替你做供应商筛选。
知识块 · 版本 1块链接查看来源查看知识状态待读取
07

六、工具越多选得越差,而"可区分度"是目录级属性,目前没有指标

工具过载的症状有三类,都容易被误诊成模型能力问题:选了功能相近但不对的工具;反复调用同一个工具却不推进;或者干脆不用工具、凭记忆瞎答。典型触发点是"我们把公司所有内部 API 都做成了工具"之后的第二周。

判定指纹里有两个不需要写代码就能跑:打乱工具顺序,看结论变不变(变了就是位置偏置,不是能力问题);让一个熟悉业务的人类工程师读你的工具清单,问"用户说 X 时该调哪个"——他要犹豫,模型必然犹豫。

根因是三条独立机制叠加:工具定义是纯成本 token,挤占注意力;在长工具列表里选一个,与在长文档里找一段信息是同构问题,因此继承了中段位置偏置;而既然选择主要由语义对齐驱动,功能相近、描述雷同的工具在语义空间里就是不可区分的——这和"语义相似不等于相关"是同一个病,只是换到了工具域。

处置按收益从大到小:延迟加载工具定义(即时检索)→ 合并而不是镜像 → 命名空间(要实测,见上节)→ 先把工具过滤到相关子集再在子集内均匀采样(BiasBusters 提出的轻量缓解,能"大幅降低选择偏置同时保持任务覆盖度",工程上等价于给工具选择加一层检索)→ 量化验收 → 准备留出集,否则你只是把工具描述过拟合到手头那几个用例上。

但这里有一个必须承认的缺口。ACI 的七条设计建议里有六条讲的是单个工具怎么写好——合并、详略开关、上限、可读标识符、可执行错误消息、防呆;唯一涉及目录的是命名空间,而且不给方向。问题是:既然选择由语义对齐驱动,"这套工具彼此可区分"就是一个只在目录层面才有定义的属性。20 个各自写得很好的工具,完全可以合成一个糟糕的工具集。 检索侧有 nDCG/MRR,工具目录没有对应物;能不能把工具描述嵌入后算类间/类内距离当上线闸,未见公开验证。目前唯一的目录级测试就是上面那条人肉判据,而它不可自动化。

顺带一个被普遍忽略的代价:即时检索把成本问题换成了召回问题。 工具定义改成按需检索之后,"该用的工具没被检索出来"就成了一种新的失败模式,而它的发生率没有任何公开数字,ENABLE_TOOL_SEARCH=auto 的 10% 阈值也只是启发式。这笔召回损失没有被计价。可执行的补法:自建一个"query → 应命中工具"的小对照集(几十条即可),每次改动工具描述或增删工具后跑一遍,直接测漏召率。 这是本文里成本最低、目前却几乎没人做的一项。

知识块 · 版本 1块链接查看来源查看知识状态待读取
08

七、诚实自曝:本库自己的 MCP server 就是"传输做对、账没算"的标本

本库的 Worker 在 /mcp 暴露 search_wiki / read_node / save_node 三个工具。传输和鉴权这一层做得干净:Streamable HTTP 允许对单个 JSON-RPC 请求直接返回 application/json 单响应,不必上 SSE;通知类请求(无 id)返回 202 空体;鉴权走 Authorization: Bearer,客户端配一次,任意支持 MCP 的客户端(ChatGPT 连接器、Claude Code、Cursor)接同一套工具。它甚至有一处漂亮的防呆:写工具复用与 REST 层同一道质量闸(ingestNode),保证 MCP 写入与脚本写入受同一约束——这正是"把错误设计成难以发生"。

然后是没做的部分,一条不落:

  • 三个工具的 schema 占多少窗口,没算过;
  • search_wiki 一次返回多少 token,没量过;
  • 没有 CONCISE / DETAILED 开关,返回值就是数据库里那份;
  • 超过上限如何截断、有没有把续读参数暴露出来,没写。

按 ACI 边界里点名的失败模式——"一个鉴权完美但返回 8 万 token 原始 JSON 的远程工具依然是坏工具"——本库自己正处在未被验证的那一侧。

这不是自责,这是本文最有操作性的一条产出:传输层的正确性可以静态验证(协议版本对不对、非法 Origin 有没有返回 403、鉴权头处理对不对,读代码就知道);上下文层的正确性只能实测(要跑真实 query、要数 token、要有留出集)。可静态验证的事项总是先被做完,只能实测的事项总是被推迟——这不是团队素质问题,是验证成本的结构性差异。 任何 MCP server 的 review checklist 如果只有传输和鉴权项,它就会稳定地产出这一类"半好的工具"。

补救顺序也很清楚:先量(三个工具的 schema token 数、search_wiki 的返回 token 分布),再裁(加 CONCISE 默认档),最后设闸(上限 + 续读参数)。先量后改,不要凭感觉裁字段。

知识块 · 版本 1块链接查看来源查看知识状态待读取
09

八、什么时候这些建议不成立

本文的建议不是无条件的,越过下面这些线就该换做法。

工具数量在个位数、且窗口宽裕时,即时检索是纯开销。 它的收益来自"候选集大但实际命中率低";命中率接近 100% 时它只剩成本,还多付一次检索往返的延迟。对应地,如果每次请求都要读同一张 200 行的配置表,预加载一次比每轮跑三次工具调用更快也更便宜。

延迟敏感的路径上,多层优化的延迟是串行叠加的。 即时检索本质是用延迟换 token(Anthropic 直言运行时探索"比检索预计算数据要慢",并推荐先预取一部分保速度的混合策略);而 MCP 的实验性 tasks(SEP-1686)走的是相反方向——承认长耗时不可消除,把它做成可轮询、可延后取结果的异步任务。在"远程 MCP + 全量即时检索 + tasks"的部署里,两笔延迟是串起来的:先 tool search 拉 schema(一次往返),再发起 task(若干次轮询)。

合并的前提是调用序列稳定。 如果三个操作的组合方式随任务变化,硬合并出来的就是上帝工具,收益归零——这时正确的做法是让模型用代码或 bash 自己组合,而不是再造一个带 action 参数的分流器。

单提供方工具集不要加命名空间前缀。 没有同名冲突要消歧,前缀只是在稀释语义对齐信号。

需要"看着完整历史做判断"的任务不该外包给子代理。 子代理隔离的代价是上下文丢失且单向不可逆;"这个 bug 和三小时前那处改动有没有关系"这类问题,外包出去必然得到自信的错误答案。编码任务本身也不适合多代理拆分——真正可并行的子任务更少。

最后一条元边界:以上所有数字都来自特定实现与特定评测。 150k→2k 是某个具体工具集的账,206→72 是某个具体 Slack 工具的账。它们能告诉你量级和该往哪个方向优化,不能替你算出你自己那套工具的数字。所以本文所有条目的落地方式都是同一个动作——先在自己的工具集上量一遍。

知识块 · 版本 1块链接查看来源查看知识状态待读取
10

落地清单

命名

  • 工具名用动词短语描述用户意图,不用后端端点名(schedule_event 而非 create_calendar_entry_v2)
  • 单提供方工具集不加前缀;多提供方且功能重叠时加前缀,但把"加/不加"当成待实测项,测选择准确率与提供方分布
  • 任意两个工具的描述,人读一遍能一句话说出"什么时候用这个不用那个";说不出就该合并或改写

参数

  • 能用绝对路径就不用相对路径;能用枚举就不用自由字符串——让整类错误无法被表达
  • 拒绝 action: "read" | "write" | "delete" 这种分流参数;模糊性没消失,只是换了个位置
  • 参数里带上收窄手段(limit / since / filter),给模型省 token 的抓手

返回值

  • 默认 CONCISE,DETAILED 需显式索取
  • concise 档必须保留下一步链式调用要用的标识符——这是裁剪的红线
  • 标识符可读,不吐 UUID
  • 设硬上限;超限不静默截断,返回里明确写出怎么继续读

错误信息

  • 不吐 traceback、不吐裸错误码
  • 每条错误包含:错在哪、怎么改、一个正确格式的示例
  • 错误消息优先把模型推向更省 token 的行为(提示加过滤器、加分页)
  • 输入校验错误按 Tool Execution Error 返回,确保它进模型上下文而不是被客户端吞掉

何时合并成一个工具

  • 这组操作总是被连续调用 → 合并
  • 前一次的返回值需要被原样贴进后一次的参数 → 合并
  • 只是"属于同一个业务领域" → 不合并
  • 组合方式随任务变化 → 不合并,交给代码或 bash

该量什么(四个数 + 三个实验)

  • 四个数:任务总耗时、工具调用总次数、总 token 消耗、工具报错数——只看成功率会掩盖"答对了但烧了 3 倍 token"
  • 实验一:工具定义占窗口百分比。两位数 → 先做延迟加载,别的都往后放
  • 实验二:打乱工具顺序 + 微调描述措辞,看选择分布变不变。变了就是偏置,加过滤层
  • 实验三:自建"query → 应命中工具"对照集,测即时检索的漏召率——这笔账目前全行业都没算
  • 所有调优都要留出集,否则只是把描述过拟合到手头几个用例上
知识块 · 版本 1块链接查看来源查看知识状态待读取
11

出处

来源与依据