learn-claude-code s02 深度解析:Agent 从单一 Bash 扩展到 5 个工具的 TOOL_HANDLERS 分派机制全源码剖析
learn-claude-code s02 深度解析Agent 从单一 Bash 扩展到 5 个工具的 TOOL_HANDLERS 分派机制全源码剖析【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code本篇基于 learn-claude-code 仓库的 s02 Tool Use 章节文档s02_tool_use/README.ja.md及其配套实现s02_tool_use/code.py撰写。核心主题是如何在不改动 s01 已建立的 Agent 主循环while Truestop_reason判定的前提下通过一张“工具名 → 处理函数”的查找字典TOOL_HANDLERS把 Agent 从仅有一个bash工具扩展到 5 个专用工具bash / read_file / write_file / edit_file / glob。读完后你将掌握 Claude Code 风格“工具分派Tool Dispatch”这一 Harness 层的完整实现工具 JSON Schema 的定义规范、safe_path路径沙箱、多工具并行调用的执行顺序以及新增一个工具所需的“两行代码”。为什么 s01 的“仅 Bash”不够用s01 章节s01_agent_loop/code.py构建的 Agent 只有一个工具bash。这意味着模型想读文件必须拼出cat path/to/file写文件要写echo ... file.py编辑文件要靠sed。问题有三多了一层“翻译”模型的意图是“读这个文件”却必须先翻译成 shell 语法再经 shell 解释执行——多出来的翻译层浪费 token且容易出错引号嵌套、转义、特殊字符都会让sed/echo失败输出不可控cat对长文件的截断行为不可预测错误信息混杂 stdout/stderr安全面不可约束每次 bash 调用都是不受限的执行面无法在工具层面做路径沙箱。s02 的解法就是文档的核心论点——“加一个工具 加一条 schema 加一个 handler”循环本身一行不改。架构总览工具分派替换硬编码调用s01 到 s02 的唯一结构性变化发生在工具执行那一行# s01: 硬编码 —— 只认 bash output run_bash(block.input[command]) # s02: 查找分派 —— 按名字路由到任意工具 handler TOOL_HANDLERS.get(block.name) output handler(**block.input) if handler else fUnknown: {block.name}对应源码位置s01 的硬编码调用在 s01_agent_loop/code.pys02 的分派调用在 s02_tool_use/code.py。除此之外LLM 调用client.messages.create(...)、stop_reason ! tool_use的退出判定、消息追加逻辑逐字保持不变——这正是该课程“每个章节只增加一个 Harness 机制”的设计原则。从 1 个工具到 5 个工具完整源码解析1. 五个工具的 Schema 定义告诉模型“能做什么”s02_tool_use/code.py 中TOOLS数组从 s01 的一条扩展到五条每条包含name、description与严格的input_schemaTOOLS [ {name: bash, description: Run a shell command., input_schema: {type: object, properties: {command: {type: string}}, required: [command]}}, {name: read_file, description: Read file contents., input_schema: {type: object, properties: {path: {type: string}, limit: {type: integer}}, required: [path]}}, {name: write_file, description: Write content to a file., input_schema: {type: object, properties: {path: {type: string}, content: {type: string}}, required: [path, content]}}, {name: edit_file, description: Replace exact text in a file once., input_schema: {type: object, properties: {path: {type: string}, old_text: {type: string}, new_text: {type: string}}, required: [path, old_text, new_text]}}, {name: glob, description: Find files matching a glob pattern., input_schema: {type: object, properties: {pattern: {type: string}}, required: [pattern]}}, ]各参数要点工具必填参数可选参数说明bashcommand—执行 shell 命令继承自 s01read_filepathlimit整数读文件limit截断行数write_filepath,content—创建/覆盖写文件edit_filepath,old_text,new_text—精确字符串一次性替换非正则globpattern—按通配符查找文件这里体现了一个重要的设计决策见 web/src/data/annotations/s02.json 中 “JSON Schemas for Every Tool” 条目每个工具都定义严格的 JSON SchemaAPI 会在执行前对入参做 schema 校验模型无法传入格式错误的参数edit_file要求old_text是精确字符串而非正则消除了“模型到底想改什么”的解析歧义。2.safe_path文件工具的路径沙箱所有文件类工具read/write/edit/glob在执行前都经过 safe_pathdef safe_path(p: str) - Path: path (WORKDIR / p).resolve() # 解析符号链接与 .. if not path.is_relative_to(WORKDIR): # 必须落在工作区内 raise ValueError(fPath escapes workspace: {p}) return path实现细节先把相对路径拼到WORKDIR下并resolve()规范化..与符号链接再用is_relative_to检查是否逃逸工作区逃逸则抛ValueError。注意这一层保护只覆盖文件工具——bash仍然不受路径限制这正是 s03 Permission 章节要解决的问题。3. 四个新工具的实现函数def run_read(path: str, limit: int | None None) - str: try: lines safe_path(path).read_text().splitlines() if limit and limit len(lines): lines lines[:limit] [f... ({len(lines) - limit} more lines)] return \n.join(lines) except Exception as e: return fError: {e} def run_write(path: str, content: str) - str: try: file_path safe_path(path) file_path.parent.mkdir(parentsTrue, exist_okTrue) # 自动建父目录 file_path.write_text(content) return fWrote {len(content)} bytes to {path} except Exception as e: return fError: {e} def run_edit(path: str, old_text: str, new_text: str) - str: try: file_path safe_path(path) text file_path.read_text() if old_text not in text: return fError: text not found in {path} # 精确匹配才允许改 file_path.write_text(text.replace(old_text, new_text, 1)) # 只替换第一处 return fEdited {path} except Exception as e: return fError: {e} def run_glob(pattern: str) - str: import glob as g try: results [] for match in g.glob(pattern, root_dirWORKDIR): if (WORKDIR / match).resolve().is_relative_to(WORKDIR): # 二次过滤逃逸结果 results.append(match) return \n.join(results) if results else (no matches) except Exception as e: return fError: {e}几个值得注意的实现细节错误即返回字符串每个 handler 都把异常捕获后以Error: {e}字符串返回而不是抛出——因为错误文本会作为tool_result回传给模型让模型自己决定如何恢复这是“模型即 Agent”哲学的直接体现run_read的截断提示超出行数限制时追加... (N more lines)让模型知道文件还有后续可以带 offset 再读run_edit的replace(..., 1)只替换第一次出现避免误伤同名文本匹配不到则明确报错run_glob的双重防护除了root_dirWORKDIR还对每个匹配结果再做一次resolve()is_relative_to检查防止通配符如../x逃逸出工作区。bash工具本身从 s01 原样继承s02_tool_use/code.py内置危险命令黑名单rm -rf /、sudo、shutdown、reboot、 /dev/、120 秒超时、输出截断到 50000 字符。4.TOOL_HANDLERS分派字典一次查找替代 if/elif 链s02_tool_use/code.pyTOOL_HANDLERS { bash: run_bash, read_file: run_read, write_file: run_write, edit_file: run_edit, glob: run_glob, }新增一个工具的全部工作量 TOOLS数组加一条 schema TOOL_HANDLERS字典加一行映射。主循环永远不需要感知“有哪些工具”——它只知道“按名字查表、解包参数、回传结果”。主循环中的分派逻辑与未知工具兜底完整循环见 s02_tool_use/code.pydef agent_loop(messages: list): while True: response client.messages.create( modelMODEL, systemSYSTEM, messagesmessages, toolsTOOLS, max_tokens8000, ) messages.append({role: assistant, content: response.content}) if response.stop_reason ! tool_use: # 与 s01 完全相同的退出判定 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 fUnknown: {block.name} print(str(output)[:200]) results.append({type: tool_result, tool_use_id: block.id, content: output}) messages.append({role: user, content: results})三个关键机制handler(**block.input)模型返回的结构化参数直接解包成函数实参schema 校验 字典解包让参数传递零解析代码未知工具兜底用.get()而非[]索引查不到时回传Unknown: {block.name}而不是崩溃——模型收到这个 tool_result 后会自行纠正tool_use_id绑定每个结果通过block.id与对应的tool_use块一一对应多个工具结果在同一轮 user 消息中各自归位。多个工具调用按原始顺序逐个执行模型经常一次返回多个tool_use块例如“读 a.py 和 b.py并列出所有 .py 文件”。s02 的策略是这些调用按它们在response.content中出现的原始顺序在一个for循环里逐个同步执行全部结果收集进同一个results列表后一次性回传messages.append({role: user, content: results})。这里没有并行执行也没有优先级调度——顺序确定、实现简单符合该课程“最小可行 Harness”的定位。观察这一行为是文档给出的动手实验要点之一见下文“试跑”。一个对照agents/ 目录下的 lambda 风格变体仓库中另有一份实现 agents/s02_tool_use.py与章节版s02_tool_use/code.py是同一机制的两种写法值得对照理解工具数量不同agents/版只有 4 个工具无glob见 agents/s02_tool_use.py而章节版有 5 个官方文档 docs/en/s02-tool-use.md 描述的也是 4 工具版本分派映射风格不同agents/版用 lambda 做参数名适配如bash: lambda **kw: run_bash(kw[command])见 agents/s02_tool_use.py章节版则直接存函数引用run_bash依赖**block.input解包时参数名自然对齐。从源码结构看两种写法等价章节版更简洁函数签名与 schema 参数名一一对应时无需中间层agents/版展示了当 handler 参数名与 schema 字段名不一致时用 lambda 适配的通用技巧。设计决策为什么工具恰好是这几个web/src/data/annotations/s02.json 记录了该章节背后的三条设计决策可作为理解工具集规模的依据Why Exactly (A Few) Toolsbash 文件读写编辑覆盖了绝大多数编码任务工具越多模型在“选哪个工具”上的认知负担越重选错概率越高同时 schema 维护成本和边界情况也越多。bash已经可以兜底list_directory、search_files之类的场景专用文件的意义在于给模型结构化的 I/O避开 bash 引号/转义易错区The Model IS the Agent主循环里没有路由器、决策树或工作流引擎——做什么、何时停、出错如何恢复全由模型决定代码只是连接模型与工具的“管道”JSON Schemas for Every Tool用严格 schema 换可靠性杜绝“自由文本 正则解析”的脆弱解析路径。与 s01 的差异总览组件s01 之前s02 之后工具数1bash5 read_file, write_file, edit_file, glob工具执行硬编码run_bash()TOOL_HANDLERS字典查找分派路径安全无safe_path校验仅文件工具Agent 循环while Truestop_reason与 s01 完全一致一行未改动手试跑环境准备依赖见 requirements.txtanthropic0.25.0、python-dotenv1.0.0pip install anthropic python-dotenv export ANTHROPIC_API_KEY... # 或在 .env 中配置 export MODEL_ID... # 代码通过 os.environ[MODEL_ID] 读取模型名 # 可选export ANTHROPIC_BASE_URL... 使用兼容端点运行章节版脚本以当前目录为工作区WORKDIR Path.cwd()cd learn-claude-code python s02_tool_use/code.py进入交互式提示符s02 输入q退出按文档推荐依次尝试Read the file README.md and tell me what this project is aboutCreate a file called test.py that prints hello, then read it backFind all Python files in this directoryRead both README.md and requirements.txt, then create a summary file观察要点终端会以黄色高亮打印每次调用的工具名 read_file等。重点对比模型只调一个工具与一次并发多个工具两种情况——多个工具调用是否按原始顺序逐个执行、tool_result是否都正确回传并驱动下一轮推理。第 4 条 prompt 最容易触发一次响应内多个tool_use。速查概念一句话TOOL_HANDLERS工具名 → 处理函数的字典加工具 加一行映射工具定义传给模型的 JSON Schema声明“能做什么、参数是什么”多工具调用模型可一次返回多个tool_use按原始顺序逐个执行循环不变s01 的while Truestop_reason判定一行未动safe_path文件工具的路径沙箱resolve()后必须is_relative_to(WORKDIR)小结与下一步s02 验证了 Harness 工程的扩展性Agent 的“能力半径”由工具集决定而工具集的增长对主循环是零侵入的——一张查找表吸收了所有变化。但安全边界只覆盖了文件工具safe_path挡住了 read/write/edit/glob 越权bash却仍是无限制的执行面黑名单之外的破坏性命令依然放行。这正是下一节 s03 Permission 的主题在工具执行前加一道权限闸门——“这个操作安全吗需要用户确认吗”【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
