Skip to content

⭐ Lesson 02:工具使用

"加一个工具,只加一个 handler" — 循环不用动,新工具注册进 dispatch map 就行。

Harness 层:工具分发 — 扩展模型能触达的边界。

[[00-课程概览/综述|← 返回综述]] | [[00-课程概览/教学大纲|📋 教学大纲]]


学习目标

完成本节课后,你将能够:

  1. 理解 dispatch map 模式:{tool_name: handler_function} 替代 if/elif 链
  2. 实现路径沙箱 safe_path() 防止工具逃逸工作区
  3. 在 Agent Loop 中注册多个工具(read / write / edit)
  4. 理解 加工具不需要改循环 这一核心原则

问题

[[01-智能体循环|Lesson 01 ⭐]] 的 Agent 只有一个工具:bash。所有操作都走 shell:

  • cat 截断大文件时输出不可预测
  • sed 遇到特殊字符就崩
  • 每次 bash 调用都是不受约束的安全面

关键洞察:专用工具可以在工具层面做安全控制。read_file 可以加行数限制,write_file 可以做路径沙箱 — 这些安全逻辑不需要混进 bash。

但更重要的是另一个洞察:加工具不需要改循环。


解决方案:Dispatch Map

架构总览

+--------+      +-------+      +------------------+
|  User  | ---> |  LLM  | ---> | Tool Dispatch    |
| prompt |      |       |      | {                |
+--------+      +---+---+      |   bash: run_bash |
                     ^           |   read: run_read |
                     |           |   write: run_wr  |
                     +-----------+   edit: run_edit |
                     tool_result | }                |
                                 +------------------+

The dispatch map is a dict: {tool_name: handler_function}.
One lookup replaces any if/elif chain.

LLM 生成 tool_use 块时,携带 name(工具名)和 input(参数)。循环只需要按 name 查字典,然后调用 handler。查找 + 调用 = 两行代码

核心模式

一个字典,一把搞定:

TOOL_HANDLERS = {
    "bash":       run_bash,
    "read_file":  run_read,
    "write_file": run_write,
    "edit_file":  run_edit,
}

模型在 response 中携带 tool_name,循环按名称查找处理函数。不需要 if/elif,不需要 switch/case。

这就是扩展点。加一个新工具 = 写一个 handler + 加一条 schema 定义。

路径沙箱

python
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

每个文件操作工具都经过 safe_path() 校验。路径沙箱在工具层集中做,而不是在每次 bash 调用中分散做。

为什么工具放外面?

bash 关在沙箱里能做任何事,但模型需要为每一次操作支付推理 token。如果每次读文件都走 bash cat,LLM 要先生成 bash 命令,然后处理 shell 的输出格式。

专用工具的好处:

  • 路径安全集中管控 — 所有文件操作统一经过 safe_path(),不依赖 shell 的通配符和转义
  • 参数明确read_file(path, limit)bash cat file | head -n 10 清晰得多
  • token 节省 — 工具调用比生成 shell 命令更短
  • 错误可预测 — 工具 handler 的返回值格式固定,LLM 容易解析

Handler 实例

python
def run_read(path: str, limit: int = None) -> str:
    text = safe_path(path).read_text()
    lines = text.splitlines()
    if limit and limit < len(lines):
        lines = lines[:limit]
    return "\n".join(lines)[:50000]

def run_write(path: str, content: str) -> str:
    safe_path(path).parent.mkdir(parents=True, exist_ok=True)
    safe_path(path).write_text(content)
    return f"Written {len(content)} bytes to {path}"

def run_edit(path: str, old_text: str, new_text: str) -> str:
    text = safe_path(path).read_text()
    if text.count(old_text) != 1:
        return "Error: old_text not found or ambiguous"
    text = text.replace(old_text, new_text)
    safe_path(path).write_text(text)
    return f"Edited {path}"

Dispatch Map

python
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"]),
}

为什么用 lambda **kw?因为不同的工具需要不同的参数。lambda **kw 将整个 input dict 展开为关键字参数,handler 只取自己需要的字段。

工具 Schema 定义

除了 handler,每个工具还需要在 tool schema 中声明自己的接口。这个 schema 告诉 LLM:

  1. 这个工具叫什么名字
  2. 它接受什么参数(类型、是否必填)
  3. 它做什么用(description 影响模型选择策略)
python
TOOLS = [
    {
        "name": "read_file",
        "description": "读取文件内容,支持行数限制",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "文件路径"},
                "limit": {"type": "integer", "description": "最大行数(可选)"},
            },
            "required": ["path"],
        },
    },
    # ... bash, write_file, edit_file 类似
]

schema 是模型和 harness 之间的契约。模型参考 description 决定用哪个工具,根据 properties 构造参数。所以 工具的 description 写得越清晰,模型选择就越准确。

Agent Loop 中的调用

python
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}"
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

和 s01 的循环完全一致。 唯一的变化是,现在 TOOL_HANDLERS 字典里有更多条目,TOOLS 列表也更长。循环体本身一个字都没变。

错误处理模式

工具 handler 的返回值分为两种:

情况策略示例
成功返回简短结果"Written 42 bytes to greet.py"
失败返回描述性错误"Error: path escapes workspace: ../../etc/passwd"

Agent Loop 对工具返回值 不做判断,直接塞回消息队列。模型自己阅读结果,决定下一步是继续调用工具还是给出最终回答。

这又是一个 Harness 工程原则:让模型做判断,harness 只传递信息。


模型如何选择工具?

LLM 选择一个工具不是随机的,而是基于三个因素:

  1. 工具描述description 字段告诉模型这个工具适合什么场景。例如"读取文件内容"和"执行 shell 命令",模型会根据任务内容匹配
  2. 参数列表 — 工具需要的参数越匹配当前的上下文,模型越倾向于选择它
  3. 历史经验 — 如果之前的工具调用成功返回了预期结果,模型更可能继续使用同一个工具

这意味着 写清楚的 description 和 parameter description 直接决定了 Agent 的行为质量。 这不是文档问题,是架构问题。

python
# 好的描述 → 模型选择准确
{
    "name": "bash",
    "description": "Execute arbitrary shell commands. Use for compilation, git ops, testing, running scripts, and package management.",
    "input_schema": { ... },
}

# 模糊的描述 → 模型困惑
{
    "name": "bash",
    "description": "run command",
    "input_schema": { ... },
}

Claude Code 中每个内置工具都有精心撰写的描述,LLM 根据这些描述在数百个工具中做选择。这就是为什么仔细打磨工具定义值得投入时间。


核心原则:循环属于 Agent,扩展属于 Harness

这是 Harness 工程最重要的原则之一:

Agent LoopTool Handlers
职责协调消息收发执行具体操作
变化频率几乎不变频繁扩展
谁来改核心框架插件/技能开发者
错误处理统一捕获异常各自返回消息

Agent Loop 就是核心骨架。Tool Handlers 是可插拔的肌肉。骨架不随肌肉的增长而改变。


相对 s01 的变更

组件之前 (s01)之后 (s02)
Tools1(仅 bash)4(bash, read, write, edit)
Dispatch硬编码 bash 调用TOOL_HANDLERS 字典
路径安全safe_path() 沙箱
Agent loop不变不变

练习

环境准备

sh
cd learn-claude-code
pip install -r requirements.txt   # 第一次需要

运行

sh
python agents/s02_tool_use.py

尝试的 Prompt

英文 prompt 对 LLM 效果更好,但也可以用中文

  1. Read the file requirements.txt
  2. Create a file called greet.py with a greet(name) function
  3. Edit greet.py to add a docstring to the function
  4. Read greet.py to verify the edit worked

观察要点

  • 模型在什么情况下选择 read_file 而不是 bash cat
  • 当你让模型写出 tools 列表时,它能看到哪些工具?
  • TOOL_HANDLERS 中没有的工具,模型还能不能调用?

挑战

如果让你加一个 delete_file 工具,需要改动几个地方?

答案:两个 — 写 handler 函数 + 注册到 TOOL_HANDLERS。循环不用碰。


⭐ 核心路径

本节课处于核心路径第二环:

s01 [智能体循环] → s02 [工具使用] → s03 [待办写入] → s04 [子智能体]
→ s05 [技能加载] → s07 [任务系统] → s09 [智能体团队]

[[01-智能体循环|← Lesson 01:智能体循环]] ⭐ | [[03-待办写入|Lesson 03:待办写入 →]] ⭐


和现实世界的关系

Claude Code 实际使用的工具数量远超 4 个 — 它内置了数十个工具(读取、写入、搜索、替换、grep、glob、LSP、终端、Web 搜索等),遵循的正是同一个 dispatch map 模式。

每当 Anthropic 发布新能力(如 WebSearch 工具、MCP 集成),本质是:

  1. TOOL_HANDLERS 里加一条条目
  2. TOOLS 列表里加一条 schema
  3. 循环不用改

这就是 加一个工具,只加一个 handler 在现实中的体现。Claude Code 的 Agent Loop 和 s01/s02 的循环在核心逻辑上完全一致。

[[任务系统|Lesson 07 ⭐]] 将演示如何让 Agent 自己管理可用的工具集,[[智能体团队|Lesson 09 ⭐]] 则引入跨 Agent 的工具共享模式。


延伸阅读

  • 《Anthropic 工具使用指南》— 了解工具描述如何影响模型选择
  • [[术语表#Tool Dispatch|Tool Dispatch]] — Dispatch Map 的详细定义
  • [[讨论课/工具安全边界|讨论课:工具安全边界]] — 路径沙箱之外的更多安全模式

基于 Learn Claude Code 项目改编