多加一个工具,只加一行:Agent 是怎么管理工具的?
多加一个工具,只加一行:Agent 是怎么管理工具的?
本文是「从零理解 Claude Code:20 个 Agent Harness 机制」系列的第 3 篇。
源码仓库:shareAI-lab/learn-claude-code
上一篇我们先搭起了最小 Agent Loop:模型调用bash,程序执行命令,再把结果交回模型。
这个版本已经能做事了,但它只有一个工具:bash。
从能力上说,bash几乎什么都能做。
读文件可以用cat,写文件可以用重定向,改文件可以用sed,找文件可以用find。
但我实际看下来,只有一个bash并不优雅。
因为模型明明想做的是读文件,却要先把这个意图翻译成一条正确的 Shell 命令。
catREADME.md模型想修改一行代码,又得写出一段不容易出错的sed命令。
sed-i's/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(path="README.md")想写总结文件时,也不必处理 Shell 的重定向:
write_file( path="summary.txt", content="……" )工具的名字更接近任务本身,模型需要处理的无关细节就少一些。
2.从单工具到多工具,核心变化只有一处
最小 Agent Loop 里,工具执行是写死的:
output=run_bash(block.input["command"])因为当时只有bash,这么写没有问题。
但工具一多,这种写法就撑不住了。总不能写成这样:
ifblock.name=="bash":output=run_bash(**block.input)elifblock.name=="read_file":output=run_read(**block.input)elifblock.name=="write_file":output=run_write(**block.input)elifblock.name=="edit_file":output=run_edit(**block.input)工具再多一点,这段判断会越来越长,主循环也会越来越乱。
这里用了一个很常见的做法:工具分发表。
TOOL_HANDLERS={"bash":run_bash,"read_file":run_read,"write_file":run_write,"edit_file":run_edit,"glob":run_glob,}模型返回工具名后,程序去这个字典里查对应的处理函数:
handler=TOOL_HANDLERS.get(block.name)output=handler(**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|None=None)->str:lines=safe_path(path).read_text().splitlines()iflimitandlimit<len(lines):lines=lines[: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(path="README.md")工具返回 README 内容。
第 2 轮:读取依赖文件
read_file(path="requirements.txt")工具返回依赖列表。
第 3 轮:写入总结
write_file( path="summary.txt", content="这是一个用于学习 Agent Harness 的项目……" )工具返回:
Wrote 48 bytes to summary.txt第 4 轮:结束任务
模型看到文件已经写入成功,不再调用工具,直接告诉用户总结文件已创建。
这里有一个值得注意的细节。
模型可以一次发起多个工具调用。比如它可能同时请求读取README.md和requirements.txt。
教学代码会按照模型返回的顺序逐个执行:
forblockinresponse.content:ifblock.type=="tool_use":handler=TOOL_HANDLERS.get(block.name)output=handler(**block.input)这样做的好处是简单,便于理解。
真正的 Claude Code 会进一步判断哪些工具可以并发。例如同时读取两个不同文件通常可以并发;修改文件和运行测试之间则可能需要保持顺序。
这一篇先不展开并发,重点只是把工具如何注册、如何分发讲清楚。
5.文件工具为什么要做路径校验
在文件工具的实现里,还有一个小细节,叫safe_path:
defsafe_path(p:str)->Path:path=(WORKDIR/p).resolve()ifnotpath.is_relative_to(WORKDIR):raiseValueError(f"Path escapes workspace:{p}")returnpath它的作用是限制文件操作只能发生在当前工作目录中。
例如,模型请求读取:
../../some-secret-file这个路径最终会跳出工作区,safe_path会拒绝它。
这不是完整的权限系统,但至少说明了一件事:给 Agent 工具时,不能只考虑工具能不能完成任务,还要考虑它能访问到哪里。
不过这一章也保留了一个明显的问题。
read_file、write_file、edit_file和glob都会经过路径校验;bash仍然有更大的操作范围。
所以这里解决的是工具如何组织,还没有真正解决权限问题。
6.从单工具 Agent 到多工具 Agent,到底变了什么
| 内容 | 单工具版本 | 多工具版本 |
|---|---|---|
| 工具数量 | 1 个bash | 5 个工具 |
| 执行方式 | 直接调用run_bash() | 通过TOOL_HANDLERS查表调用 |
| 文件操作 | 依赖 Shell 命令 | 使用专门的读、写、改工具 |
| 路径限制 | 没有 | 文件工具会校验工作区路径 |
| Agent Loop | while True | 没有变化 |
看下来,多工具 Agent 并没有让模型变得更会思考。
它做的是另一件事:把模型能触达的外部能力,从一个很粗的bash,拆成几个更容易调用、也更容易约束的工具。
7.小结
这一章我觉得最值得记住的不是 5 个工具,而是工具分发这个思路。
主循环只负责维持模型和工具结果之间的来回。
至于模型请求的是读文件、写文件、搜索文件还是执行命令,交给分发表去处理。
handler=TOOL_HANDLERS.get(block.name)output=handler(**block.input)这样以后新增工具时,不需要反复修改 Agent Loop。
下一篇会继续顺着这个问题往下:工具越来越多以后,哪些操作可以直接执行,哪些操作应该拦住并征求用户同意?
这就是权限检查要解决的事。
参考资料:
- 工具分发:源码与文档
- learn-claude-code 项目仓库
