Skip to content

第5章 Tool Design:给 Agent 造趁手的兵器

5.1 Tool 的本质:让模型长出手脚

大语言模型天生只有一种能力——生成文本。无论它的推理能力多强,面对"帮我创建一个文件"这种请求,它能做的只是输出一段文字描述应该如何创建文件。要让模型真正"做事",必须给它提供可调用的工具(Tool)。

在 Agent 系统中,一个 Tool 本质上由四部分组成:

typescript
interface Tool {
  name: string           // 工具名称,模型用它来选择调用哪个工具
  description: string    // 工具描述,模型理解工具能力的唯一依据
  parameters: JSONSchema // 参数模式,定义输入的结构和约束
  execute: (params) => Result  // 执行逻辑,Harness 层负责实际运行
}

一个工具从定义到执行的完整生命周期:

这四部分的设计质量,直接决定了 Agent 的行为质量。名字起得不好,模型选错工具;描述写得不清,模型用错场景;参数定义不严,模型传错数据;执行逻辑不健壮,系统崩在运行时。

一个常见的误解是把 Tool Design 等同于"写几个函数然后注册一下"。实际上,Tool Design 是 Harness Engineering 中最考验工程判断力的部分之一。你不是在给人类程序员设计 API,你是在给一个概率推理引擎设计交互界面——这两件事的设计约束完全不同。

5.2 粒度之争:一把瑞士军刀还是一整个工具箱

工具粒度是 Tool Design 的第一个关键决策。

太粗的工具——一个 do_everything(action, target, options) 包办一切——看起来简洁,实则灾难。模型需要在一个巨大的参数空间里做选择,action 和 options 之间的组合爆炸让描述无法覆盖所有用法。更糟糕的是,权限控制粒度也随之丧失:你没办法允许"读文件"但禁止"删文件",因为它们是同一个工具的不同参数。

太细的工具——100 个微操作工具,read_line、read_char、move_cursor——则会淹没模型的选择能力。模型在选择工具时,需要把所有工具的名称和描述都放进上下文窗口。工具越多,上下文开销越大,选择准确率越低。

Claude Code 的做法提供了一个值得参考的平衡点。它定义了约 40 多个工具,按功能域组织:

功能域工具示例设计思路
文件读取Read, Glob, Grep按搜索模式拆分,而非按文件类型
文件写入Write, Edit全量写入 vs 增量编辑,两种操作模式
系统交互Bash一个通用入口,覆盖所有命令行操作
网络访问WebFetch, WebSearch按意图拆分:获取内容 vs 搜索信息
笔记本NotebookEdit专用工具处理特殊格式

这里有几个值得注意的设计决策:

Read 和 Grep 为什么分开? 两者都能获取文件内容,但意图不同。Read 是"我知道要看哪个文件",Grep 是"我不知道内容在哪个文件里"。按意图拆分工具,让模型更容易做出正确选择。

Bash 为什么是一个大工具? 命令行操作的可能性几乎无限,拆成 100 个工具不现实。Bash 作为一个"逃生舱口"存在——当专用工具覆盖不到时,模型可以退回到通用命令行。但 Claude Code 通过描述明确引导模型优先使用专用工具:在 Grep 工具的描述中写着"ALWAYS use Grep for search tasks. NEVER invoke grep or rg as a Bash command"。

Write 和 Edit 为什么分开? 全量覆盖和增量修改是两种截然不同的操作。Edit 只发送 diff,更安全、更高效,适合修改已有文件;Write 适合创建新文件或完全重写。分开设计让权限控制更精细,也让模型的意图表达更明确。

我总结一条经验法则:按用户意图拆分工具,而非按技术实现拆分。 用户(这里指模型)想做的事情自然地映射到不同的工具上,比按底层实现拆分要直观得多。

5.3 描述工程:Tool 的描述就是它的全部接口

对于人类开发者,一个函数的接口是它的类型签名。对于 AI 模型,一个 Tool 的接口是它的描述文本。模型不会读你的实现代码,它对工具的全部认知来自 name + description + parameters schema。

这意味着描述的质量直接决定了工具被正确使用的概率。来看一个对比:

差的描述:

name: "search"
description: "Search for things"

模型看到这个描述,完全无法判断:搜什么?文件名还是文件内容?本地还是网络?返回什么格式?什么时候该用这个工具而不是其他工具?

好的描述:

name: "Grep"
description: "A powerful search tool built on ripgrep.
- ALWAYS use Grep for search tasks. NEVER invoke grep or rg as a Bash command.
- Supports full regex syntax (e.g., 'log.*Error', 'function\\s+\\w+')
- Filter files with glob parameter (e.g., '*.js', '**/*.tsx')
- Output modes: 'content' shows matching lines, 'files_with_matches' shows only file paths
- Use Agent tool for open-ended searches requiring multiple rounds"

好的描述有几个特征:

  1. 说明"是什么":基于 ripgrep 的搜索工具,让模型理解能力边界
  2. 说明"何时用"和"何时不用":明确告诉模型该用这个工具搜索,不要用 Bash 调 grep
  3. 给出使用示例:正则表达式的写法、glob 过滤的写法
  4. 说明关键参数的含义:output_mode 的各选项是什么意思
  5. 指向替代方案:复杂搜索用 Agent 工具

Claude Code 的工具描述有一个显著特点:大量使用 MUST、NEVER、ALWAYS 等强指令词。 这不是随意的措辞选择——模型对这类强指令词的遵从度明显高于温和措辞。当你写"prefer using Grep"时,模型可能偶尔忽略;当你写"ALWAYS use Grep, NEVER invoke grep as a Bash command"时,遵从度会显著提升。

另一个重要实践是在描述中编码优先级关系。Read 工具的描述中包含这样的内容:"Avoid using this tool to run find, grep, cat commands, unless explicitly instructed"——这在工具之间建立了清晰的优先级:有专用工具就用专用工具,Bash 是最后手段。

5.4 参数设计:JSON Schema 是你的契约

参数设计的核心原则是:让模型容易生成正确的参数,让 Harness 容易校验参数。

JSON Schema 是当前 Agent 生态中事实上的参数描述标准。OpenAI、Anthropic、Google 的 function calling 接口都基于它。一个好的参数 schema 应该:

json
{
  "type": "object",
  "properties": {
    "file_path": {
      "type": "string",
      "description": "The absolute path to the file to read (must be absolute, not relative)"
    },
    "offset": {
      "type": "number",
      "description": "The line number to start reading from. Only provide if the file is too large to read at once"
    },
    "limit": {
      "type": "number",
      "description": "The number of lines to read. Only provide if the file is too large to read at once."
    }
  },
  "required": ["file_path"]
}

几个关键设计原则:

参数描述要包含约束。 "must be absolute, not relative"——这种约束不写在描述里,模型就可能传一个相对路径进来。不要假设模型"应该知道",它的行为完全由你提供的文本决定。

可选参数要说明何时提供。 "Only provide if the file is too large to read at once"——这让模型知道正常情况下不需要传这个参数,避免了模型每次调用都费力填写所有字段。

避免深层嵌套。 模型生成 JSON 时,嵌套越深,出错概率越高。一个三层嵌套的 options 对象远不如三个平铺的参数来得可靠:

json
// 不好:深层嵌套
{
  "options": {
    "search": {
      "pattern": "foo",
      "flags": { "case_sensitive": false, "multiline": true }
    }
  }
}

// 好:扁平化
{
  "pattern": "foo",
  "case_sensitive": false,
  "multiline": true
}

使用枚举约束值域。 当参数只有几个合法值时,用 enum 而非 string:

json
{
  "output_mode": {
    "type": "string",
    "enum": ["content", "files_with_matches", "count"],
    "description": "Output mode: 'content' shows matching lines, 'files_with_matches' shows file paths"
  }
}

枚举不仅帮助模型生成正确的值,也让 Harness 层可以在执行前做校验,把错误拦在最早的阶段。

5.5 工具分类:读、写、毁

并非所有工具的风险等级相同。一个成熟的 Agent 系统必须对工具进行安全分类:

只读工具(Read-only):不修改任何状态。Read、Glob、Grep、WebSearch 都属于这一类。这类工具可以安全地自动执行,不需要人类确认。即使模型调错了,最大的损失也就是浪费了一些上下文空间。

写入工具(Write):修改系统状态但可逆。Write、Edit、Bash(大部分命令)属于这一类。这类工具需要谨慎对待——可能覆盖文件、创建不必要的内容。很多 Agent 系统在这个级别引入人类确认。

破坏性工具(Destructive):造成不可逆的后果。git push --forcerm -rf、发送邮件、调用支付 API 都属于这一类。这类工具必须有严格的保护机制。

Claude Code 在系统提示中对此有明确的分层策略。它不只是把工具分类,而是在工具描述中直接编码安全规则:"NEVER run destructive git commands unless the user explicitly requests"、"DO NOT push to the remote repository unless the user explicitly asks"。

这种"在描述中编码安全规则"的做法比"在 Harness 层硬编码检查"更灵活。模型能够理解语境——比如用户说"帮我把这个分支强制推到远端"时,模型知道这是用户的明确授权。而纯硬编码的安全检查做不到这种语境理解。

当然,最高风险的操作不应该仅靠模型的"理解"来保护。对于真正危险的操作,Harness 层应该有独立于模型的硬性拦截。这是第 14 章"权限模型"的主题,这里只强调一点:工具的安全分类应该在设计阶段就确定,而不是事后补救。

5.6 幂等性与容错:为失败而设计

Agent 的执行环境充满不确定性。网络可能断开,文件可能被其他进程修改,命令可能超时。一个健壮的工具设计必须假设失败是常态。

幂等性是第一原则。幂等的工具可以安全地重试——执行一次和执行多次的效果相同。Write 工具天然幂等(写同样的内容到同一个文件,结果不变),但 Bash 工具执行 echo "line" >> file.txt 就不幂等(每次追加一行)。

设计幂等工具的几个技巧:

  • 用"设置状态"代替"改变状态"set_content(file, content)append_line(file, line) 更安全
  • 用唯一标识做去重:如果工具创建资源,接受一个客户端 ID,重复调用不会创建重复资源
  • 让读取操作成为验证手段:执行写入后返回新状态,让模型能验证操作是否成功

超时处理也至关重要。Claude Code 的 Bash 工具允许指定超时时间(默认 120 秒,最长 600 秒),并在超时时返回明确的错误信息。这比让模型无限期等待然后丢失上下文要好得多。

错误信息的设计同样重要。工具返回的错误信息不是给人看的——是给模型看的。模型需要从错误信息中判断:这是暂时性错误(值得重试)还是永久性错误(需要换方案)?错误的根因是什么?

// 差的错误信息
"Error: operation failed"

// 好的错误信息
"Error: file '/src/main.rs' not found. The path may be incorrect
 or the file may have been deleted. Use Glob to search for the file."

好的错误信息不仅描述了什么出错了,还提示了模型下一步该怎么做。这是一种引导——通过错误回复来教模型如何恢复。

5.7 返回格式:告诉模型它需要知道的,仅此而已

工具执行完毕后,返回结果会被塞进上下文窗口。这意味着返回格式的设计直接影响上下文效率。

结构化 vs 原始输出。 大部分情况下,适度结构化的输出优于纯文本 dump。但"适度"是关键——模型并不需要机器可解析的严格 JSON,它需要的是人类可读的、信息密度高的文本。

Claude Code 的 Read 工具返回格式就是一个好例子:带行号的纯文本,类似 cat -n 的输出。不是 JSON 包裹的行数组,也不是没有行号的裸文本。行号让模型能精确引用位置("第 42 行有个 bug"),纯文本避免了 JSON 转义带来的噪音。

截断策略是必须的。 一个 10 万行的文件不可能塞进上下文窗口。Read 工具默认只读 2000 行,Grep 默认限制 250 条结果。这种截断不是功能缺陷,而是设计选择——它迫使模型学会精确地指定自己需要什么。

截断时要让模型知道发生了截断:

[Showing 250 of 1,847 matches. Use offset parameter to see more.]

这行提示信息让模型知道还有更多结果,可以用分页参数继续获取。如果悄悄截断而不告知,模型会基于不完整的信息做出错误判断。

避免返回无用的大量结构化数据。 当 Bash 执行 ls -la 时,直接返回命令输出即可。不需要把每个文件解析成 JSON 对象——模型处理纯文本表格的能力完全够用,额外的 JSON 包装只会浪费 token。

5.8 横向对比:不同系统的 Tool Design 哲学

对比几个主流系统的工具设计,可以看到不同的工程哲学。

Claude Code 的工具设计哲学是"专用工具优先,通用工具兜底"。它为常见操作(读文件、搜索、编辑)提供专用工具,用丰富的描述引导模型选择正确的工具,同时保留 Bash 作为通用后备。工具数量适中(约 40 个),描述极其详尽(单个工具描述可达数百字)。

OpenAI Function Calling 的设计更偏向"开发者自定义"。它不预设工具集,而是提供一套规范让开发者定义自己的 function schema。这种方式灵活性最高,但也意味着工具设计的质量完全取决于开发者。schema 要求是标准 JSON Schema,支持枚举、嵌套对象、必填/可选字段等完整特性。

LangChain Tools 走的是"框架封装"路线。它把常见的外部服务(Google 搜索、Wikipedia、Calculator 等)封装成开箱即用的 Tool 类。优点是上手快,缺点是描述通常比较简略。LangChain 的 Tool 抽象还引入了 return_direct 等控制参数来影响 Agent 循环行为——这让工具定义和流程控制耦合在了一起,是一个见仁见智的设计决策。

从这些对比中可以提炼出一个趋势:描述的信息密度越来越重要。 早期的 function calling 描述往往只有一两句话,现在 Claude Code 的工具描述动辄数百字、包含使用指南和反面示例。模型在工具选择上的表现和描述质量高度正相关——投入在描述上的每一个字都是值得的。

5.9 反模式清单

总结几个在实践中反复出现的 Tool Design 反模式:

瑞士军刀工具。 一个工具通过 action 参数实现十几种功能。模型选对了工具还不够,还得选对 action、匹配对应的参数组合。调试噩梦,权限控制也无从下手。

无描述或弱描述工具。 name: "process", description: "Process data"——这等于没说。模型只能靠名字猜,猜错了就是错误的工具调用。永远不要低估描述的重要性。

HTML/XML dump 返回。 工具返回完整的 HTML 页面或大段 XML。模型需要从几千行标签噪音中提取几个关键信息,浪费 token,降低准确率。正确的做法是在工具层做提取,只返回模型需要的数据。

无限制返回。 工具执行数据库查询,返回 10 万条记录。没有分页,没有截断,直接把上下文窗口撑爆。每个可能返回大量数据的工具都必须有默认限制。

隐式状态依赖。 工具 A 必须在工具 B 之后调用才能工作,但这个依赖没有在任何描述中说明。模型不知道调用顺序的约束,随机排列工具调用,导致莫名其妙的失败。要么消除这种依赖,要么在描述中明确说明。

参数类型模糊。 一个参数既接受字符串又接受数组,行为还不一样。模型很难推断多态参数的正确用法。保持参数类型单一明确。

5.10 小结

Tool Design 的核心思想可以浓缩为一句话:你不是在给程序员设计 API,你是在给语言模型设计认知接口。

模型通过文本描述理解工具能力,通过 JSON Schema 构造调用参数,通过返回文本理解执行结果。这整个链条都是文本驱动的。描述的每一个字、参数的每一个约束、返回的每一行输出,都在影响模型的决策质量。

记住这几条核心原则:

  1. 按意图拆分工具,保持粒度适中
  2. 描述是接口,投入足够的精力写好描述
  3. 参数扁平化,用 Schema 约束值域
  4. 分级安全模型,区分读、写、毁三类操作
  5. 为失败而设计,保证幂等性和有意义的错误信息
  6. 控制返回体积,截断并告知模型

下一章我们将讨论 Tool Orchestration——当 Agent 拥有了趁手的兵器之后,如何编排这些工具的调用顺序和组合策略。

基于 VitePress 构建