工具执行链路
Claude Code 的工具执行分两层:
toolOrchestration.ts:处理多个工具调用的调度。toolExecution.ts:处理单个工具调用的完整生命周期。
多工具调度
模型一次响应里可能输出多个 tool_use。Claude Code 会先判断哪些工具可以并发。
判断依据来自工具自己的 isConcurrencySafe(input)。这点比“只读工具就一定并发”更准确:只读性是一个重要信号,但 Claude Code 真正用于调度的是 isConcurrencySafe()。
可以并发的典型工具:
- 读文件。
- 搜索文件。
- 列目录。
应该串行的典型工具:
- 编辑文件。
- 写文件。
- 运行可能修改环境的命令。
简化版可以先用一个规则:
ts
if (tool.readonly) {
runConcurrently(toolUses)
} else {
runSerially(toolUses)
}1
2
3
4
5
2
3
4
5
但在正式产品里,建议像 Claude Code 一样把并发判断做成工具接口的一部分。某些看似只读的工具也可能依赖共享状态,某些写入工具也可能在特定输入下安全。
单工具执行步骤
单个工具的执行链路可以抽象为:
text
find tool
-> parse input
-> validate input
-> run pre hooks
-> check permission
-> execute tool
-> run post hooks
-> map result
-> append tool_result1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
对应 Claude Code 的调用链是:
text
queryLoop()
-> runTools()
-> runToolUse()
-> checkPermissionsAndCallTool()
-> tool.call()
-> mapToolResultToToolResultBlockParam()
-> createUserMessage(tool_result)1
2
3
4
5
6
7
2
3
4
5
6
7
这条链路的顺序很重要。权限提示应该发生在 schema 校验之后,因为无效参数不应该打扰用户确认。
工具失败也要返回结果
AgentLoop 最容易出错的地方是工具执行失败。
错误做法:
text
assistant: tool_use(...)
系统内部抛异常
下一轮模型调用没有 tool_result1
2
3
2
3
正确做法:
text
assistant: tool_use(...)
user: tool_result(is_error=true, content="错误原因")1
2
2
模型 API 通常要求 tool_use 和 tool_result 成对出现。即使工具失败,也要返回错误型结果。
案例:FileReadTool
读文件工具展示了一个重要设计:模型结果和 UI 结果分离。
文件内容会作为模型可见的 tool result 返回,但 UI 不一定直接把完整文件内容显示给用户。这样可以减少终端噪音,同时让模型获得足够信息。
自研 Agent 也应该这样设计:
ts
type ToolExecution = {
modelResult: ToolResultContent
uiEvent?: ToolProgressEvent
}1
2
3
4
2
3
4
不要让 UI 渲染格式污染模型上下文。