claude-code-learn_
04TOOLS

04 - 工具系统

工具是 Agent 与外部世界交互的唯一通道。模型不直接读写文件、不直接执行命令——所有副作用都通过工具调用完成。

读完你能回答

问题
一个工具在模型侧和执行侧各由什么组成?
tools[] 为什么每次请求都要重算?
一次工具调用要经过哪几层权限检查?
模型同一次响应里请求了 3 个工具调用,客户端什么时候并发、什么时候串行?
工具执行失败时,信息怎么回到模型?

工具的"两张脸"

一个工具由三部分组成,但模型只看得见前两部分

部分 形式 模型侧可见?
元信息 名称、描述
参数 schema JSON Schema
执行体 函数或外部进程 ✗(黑箱)

这套分离让同一个模型可以配上不同的工具集,也让工具的实现细节不会污染模型的上下文。

内建工具分类

分类仅用于组织,模型调用时只关心工具名。

分类 代表工具 用途
文件读写 Read、Write、Edit 单文件的增删改查
文件搜索 Glob、Grep 按路径模式或内容模式定位
Shell 执行 Bash 运行命令、脚本
任务管理 TaskCreate、TaskUpdate、TaskList 维护本次会话待办
子 Agent Agent 派遣子 Agent 处理独立任务
外部信息 WebFetch、WebSearch 访问网页与搜索
计划模式 EnterPlanMode、ExitPlanMode 切换到"只规划不动手"
Skills 与延迟加载 Skill、ToolSearch 按名调用 Skill / 按需取回 schema
展开一个真实的工具 schema(Read)

模型看到的 Read 工具完整定义长这样:

{
  "name": "Read",
  "description": "Reads a file from the local filesystem. ...",
  "input_schema": {
    "type": "object",
    "properties": {
      "file_path": {
        "type": "string",
        "description": "The absolute path to the file to read"
      },
      "limit": {
        "type": "integer",
        "description": "The number of lines to read. Only provide if the file is too large to read at once."
      },
      "offset": {
        "type": "integer",
        "description": "The line number to start reading from."
      },
      "pages": {
        "type": "string",
        "description": "Page range for PDF files (e.g., \"1-5\")."
      }
    },
    "required": ["file_path"]
  }
}
  • description 里写使用时机副作用比写"功能描述"更有价值,模型决定是否调用时主要靠它
  • required 严格,缺了字段 API 侧会直接拒绝,不进模型
  • 可选参数的 description 应当说明"什么时候需要填"

工具清单不是稳定的

每次 API 请求组装 tools[] 都是一次重新计算。影响结果的因素:

因素 影响
当前模式 计划模式下隐藏所有写工具
用户权限配置 settings.json 中 deny / allow 列表
MCP 连接状态 未连上的 MCP server 的工具不出现
特性开关 实验性工具被开关控制
Deferred 机制 部分工具延迟加载,需 ToolSearch 显式取回 schema

结论:模型在同一会话的不同轮次可能看到不同的 tools[]。这是设计的,不是 bug。

权限四层检查

每次工具调用进入执行前,经过四层过滤。任何一层拒绝,调用立即终止并把拒绝原因回写给模型。

高风险操作(删除文件、force push、修改共享基础设施)默认不会自动放行,即使在"接受编辑"模式下也会强制确认。

执行流程的六个阶段

阶段 关键动作
1. 参数校验 input_schema 校验,不通过直接回错误
2. 权限判定 四层,见上节
3. 预处理 相对路径转绝对、环境变量展开、默认值填充
4. 执行 本地函数 / 子进程 / 远程 MCP,可能是异步流式
5. 后处理 结果截断(模型 token 预算有限)、敏感信息脱敏
6. 序列化 字符串或结构化 content,设置 is_error 标志

并发策略

模型可在一次响应中请求多个 tool_use。调度器决定并发或串行。

情况 策略
全部只读工具(Read / Grep / Glob) 并发
同一文件的多次 Edit 串行,顺序按模型给出的次序
不同文件的 Write 可并发
Bash 命令 默认串行,除非显式标注可并发
写工具 + 依赖其结果的读工具 强制串行

本节是观察所得的经验规律,具体策略随版本调整。

展开一次并发 tool_use 的请求/响应

模型响应:

{
  "stop_reason": "tool_use",
  "content": [
    { "type": "tool_use", "id": "toolu_A", "name": "Read", "input": { "file_path": "a.ts" } },
    { "type": "tool_use", "id": "toolu_B", "name": "Read", "input": { "file_path": "b.ts" } },
    { "type": "tool_use", "id": "toolu_C", "name": "Grep", "input": { "pattern": "TODO" } }
  ]
}

客户端并发执行完之后的回填:

{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "toolu_A", "content": "export function a() { ... }" },
    { "type": "tool_result", "tool_use_id": "toolu_B", "content": "export function b() { ... }" },
    { "type": "tool_result", "tool_use_id": "toolu_C", "content": "src/a.ts:12: // TODO: refactor" }
  ]
}

关键点:三个 tool_result 打包在同一条 role:user 消息的 content[],不是三条独立消息。顺序可以不一致,用 tool_use_id 配对。

结果回写

执行结果必须回到下一轮的 messages[],否则模型丢失中间状态。格式:

{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "toolu_xxx", "content": "..." }
  ]
}

错误同样通过 tool_result 传达,设置 is_error: true

展开一次错误回填
{
  "type": "tool_result",
  "tool_use_id": "toolu_01ABCD",
  "content": "ENOENT: no such file or directory, open 'missing.md'",
  "is_error": true
}

模型看到 is_error: true 后会:

  1. 在输出里解释发生了什么
  2. 决定是重试(换参数)、换工具(Glob 找一下文件)、还是放弃并向用户报告

不要在客户端层面吃掉工具错误——模型需要错误信息做判断。

扩展:MCP 工具

通过 MCP (Model Context Protocol) 连接的外部 server 可以注册额外工具。这些工具在 tools[] 中的呈现形式与内建工具一致,模型无从区分,但执行路径会跨进程。

命名通常带前缀以表明来源:mcp__<server>__<tool>。详见 06-mcp-and-hooks.md