跳到主要内容

工具文档字符串考古学:描述字段是你杠杆率最高的提示词

· 阅读需 13 分钟
Tian Pan
Software Engineer

你的智能体中杠杆率最高的 prompt 并不在你的系统 prompt(system prompt)中。它是你六个月前在某个工具定义下写的那句描述,它随实现代码一起提交,之后就再也没动过。模型在每一轮对话中都会读取它,以此决定是否调用该工具、绑定哪些参数,以及当响应不符合预期时如何恢复。工程师将其视为面向人类的 API 文档,而模型则将其视为一个 prompt。

这两种视角之间的鸿沟,正是最糟糕的工具使用(tool-use)类 bug 的温床:模型调用了正确的函数名,传入了正确的参数,发出了正确的 API 调用 —— 但原因却是错的,场景是错的,或者它放着旁边更合适的工具不用。没有任何异常抛出。你的评估套件依然通过。这种退化(regression)只会表现为衡量智能体是否真正起到帮助的指标在缓慢下降。

最近一项针对包含 856 个工具的 103 个 MCP 服务器的实证研究发现,仅靠增强工具描述,就能使任务成功率在统计学意义上显著提升 5.85 个百分点。对于一项在推理时零成本且不需要更换新模型的改动来说,这种效果规模(effect size)是非常惊人的。但同一项研究也发现,在 16.67% 的案例中出现了退化 —— 这意味着大约每六次改动中,就有一次盲目的描述修改反而起到了反作用。描述字段功能强大,但也非常锋利。大多数团队使用它的方式都搞错了。

Docstring 会被编译为 Prompt Token,而非渲染给人类阅读

当你使用 LangChain 或 Anthropic SDK 之类的框架注册工具时,该函数的 docstring 会在每次模型调用时直接拉入系统上下文(system context)。在 LangChain 的 @tool 装饰器中,docstring 会逐字变成工具描述。在 Anthropic API 中,每个工具 schema 的 description 字段会被内联到模型看到的 prompt 中。它不是文档,它是多绕了几步路的 prompt 文本。

这种视角的重塑改变了对“好描述”的定义。传统的 docstring 优点 —— 简洁、中性、强调“它做什么”而非“何时使用” —— 当读者是需要在 12 个相似工具中做选择的模型时,这些优点反而会有害。模型需要的是边界条件,而不是口号。对比一下:

  • 差的写法:“在内部知识库中搜索并呈现最相关的结果。”
  • 好的写法:“在客户支持知识库中搜索与用户问题相关的文章。仅用于关于产品功能、定价或故障排除的问题。不要用于关于账单历史、账户状态或任何需要个性化数据的问题 —— 针对这些问题请使用 get_account_status。返回最多 5 个排序结果;空列表表示无匹配项,而不代表置信度低。”

第二种写法读起来像是糟糕的文案,却是优秀的 prompt 工程。它点出了与其竞争的工具,建立了“何时不调用”的规则,并告诉模型如何解释空输出。这些信息对于浏览代码库的人类读者毫无帮助。但对于一个试图在凌晨三点的生产环境中进行正确路由的 LLM 来说,这至关重要。

在大规模应用中,这比任何单个工具都重要。一个典型的包含 58 个工具的五服务器 MCP 配置,在用户进行第一轮对话前就会消耗大约 55,000 个 context token。仅增加一个 Jira 服务器就可能让这个数字冲到 70,000。每个描述中的每个字符都在上下文窗口中占用成本,而一个工具描述中每一个不精确的词组,都会改变所有其他工具被调用概率的分布。工具描述并非彼此独立。它们构成了一个共同的 prompt,其中每一个工具都通过排除其他工具来定义自己。

真实世界工具描述的四个考古地层

挖掘任何生产环境中的智能体代码库,你会发现工具描述属于项目不同的地质年代:

  1. MVP 层:由最初搭建工具的人编写,为通过“能否调用成功”测试而优化。通常对函数的机制描述准确,但对于何时优先选择它而非其他替代方案闭口不谈,因为当时还没有替代方案。
  2. Bug 修复层:在特定的生产事故后添加的行。“不要用空字符串作为参数调用此工具。”“仅在用户明确确认后使用。”这些读起来像是伪装成文档的编译器警告。
  3. 功能漂移层:实现代码变了,但描述没变。参数 customer_id 在代码、schema 和每个调用方中都变成了 account_id —— 除了那个仍在谈论“customer”的自然语言描述。
  4. 能力扩张层:有人通过在原本只针对一个场景的描述中堆砌新的要点,强行增加了第二个用例。现在,模型看到一个声称能做两件关联性不强的事情的工具,并自信地将其用于这两个场景,但效果很差。

沉积物不断堆积。新贡献者看到的是一段看起来连贯、实际上却是四个人在两年间写就的重写羊皮纸(palimpsest),每一层都针对不同的故障模式进行了优化,而没有人能把全局逻辑装在脑子里。这就是为什么描述修改会有 16.67% 的退化概率 —— “修复”与早期地层建立的不变量产生了冲突,而已经没人记得那些不变量了。

考古式的修复方法不是从头重写。而是将描述视为具有所有权、版本历史和明确变更理由的一等代码产物(first-class code artifacts)。当你修改描述时,请说明原因 —— 写在 commit message 里,而不是描述本身。描述保持为一个 prompt;而 commit 历史则成为考古记录。

这种通过了所有测试的隐性失败模式

最可怕的工具使用 bug 并不是模型调用了错误的工具。这种 bug 至少在工具失败或返回无意义内容时会产生可见的错误。真正可怕的是模型因为错误的理由调用了正确的工具,且在这种情况下工具恰好能运行并返回一个看起来合理的结果。

具体模式:一个 search_users 工具的描述写着“通过姓名、电子邮件或用户 ID 查找用户”。用户询问“查找来自 dorathy 的支持工单”。由于模型在描述空间中找不到 search_tickets 工具,它将 dorathy 绑定为姓名并调用了 search_users。API 返回了一个匹配的用户。接着,智能体从该用户的个人资料中幻觉出了工单内容。用户得到了一个自信但错误的答案。工具运行了。API 成功了。没有触发任何异常。你的评估套件(测试了使用姓名、电子邮件和 ID 调用 search_users 的情况)也通过了。

关于工具选择幻觉的研究将其归类为“工具类型幻觉 (tool-type hallucination)”与“参数绑定幻觉 (parameter binding hallucination)”的结合。当可用工具列表存在模型不知情的覆盖空白时,这种情况出现的比例会异常高。模型只能从它看到的工具中选择;如果没有描述明确排除该工具适用于用户的真实意图,那么最接近的工具将默认胜出。在实践中,“什么时候不要调用我”比“什么时候调用我”是更重要的信息,因为调用错误的工具绝对比完全不调用要糟糕——如果不调用,智能体至少可以询问一个澄清性问题。

两种有帮助的模式:

  • 描述中的负面示例:“请勿使用此工具搜索工单、订单或对话;这些有专门的工具。” 勾勒出模型不应触及的负面空间。
  • 易混淆工具的消歧线索:如果你同时拥有 get_usersearch_users,每个工具的描述都应该通过名称引用另一个,并解释两者的区别(精确查找与模糊匹配)。模型无法对它不知道相关的工具进行消歧。

描述与实现间的偏差,以及捕捉它的 Lint 规则

工具描述是与模型签订的合同。而实现则是与运行环境签订的合同。当两者发生偏差时,模型正确地调用了一个工具,但该工具的行为已不再符合描述中的承诺。每一个下游推理步骤现在都被一个听起来很真实但已不再成立的前提所污染。

模式偏差 (Schema drift) 是最容易检测的情况:如果 JSON 模式中的 customer_id 被重命名为 account_id,但描述仍然在讨论查找客户,那么一个比较描述中命名参数与模式中实际参数列表的基础 linter 就能发现它。现在已经有几种开源工具将此作为智能体 CI 的一部分。

更难的情况是语义偏差 (semantic drift)。描述说“最多返回 5 条结果”。有人将实现更改为最多返回 10 条。模式没有改变——它一直是 results: array of objects。从狭义上讲,合同没有任何破损。但模型数月来一直基于“最多 5 条”的约束做出规划决策,现在这些规划出现了细微的偏差(智能体可能在应该分页时没有分页,或者因为预期结果较少而过度过滤)。

捕捉语义偏差需要超越结构化 linting 的手段:

  • 将描述视为断言测试:将描述中的每个事实声明视为一个需要测试的属性。“最多返回 5 条结果”变成一个测试,该测试使用宽泛的查询调用工具,并断言 len(results) <= 5。如果无法测试,就不要这样声明。
  • 黄金调用回放 (Golden invocation replay):保留一组固定的规范化工具调用追踪记录。当描述更改时,针对留出的评估集回放这些追踪记录,并对比模型调用决策的差异。改变路由行为的描述重写不应悄无声息地发布。
  • 描述变更日志:要求在任何描述编辑的提交信息中包含简短的理由。这听起来很官僚;但在实践中,它可以捕捉到那些“我精简了措辞”却意外改变了语义的编辑。
  • 将描述作为输入的集成测试:这是最被低估的模式。你的测试使用工具模式和合成的用户查询来调用模型,并断言模型以预期方式调用(或不调用)该工具。这直接测试了描述,而非实现。

将描述视为生产环境中的提示词资产

实际的转变在于重新审视:工具描述不是文档,也不是代码注释。它们是提示词 (prompts),被持续部署到生产环境中,支配着智能体每一轮对话的行为。适用于系统提示词的纪律——带有理由的版本控制、回归测试、 A/B 实验、发布门禁——也应该以同样严谨的态度应用于描述。

三种值得借鉴的模式:

  • 描述的合并请求 (PR) 需要与提示词更改相同的审核,而不是像注释清理那样对待。如果你的团队将系统提示词的编辑置于评估运行的门禁之后,那么同样的门禁也适用于描述编辑。如果你没有对系统提示词编辑设置门禁,那么你面临的问题比这篇文章能解决的还要大。
  • 描述应该有负责人,而不只是作者。像为关键 API 端点分配负责人一样,指派一名团队成员负责每个工具描述的质量。轮换是可以的;但缺乏负责制会导致描述变成“考古堆积层”。
  • 在编写描述时要考虑整个工具列表,而不仅仅是单个工具。在添加新工具之前,对现有工具列表进行“混淆度”检查:当前是否有任何描述的范围与新工具重叠?如果是这样,两个描述都需要明确提及对方并划清界限。

认真对待描述所带来的收益是不对称的。仅靠描述优化就能带来的 5.85 个百分点的提升,比大多数提示词微调环节产生的效果都要好,并且它会在工具的整个生命周期中复利作用于每一次用户交互。忽视它的代价则是产生一类隐蔽且看似合理的失败,这些失败会以仪表盘难以衡量的方式侵蚀用户信任。

从两小时审计开始

如果你今天发布了一个智能体,在进入下一个冲刺之前,有一项具体的练习值得一做:按顺序大声朗读代码库中的每一个工具描述,就好像你是第一次在上下文窗口中看到它们的模型一样。对于每一个描述,请思考:

  • 如果我只有这个描述,我会知道什么时候 调用这个工具吗?
  • 列表中是否有其他工具的描述与这个重叠?如果有,它们是否相互引用了?
  • 这里的每一个事实陈述是否都与当前的实现相匹配?
  • 如果工具返回空结果、错误或部分结果会发生什么 —— 描述是否告知了模型该如何解释这些情况?

大多数团队会发现,他们一半的描述至少在其中两个问题上不及格。第一次修正通常只需要一个下午,而且比原本计划在冲刺中的任何提示词工程任务都能带来更大的质量提升。第二次修正通常是在三个月后,当你了解了生产环境下的真实故障模式时进行的,那才是产生真正收益的地方。描述字段是你智能体中杠杆率最高的提示词,因为你已经在编写它了;唯一的问题在于,你是否将其视为它已悄然变成的生产环境交付物。

References:Let's stay in touch and Follow me for more thoughts and updates