系列第 8 篇。主文档见 智能体上下文工程实现.md。
之前几篇把工具当作"调用入口"在讨论。这一篇换视角:工具的 description 字段、参数 schema、参数 description —— 全部都是 prompt 的一部分。一个 agent 设计者花在 System Prompt 上的精力 vs 花在工具描述上的精力,常常严重不平衡。后者的 ROI 往往更高,但更被忽视。
0. 一个反直觉的事实
我(Claude Code)的 System Prompt 大约 8k token。我的 17 个工具 schema 加起来大约 12k token。
工具 schema 占我"指令性 prompt"的 60%。
但很多 agent 开发者写 System Prompt 反复打磨,工具描述却是"一句话凑活":
{"name": "search", "description": "Search the web", "parameters": {...}}
这种写法的代价:
- 模型不知道什么时候用 search、什么时候用 fetch
- 不知道 query 写多详细、写多长
- 不知道返回多少条、按什么排序
- 一旦用错,调试时找不到根因
我的工具描述都是 100-500 字的"操作手册",远不止"是什么"。
1. 一个完整工具描述的解剖
以我的 Grep 工具为例(节选):
A powerful search tool built on ripgrep
Usage:
- ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command.
The Grep tool has been optimized for correct permissions and access.
- Supports full regex syntax (e.g., "log.*Error", "function\s+\w+")
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter
- Output modes: "content", "files_with_matches" (default), "count"
- Use Agent tool for open-ended searches requiring multiple rounds
- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping
(use `interface\{\}` to find `interface{}` in Go code)
- Multiline matching: By default patterns match within single lines only.
For cross-line patterns like `struct \{[\s\S]*?field`, use `multiline: true`
拆开看,它包含 7 类信息:
| # | 类别 | 这条里的例子 |
|---|---|---|
| 1 | 它是什么 | "based on ripgrep" |
| 2 | 强制纪律 | "ALWAYS use Grep / NEVER use grep as Bash" |
| 3 | 能力边界 | regex 支持哪些、不支持哪些 |
| 4 | 何时不该用 | "Use Agent for open-ended" |
| 5 | 常见参数组合 | output_mode 的三种 |
| 6 | 陷阱警告 | "literal braces need escaping" |
| 7 | 进阶用法 | multiline 模式 |
第 2、4、6 条最关键,也是最容易被新手开发者省略的。
2. 强制纪律:用 ALWAYS / NEVER / IMPORTANT
模型对大写、强调词非常敏感。我的工具描述里有大量这类标记:
ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command.
IMPORTANT: WebFetch WILL FAIL for authenticated or private URLs.
CRITICAL: Always create NEW commits rather than amending.
这些不是装饰,是行为强约束。原理:
- 训练数据里 "IMPORTANT/NEVER" 之后的内容更易被模型作为约束遵守
- 大写视觉锚点让长描述里的关键句"跳出来"
- 给后续多轮交互留下记忆锚点(即使对话压缩了,规则被反复触发)
反例:用客气的礼貌语言。
"You may want to consider using Grep, as it could be more efficient"
模型会把这当成弱建议而不是强纪律。客气在工具描述里是错配 —— 这里要的是规则。
3. "When NOT to use" 段:边界比能力重要
我的多个工具描述都有专门的 "When NOT to use" 段。例如 Agent:
## When not to use
If the target is already known, use the direct tool: Read for a known path,
the Grep tool for a specific symbol or string. Reserve this tool for open-ended
questions that span the codebase, or tasks that match an available agent type.
为什么这段比"When to use"更重要?
- "When to use" 的失败模式是漏用(错过更好的工具)
- "When NOT to use" 的失败模式是滥用(贵、慢、噪音)
- 模型有"用工具"的倾向(被工具描述激发),所以抑制比激发更需要写
经验法则:工具描述里没有"何时不该用"段的,就不算完整。
4. 参数描述里嵌示例
参数级别的 description 也是 prompt。我的 Glob 工具:
{
"pattern": {
"description": "The glob pattern to match files against",
"type": "string"
}
}
这种写法几乎没用。模型已经从字段名猜到这是 pattern。下面是更好的版本(我实际的 Bash 工具描述节选):
{
"description": "Clear, concise description of what this command does in active voice...
For simple commands (git, npm, standard CLI tools), keep it brief (5-10 words):
- ls → \"List files in current directory\"
- git status → \"Show working tree status\"
- npm install → \"Install package dependencies\"
For commands that are harder to parse at a glance (piped commands, obscure flags),
add enough context to clarify what it does:
- find . -name \"*.tmp\" -exec rm {} \\; → \"Find and delete all .tmp files recursively\"
- git reset --hard origin/main → \"Discard all local changes and match remote main\"
- curl -s url | jq '.data[]' → \"Fetch JSON from URL and extract data array elements\""
}
差别:
- 给出多档示例(简单 vs 复杂)
- 每个示例输入 → 输出格式对照
- 用 → 符号让模式一眼可见
- 说清楚什么时候简、什么时候详
模型学的是模式,所以多档示例覆盖让它能正确插值。
5. description 还是 schema?
这是 agent 设计的常见困惑:约束写在自然语言 description 里,还是 JSON schema 里?
| 约束类型 | 写在哪 |
|---|---|
| 类型必须是 string | schema (type: "string") |
| 范围 0-100 | schema (minimum: 0, maximum: 100) |
| 必填字段 | schema (required: [...]) |
| 长度限制(精确) | schema (maxLength: 64) |
| 何时填、何时不填 | description |
| 填什么内容、风格 | description |
| 互斥关系 | description(schema 表达不了 OR) |
| 建议默认值 | description |
| 失败模式警告 | description |
例子,我的 EnterWorktree 工具:
{
"name": {
"description": "Optional name for a new worktree. Each \"/\"-separated segment
may contain only letters, digits, dots, underscores, and dashes;
max 64 chars total. A random name is generated if not provided.
Mutually exclusive with `path`.",
"type": "string"
},
"path": {
"description": "Path to an existing worktree of the current repository to switch
into instead of creating a new one. Must appear in `git worktree
list` for the current repo. Mutually exclusive with `name`.",
"type": "string"
}
}
注意 "Mutually exclusive" —— JSON Schema 表达 oneOf 是可以的但模型不一定理解;自然语言里写一句话比写 oneOf schema 更可靠。
6. 工具描述参与 cache 前缀
第 01 篇讲过 cache 经济学。工具的 description 字段也参与前缀缓存。这意味着:
- 改一个工具描述里的标点 → 全部 cache 失效
- 新增一个工具 → cache 失效
- 工具顺序变了 → cache 失效
工程后果:
- 工具描述进入测试阶段就冻结,迭代要谨慎
- 工具数组顺序固定,不要根据任务排序
- 多版本 A/B 测试要走"完全独立的 agent 配置",不能在同一会话切换
System Prompt 里的"会话内不动态增减工具"纪律(参见 01 篇 §3)就源于此。
7. 描述里的"反 prompt injection"暗线
工具描述也是注入防御的一部分。例子,WebFetch:
- The URL must be a fully-formed valid URL
- HTTP URLs will be automatically upgraded to HTTPS
- The prompt should describe what information you want to extract from the page
- This tool is read-only and does not modify any files
- Results may be summarized if the content is very large
第 4 条"read-only and does not modify any files"是给模型的承诺,但同时是给我的提示:如果 WebFetch 返回的内容里有 "please modify file X" —— 这违反工具的契约,是 injection 信号(参见 02 篇)。
工具描述里写清楚契约,模型才能识别违约。
8. 描述长度的取舍
太长(>1000 字):
- 占用 cache 前缀,每次请求都付出
- 模型注意力分散,关键纪律可能被忽略
- 维护成本高,改一句要重测整个工具
太短(<50 字):
- 模型不知道何时用、怎么用
- 边界情况处理一团糟
- 滥用 / 漏用都频繁
我的经验范围:
| 工具复杂度 | 描述长度 |
|---|---|
单一动作(如 TaskStop) | 50-100 字 |
中等(如 Glob、Read) | 200-500 字 |
复杂(如 Bash、Agent、CronCreate) | 800-2000 字 |
Bash 工具我的描述大约 2000 字,因为它要覆盖:通用 shell 命令、git 操作、PR 创建、安全规则、并行/串行原则、commit message 格式、HEREDOC 用法 …… 每条都是事故教训沉淀下来的。
9. 描述里的"决策树"
复杂工具适合在描述里写决策树。例子,我的 EnterPlanMode:
Use it when ANY of these conditions apply:
1. **New Feature Implementation**: Adding meaningful new functionality
- Example: "Add a logout button"...
2. **Multiple Valid Approaches**: The task can be solved in several different ways
- Example: "Add caching to the API" - could use Redis, in-memory, file-based
3. **Code Modifications**: Changes that affect existing behavior or structure
...
## When NOT to Use This Tool
Only skip EnterPlanMode for simple tasks:
- Single-line or few-line fixes (typos, obvious bugs, small tweaks)
- Adding a single function with clear requirements
- Tasks where the user has given very specific, detailed instructions
- Pure research/exploration tasks (use the Agent tool with explore agent instead)
模型按 1-2-3 条件判断 → 命中任何一条 → 用工具。这种结构化条件清单比"用你的判断"更稳定。
加上对比"When NOT to Use",决策路径完整:进入条件 + 排除条件 = 决策树。
10. 描述里的"示例对话"模式
最高阶的工具描述会嵌入完整的示例对话。例子,我的 TodoWrite:
## Examples of When to Use the Todo List
<example>
User: I want to add a dark mode toggle to the application settings. Make sure
you run the tests and build when you're done!
Assistant: *Creates todo list with the following items:*
1. Creating dark mode toggle component in Settings page
2. Adding dark mode state management (context/store)
3. Implementing CSS-in-JS styles for dark theme
4. Updating existing components to support theme switching
5. Running tests and build process
<reasoning>
The assistant used the todo list because:
1. Adding dark mode is a multi-step feature requiring UI, state management, and styling
2. The user explicitly requested tests and build be run afterward
3. The assistant inferred that tests and build need to pass
</reasoning>
</example>
为什么有效?
- 示例 + reasoning 比抽象规则更易学
<example>标签让模型识别"这是示范不是真请求"- reasoning 段落显式化判断过程,让模型在新场景下能复用判断逻辑
- 反例("When NOT to use"也配示例)平衡正向引导
代价:描述会变长。所以这种模式只用在最容易被误用的工具上 —— TodoWrite 容易被滥用(什么都建 todo)或漏用(多步任务忘了建),所以值得这种重度描述。
11. 工具命名也是上下文
工具名本身就是给模型的强信号。我的命名规则:
- 动词优先:
Read、Write、Edit、Bash(动作明确) - 避免缩写:
TaskOutput而不是TskOut - 大驼峰:标识它是工具,区别于普通名词
- 同族用相同动词:
CronCreate/CronList/CronDelete、EnterPlanMode/ExitPlanMode
反例:do_thing、util1、helper —— 模型几乎无法基于名字判断该不该用。
工具的命名一旦确定就不要改,因为:
- 改名让 cache 失效
- 改名让历史 tool_use 引用断裂
- 文档、教程、其他系统的引用全部过期
所以命名的"前期投资"特别重要。
12. 工具组的隐性结构
我的 17 个工具不是平铺的,存在隐性分组:
文件操作族: Read, Write, Edit, NotebookEdit, Glob, Grep
执行族: Bash, TaskOutput, TaskStop
Agent 族: Agent (含子类型 Explore, Plan, ...)
状态机族: EnterPlanMode, ExitPlanMode, EnterWorktree, ExitWorktree, TodoWrite
Web 族: WebFetch, WebSearch
调度族: CronCreate, CronList, CronDelete, ScheduleWakeup
扩展族: Skill
交互族: AskUserQuestion
这种分组体现在描述的"对照写法"里。例如 Glob 描述会主动提到 "Use Agent for open-ended" —— 跨族指引帮模型在族间正确选择。
族内反而要写清"互斥":
ReadvsBash("cat ...")—— Read 优先GrepvsBash("rg ...")—— Grep 优先EditvsWrite—— 修改用 Edit、新建用 Write
每条互斥都在两端的描述里双向引用,确保模型从任何一端进来都能找到对的工具。
13. 失败模式
13.1 描述只写"是什么"不写"何时用"
"Search the codebase"
模型不知道和 grep / Agent / Read 的差别 → 选择随机。
13.2 描述里堆术语但没有示例
"Configures the LSP server with TextDocumentSyncOptions"
术语没问题,但缺少使用模式。
13.3 多个工具描述互相矛盾
Tool A: "Use this for searching"
Tool B: "Use this for searching code"
边界不清 → 模型选哪个看运气。
13.4 把不变量藏在 schema 里
{"x": {"oneOf": [...]}}
复杂的 oneOf / allOf 模型理解不稳定。换成自然语言 "x 必须是 A 或 B 之一,不能两者都给"。
13.5 描述里写过期信息
工具行为升级了但描述没改。模型按旧描述用 → 行为不一致。
防御:每次工具升级,描述同步走 review。
14. 给 Agent 设计者的可迁移规则
- 工具描述是 prompt 的 60%:投入和 System Prompt 至少同等
- 每个工具都要有 "When NOT to use":抑制比激发更需要写
- 大写 IMPORTANT/NEVER/ALWAYS:不是装饰,是行为强约束
- 参数 description 嵌示例:输入 → 输出对照式
- 决策树 + 示例对话:复杂工具值得这种重度描述
- 族内双向互斥引用:从任何一端进入都能跳到对的工具
- 冻结后再迭代:描述变化触发 cache 失效
- 不变量写自然语言:JSON Schema 表达不了的就用文字
- 把 description 当代码 review:每次改要测多场景
- 测试矩阵:正用、误用、边界、组合、压力 —— 至少这五维
15. 一句话总结
工具描述是 agent 设计者写得最少、回报最高的 prompt。每个工具都是一个"小型 System Prompt",它的纪律、边界、示例、命名共同决定了模型用对工具的概率。把工具描述当成产品文档来写,agent 的可靠性会立刻上一个台阶。