Claude Code 的工具提示词
系统提示词管全局行为,工具提示词管局部动作。模型准备调用某个工具时,看到的不只是工具名和参数 schema,还会看到这个工具自己的使用协议:什么时候用、什么时候别用、参数怎么写、错误怎么恢复、有哪些安全禁区。
这一章不再讲工具注册和执行,而是专门读工具 prompt 源码,看看 Claude Code 如何把每个工具做成一个“微型控制面”。
核心直觉
工具提示词不是说明书,而是局部行为协议。它把“功能描述、正向引导、负面禁令、参数语义、恢复路径、资源预算”一起交给模型。
先看源码入口
| 源码定位点 | 关键符号 | 负责什么 |
|---|---|---|
restored-src/src/tools/BashTool/prompt.ts:275-369 | getSimplePrompt() | Bash 工具主提示词:工具偏好、命令执行、sleep 反模式 |
restored-src/src/tools/BashTool/prompt.ts:42-161 | getCommitAndPRInstructions() | Git 安全协议和 commit/PR 工作流 |
restored-src/src/tools/BashTool/prompt.ts:172-273 | getSimpleSandboxSection() | 沙箱策略 JSON 内联 |
restored-src/src/tools/FileEditTool/prompt.ts:1-28 | getPreReadInstruction()、old_string 规则 | 编辑前必须读取、最小唯一匹配、行号前缀处理 |
restored-src/src/tools/FileReadTool/prompt.ts:1-49 | MAX_LINES_TO_READ、offset/limit 指令 | 默认读取 2000 行、分段读取、多媒体能力声明 |
restored-src/src/tools/GrepTool/prompt.ts:1-18 | Grep 使用规则 | 搜索必须用 Grep,避免 Bash grep/rg |
restored-src/src/tools/GrepTool/GrepTool.ts:33-89 | input schema | 输出模式、head_limit 等结果控制 |
restored-src/src/tools/AgentTool/prompt.ts:1-287 | agent 列表、fork 指引 | 委派任务质量、动态 agent 列表缓存优化 |
restored-src/src/tools/SkillTool/prompt.ts:1-242 | skill 列表预算和调用协议 | 1% 上下文预算、三级截断、阻塞调用要求 |
读工具提示词要和运行时实现一起读。提示词负责提前引导模型,运行时代码负责真正检查。
总流程图
一个工具提示词通常同时回答六个问题:
| 问题 | 例子 |
|---|---|
| 这个工具做什么 | Grep 搜索内容,Edit 替换文本 |
| 什么时候用 | 搜索任务必须用 Grep |
| 什么时候不用 | Bash 不要拿来 cat、grep、sed |
| 参数怎么写 | Edit 的 old_string 必须唯一,Read 可用 offset/limit |
| 出错怎么修 | 搜索过多用 head_limit,编辑失败扩大上下文 |
| 结果如何节制 | Read 默认 2000 行,Skill 列表最多 1% 上下文 |
万能执行入口:BashTool 必须被导流
Bash 是最危险也最容易被滥用的工具。因为它几乎什么都能做,所以提示词首先要告诉模型:能用 Bash,不代表应该用 Bash。
工具偏好矩阵:把 Bash 流量导到专用工具
原文给出的 Bash 提示词片段:
const toolPreferenceItems = [
`File search: Use ${GLOB_TOOL_NAME} (NOT find or ls)`,
`Content search: Use ${GREP_TOOL_NAME} (NOT grep or rg)`,
`Read files: Use ${FILE_READ_TOOL_NAME} (NOT cat/head/tail)`,
`Edit files: Use ${FILE_EDIT_TOOL_NAME} (NOT sed/awk)`,
`Write files: Use ${FILE_WRITE_TOOL_NAME} (NOT echo >/cat <<EOF)`,
'Communication: Output text directly (NOT echo/printf)',
];
这段代码证明 BashTool prompt 的第一任务不是教模型写 shell,而是做工具路由。
| 场景 | 应用工具 | 禁止倾向 |
|---|---|---|
| 文件查找 | Glob | find、ls |
| 内容搜索 | Grep | grep、rg |
| 读文件 | Read | cat、head、tail |
| 改文件 | Edit | sed、awk |
| 写文件 | Write | echo >、HEREDOC |
| 输出文本 | 直接回复 | echo、printf |
为什么要这么强?因为专用工具外面包了 schema、权限、结果预算和 UI 渲染。Bash 字符串没有这些结构化边界。
容易误解
“Bash 能做”不是工具选择标准。Claude Code 希望模型优先选择可审计、可校验、可预算的专用工具。
多命令并发:提示词直接给调度语义
BashTool 提示词还有多命令规则:
If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
DO NOT use newlines to separate commands.
这段文本证明工具提示词可以承载调度策略:
| 情况 | 推荐写法 | 语义 |
|---|---|---|
| 独立命令 | 多个 Bash tool calls | 交给工具编排并发 |
| 有依赖 | cmd1 && cmd2 | 前一个成功才执行后一个 |
| 顺序但允许失败 | cmd1; cmd2 | 不关心前一个失败 |
| 换行分隔 | 禁止 | 避免模型生成不可控脚本块 |
这和第 4 章的并发调度是配套的:提示词让模型正确表达依赖关系,执行器再根据工具输入做调度。
版本控制安全协议:把风险场景写成具体禁令
BashTool 的 Git 安全协议:
Git Safety Protocol:
- NEVER update the git config
- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless the user explicitly requests these actions
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
- NEVER run force push to main/master, warn the user if they request it
- CRITICAL: Always create NEW commits rather than amending, unless the user explicitly requests a git amend.
- When staging files, prefer adding specific files by name rather than using "git add -A" or "git add ."
- NEVER commit changes unless the user explicitly asks you to
这段提示词证明好的工具 prompt 会把风险展开成具体操作,而不是只写“注意安全”。
| 禁令 | 防什么 |
|---|---|
| 不改 git config | 污染用户环境 |
| 不跑 destructive git | 覆盖历史、丢失工作区 |
| 不跳过 hooks | 绕过质量和签名检查 |
| 不 force push main/master | 破坏共享主分支 |
| 默认新建 commit,不 amend | hook 失败后 amend 可能改到上一个 commit |
| staging 指定文件 | 避免把 .env、凭证等扫进去 |
| 不主动 commit | 避免 Agent 过度自治 |
核心直觉
Git 协议里的 NEVER 不是风格用词。它告诉模型这些操作默认不属于可自由推断的行动空间,除非用户显式授权。
沙箱 JSON:把机器策略直接交给模型
沙箱启用时,BashTool 会把文件系统策略内联为 JSON:
const filesystemConfig = {
read: {
denyOnly: dedup(fsReadConfig.denyOnly),
allowWithinDeny: dedup(fsReadConfig.allowWithinDeny),
},
write: {
allowOnly: normalizeAllowOnly(fsWriteConfig.allowOnly),
denyWithinAllow: dedup(fsWriteConfig.denyWithinAllow),
},
};
这段代码证明工具提示词不一定只写自然语言。安全策略需要精确时,JSON 更合适。
原文还提到两个细节:
dedup()去重路径,减少重复 token。normalizeAllowOnly()把用户临时目录归一成$TMPDIR,既省 token,也提升 prompt cache 稳定性。
模型看到沙箱策略后,可以在生成命令前避开不允许的路径,而不是等运行时拒绝后再修正。
等待反模式:把无效 sleep 压下去
BashTool 还专门抑制 sleep:
Do not sleep between commands that can run immediately.
If your command is long running, use run_in_background.
Do not retry failing commands in a sleep loop - diagnose the root cause.
If waiting for a background task, do not poll.
If you must sleep, keep the duration short (1-5 seconds).
这证明提示词会针对模型常见坏习惯写“反模式抑制”。模型很容易用 sleep && retry 模拟异步等待,但交互式 Agent 更需要诊断、后台执行或事件反馈。
文件编辑工具:编辑前必须先读取
FileEditTool 提示词短,但每一句都在维护编辑正确性。
前置读取是硬约束,不是建议
function getPreReadInstruction(): string {
return `You must use your \`${FILE_READ_TOOL_NAME}\` tool at least once
in the conversation before editing. This tool will error if you
attempt an edit without reading the file.`;
}
这段代码证明“先读后改”是双层防线:
| 层 | 作用 |
|---|---|
| prompt | 让模型提前知道不读会失败,避免浪费工具调用 |
| runtime | 没有 Read 历史时 Edit 直接报错 |
这解决的是幻觉编辑:模型不能凭记忆或猜测修改文件。
old_string 必须唯一,但也不能太大
FileEditTool 要求:
The edit will FAIL if old_string is not unique in the file.
Either provide a larger string with more surrounding context to make it unique
or use replace_all to change every instance.
内部用户还有更细的 token 经济学提示:
Use the smallest old_string that's clearly unique - usually 2-4 adjacent lines is sufficient.
Avoid including 10+ lines of context when less uniquely identifies the target.
这证明 old_string 要在两个目标之间平衡:
| 目标 | 太低会怎样 | 太高会怎样 |
|---|---|---|
| 唯一性 | 匹配多个位置,工具失败或误改 | 过多上下文浪费 token,且更容易因无关变化匹配失败 |
| 精确性 | 模型凭猜测替换 | 编辑成本变高 |
读文件输出和编辑输入之间有接口契约
提示词还说明从 Read 输出复制文本时,要去掉行号前缀:
const prefixFormat = isCompactLinePrefixEnabled()
? 'line number + tab'
: 'spaces + line number + arrow';
并要求:
Everything after that is the actual file content to match.
Never include any part of the line number prefix in the old_string or new_string.
这段代码证明工具提示词承担了工具间接口文档的角色。Read 为了 UI 和定位添加行号,但 Edit 的匹配必须基于真实文件内容。
文件读取工具:读取也要预算感知
FileReadTool 的提示词主要控制读取范围和运行时能力声明。
默认 2000 行不是随便写的
export const MAX_LINES_TO_READ = 2000;
提示词里写:
By default, it reads up to 2000 lines starting from the beginning of the file.
这证明 Read 默认不是“全文件无上限”。2000 行足以覆盖多数单文件理解,又避免一次读取吞掉太多上下文。
分段读取参数有两种引导语气
export const OFFSET_INSTRUCTION_DEFAULT =
"You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters";
export const OFFSET_INSTRUCTION_TARGETED =
'When you already know which part of the file you need, only read that part. This can be important for larger files.';
这段代码证明同一个参数在不同阶段有不同策略:
| 模式 | 适合阶段 | 引导 |
|---|---|---|
| DEFAULT | 初次理解文件 | 尽量读完整文件,避免漏上下文 |
| TARGETED | 已知目标位置 | 只读需要的部分,省 token |
能力声明必须和运行时对齐
ReadTool 提示词会声明图片、PDF、Notebook 支持,但 PDF 段落受 isPDFSupported() 条件控制。大 PDF 还要求指定 pages,每次最多 20 页。
这说明工具 prompt 不能承诺运行时做不到的能力:
运行时支持 PDF -> 提示词声明 PDF 能力和页数限制
运行时不支持 PDF -> 不在提示词里提 PDF
容易误解
在 prompt 里写“可以读 PDF”,不会让运行时突然具备 PDF 解析能力。能力声明必须由代码条件控制。
搜索工具:搜索必须走受控入口
GrepTool 提示词短,但非常硬:
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.
这段说明和 BashTool 的偏好矩阵形成双向闭环:
BashTool: 搜索不要用 Bash grep/rg
GrepTool: 搜索必须用 Grep
为什么同样底层可能是 ripgrep,还要用 GrepTool?原文指出 GrepTool 外面包了:
checkReadPermissionForToolgetFileReadIgnorePatternsVCS_DIRECTORIES_TO_EXCLUDE- 结构化结果和预算
通过 Bash 直接跑 rg 会绕过这些边界。
搜索语法纠偏
GrepTool 提示词还提醒:
Supports full regex syntax.
Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping.
Multiline matching: use multiline: true.
这里不是科普正则,而是在减少模型生成错误搜索的概率。multiline: true 底层对应 -U --multiline-dotall,但提示词选择暴露模型需要知道的参数,而不是暴露命令行细节。
head_limit:安全默认值和逃生口
GrepTool schema 里有:
const DEFAULT_HEAD_LIMIT = 250;
描述中说明:
Defaults to 250 when unspecified.
Pass 0 for unlimited (use sparingly - large result sets waste context).
这证明 Grep 的默认不是“尽量多返回”。250 是上下文保护;0 是明确逃生口,但提示词提醒谨慎使用。
子代理工具:动态列表外移,委派也要有质量标准
AgentTool 的提示词复杂,因为它既要告诉模型有哪些 agent,又要教模型如何写好委派任务。
子代理列表:内联还是附件
源码里有开关:
export function shouldInjectAgentListInMessages(): boolean {
if (isEnvTruthy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES)) return true;
if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES))
return false;
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_agent_list_attach', false);
}
这段代码证明 agent 列表有两种注入方式:
| 方式 | 行为 | 取舍 |
|---|---|---|
| 内联工具描述 | agent 列表直接写进 tool description | 模型一眼看到,但 schema 频繁变 |
| 附件消息 | tool description 保持静态,列表放 @@INLINE_0@@ | 保护工具 schema prompt cache |
原文提到动态 agent 列表曾占全局 cache_creation token 约 10.2%。所以把列表移到附件,是为了保护工具 schema 层缓存。
agent 行格式:
export function formatAgentLine(agent: AgentDefinition): string {
const toolsDescription = getToolsDescription(agent);
return `- ${agent.agentType}: ${agent.whenToUse} (Tools: ${toolsDescription})`;
}
这说明模型不只需要知道 agent 类型,还要知道这个 agent 能用哪些工具。委派边界也是工具提示词的一部分。
分叉子代理:不要偷看,不要抢跑
fork 开启时,提示词会加入纪律:
Don't peek. The tool result includes an output_file path - do not Read or tail it unless the user explicitly asks for a progress check.
Don't race. After launching, you know nothing about what the fork found. Never fabricate or predict fork results in any format.
Writing a fork prompt. Since the fork inherits your context, the prompt is a directive - what to do, not what the situation is.
这段证明 fork 的难点不是“怎么启动子 agent”,而是防止父 agent 污染并行过程:
- 不偷看中间文件,避免把 fork 的噪音拉回父上下文。
- 不抢跑预测结果,避免在子 agent 完成前编造结论。
- fork 继承上下文,所以 prompt 应该是任务指令,不是重复背景。
“不要委派理解”
AgentTool 提示词里最关键的一句:
Never delegate understanding.
它要求父 agent 不能写“based on your findings, fix the bug”这种模糊任务,把综合判断甩给子 agent。父 agent 要先完成理解和分解,再把明确任务交出去。
核心直觉
子 agent 可以执行研究或实现片段,但父 agent 不能把“理解任务本身”的责任外包掉。
技能工具:工具提示词也要管理自己的体积
SkillTool 的特别之处是:它不只控制行为,还控制技能列表本身的 token 成本。
1% 上下文预算
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01;
export const CHARS_PER_TOKEN = 4;
export const DEFAULT_CHAR_BUDGET = 8_000;
这段代码证明技能列表最多占上下文窗口约 1%。对于 200K token,约等于 8000 字符。
为什么?因为技能列表只是目录,不是技能正文。模型只需要判断是否调用技能,真正内容在调用后加载。
三级截断
formatCommandsWithinBudget() 有三级策略:
| 级别 | 条件 | 行为 |
|---|---|---|
| 完整保留 | 总描述未超预算 | 全部技能完整展示 |
| 描述裁剪 | 超预算但平均描述长度足够 | 非内置技能描述裁剪,内置技能保留完整 |
| 仅保留名称 | 平均描述低于 MIN_DESC_LENGTH | 非内置技能只显示名称 |
每个条目还有硬上限:
export const MAX_LISTING_DESC_CHARS = 250;
getCommandDescription() 先把单条描述截到 250 字符,再参与总预算。
这说明动态工具/技能目录必须预算感知。否则插件生态越大,第一轮上下文越被目录挤爆。
技能匹配是阻塞要求
SkillTool 的调用协议里有强约束:
When a skill matches the user's request, this is a BLOCKING REQUIREMENT:
invoke the relevant Skill tool BEFORE generating any other response about the task
还有重复加载防护:
If you see a <command-name> tag in the current conversation turn,
the skill has ALREADY been loaded - follow the instructions directly
instead of calling this tool again
这证明 SkillTool 要解决两个常见问题:
- 匹配技能时,模型不能先自由发挥再加载技能。
- 技能已加载时,不能重复调用造成循环和上下文浪费。
状态和数据结构
| 工具 | 提示词核心结构 | 影响 |
|---|---|---|
BashTool | 工具偏好矩阵、Git 协议、沙箱 JSON、sleep 反模式 | 把万能入口限制为受控兜底 |
FileEditTool | getPreReadInstruction()、old_string 唯一性、行号前缀说明 | 确保编辑基于真实文件内容 |
FileReadTool | MAX_LINES_TO_READ、offset/limit、PDF/page 条件能力 | 控制读取体积和能力承诺 |
GrepTool | ALWAYS/NEVER、ripgrep 语法、head_limit | 让搜索走受控工具,避免上下文爆炸 |
AgentTool | agent 列表注入、fork 纪律、委派 prompt 规范 | 控制多 agent 协作质量和缓存稳定 |
SkillTool | 1% budget、三级截断、BLOCKING REQUIREMENT | 控制技能目录体积和调用顺序 |
设计取舍
| 取舍点 | 源码体现 | 工程判断 |
|---|---|---|
| 万能工具降级 | Bash 偏好矩阵 | Bash 是兜底,不是默认入口 |
| 双向约束 | Bash 说别 grep,Grep 说搜索用我 | 单向提示容易漏,双向闭环更稳 |
| 前置条件双层防御 | Edit prompt + runtime error | prompt 避免浪费,代码兜底 |
| 运行能力动态声明 | isPDFSupported() | 不向模型承诺运行时做不到的能力 |
| 安全默认 + 逃生口 | head_limit=250、0 unlimited | 默认省上下文,必要时显式放宽 |
| 动态内容外移 | tengu_agent_list_attach | 降低工具 schema 变化对 prompt cache 的影响 |
| 目录预算 | Skill 1% 上下文预算 | 动态生态不能吞掉工作上下文 |
| 阻塞加载 | Skill BLOCKING REQUIREMENT | 匹配技能时先加载指令,再开始任务 |
读源码抓手
- 先看
BashTool/prompt.ts的工具偏好矩阵,确认它如何把模型导向专用工具。 - 再看 Git Safety Protocol,注意
NEVER、unless explicitly requests和CRITICAL的组合。 - 读沙箱 section,关注 JSON 策略如何内联、
dedup()和$TMPDIR归一化如何省 token。 - 看
FileEditTool/prompt.ts,把Read输出格式和Edit的old_string匹配连起来。 - 看
FileReadTool/prompt.ts,确认默认 2000 行、targeted offset、PDF 条件能力。 - 看
GrepTool/prompt.ts和 schema,理解head_limit的默认值与逃生口。 - 看
AgentTool/prompt.ts,重点读 agent 列表注入方式和 fork 纪律。 - 最后读
SkillTool/prompt.ts的预算和三级截断,学习动态目录如何不挤爆上下文。
小结
- 工具提示词是每个工具自己的局部行为协议。 - BashTool 的核心是把万能入口导向专用工具,并写清 Git、安全、沙箱和等待规则。 - FileEditTool 把“先读后改”做成 prompt 和 runtime 双层约束。 - FileReadTool 和 GrepTool 体现资源预算:默认限制,必要时显式放宽。 - AgentTool 和 SkillTool 说明工具提示词还要处理动态列表、缓存稳定、委派质量和目录预算。