Tool Use - 工具扩展的优雅机制

2522 字
13 分钟
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)变化
Tools1(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 三件事:

  1. 工具做什么(Read contents)
  2. 何时用(need to see, check, understand)
  3. 与其他工具的关系(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 MapTOOL_HANDLERS = {...}不变——按名查找
Tool SchemaTOOLS = [...]不变——LLM 读取 schema
safe_pathrun_read/run_write/run_edit 内部不变——护栏在 handler 层

循环本身仍是 L01 的 while True + stop_reason + messages[]。L02 只是在”执行层”叠加了分发和安全机制。

与后续课程的关系#

理解 L02 后,你会发现后续课程都在 Dispatch Map 上叠加——循环本身始终不变:

课程叠加什么变化在哪
L03 TodoWrite+ todo handlerDispatch Map 加一项
L04 Subagent独立 Dispatch Map子循环有自己的字典
L05 Skill Loading知识注入tool_result 中返回 SKILL.md
L06 Context Compact压缩 tool_resultmessages 管理策略

核心洞察:无论机制多复杂,骨架始终是 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 层级回答 循环不需要改动。只需三步:

  1. 加 handlerTOOL_HANDLERS["search_files"] = lambda **kw: run_search(kw["pattern"])
  2. 加 schemaTOOLS.append({name, description, input_schema})
  3. 加护栏:handler 内部调用 safe_path() 确保只在工作区搜索

这就是开闭原则的实践:扩展点是字典注册,循环代码零改动。安全护栏在 handler 层实现,符合信任边界原则。 :::

本章目标:让每个核心概念都从 L0 升级到 L2。理解 L02,不只是记住代码,而是掌握”如何在循环上安全扩展能力”的设计原则。

六、复习可视化#

Tool Dispatch 流程#

flowchart TB subgraph User U[User Prompt] end subgraph LLM L[LLM Decision] L --> |block.name| D{Which Tool?} end subgraph Dispatch D --> |bash| B[run_bash] D --> |read_file| R[run_read] D --> |write_file| W[run_write] D --> |edit_file| E[run_edit] D --> |unknown| X[Unknown Tool] end subgraph Safety R --> SP[safe_path] W --> SP E --> SP SP --> |escape?| ERR[Error: Path Escapes] SP --> |safe| OK[Execute] end subgraph Loop B --> TR[tool_result] OK --> TR ERR --> TR X --> TR TR --> |append| L end U --> L style D fill:#4a90d9,color:#fff style SP fill:#e74c3c,color:#fff style TR fill:#50c878,color:#fff

L01 vs L02 对比#

flowchart LR subgraph L01_s01["L01 (s01)"] A1[Agent Loop] --> B1[bash only] B1 --> C1[No Safety] end subgraph L02_s02["L02 (s02)"] A2[Agent Loop] --> B2[Dispatch Map] B2 --> C2[safe_path] B2 --> D2[专用工具] end L01_s01 -.->|不变| A2 style A1 fill:#95a5a6,color:#fff style A2 fill:#4a90d9,color:#fff style B2 fill:#50c878,color:#fff style C2 fill:#e74c3c,color:#fff

开闭原则示意#

flowchart TB subgraph Open["对扩展开放"] ADD[Add Tool] --> |+ handler| DM[Dispatch Map] ADD --> |+ schema| TS[Tool Schema] end subgraph Closed["对修改封闭"] LOOP[Agent Loop] --> |零改动| SAME[Same Code] end DM --> LOOP TS --> LLM[LLM sees new tool] style ADD fill:#27ae60,color:#fff style LOOP fill:#3498db,color:#fff style SAME fill:#f39c12,color:#fff

信任边界#

flowchart TB subgraph LLM_Layer["LLM 层(不可信)"] LLM[LLM] --> |可能犯错| REQ[Request: read /etc/passwd] end subgraph Harness_Layer["Harness 层(可信)"] REQ --> SAFE[safe_path] SAFE --> |escape| BLOCK[Block] SAFE --> |safe| ALLOW[Allow] end BLOCK --> |Error| LLM ALLOW --> |Result| LLM style LLM fill:#e74c3c,color:#fff style SAFE fill:#27ae60,color:#fff style BLOCK fill:#c0392b,color:#fff

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Tool Use - 工具扩展的优雅机制
https://firefly.cuteleaf.cn/posts/learn-claude-code/02-tool-use/
作者
AltumSisy
发布于
2026-04-05
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
AltumSisy
Hello, I'm AltumSisy.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
3
标签
25
总字数
68,347
运行时长
0
最后活动
0 天前

文章目录