24 讲路线 · 与 S02 配对
D02: Tool System 深挖 · 工具系统
本讲在 S02 主线之上,聚焦实现细节、边界条件与自测;导图与主线相同模块,便于对照。
建议:先读完 S02,再按下方顺序走读源码与练习。
🔬 深挖目标
工具不是「一个函数」,而是 Schema + 权限钩子 + 执行器 + 结果契约 四层叠在一起;本讲把每一层的失败形态对齐到可观测现象。
📐 ToolDef 契约(概念层)
// 与 S02 对照:抓住这四件事
name + description // 给模型的「广告」
inputSchema // 宿主侧校验,防 JSON 胡写
hasPermission? → execute // 顺序不能反
execute → ToolResult // 必须可序列化回消息
🔍 失败矩阵
| 阶段 | 典型原因 | 用户可见现象 |
|---|---|---|
| Schema 校验 | 缺字段 / 类型错 | 工具未执行,直接报错或模型重试 |
| 权限 | 策略为 ask 且用户拒绝 | 明确 deny,循环继续但无结果块 |
| 执行期 | 超时、退出码非 0 | stderr / is_error 标记进入上下文 |
工具失败如何回灌模型、何时重试:参考答案见 D01 · 工具执行失败(与本表「执行期」一行对照读)。
🔗 与相邻章节
- 扩展 Read/Write/Edit 的实践练习骨架见 D01 练习 2。
- 进入执行器之前几乎必经 D03 权限;别把「业务错误」和「策略拒绝」混在同一分支里。
- MCP 动态工具(D07)与内置工具共用同一套「调用外壳」时,重点看名称空间是否冲突。
📖 走读顺序
- 列出仓库里所有内置工具的注册表初始化点。
- 找一个「重」工具(如 bash)和一个「轻」工具(如 read),对比权限字段差异。
- 追踪一次校验失败:错误对象最终如何变成模型可见的文本。
✏️ 自测 1 · 参考答案:execute 为何不能「假成功」?
题干
为什么 execute 不应吞掉异常而返回「假成功」?
结论
- 模型把
tool_result当作地面真值;若失败被伪装成空输出或成功,下一轮会基于幻觉继续改代码、提交或删除文件。 - 宿主无法区分「业务语义失败」(测试挂、编译错)与「传输/执行失败」;吞异常会把两类混在一起,用户也无法在日志里追责。
- 正确做法是:结构化失败(
is_error或等价位 + 人类可读摘要 + 可选 stderr 尾),与 D01 · 工具失败 一致。
✏️ 自测 2 · 参考答案:MCP 与内置同名 tool 如何消歧?
题干
若同一 tool name 被 MCP 与内置同时注册,你会如何在代码层消歧?
结论
在注册表合并阶段定死唯一真相,不要让运行时随机命中:
- 命名空间前缀(推荐):内置保持短名,MCP 工具加
mcp__serverId__toolName或文档约定前缀;模型侧 schema 与调用必须一致。 - 显式优先级:若允许覆盖,在合并表里记录
source: builtin | mcp,冲突时内置优先或 MCP 优先(写进项目文档),并打日志。 - 禁止静默二义:启动或
tools/list后若仍有重名,应 fail-fast 或自动改名并通知用户,避免「有时打到 A 有时打到 B」。