Coding Agent 工具层:别把 Agent 工具写成一堆函数
你好,我是程序员无隅
🌈 这是我的个人主页:无隅的主页
🌟 "技术永无止境,希望我的内容能帮到你"
🔥 热门专栏:Java 面试八股文 | LeetCode 算法笔记 | Claude Code 源码解读

写 Agent 的时候,很多人会先盯着“模型怎么调用工具”,但真正上线之后你会发现,最难的不是调用,而是控制。
工具层要做的事情其实很朴素:让模型看得懂工具,让本地管得住工具,让结果还能回得来。
💡 一句话:Agent 工具层,不是把函数丢给模型,而是把模型动作包装成一条可校验、可并发、可回填的执行链路。
一、先退一步:工具层到底在干什么?
把问题说直白一点:模型会“想做事”,但它不会直接碰你的磁盘、终端和文件系统。
所以中间一定要有一层“翻译器”:把模型的意图翻译成本地可以执行的动作,再把执行结果翻译回模型能继续推理的消息。
这层东西看起来像工具集合,但它真正做的是“接住模型请求,然后把请求变成本地可控动作”。
比如模型说“我要读这个文件”,**工具层不会立刻去读。**它会先判断这个工具有没有注册,参数是否合法,当前模式允不允许执行。确认没问题后,才调用真正的 ReadFile.execute(),最后再把读取结果包装回模型能理解的消息。
所以工具层至少有三件事:
- 对模型:提供工具说明书,让模型知道能调用什么
- 对本地:找到真实工具对象,并做参数和权限校验
- 对消息:把执行结果重新回填,让模型继续推理

这也是我读工具层源码时最想先抓住的本质:它不是“有哪些工具”,而是“工具怎么被安全地跑起来”。
二、让模型看得懂:Tool 不是函数,是对象
这个项目里的工具不是裸函数,它先被抽象成一个统一对象 Tool。
你可以把它理解成一张工具身份证,里面有几类信息:
name:模型调用时的名字description:这个工具适合干什么params_model:参数应该长什么样category:读、写、命令is_concurrency_safe:能不能并行should_defer:要不要先藏起来,等需要时再暴露
这套设计的价值不在“字段多”,而在“信息完整”。
模型需要的是说明书,本地需要的是可执行对象。Tool 把这两件事放到了一起,所以后面才能自动生成 schema、自动校验参数、自动做权限判断。
这里还有一个很重要的点:params_model 不是摆设,它会直接变成工具参数的 schema。这样做的好处很清楚:参数定义只写一份,模型看的是这份,本地验的也是这份,不用手写两套容易打架的描述。
源码里对应的函数名是 get_schema()。它不执行工具,只负责把 params_model 转成模型能看的工具描述。真正执行工具的是 execute(),所以 Tool 这一层其实把“给模型看的说明书”和“本地真正执行的入口”放在了同一个对象里。
如果一个工具只剩
execute(),它对本地来说也许还能运行,但对模型来说就缺少了“说明书”。模型不知道它叫什么、什么时候该用、参数该怎么传;本地也不好统一做 schema 生成、参数校验和权限控制。这样工具层就会退化成一堆散落脚本,而不是一套可管理的 Agent 工具系统。
三、让本地管得住:Registry 不是字典,是控制面
当工具数量变多以后,最容易坏掉的地方不是执行,而是管理。
如果每个工具都自己挂自己,Agent 很快就会变成一堆分散的 if/else;模型想调用谁、谁当前可用、谁要延迟曝光,都会越来越乱。
所以项目里用 ToolRegistry 把工具集中起来。它做的事情也很简单:
register:把工具收进来get:按名字找到真实工具enable / disable:控制当前可不可用get_all_schemas:把当前能看的工具整理成模型需要的格式should_defer:延迟发现,先只暴露必要工具
这里最关键的是 get_all_schemas()。它会先过滤禁用工具和未发现的延迟工具,再统一调用每个工具的 get_schema()。所以模型每轮看到哪些工具,不是工具自己决定的,而是 Registry 统一决定的。
如果把它看成一张控制面板,就比较好理解了:
- 模型看到的是“可选工具列表”
- Agent 持有的是“真实对象引用”
- Registry 站在中间做过滤和适配
默认装配的六个工具也很有代表性:
ReadFile、WriteFile、EditFile负责文件操作Glob、Grep负责找文件和搜内容Bash负责跑命令
这六个工具不是为了“看起来完整”,而是为了覆盖 Coding Agent 的基本闭环:先找到,再读取,再修改,最后验证。
它们的装配入口是 create_default_registry()。这个函数先创建 FileStateCache,再依次 register() 六个内置工具。也就是说,项目启动时不是到处散落初始化工具,而是在一个默认注册函数里完成基础工具集的装配。
四、让执行不失控:安全、并发、流式,这三件事缺一不可
这部分才是工具层最值钱的地方,因为它决定了工具不是“能跑”,而是“能稳地跑”。
4.1 文件安全:为什么要先读后写?

文件层面最容易出事故的点就是**“盲写”**。
这里把文件相关能力分成两层缓存:
FileCache:缓存文件内容,减少重复读磁盘FileStateCache:记录读过时的 mtime,防止写的时候覆盖外部修改
这两个东西经常被一起提,但职责完全不同。
FileCache 解决的是**“读得快不快”;FileStateCache 解决的是“能不能写”**。
这就是我很喜欢的一种设计:性能和安全分层处理,不把两个问题混在一块。
读文件时,工具会记录当前文件内容和时间戳;写文件和编辑文件时,会先检查这个时间戳有没有变化。
换句话说,Agent 想改文件之前,必须先证明自己看过的是“当前版本”。
对应到函数名上,ReadFile.execute() 读取文件后会调用 FileStateCache.record() 记录状态;WriteFile.execute() 和 EditFile.execute() 写入前会调用 FileStateCache.check() 做门控;写成功后再用 update() 刷新状态。这样读、查、写三步就串成了一条安全链路。
4.2 并发和流式:为什么有的工具能并行,有的必须串行?
默认情况下,工具并不是并发安全的。只有明确声明 is_concurrency_safe=True 的工具,才会被并行执行。
典型的安全工具是:
ReadFileGlobGrep
它们只读,不改状态,所以能一起跑。
而写文件、编辑文件、命令执行,默认还是串行。
这一步的意义很直接:读操作提速,写操作保守。
这样做的好处是,Agent 在查找和读取阶段不会太慢,但在真正修改状态时仍然保持稳定。
Agent 里对应的调度函数是 partition_tool_calls()。它会看工具对象上的 is_concurrency_safe,把可并发的只读调用放进同一批,把不安全的写操作单独拆开。这个函数不关心工具内部怎么执行,只负责把执行顺序先排安全。
再往前看一层,模型返回工具调用也不是一次性完整到达的,而是流式分片到达。
所以客户端会把 ToolCallStart、ToolCallDelta、ToolCallComplete 这三类事件拆开处理:
- 开始时先记录工具名和 id
- 中间把 JSON 参数片段先缓冲起来
- 完成时再统一解析成参数字典

这个设计看似普通,实际上很关键,因为它把**“还没说完”的参数和“可以执行”的参数**分开了。
只有 Complete 到了,才允许真正执行工具。
这里可以记三个事件名:ToolCallStart 记录工具名和调用 id,ToolCallDelta 只收集参数片段,ToolCallComplete 才代表参数已经拼完整。后面真正进入执行的是完整的 ToolCallComplete。
Delta 阶段只能说明**“参数正在传过来”,还不能说明“参数已经完整”**。如果这时候就执行工具,拿到的可能只是半截 JSON,参数解析会失败,甚至会让 Agent 执行一个不完整的动作。所以工具真正执行前,必须等到
ToolCallComplete。
五、回到项目里:为什么要这么设计?
看完这些机制后你会发现,Agent 工具层并不是追求“工具越多越好”,而是在四个方向上做了平衡:
- 对模型:工具要看得懂
- 对本地:工具要管得住
- 对执行:工具要能校验
- 对结果:工具要能回填
这就是它最像工程的地方。
不是所有工具都要一次性暴露,不是所有调用都要并行,不是所有写操作都能直接执行。
你甚至可以把这套链路总结成一句更直白的话:
工具调用骨架:tool_use -> registry.get -> validate -> permission -> execute -> tool_result
这行不是源码,但它几乎就是工具层的骨架。
如果看源码里的名字,执行时会先用 registry.get() 找工具,再用 tool.params_model.model_validate() 校验参数,最后调用 tool.execute()。执行结果再包装成 ToolResultBlock,通过 tool_use_id 回到消息管道。
六、验证和面试表达
如果你要确认自己真的学会了,可以先回答这几个问题:
- 为什么工具不能只写
execute()? - 为什么
params_model比手写 schema 更稳? - 为什么 Registry 要集中管理工具?
- 为什么
ReadFile要记录状态,而WriteFile/EditFile要先检查状态? - 为什么只读工具能并行,而写工具默认串行?
- 为什么流式工具参数必须先缓冲再执行?
- 为什么结果回填靠的是
tool_use_id,不是工具名?
如果面试官让你讲“Agent 工具层怎么设计”,可以这样说:
我们不是把工具当普通函数调用,而是做成了统一对象。每个工具都带名称、描述、参数模型、分类和并发标记,先通过 Registry 集中管理,再生成模型可见的 schema。模型返回 tool_use 后,Agent 会按名字找到真实工具对象,经过权限和参数校验后执行,最后用 tool_use_id 把结果回填到消息里。
如果对方继续追问文件安全,你可以接着说:
文件工具里我们分了两个缓存,
FileCache负责内容缓存,FileStateCache负责先读后写门控。ReadFile会记录文件内容和 mtime,WriteFile/EditFile写前会检查文件是否读过、是否被外部修改。这样能避免模型在不了解当前状态时直接覆盖文件。
最后一句
Agent 工具层的本质,是在模型意图和本地执行之间,加了一层**“能看、能管、能校验、能回填”的协议**。
更多推荐


所有评论(0)