首页
/ learn-claude-code s02 工具分发:给 Agent 加一个工具,只需加一行代码

learn-claude-code s02 工具分发:给 Agent 加一个工具,只需加一行代码

2026-09-04 11:34:18作者:沈韬淼Beryl

本文基于 learn-claude-code 课程第 s02 章(Tool Use)文档与配套源码展开,讲解如何在一个已有的 Agent 循环上,通过“工具定义数组 + 分发映射表”两个数据结构,把工具集从 1 个 bash 扩展到 5 个专用工具(读、写、改、glob)。读完后你将掌握:如何为 LLM Agent 编写带 JSON Schema 的工具定义、如何用一张 TOOL_HANDLERS 字典完成工具调用分发、如何用 safe_path 约束文件类工具的活动范围,以及模型一次返回多个 tool_use 时的执行顺序约定。

s02 Tool Dispatch 架构图:用户 prompt 进入 LLM,LLM 输出按 TOOL_HANDLERS 分发到 bash/read_file/write_file/edit_file/glob 五个处理器,结果以 tool_result 回传

为什么只靠 Bash 不够

s01(Agent Loop)阶段的 Agent 只有一个工具:bash。它想读文件得拼 cat path/to/file,想写文件得拼 echo "..." > file.py,想改文件得拼 sed。文档指出的核心问题是:模型脑子里想的是“读这个文件”,却必须把意图翻译成一条 shell 命令——这层额外的翻译既浪费 token,又容易出错(引号转义、特殊字符、shell 方言差异)。

s02 的解法就是课程标题所概括的:

"Add a tool, add just one handler" —— 循环保持不变,把新工具注册进分发表就完事。

本章属于整个课程 Harness 层的 Tool Dispatch(工具分发)环节,主题是“扩展模型能够触及的范围”。

从 1 个工具到 5 个工具:工具定义层

s01 中工具定义只有一个 bash:

TOOLS = [{"name": "bash", ...}]

def run_bash(command): ...

s02 将其扩展为 5 个工具,每个工具都是“一份 JSON 定义 + 一个独立的实现函数”:

TOOLS = [
    {"name": "bash",       "description": "Run a shell command.", ...},
    {"name": "read_file",  "description": "Read file contents.",  ...},
    {"name": "write_file", "description": "Write content to file.", ...},
    {"name": "edit_file",  "description": "Replace text in file once.", ...},
    {"name": "glob",       "description": "Find files by pattern.", ...},
]

完整的工具定义在 s02_tool_use/code.py 中,每一项都是标准的 Anthropic Messages API 工具格式(name + description + input_schema)。以 read_file 为例:

{"name": "read_file", "description": "Read file contents.",
 "input_schema": {"type": "object",
                  "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}},
                  "required": ["path"]}},

结合源码可以看到每个工具的实现细节与边界行为:

工具 实现函数 参数 源码中的关键行为
bash run_bash command(必填) 危险命令黑名单(rm -rf /sudoshutdownreboot> /dev/)直接拦截;subprocess.run 超时 120s;stdout+stderr 合并后截断到 50000 字符
read_file run_read path(必填)、limit(可选) 超过 limit 行时追加 ... (N more lines) 提示,避免大文件撑爆上下文
write_file run_write pathcontent(均必填) 自动 mkdir(parents=True, exist_ok=True) 创建父目录;返回 Wrote N bytes to path
edit_file run_edit pathold_textnew_text(均必填) text.replace(old_text, new_text, 1) 精确替换一次;old_text 不存在时返回 Error: text not found in {path} 而非静默成功
glob run_glob pattern(必填) glob.glob(pattern, root_dir=WORKDIR) 限定搜索根目录,并对每个匹配结果二次校验是否仍在工作区内;无匹配返回 (no matches)

注意两个实现层面的设计取舍:

  1. 所有工具失败时都返回字符串错误Error: ...)而不是抛异常。异常若直接向上抛会打断 agent_loop;返回错误字符串则能让模型“看到”失败信息并自行修正重试——这是 Agent 工具设计的常见范式。
  2. run_bash 只做了极轻量的危险命令拦截(从 s01 原样继承),真正的权限门控留到了 s03 Permission 章节。文档在结尾明确提示:文件类工具有 safe_path 保护,但 bash 基本不设防,rm -rf / 依然会被执行(只被那行黑名单挡住字面匹配)。

工具分发:TOOL_HANDLERS 映射表

s02 唯一的结构性新增是一张分发映射表(s02_tool_use/code.py):

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

Agent 循环里原来硬编码的 run_bash() 调用被替换成一次字典查表(agent_loop):

# s01: output = run_bash(block.input["command"])
# s02: output = TOOL_HANDLERSblock.name

def agent_loop(messages: list):
    while True:
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            return

        results = []
        for block in response.content:
            if block.type == "tool_use":
                print(f"\033[33m> {block.name}\033[0m")
                handler = TOOL_HANDLERS.get(block.name)
                output = handler(**block.input) if handler else f"Unknown: {block.name}"
                print(str(output)[:200])
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

        messages.append({"role": "user", "content": results})

对照 s01_agent_loop/code.py 中的原始循环,可以逐行确认文档的说法“循环一个字都没改”:while Trueclient.messages.createstop_reason 判断、assistant 消息追加、tool_result 回传,全部原样保留;差异仅在两处——

  1. 执行语句从 run_bash(block.input["command"]) 变为 TOOL_HANDLERS.get(block.name) 查表后再以 handler(**block.input) 调用,参数按模型返回的 block.input 字典解包传入;
  2. .get() 而非 [] 索引,查不到 handler 时返回 Unknown: {name} 字符串,保证即使模型幻觉出一个不存在的工具名,循环也能带着错误信息继续而不是崩溃。

因此“给 Agent 加工具”的成本被压缩成固定两步:TOOLS 数组里加一个条目(告诉模型能做什么),在 TOOL_HANDLERS 里加一行映射(告诉运行时怎么执行)。循环本身对工具数量零感知。

路径安全:safe_path 约束文件类工具

s02 相比 s01 的另一项实质变化是给文件类工具加上了工作区边界校验(s02_tool_use/code.py):

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

其原理值得展开:模型给出的路径是相对 WORKDIR 拼接后再 resolve()(解析 .. 与符号链接),然后用 is_relative_to(WORKDIR) 断言解析结果必须仍在工作区内。这样 ../../etc/passwd、指向工作区外的符号链接等逃逸手段都会在拼接阶段被 ValueError 拦下,再由 run_read 等函数外层的 except 转成 Error: Path escapes workspace: ... 返回给模型。

从源码结构看,该保护只覆盖 read_file/write_file/edit_file 以及 glob 的结果校验(run_glob 中每个匹配都再次检查 is_relative_to(WORKDIR)),run_bash 不受此约束——这正是文档在 “What's Next” 中引向 s03 Permission 的动机:在工具执行前插入一道“这个操作安全吗?需要用户批准吗?”的关卡。

多个工具调用:一次响应,顺序执行

文档指出一个实际观察到的行为模式:模型经常在单次响应中返回多个 tool_use 块,例如“读 a.py 和 b.py,再列出所有 .py 文件”。源码中的处理逻辑是:遍历 response.content 中所有 type == "tool_use" 的块,按它们在 response.content 中的原始顺序逐一执行,把每个结果以 {"type": "tool_result", "tool_use_id": block.id, "content": output} 的格式收集进 results,最后作为一条 user 消息整体回传。

这里有两个可验证的细节:

  • 每个 tool_result 通过 tool_use_id 与对应的 tool_use 块一一绑定,模型回看历史时能明确区分“哪个调用产生了哪个结果”;
  • 执行是串行且保序的,多个工具之间没有并行、没有乱序——这一点可以用文档给出的验证 prompt 实测确认。

动手验证

运行前提(参考 根 README 的 Quick Start):

cd learn-claude-code
pip install -r requirements.txt
cp .env.example .env   # 配置 ANTHROPIC_API_KEY(另需 MODEL_ID 环境变量,见 code.py 中 os.environ["MODEL_ID"])

python s02_tool_use/code.py

进入交互式 REPL(s02 >> 提示符,输入 q 退出)后,文档建议用以下四个 prompt 观察工具分发行为:

  1. Read the file README.md and tell me what this project is about
  2. Create a file called test.py that prints "hello", then read it back
  3. Find all Python files in this directory
  4. Read both README.md and requirements.txt, then create a summary file

观察要点:模型什么时候只调用一个工具,什么时候一次调用多个?多个工具调用是否按正确顺序执行?控制台会打印每个工具名(黄色 > read_file)和输出前 200 字符,便于逐次核对。

s01 → s02 变更清单

文档给出的对比表完整继承如下:

组件 之前(s01) 之后(s02)
工具数量 1(bash) 5(+read、write、edit、glob)
工具执行 硬编码 run_bash() TOOL_HANDLERS 分发查表
路径安全 safe_path 校验(仅文件类工具)
Agent 循环 while True + stop_reason 判断 与 s01 完全一致

概念速查表:

概念 一句话解释
TOOL_HANDLERS 工具名 → 处理函数的字典;加工具 = 加一行映射
工具定义(TOOLS 用 JSON Schema 告诉模型“我能做什么”
多工具调用 模型可能一次返回多个 tool_use;按原始顺序逐一执行
循环不变 s01 的 while True 循环一行未改

对照阅读:12 课旧版实现

仓库中另有一条 legacy 12 课轨道,对应实现是 agents/s02_tool_use.py。它与本章 s02_tool_use/code.py 的差异可用于理解同一模式的演进:旧版只有 4 个工具(没有 glob),且分发映射用的是 lambda 适配层(agents/s02_tool_use.py):

TOOL_HANDLERS = {
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    ...
}

新版改为直接绑定函数、以 handler(**block.input) 解包调用,少了一层 lambda 中转。两条实现的分发思想完全一致,印证了本章的核心结论:“循环一点没变,我只是加了工具”

小结与下一步

s02 用最小改动回答了 Agent 工程中一个高频问题——如何以 O(1) 的成本扩展工具集:工具定义层(TOOLS)负责模型侧的能力声明,分发表(TOOL_HANDLERS)负责运行时的路由,两者加在一起就是全部增量,Agent 循环对工具数量保持无感知。文件类工具经 safe_path 被约束在工作区内,但 bash 通道依然敞开;这正是下一章 s03 Permission 的入口:在工具执行前加一道安全检查与用户批准的关卡。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384