Tool Use - 工具扩展的优雅机制
L02 要回答一个问题:如何在 L01 的循环上扩展工具,而不破坏循环的简洁性?
L01 给了我们骨架:while True + stop_reason + messages[]。但真实 Agent 不能只有一个 bash——它需要读文件、写文件、编辑文件。怎么加?
本章讲三个机制:
| 机制 | 解决什么问题 | 核心代码 |
|---|---|---|
| Dispatch Map | 加工具不改循环 | {name: handler} 字典 |
| Tool Schema | 模型如何知道何时调用 | name + description + input_schema |
| safe_path | 路径安全护栏 | resolve() + is_relative_to() |
这三个机制叠加在 L01 循环上,循环本身不变。理解 L02,就理解了 Agent 如何”安全地扩展能力”。
一、Dispatch Map:字典分发机制
“加一个工具,只加一个 handler。”
这句话概括了 Dispatch Map 的设计哲学。看代码:
TOOL_HANDLERS = { "bash": lambda **kw: run_bash(kw["command"]), "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),}
# 循环体不变,按名查找for block in response.content: if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input) if handler else f"Unknown tool: {block.name}"字典查找一行代码,把”改循环”变成了”注册 handler”。
s01 vs s02:对比理解
| 组件 | s01(L01) | s02(L02) | 变化 |
|---|---|---|---|
| Tools | 1(bash) | 4(bash, read, write, edit) | +3 |
| Dispatch | 硬编码 | TOOL_HANDLERS 字典 | 结构化 |
| 安全 | 无 | safe_path() 沙箱 | 新增 |
| 循环 | - | 不变 | 核心 |
s01 的循环直接调用 bash,s02 的循环通过字典查找。循环代码零改动——这就是开闭原则。
开闭原则:对扩展开放,对修改封闭
为什么字典查找能实现开闭原则?
加工具 = 加 handler + 加 schema循环代码 = 零改动TOOL_HANDLERS.get(block.name) 这行代码的关键在于:循环不知道具体工具,只知道”按名查找”。工具的增删改,都在字典里发生,循环体代码不受影响。
| 原则 | 含义 | Dispatch Map 实现 |
|---|---|---|
| 对扩展开放 | 加新工具不影响现有代码 | 字典注册新 handler |
| 对修改封闭 | 核心逻辑稳定不变 | 循环代码零改动 |
验证你的理解
Q1:如果用 if/elif 链而不是字典,加第 100 个工具要改几处?
:::details 答案 循环体要改 100 处——每加一个工具,就要加一个 elif 分支。
字典结构的好处:加第 100 个工具,只需在字典里加一项,循环体代码不变。 :::
Q2:block.name 从哪里来?
:::details 答案 block.name 由 LLM 决定。LLM 看到 Tool Schema,根据 description 判断调用时机,在 tool_use block 中提供 name。Harness 只是执行。
这意味着:LLM 决策,Harness 执行。字典查找是”执行”层,不参与决策。 :::
二、Tool Schema:模型调用时机
Dispatch Map 解决了”如何执行”,Tool Schema 解决”何时调用”。
LLM 不是魔法——它需要知道有哪些工具可用、每个工具做什么。这个信息通过 Tool Schema 提供:
TOOLS = [ { "name": "read_file", "description": "Read the contents of a file. Use this when you need to see what's in a file.", "input_schema": { "type": "object", "properties": { "path": {"type": "string", "description": "The path to the file"}, "limit": {"type": "integer", "description": "Optional limit on lines to read"} }, "required": ["path"] } }, # ... 其他工具]三要素:
| 要素 | 作用 | LLM 如何使用 |
|---|---|---|
| name | 工具标识符 | 在 tool_use block 中提供 block.name |
| description | 功能描述 | 判断调用时机——“这个工具做什么” |
| input_schema | 参数结构 | 构造调用参数——“需要传什么” |
description 是 LLM 的唯一依据
LLM 没有上下文之外的”直觉”。它判断何时调用工具,完全依赖 description。
看两个对比:
# 差的 description"description": "Read a file." # 太模糊,LLM 不知道何时该用
# 好的 description"description": "Read the contents of a file. Use this when you need to see what's in a file, check existing code, or understand file structure before making changes."好的 description 告诉 LLM 三件事:
- 工具做什么(Read contents)
- 何时用(need to see, check, understand)
- 与其他工具的关系(before making changes)
原则:description 越精准,LLM 调用越准确。但过度详细可能限制 LLM 的创造性用法——这是个权衡。
验证你的理解
Q3:如果 description 写错了,会发生什么?
:::details 答案 LLM 会在错误的时机调用工具。比如把 read_file 的 description 写成”Delete a file”,LLM 就会在想删除时调用 read——行为完全错误。
这再次证明:LLM 决策依赖 description,description 是”契约”,必须准确。 :::
三、safe_path:信任边界与安全护栏
Dispatch Map 让工具扩展变得简单,但简单不代表安全。LLM 可以要求读取任意路径——/etc/passwd、../../../secret.key。谁来拦截?
答案是:Harness 不信任 LLM 的安全判断。
信任边界原则
| 层级 | 职责 | 可信度 |
|---|---|---|
| LLM | 决策、推理、行动 | 不可信——可能犯错或被攻击 |
| Harness | 执行、反馈、护栏 | 可信——安全检查在这里 |
LLM 要求读取 /etc/passwd,不是 LLM 的错——它只是”决策”。Harness 的职责是拦截这个请求——这是”护栏”。
safe_path 实现:路径沙箱
def safe_path(p: str) -> Path: path = (WORKDIR / p).resolve() # 解析符号链接、消除 ../ if not path.is_relative_to(WORKDIR): raise ValueError(f"Path escapes workspace: {p}") return path两步检查:
| 步骤 | 代码 | 解决什么攻击 |
|---|---|---|
| resolve() | 解析符号链接、消除 ../ | ../../../secret、symlink 逃逸 |
| is_relative_to() | 检查最终路径是否在工作区 | 跨目录访问 |
# 攻击示例:路径逃逸LLM: "read_file(path='../../../etc/passwd')"Harness: safe_path("../../../etc/passwd") → resolve() → "/etc/passwd" → is_relative_to(WORKDIR) → False → raise ValueError: "Path escapes workspace"LLM 可能犯错,可能被 prompt injection 攻击,但 Harness 的 safe_path 是硬边界——逃逸请求直接报错。
专用工具优先:减少攻击面
为什么优先用 read_file/write_file/edit_file,而不是 bash?
| 工具 | 能做什么 | 攻击面 |
|---|---|---|
| bash | 任意 shell 命令 | 最大——rm -rf、curl malicious、eval… |
| read_file | 只读文件内容 | 最小——只读,不执行 |
| write_file | 只写文件内容 | 小——只写,不执行 |
| edit_file | 只编辑文件 | 小——只改,不执行 |
原则:专用工具约束操作范围,bash 作为最后手段。
验证你的理解
Q4:如果 LLM 被 prompt injection 攻击,要求执行
rm -rf /,会发生什么?:::details 答案 取决于用什么工具:
- 如果用 bash:
run_bash("rm -rf /")执行,灾难性后果。- 如果用专用工具:没有 delete_file 工具,LLM 无法删除——攻击被工具范围约束。
这就是”减少攻击面”的价值:专用工具把 bash 的”任意命令”降级为”特定操作”。 :::
四、回顾:L02 在循环上叠加了什么
回到开头的问题:如何在 L01 的循环上扩展工具,而不破坏循环的简洁性?
答案:
| 叠加机制 | 代码位置 | 循环的变化 |
|---|---|---|
| Dispatch Map | TOOL_HANDLERS = {...} | 不变——按名查找 |
| Tool Schema | TOOLS = [...] | 不变——LLM 读取 schema |
| safe_path | run_read/run_write/run_edit 内部 | 不变——护栏在 handler 层 |
循环本身仍是 L01 的 while True + stop_reason + messages[]。L02 只是在”执行层”叠加了分发和安全机制。
与后续课程的关系
理解 L02 后,你会发现后续课程都在 Dispatch Map 上叠加——循环本身始终不变:
| 课程 | 叠加什么 | 变化在哪 |
|---|---|---|
| L03 TodoWrite | + todo handler | Dispatch Map 加一项 |
| L04 Subagent | 独立 Dispatch Map | 子循环有自己的字典 |
| L05 Skill Loading | 知识注入 | tool_result 中返回 SKILL.md |
| L06 Context Compact | 压缩 tool_result | messages 管理策略 |
核心洞察:无论机制多复杂,骨架始终是 L01 的循环 + L02 的 Dispatch Map。理解 L01 和 L02,就理解了整个 Agent 系统的根基。
五、心智模型升级:从 L0 到 L2
读完这篇文章,你的理解经历了怎样的升级?
L02 核心概念的理解升级路径
| 概念 | L0(表面) | L1(关联) | L2(深层) |
|---|---|---|---|
| Dispatch Map | ”字典分发" | "查表解耦循环和工具,加工具不改循环" | "开闭原则——对扩展开放,对修改封闭。任何’注册+查表’机制都符合此原则:插件系统、策略模式、中间件链同理” |
| safe_path | ”检查路径" | "resolve 消逃逸,is_relative_to 判边界,LLM 可犯错 Harness 拦截" | "信任边界原则——LLM 决策不可信,Harness 提供硬边界。安全护栏必须在执行层,不能指望模型判断。适用于所有 LLM-工具交互场景” |
| 专用工具优先 | ”更安全" | "bash 攻击面最大,专用工具约束操作范围" | "减少攻击面原则——约束能力范围,降低风险。与最小权限原则、零信任架构同理。工具设计应优先’窄而深’而非’宽而浅‘“ |
如何检验你达到了 L2?
用这个问题自测:
如果要加一个新工具”search_files”,你会怎么设计?循环需要改动吗?
:::details L2 层级回答 循环不需要改动。只需三步:
- 加 handler:
TOOL_HANDLERS["search_files"] = lambda **kw: run_search(kw["pattern"])- 加 schema:
TOOLS.append({name, description, input_schema})- 加护栏:handler 内部调用
safe_path()确保只在工作区搜索这就是开闭原则的实践:扩展点是字典注册,循环代码零改动。安全护栏在 handler 层实现,符合信任边界原则。 :::
本章目标:让每个核心概念都从 L0 升级到 L2。理解 L02,不只是记住代码,而是掌握”如何在循环上安全扩展能力”的设计原则。
六、复习可视化
Tool Dispatch 流程
L01 vs L02 对比
开闭原则示意
信任边界
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!