多加一个工具只加一行Agent 是怎么管理工具的本文是「从零理解 Claude Code20 个 Agent Harness 机制」系列的第 3 篇。源码仓库shareAI-lab/learn-claude-code上一篇我们先搭起了最小 Agent Loop模型调用bash程序执行命令再把结果交回模型。这个版本已经能做事了但它只有一个工具bash。从能力上说bash几乎什么都能做。读文件可以用cat写文件可以用重定向改文件可以用sed找文件可以用find。但我实际看下来只有一个bash并不优雅。因为模型明明想做的是读文件却要先把这个意图翻译成一条正确的 Shell 命令。catREADME.md模型想修改一行代码又得写出一段不容易出错的sed命令。sed-is/old_text/new_text/app.py命令写错一个引号、一个转义符事情就会变得麻烦。这一篇要解决的就是工具分发问题当 Agent 不再只有bash时怎样继续保持主循环简单。1.一个工具能做所有事为什么还要拆开先看一个很普通的任务读取 README.md 和 requirements.txt 然后创建一个 summary.txt总结这个项目是做什么的。如果只有bash模型可能需要连续调用catREADME.mdcatrequirements.txtecho项目总结……summary.txt这当然可以完成任务。但模型需要同时记住文件读取、重定向写入、引号转义等 Shell 细节。它做的不是单纯的代码理解还要不断把自己的意图翻译成命令行语法。这一章给 Agent 准备了 4 个更直接的工具工具用途read_file读取文件内容write_file写入文件edit_file替换文件中的一段文本glob按模式查找文件再加上原本的bash一共 5 个工具。这样模型想读README.md时不必再拼命令read_file(pathREADME.md)想写总结文件时也不必处理 Shell 的重定向write_file( pathsummary.txt, content…… )工具的名字更接近任务本身模型需要处理的无关细节就少一些。2.从单工具到多工具核心变化只有一处最小 Agent Loop 里工具执行是写死的outputrun_bash(block.input[command])因为当时只有bash这么写没有问题。但工具一多这种写法就撑不住了。总不能写成这样ifblock.namebash:outputrun_bash(**block.input)elifblock.nameread_file:outputrun_read(**block.input)elifblock.namewrite_file:outputrun_write(**block.input)elifblock.nameedit_file:outputrun_edit(**block.input)工具再多一点这段判断会越来越长主循环也会越来越乱。这里用了一个很常见的做法工具分发表。TOOL_HANDLERS{bash:run_bash,read_file:run_read,write_file:run_write,edit_file:run_edit,glob:run_glob,}模型返回工具名后程序去这个字典里查对应的处理函数handlerTOOL_HANDLERS.get(block.name)outputhandler(**block.input)这就是多工具 Agent 最核心的变化。工具名负责找到处理函数工具参数负责传给处理函数。主循环不需要知道当前到底有几个工具也不需要知道每个工具的具体实现。3.加一个工具到底要改哪里以read_file为例。第一步要告诉模型有这个工具以及它需要什么参数{name:read_file,description:Read file contents.,input_schema:{type:object,properties:{path:{type:string},limit:{type:integer},},required:[path],},}模型会根据这里的名字、描述和参数结构决定什么时候调用它。第二步程序里要有真正的实现defrun_read(path:str,limit:int|NoneNone)-str:linessafe_path(path).read_text().splitlines()iflimitandlimitlen(lines):lineslines[:limit][f... ({len(lines)-limit}more lines)]return\n.join(lines)最后把它注册进分发表TOOL_HANDLERS{read_file:run_read,}这三处缺一不可。位置少了会怎样工具定义模型不知道有这个工具处理函数程序没有真正的执行逻辑分发映射模型调用后程序找不到对应函数所谓多加一个工具只加一行真正想表达的是主循环只需要维持这一种分发方式。新增工具不会让 Agent Loop 再长出一串if else。4.跑一个任务看看工具是怎么配合的继续用刚才的任务读取 README.md 和 requirements.txt 然后创建一个 summary.txt总结这个项目是做什么的。模型可能会按下面的顺序行动。第 1 轮读取项目说明read_file(pathREADME.md)工具返回 README 内容。第 2 轮读取依赖文件read_file(pathrequirements.txt)工具返回依赖列表。第 3 轮写入总结write_file( pathsummary.txt, content这是一个用于学习 Agent Harness 的项目…… )工具返回Wrote 48 bytes to summary.txt第 4 轮结束任务模型看到文件已经写入成功不再调用工具直接告诉用户总结文件已创建。这里有一个值得注意的细节。模型可以一次发起多个工具调用。比如它可能同时请求读取README.md和requirements.txt。教学代码会按照模型返回的顺序逐个执行forblockinresponse.content:ifblock.typetool_use:handlerTOOL_HANDLERS.get(block.name)outputhandler(**block.input)这样做的好处是简单便于理解。真正的 Claude Code 会进一步判断哪些工具可以并发。例如同时读取两个不同文件通常可以并发修改文件和运行测试之间则可能需要保持顺序。这一篇先不展开并发重点只是把工具如何注册、如何分发讲清楚。5.文件工具为什么要做路径校验在文件工具的实现里还有一个小细节叫safe_pathdefsafe_path(p:str)-Path:path(WORKDIR/p).resolve()ifnotpath.is_relative_to(WORKDIR):raiseValueError(fPath escapes workspace:{p})returnpath它的作用是限制文件操作只能发生在当前工作目录中。例如模型请求读取../../some-secret-file这个路径最终会跳出工作区safe_path会拒绝它。这不是完整的权限系统但至少说明了一件事给 Agent 工具时不能只考虑工具能不能完成任务还要考虑它能访问到哪里。不过这一章也保留了一个明显的问题。read_file、write_file、edit_file和glob都会经过路径校验bash仍然有更大的操作范围。所以这里解决的是工具如何组织还没有真正解决权限问题。6.从单工具 Agent 到多工具 Agent到底变了什么内容单工具版本多工具版本工具数量1 个bash5 个工具执行方式直接调用run_bash()通过TOOL_HANDLERS查表调用文件操作依赖 Shell 命令使用专门的读、写、改工具路径限制没有文件工具会校验工作区路径Agent Loopwhile True没有变化看下来多工具 Agent 并没有让模型变得更会思考。它做的是另一件事把模型能触达的外部能力从一个很粗的bash拆成几个更容易调用、也更容易约束的工具。7.小结这一章我觉得最值得记住的不是 5 个工具而是工具分发这个思路。主循环只负责维持模型和工具结果之间的来回。至于模型请求的是读文件、写文件、搜索文件还是执行命令交给分发表去处理。handlerTOOL_HANDLERS.get(block.name)outputhandler(**block.input)这样以后新增工具时不需要反复修改 Agent Loop。下一篇会继续顺着这个问题往下工具越来越多以后哪些操作可以直接执行哪些操作应该拦住并征求用户同意这就是权限检查要解决的事。参考资料工具分发源码与文档learn-claude-code 项目仓库