这是「Claude Code 第一性原理拆解」的第 3 篇。
前一篇我在讲"回合怎么推进",这一篇我只讨论"动作怎么落地":也就是 Tool Runtime 为什么会成为 Agent 的行动层。
摘要
这一篇我会把 Claude Code 的工具层压成一个更稳定的理解框架:
- 工具不是孤立函数,而是带 schema、上下文、权限和状态回流语义的运行时对象。
ToolUseContext、toolOrchestration.ts、toolExecution.ts一起定义了 Claude Code 的动作协议。- FileRead / FileEdit 这类基础工具,最能暴露一个 Agent 系统到底是不是认真。
读完这一篇你应该能回答
- 为什么 Tool Runtime 比"加几个工具给模型"要复杂得多。
- 为什么并发必须建立在动作语义而不是性能冲动上。
- 为什么错误分类、结果规范化和消息回写都属于工具主路径。
如果说主循环决定了 Agent 怎么活着,那 Tool Runtime 决定的就是 Agent 怎么动手。
这也是我读 Claude Code 时最强烈的一个感受:
它并不是简单地给模型"挂了几个工具",而是认真地把"动作执行"做成了一层独立系统。
很多人第一次做 Agent,都会天然把工具理解成:
- 一个函数
- 一个 API
- 一个被模型触发的外部能力
这个理解不算错,但太轻。
因为在一个真正会行动的系统里,工具从来不只是"可调用函数",它至少还意味着:
- 输入要校验
- 权限要判断
- 执行要编排
- 错误要分类
- 结果要回到消息链里
- 状态可能会被修改
也就是说,工具不是"插件列表",而是 Agent 的行动层。
一、从第一性原理看,为什么 Agent 一定会长出 Tool Runtime
如果我只从问题出发,而不是从 Claude Code 的代码出发,我会这样推。
模型只能产生意图,不能直接产生真实世界动作
比如模型说:
- 读这个文件
- 改这个函数
- 跑一下测试
这些都只是"动作意图",还不是系统动作本身。
系统必须负责把它们翻译成:
- 哪个工具
- 用什么参数
- 在什么环境里执行
- 执行完后如何反馈
所以只要一个 Agent 不止是聊天,它几乎一定会长出一层:
intent -> action 的运行时翻译层
Claude Code 的 Tool Runtime,本质上就是这层。
二、源码主线:我为什么重点看 Tool.ts、tools.ts、toolOrchestration.ts、toolExecution.ts
如果我要抓 Claude Code 的工具主线,我会先锁定四个文件:
1. src/Tool.ts
这里是整个工具抽象的地基。
我最关心的不是某个 interface 名字,而是它到底把"工具调用"理解成什么。
里面最重要的,是 ToolUseContext 这种设计。
2. src/tools.ts
这里不是单纯列工具清单,而是在构造:
- 当前环境下真正对模型可见的工具集合
3. src/services/tools/toolOrchestration.ts
这里处理的是:
- 多个工具调用如何分批
- 什么能并发
- 什么必须串行
4. src/services/tools/toolExecution.ts
这里处理的是:
- 单个工具调用到底怎么被执行
- 在执行前后,权限、hook、错误分类、结果规范化如何串起来
把这四个文件连起来,我脑子里才有完整的"工具运行时"。
三、ToolUseContext 为什么是关键
很多 Agent demo 对工具的理解都很薄:
python
def search(query: str) -> str:
...
这种接口当然够写 demo,但一旦系统复杂起来,马上就会出现问题:
- 权限从哪拿
- 当前消息从哪拿
- 当前缓存从哪拿
- 当前工具集和资源从哪拿
- 工具执行后如何更新状态
Claude Code 用 ToolUseContext 的方式,把这些运行时状态聚到了一起。
我会把它翻译成这样的 Python 伪代码:
python
class ToolUseContext:
def __init__(self):
self.options = {
"tools": [],
"commands": [],
"mcp_clients": [],
"mcp_resources": {},
"main_loop_model": None,
}
self.abort_controller = AbortController()
self.read_file_state = FileStateCache()
self.messages = []
def get_app_state(self):
...
def set_app_state(self, updater):
...
def set_response_length(self, updater):
...
def update_file_history_state(self, updater):
...
这说明在 Claude Code 里,工具不是独立执行单元,而是:
- 在会话内执行
- 对会话负责
- 依赖会话上下文
这是一个非常成熟的判断。
四、tools.ts 为什么不是工具列表,而是工具暴露策略
我觉得很多人看 tools.ts,第一反应会是:
- 哦,项目里有哪些工具
但我更在意的是另一件事:
当前这一轮、当前这个环境,模型到底应该看到什么工具?
这件事很重要,因为模型能看到什么能力,直接决定了它怎么规划动作。
从 Claude Code 的做法可以看出来:
- 工具集合不是静态的
- 它会受 feature gate、环境能力、权限模式等因素影响
这其实是一种 runtime exposure control。
我自己以后设计 Agent,也会很重视这一点:
- 工具不是越多越好
- 工具要按当前场景有选择地暴露
五、toolOrchestration.ts 让我最认同的地方:并发必须以动作语义为前提
我非常认同 Claude Code 在工具调度上的一个核心判断:
并发不是默认值,并发应该建立在"动作语义安全"之上。
它大致的逻辑是:
- 找到具体工具
- 用 schema 解析输入
- 调工具自己的
isConcurrencySafe - 连续的并发安全工具打成一批
- 非并发安全工具串行跑
我会把它压成下面的 Python 伪代码:
python
def partition_tool_calls(tool_calls, context):
batches = []
for call in tool_calls:
tool = find_tool(call.name)
parsed = tool.input_schema.parse(call.input)
safe = tool.is_concurrency_safe(parsed)
if safe and batches and batches[-1].is_safe:
batches[-1].calls.append(call)
else:
batches.append(Batch(is_safe=safe, calls=[call]))
return batches
def run_tools(tool_calls, context):
current_context = context
for batch in partition_tool_calls(tool_calls, current_context):
if batch.is_safe:
updates = run_concurrently(batch.calls, current_context)
current_context = apply_context_modifiers_in_logical_order(
current_context, updates
)
else:
for call in batch.calls:
update = run_single_tool(call, current_context)
current_context = update.new_context
yield current_context
我特别喜欢"apply context modifiers in logical order"这一层意思。
因为在工具系统里,真正难的不只是并发执行,而是:
- 并发执行完以后,状态怎么不乱
这恰好是很多 demo 最容易忽略的地方。
六、toolExecution.ts 才是完整的行动协议
如果说 toolOrchestration 是调度器,那 toolExecution 更像执行协议层。
它大致会负责:
- tool 定位
- schema 校验
- 权限判断
- hook 执行
- tracing / telemetry
- 错误分类
- 结果标准化
这说明 Claude Code 对工具执行的理解不是:
- 调个函数就完了
而是:
- 每次工具调用都要走固定管道
我脑子里的版本大概是这样:
python
def run_tool_use(tool_use, context):
tool = registry.get(tool_use.name)
parsed_input = validate(tool.input_schema, tool_use.input)
pre_hooks(tool, parsed_input, context)
permission = can_use_tool(tool, parsed_input, context)
if permission.behavior == "deny":
return denied_result(tool_use)
if permission.behavior == "ask":
return ask_user_then_resume(tool_use)
start_trace(tool_use)
try:
result = tool.call(parsed_input, context)
post_hooks(tool, result, context)
return normalize_result(result)
except Exception as e:
kind = classify_tool_error(e)
failure_hooks(tool, kind, e, context)
return normalize_error(kind, e)
finally:
finish_trace(tool_use)
如果一个系统愿意把工具执行做成这样一条固定管道,我就会认为它已经不再是"工具拼装应用",而是在做真正的运行时。
七、从 FileReadTool 和 FileEditTool 我看到的,不是两个工具,而是工程态度
我很喜欢看这种"看似基础"的工具,因为越基础,越能暴露团队的工程态度。
FileReadTool 暴露出的判断
它不只是读文本文件,还在认真处理:
- 路径标准化
- 设备文件阻断
- 图片、PDF、notebook 特殊逻辑
- token 预算
- 权限检查
也就是说,它不是:
- 读文件函数
而是:
- 在 Agent 语境下受约束的文件读取能力
FileEditTool 暴露出的判断
它也不只是 replace string,而是在处理:
- 文件是否存在
- old/new string 是否合理
- 文件过大怎么办
- deny rule 怎么判
- 并发修改冲突怎么办
- line ending / quote style 如何保留
所以我会说:
真正的 Agent 工程,不体现在华丽能力上,而体现在基础能力是否被认真约束。
八、架构图:我心里的 Tool Runtime 是什么形状
我觉得这张图特别重要,因为它说明:
- Tool Runtime 不是功能边角
- 它是 Agent 从"想法"到"动作"的中枢
九、我自己的个人判断:Tool Runtime 比大多数人想象得更重要
我现在越来越觉得,一个团队有没有真正理解 Agent,往往看它怎么理解 tools。
如果它只把 tools 当成:
- 给模型加点能力
那系统通常会很快碰到:
- 状态混乱
- 错误不可恢复
- 权限四处散落
- 观测难统一
但如果它把 Tool Runtime 当成行动层来设计,很多问题就会自然变得更清楚:
- 什么时候可以行动
- 怎么行动
- 行动后如何反馈
- 出错后如何恢复
十、这一篇我想留下的结论
如果只留一句话,我会写:
Claude Code 的 Tool Runtime 最值得学的地方,不是它托管了多少工具,而是它把"模型意图如何转成受控动作"这件事做成了一层清晰的运行时协议。
而这恰恰是我认为,大模型应用真正开始走向智能体系统时,最该认真设计的一层。