AI Agent 的工具层到底怎么设计?Claude Code 给了一个范本
Claude Code 的 Tool,不是函数,而是一套 Agent 能力系统
读 Claude Code 的工具能力块时,我最大的感受是:
它并没有把工具当成“几个可以调用的函数”,而是把每个工具都设计成了一个完整的能力对象。
这个能力对象要同时面对很多东西:
模型要能理解它
权限系统要能约束它
调度器要能执行它
UI 要能展示它
消息流还要能接住它的返回结果
所以这一块其实是在回答一个很关键的问题:
当模型输出 tool_use 之后,Claude Code 内部到底发生了什么?
如果和前面的模型执行引擎连起来看,可以这样理解:
模型执行引擎:决定什么时候该调工具。
工具能力块:决定这个工具到底是什么、能不能调、怎么执行、结果怎么返回。
这一块可以拆成 3 条主线
1. 什么才算一个 Tool?
核心在 Tool.ts。
这里的 Tool 不是普通 util 函数,而是一套正式契约。
一个工具至少要包含:
inputSchemadescriptioncall(...)checkPermissions(...)isConcurrencySafe(...)
UI 渲染方法
运行时上下文 ToolUseContext
所以这里一定要换个理解方式:
Tool 是能力对象,不是某个 util 函数。
这也是 Claude Code 工具系统最关键的抽象。
2. 模型每一轮能看到哪些工具?
这部分主要看 tools.ts。
重点是:
工具池不是一个写死的数组,而是运行时动态装配出来的。
大概链路是:
getAllBaseTools()
先给出理论上的内建工具候选集。
getTools()
再根据 simple mode、REPL mode、deny rules 做过滤。
assembleToolPool()
最后把 built-in 工具和 MCP 工具合并、排序、去重。
所以模型每一轮真正“看见”的工具,并不是固定清单,而是动态计算后的结果。
这里有一个很重要的点:
deny rules 不只是调用时才生效,它也会影响模型能看到哪些工具。
也就是说,Claude Code 不是等模型乱调工具之后再拦截,而是在工具暴露阶段就已经做了一层约束。
3. 一次 tool_use 是怎么真正执行的?
这部分主要看 toolExecution.ts。
模型发出工具调用后,系统不会直接执行,而是会先过一整套流程。
一次工具调用大概会走这条链路:
找到对应工具
↓
用 inputSchema.safeParse(...) 做 schema 校验
↓
执行 validateInput(...) 做工具级校验
↓
跑 pre-tool hooks
↓
调用 checkPermissions(...) 做权限判断
↓
真正执行 tool.call(...)
↓
通过 mapToolResultToToolResultBlockParam(...) 映射成标准 tool_result
↓
最后回到主循环
这条链路说明一件事:
Claude Code 的工具调用,不是“模型说调就调”,而是一个被 schema、hook、权限和消息协议共同约束的执行过程。
为什么 BashTool 很有代表性?
具体工具可以看 BashTool.tsx。
它不是简单包了一层 bash 命令,而是包含了:
输入处理
权限判断
命令执行
结果映射
UI 展示
运行状态处理
所以 BashTool 很适合作为理解 Claude Code 工具设计的样本。
它说明:
一个工具往往就是一个小型子系统,而不是一个小函数。
我会把这一块画成这样一条线
Tool.ts
定义什么叫工具
↓tools.ts
决定当前有哪些工具可用
↓toolExecution.ts
决定某次 tool_use 怎么真正落地执行
↓
具体工具文件,比如 BashTool.tsx
负责单个工具自己的业务逻辑
分工其实很清楚:
Tool.ts:统一契约tools.ts:工具池装配toolExecution.ts:通用执行入口
具体 Tool 文件:单个能力实现
这块最值得记住的 6 个结论
-
Claude Code 的工具是正式能力对象,不是普通函数。 -
工具同时面向模型、权限系统、调度器、UI 和消息流。 -
工具池是动态装配结果,不是静态清单。 -
deny rules 不只在调用时生效,也会影响模型能看到哪些工具。 -
工具执行前会经过 schema 校验、工具级校验、hook 和权限决策。 -
工具结果必须被重新映射成标准 tool_result消息,才能回到主循环。
如果你也在读 Claude Code 源码,可以优先想明白这 3 个问题:
为什么 Claude Code 需要一整套 Tool 契约,而不是随便给模型几个函数?
为什么工具池必须动态装配,而不是固定写死?
为什么工具结果不能直接把内部返回值塞回模型?
这三个问题想通之后,Claude Code 的工具系统基本就串起来了。
夜雨聆风