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 后会:
- 在输出里解释发生了什么
- 决定是重试(换参数)、换工具(
Glob找一下文件)、还是放弃并向用户报告
不要在客户端层面吃掉工具错误——模型需要错误信息做判断。
扩展:MCP 工具
通过 MCP (Model Context Protocol) 连接的外部 server 可以注册额外工具。这些工具在 tools[] 中的呈现形式与内建工具一致,模型无从区分,但执行路径会跨进程。
命名通常带前缀以表明来源:mcp__<server>__<tool>。详见 06-mcp-and-hooks.md。