专栏说明:本文是《Claude Code 代码全景解析》第 14 篇,属于"核心引擎"章节的第 4 篇。在前面的文章中,我们已经了解了 QueryEngine 的整体架构、消息系统的流转机制以及权限系统的设计。本文将深入最核心的工具调用循环,揭示 Claude Code 作为 AI Agent 的"心脏"是如何跳动的。
一、循环全貌:Agent 的心脏
如果说 QueryEngine 是 Claude Code 的躯体,那么**工具调用循环(Tool Use Loop)**就是它的心脏。每一次用户请求的处理,本质上都是这个循环的一次完整运转。理解了这个循环,你就理解了 Agent 的本质——模型思考 → 调用工具 → 观察结果 → 再次思考的循环往复。
Claude Code 的工具调用循环位于 src/query.ts 中,是一个典型的 while 循环结构,每一轮迭代对应一次模型与工具的交互回合。让我们先看看整个循环的全貌:
flowchart TD
A["用户输入"] --> B["QueryEngine.submitMessage()"]
B --> C["组装请求消息队列"]
C --> D["调用 Anthropic API"]
D --> E{"模型返回内容"}
E -->|纯文本回答| F["直接回答用户
循环终止"]
E -->|Tool Use Block| G["解析 Tool Call"]
G --> H["Zod 参数验证"]
H -->|验证失败| I["生成错误 tool_result"]
H -->|验证通过| J["权限检查 canUseTool"]
J -->|拒绝| K["生成拒绝 tool_result"]
J -->|允许| L["路由到具体工具执行"]
L --> M{"并发安全?"}
M -->|是| N["并发执行"]
M -->|否| O["串行执行"]
N --> P["生成 tool_result"]
O --> P
P --> Q["结果回流到消息队列"]
Q --> R["应用结果预算压缩"]
R --> S{"终止条件?"}
S -->|是| F
S -->|否| C
I --> Q
K --> Q这个循环的起点是 QueryEngine.submitMessage()(src/QueryEngine.ts,第 380 行附近),它接收用户输入后,构建完整的请求上下文,包括系统提示词、历史消息和可用工具列表,然后进入 query() 函数的核心循环。
在 src/query.ts 第 554 行附近,有一个关键注释说明了循环的核心信号:
"Note: stop_reason === 'tool_use' is unreliable – it’s not always set correctly. Set during streaming whenever a tool_use block arrives – the sole loop-exit signal. If false after streaming, we’re done."
这意味着模型是否生成 tool_use 内容块,是决定是否继续循环的唯一信号,而不是依赖 API 返回的 stop_reason。
二、模型决策阶段:LLM 如何选择工具
2.1 工具 Schema 的传递
在每次 API 调用前,Claude Code 会将可用工具的 Schema 注入到请求中。工具的定义位于 src/Tool.ts,每个工具都有严格的类型定义(第 340-490 行):
export type Tool<Input extends AnyObject = AnyObject, Output = unknown, ...> = {
aliases?: string[]
searchHint?: string
call(args: z.infer<Input>, context: ToolUseContext, ...): Promise<ToolResult<Output>>
description(input: z.infer<Input>, options: {...}): Promise<string>
readonly inputSchema: Input
readonly inputJSONSchema?: ToolInputJSONSchema
outputSchema?: z.ZodType<unknown>
isConcurrencySafe(input: z.infer<Input>): boolean
isEnabled(): boolean
isReadOnly(input: z.infer<Input>): boolean
isDestructive?(input: z.infer<Input>): boolean
maxResultSizeChars: number
readonly strict?: boolean
// ...
}其中 inputSchema 是一个 Zod Schema,它定义了工具期望的参数结构。这个 Schema 会被转换为 JSON Schema 格式发送给 Anthropic API,模型就是依据这个 Schema 来决定如何构造 Tool Call 的参数。
值得注意的是,Claude Code 支持**工具延迟加载(Deferred Loading)**机制。对于大量 MCP 工具,不会一次性将所有工具的 Schema 注入提示词,而是通过 ToolSearch 工具让模型按需发现。src/Tool.ts 第 444 行定义了 shouldDefer 属性:
"When true, this tool is deferred (sent with defer_loading: true) and requires ToolSearch to be used before it can be called."
相反,alwaysLoad 属性(第 469 行)则确保某些关键工具在首轮就一定可见:
"When true, this tool is never deferred – its full schema appears in the initial prompt even when ToolSearch is enabled."
2.2 并行 Tool Call 的支持
现代 LLM(尤其是 Claude 3.5 Sonnet 及更新的模型)支持在一次响应中发起多个并行的 Tool Call。Claude Code 充分利用了这一特性。在 src/query.ts 第 830 行附近,模型返回的 assistant message 会被解析出所有 tool_use 类型的 content block:
const msgToolUseBlocks = message.message.content.filter(
content => content.type === 'tool_use'
) as ToolUseBlock[]
if (msgToolUseBlocks.length > 0) {
toolUseBlocks.push(...msgToolUseBlocks)
needsFollowUp = true
}当检测到 tool_use block 时,needsFollowUp 被置为 true,这告诉外层循环:本轮结束后需要继续下一轮,将工具结果回传给模型。
2.3 决策流程
模型如何决策调用哪个工具,本质上是一个"函数选择"问题。Anthropic API 将工具的 description 和 inputSchema 作为提示词的一部分,模型根据当前任务上下文推断出最合适的工具组合。Claude Code 中模型决策的大致流程如下:
flowchart LR
A["用户请求 + 历史消息 + 系统提示"] --> B["Anthropic API"]
B --> C{"模型推理"}
C -->|"需要工具辅助"| D["生成 tool_use block"]
C -->|"可直接回答"| E["生成 text block"]
D --> F["Tool Use Block 包含:
- tool name
- tool_use_id
- input JSON"]
E --> G["循环终止
返回结果"]模型在一次响应中可以同时调用多个工具,这些 Tool Call 通过唯一的 tool_use_id 标识,后续的工具结果需要携带对应的 tool_use_id 进行匹配。
三、Tool Call 解析:从模型输出到可执行调用
3.1 提取 Tool Use Block
当模型返回的流式响应中检测到 tool_use 类型的内容块时,Claude Code 会将其收集起来。在 src/query.ts 的流式处理循环中(第 750-830 行),assistant message 被逐步构建:
for await (const message of deps.callModel({...})) {
// ...
if (message.type === 'assistant') {
assistantMessages.push(message)
const msgToolUseBlocks = message.message.content.filter(
content => content.type === 'tool_use'
) as ToolUseBlock[]
if (msgToolUseBlocks.length > 0) {
toolUseBlocks.push(...msgToolUseBlocks)
needsFollowUp = true
}
// 如果是流式工具执行,立即将工具加入执行队列
if (streamingToolExecutor && !toolUseContext.abortController.signal.aborted) {
for (const toolBlock of msgToolUseBlocks) {
streamingToolExecutor.addTool(toolBlock, message)
}
}
}
}这里的关键设计是流式工具执行(Streaming Tool Execution)。传统的做法是等模型完整返回后再执行工具,但 Claude Code 使用了 StreamingToolExecutor(src/services/tools/StreamingToolExecutor.ts),它可以在模型还在生成后续 token 时,就提前开始执行已经解析出的 Tool Call,从而显著减少整体延迟。
3.2 参数解析和 Zod 验证
模型生成的参数是 JSON 格式,但模型的输出并不总是严格符合 Schema。因此,每个 Tool Call 在执行前都必须经过严格的参数验证。这个验证发生在 src/services/tools/toolExecution.ts 的 checkPermissionsAndCallTool 函数中(第 650-720 行):
// Validate input types with zod (surprisingly, the model is not great at generating valid input)
const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
let errorContent = formatZodValidationError(tool.name, parsedInput.error)
// ... 构建错误 tool_result
}注释非常直白地指出了现实:" surprisingly, the model is not great at generating valid input "——模型并不擅长生成完全符合 Schema 的有效输入。
当验证失败时,formatZodValidationError 函数(src/utils/toolErrors.ts,第 56-110 行)会将 Zod 错误转换为人类可读且对 LLM 友好的错误消息:
export function formatZodValidationError(toolName: string, error: ZodError): string {
const missingParams = error.issues
.filter(err => err.code === 'invalid_type' && err.message.includes('received undefined'))
.map(err => formatValidationPath(err.path))
const unexpectedParams = error.issues
.filter(err => err.code === 'unrecognized_keys')
.flatMap(err => err.keys)
const typeMismatchParams = error.issues
.filter(err => err.code === 'invalid_type' && !err.message.includes('received undefined'))
.map(err => ({ param: formatValidationPath(err.path), expected: err.expected, received: ... }))
// 构建可读的错误信息...
}这个函数会分类处理三种常见错误:
- 缺少必需参数:
The required parameter \file_path` is missing` - 未预期的参数:
An unexpected parameter \content` was provided` - 类型不匹配:
The parameter \line` type is expected as `number` but provided as `string``
这些错误信息会被包装成 tool_result 回传给模型,让模型有机会修正参数后重新调用。
3.3 工具查找与别名支持
在 src/services/tools/toolExecution.ts 的 runToolUse 函数中(第 275-330 行),系统首先尝试通过工具名查找对应的工具定义:
let tool = findToolByName(toolUseContext.options.tools, toolName)
// 如果找不到,检查是否是已弃用工具的别名调用
if (!tool) {
const fallbackTool = findToolByName(getAllBaseTools(), toolName)
if (fallbackTool && fallbackTool.aliases?.includes(toolName)) {
tool = fallbackTool
}
}findToolByName 函数(src/Tool.ts,第 340-350 行)支持通过主名称或别名查找工具:
export function findToolByName(tools: Tools, name: string): Tool | undefined {
return tools.find(t => toolMatchesName(t, name))
}
export function toolMatchesName(tool: { name: string; aliases?: string[] }, name: string): boolean {
return tool.name === name || (tool.aliases?.includes(name) ?? false)
}如果工具根本不存在(既不是主名称也不是别名),系统会生成一个错误 tool_result:
<tool_use_error>Error: No such tool available: unknown_tool_name</tool_use_error>3.4 输入值的业务验证
通过 Zod 类型验证后,还有一个可选的业务级验证步骤——validateInput(src/services/tools/toolExecution.ts,第 720-760 行):
const isValidCall = await tool.validateInput?.(parsedInput.data, toolUseContext)
if (isValidCall?.result === false) {
// 返回业务验证失败的 tool_result
}与 Zod 的类型验证不同,validateInput 是工具特定的业务逻辑验证。例如,文件读取工具可能会检查文件路径是否在工作目录范围内,网络工具可能会验证 URL 格式等。这种分层验证设计确保了安全性——类型正确不等于调用合法。
四、工具执行阶段:从决策到行动
4.1 权限检查
在工具真正执行之前,必须通过权限系统的审查。canUseTool 函数(在 QueryEngine 中被包装为 wrappedCanUseTool)是这一关卡的入口。src/QueryEngine.ts 第 243-270 行展示了权限检查的包装逻辑:
const wrappedCanUseTool: CanUseToolFn = async (tool, toolUseContext, toolUseID, input) => {
const result = await canUseTool(tool, toolUseContext, toolUseID, input)
if (result.behavior === 'deny') {
this.permissionDenials.push({
tool_name: sdkCompatToolName(tool.name),
tool_use_id: toolUseID,
tool_input: input,
// ...
})
}
return result
}权限检查的结果有三种可能:
allow:直接执行,无需用户确认ask:弹出权限对话框,等待用户确认deny:拒绝执行,生成拒绝原因的tool_result
权限决策的详细流程涉及规则匹配、Hook 系统、分类器等多个子系统,这在专栏前面的文章中已有详细分析。
4.2 并发与串行执行
Claude Code 的工具执行有一个精妙的并发控制机制。在 src/services/tools/toolOrchestration.ts 中,partitionToolCalls 函数(第 75-100 行)将多个 Tool Call 分组为"可并发"和"需串行"两类:
function partitionToolCalls(toolUseMessages: ToolUseBlock[], toolUseContext: ToolUseContext): Batch[] {
return toolUseMessages.reduce((acc: Batch[], toolUse) => {
const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
const parsedInput = tool?.inputSchema.safeParse(toolUse.input)
const isConcurrencySafe = parsedInput?.success
? (() => {
try {
return Boolean(tool?.isConcurrencySafe(parsedInput.data))
} catch {
return false // 保守策略:出错则视为不安全
}
})()
: false
// 将连续的安全工具归为一组并发执行
if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
acc[acc.length - 1]!.blocks.push(toolUse)
} else {
acc.push({ isConcurrencySafe, blocks: [toolUse] })
}
return acc
}, [])
}这里的核心判断依据是每个工具的 isConcurrencySafe 方法。例如,文件读取工具(Read)通常是并发安全的,因为它只读取而不修改状态;而文件写入工具(Write/Edit)则不是并发安全的,因为它们会修改文件系统状态。
并发执行的上限由环境变量控制(src/services/tools/toolOrchestration.ts,第 10-14 行):
function getMaxToolUseConcurrency(): number {
return parseInt(process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || '', 10) || 10
}默认最大并发数为 10。
4.3 流式工具执行器
StreamingToolExecutor(src/services/tools/StreamingToolExecutor.ts)是 Claude Code 执行效率的关键优化。它维护了一个工具执行队列,并支持以下核心行为:
- 即时入队:当流式响应中解析出
tool_useblock 时,立即加入执行队列 - 并发控制:检查当前执行状态,决定是否立即启动新工具
- 结果缓冲:完成的工具结果按原始顺序缓冲,确保输出有序
- 错误级联:如果一个并发执行的 Bash 工具出错,会自动取消其他并行的 Bash 工具(第 45-50 行)
export class StreamingToolExecutor {
private tools: TrackedTool[] = []
private siblingAbortController: AbortController // Bash 错误时取消兄弟进程
addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
// 解析工具,判断并发安全性,加入队列
// 立即触发 processQueue()
}
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = this.tools.filter(t => t.status === 'executing')
return executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
}
}当流式响应完成后,query.ts 会调用 getRemainingResults() 消费所有未完成的工具结果,确保每个 tool_use 都有对应的 tool_result。
4.4 超时控制与用户中断
每个工具的执行都受到 abortController 的控制。当用户按下 ESC 键中断时,QueryEngine.interrupt() 会触发 abortController.abort(),信号会传递到所有正在执行的工具。
在 src/services/tools/toolExecution.ts 的 runToolUse 函数中(第 340-380 行),每次执行前都会检查中断信号:
if (toolUseContext.abortController.signal.aborted) {
logEvent('tengu_tool_use_cancelled', { ... })
const content = createToolResultStopMessage(toolUse.id)
content.content = withMemoryCorrectionHint(CANCEL_MESSAGE)
yield {
message: createUserMessage({
content: [content],
toolUseResult: CANCEL_MESSAGE,
sourceToolAssistantUUID: assistantMessage.uuid,
}),
}
return
}4.5 沙箱与执行隔离
Bash 工具等可能执行任意命令的工具,其执行受到多层沙箱保护。具体的沙箱机制涉及进程隔离、工作目录限制、环境变量控制等,这些在专栏前面的权限系统文章中已有详述。值得强调的是,工具层面的 isDestructive 标记(src/Tool.ts)让系统能够识别哪些工具执行了不可逆操作(如删除文件、覆盖文件、发送网络请求),从而在权限检查时给予更高的安全审查级别。
五、结果回流:让模型"看到"工具输出
5.1 工具结果格式化为消息
工具执行完成后,其结果必须被格式化为 Anthropic API 可接受的 tool_result 内容块。在 src/services/tools/toolExecution.ts 中,processToolResultBlock 函数(或 processPreMappedToolResultBlock)负责将工具的原始输出转换为标准的 ToolResultBlockParam:
export async function processToolResultBlock<T>(
tool: { name: string; maxResultSizeChars: number; mapToolResultToToolResultBlockParam: ... },
toolUseResult: T,
toolUseID: string,
): Promise<ToolResultBlockParam> {
const toolResultBlock = tool.mapToolResultToToolResultBlockParam(toolUseResult, toolUseID)
return maybePersistLargeToolResult(
toolResultBlock,
tool.name,
getPersistenceThreshold(tool.name, tool.maxResultSizeChars),
)
}一个标准的 tool_result 包含以下字段:
type: 'tool_result'tool_use_id: 对应tool_use的唯一标识content: 工具输出内容(字符串或内容块数组)is_error: 是否为错误结果(可选)
5.2 结果压缩:避免上下文爆炸
工具输出可能非常庞大(例如读取大文件、执行产生大量输出的命令)。如果直接将完整结果回传给模型,会导致上下文窗口迅速耗尽。Claude Code 实现了一套精密的结果持久化与压缩机制,核心代码位于 src/utils/toolResultStorage.ts。
5.2.1 单个工具结果限制
每个工具有自己的 maxResultSizeChars 属性。当结果超过阈值时,maybePersistLargeToolResult 函数会将结果写入磁盘文件,并向模型返回一个预览:
async function maybePersistLargeToolResult(
toolResultBlock: ToolResultBlockParam,
toolName: string,
persistenceThreshold?: number,
): Promise<ToolResultBlockParam> {
const size = contentSize(content)
const threshold = persistenceThreshold ?? MAX_TOOL_RESULT_BYTES
if (size <= threshold) {
return toolResultBlock // 未超限,直接返回
}
// 超限:持久化到磁盘,返回预览
const result = await persistToolResult(content, toolResultBlock.tool_use_id)
if (isPersistError(result)) return toolResultBlock // 持久化失败则回退
const message = buildLargeToolResultMessage(result)
return { ...toolResultBlock, content: message }
}持久化后的消息格式如下(src/utils/toolResultStorage.ts,第 130-140 行):
<persisted-output>
Output too large (128KB). Full output saved to: /path/to/session/tool-results/abc123.txt
Preview (first 2KB):
[预览内容]
...
</persisted-output>5.2.2 消息级别聚合预算
除了单个工具结果的限制,Claude Code 还引入了消息级别的聚合工具结果预算(Per-Message Budget)。在 src/utils/toolResultStorage.ts 的 enforceToolResultBudget 函数中(第 380-550 行),系统会计算每个 API 消息中所有 tool_result 的总大小:
export async function enforceToolResultBudget(
messages: Message[],
state: ContentReplacementState,
skipToolNames: ReadonlySet<string> = new Set(),
): Promise<{ messages: Message[]; newlyReplaced: ToolResultReplacementRecord[] }> {
// 收集候选 tool_result
const candidatesByMessage = collectCandidatesByMessage(messages)
// 按消息分组检查预算
for (const candidates of candidatesByMessage) {
const { mustReapply, frozen, fresh } = partitionByPriorDecision(candidates, state)
// ... 对超限的消息中的最大结果进行替换
}
}这个机制的关键设计是状态冻结:一旦某个 tool_result 被决定替换或保留,该决策会在整个会话中保持不变,以确保 prompt cache 的稳定性。状态通过 ContentReplacementState 跟踪:
export type ContentReplacementState = {
seenIds: Set<string> // 已见过的 tool_use_id
replacements: Map<string, string> // tool_use_id -> 替换后的预览字符串
}5.3 空结果与错误结果的特殊处理
5.3.1 空结果
某些工具可能产生空输出(例如静默成功的 shell 命令)。直接返回空字符串给模型可能导致问题——某些模型会将空 tool_result 误认为是回合边界,从而提前终止输出。src/utils/toolResultStorage.ts 第 200-210 行对此做了防御性处理:
if (isToolResultContentEmpty(content)) {
logEvent('tengu_tool_empty_result', { toolName: sanitizeToolNameForAnalytics(toolName) })
return {
...toolResultBlock,
content: `(\${toolName} completed with no output)`,
}
}5.3.2 错误结果
当工具执行出错时,错误信息需要被格式化为模型能理解的格式。src/utils/toolErrors.ts 的 formatError 函数(第 8-26 行)提供了统一错误格式化:
export function formatError(error: unknown): string {
if (error instanceof AbortError) {
return error.message || INTERRUPT_MESSAGE_FOR_TOOL_USE
}
if (!(error instanceof Error)) {
return String(error)
}
const parts = getErrorParts(error)
const fullMessage = parts.filter(Boolean).join('\n').trim() || 'Command failed with no output'
// 超过 10000 字符则截断
if (fullMessage.length <= 10000) {
return fullMessage
}
const halfLength = 5000
const start = fullMessage.slice(0, halfLength)
const end = fullMessage.slice(-halfLength)
return `\${start}\n\n... [\${fullMessage.length - 10000} characters truncated] ...\n\n\${end}`
}错误结果被标记为 is_error: true,这样模型就知道这是一个失败的调用,可能需要重试或调整策略。
5.4 结果回流到消息队列
工具结果最终被格式化为 user 类型的消息,追加到消息队列中。在 src/query.ts 中,流式执行器完成的工具结果会被收集并归一化:
for (const result of streamingToolExecutor.getCompletedResults()) {
if (result.message) {
yield result.message
toolResults.push(
...normalizeMessagesForAPI([result.message], toolUseContext.options.tools)
.filter(_ => _.type === 'user'),
)
}
}这些 toolResults 随后与 assistantMessages(包含 tool_use block)一起,构成完整的"请求-响应"回合,作为下一轮循环的输入。
结果回流的整体流程可以用下图表示:
flowchart TD
A["工具执行完成"] --> B["原始输出"]
B --> C{"结果大小检查"}
C -->|空结果| D["注入占位文本
'(tool completed with no output)'"]
C -->|超过阈值| E["持久化到磁盘
返回预览消息"]
C -->|正常大小| F["直接格式化"]
D --> G["构建 tool_result block"]
E --> G
F --> G
G --> H{"是否有错误?"}
H -->|是| I["标记 is_error=true"]
H -->|否| J["标准结果"]
I --> K["追加到消息队列"]
J --> K
K --> L["应用消息级预算压缩"]
L --> M["准备下一轮 API 调用"]六、循环终止条件:何时结束
工具调用循环不是无限运行的,它会在满足特定条件时优雅终止。src/query.ts 和 src/QueryEngine.ts 中定义了多种终止条件:
6.1 模型不再调用工具
最常见的终止情况是模型决定不再调用任何工具,而是直接给出最终答案。这在 src/query.ts 第 1050-1060 行附近判断:
if (!needsFollowUp) {
// 没有 tool_use,本轮自然结束
const lastMessage = assistantMessages.at(-1)
// 处理可能的恢复逻辑(prompt-too-long、media error 等)
}当 needsFollowUp 为 false 时,说明模型返回的 assistant message 中没有 tool_use block,循环可以结束。
6.2 达到最大轮次限制
为了防止循环无限进行,Claude Code 支持设置 maxTurns 参数。src/QueryEngine.ts 第 1000-1020 行检查了这个限制:
if (message.attachment.type === 'max_turns_reached') {
yield {
type: 'result',
subtype: 'error_max_turns',
is_error: true,
num_turns: message.attachment.turnCount,
errors: [`Reached maximum number of turns (${message.attachment.maxTurns})`],
}
return
}当轮次达到上限时,系统会返回一个带有 error_max_turns 子类型的错误结果。
6.3 用户中断
当用户按下 ESC 键或发送中断信号时,abortController 被触发。src/query.ts 第 1030-1060 行处理了这种情况:
if (toolUseContext.abortController.signal.aborted) {
if (streamingToolExecutor) {
// 消费剩余结果,生成合成的 tool_result
for await (const update of streamingToolExecutor.getRemainingResults()) {
if (update.message) { yield update.message }
}
}
// 返回中断结果
return { reason: 'aborted_streaming' }
}6.4 预算耗尽
Claude Code 支持按美元金额设置查询预算。src/QueryEngine.ts 第 980-1000 行检查费用限制:
if (maxBudgetUsd !== undefined && getTotalCost() >= maxBudgetUsd) {
yield {
type: 'result',
subtype: 'error_max_budget_usd',
is_error: true,
errors: [`Reached maximum budget ($${maxBudgetUsd})`],
}
return
}6.5 结构化输出重试超限
当使用结构化输出(JSON Schema)模式时,如果模型多次无法生成符合 Schema 的输出,系统会在重试次数达到上限后终止。src/QueryEngine.ts 第 1000-1015 行:
const currentCalls = countToolCalls(this.mutableMessages, SYNTHETIC_OUTPUT_TOOL_NAME)
const callsThisQuery = currentCalls - initialStructuredOutputCalls
const maxRetries = parseInt(process.env.MAX_STRUCTURED_OUTPUT_RETRIES || '5', 10)
if (callsThisQuery >= maxRetries) {
yield {
type: 'result',
subtype: 'error_max_structured_output_retries',
is_error: true,
errors: [`Failed to provide valid structured output after ${maxRetries} attempts`],
}
return
}6.6 执行错误
当查询过程中发生不可恢复的错误时(例如 API 调用失败、网络中断等),循环也会终止。src/query.ts 第 990-1010 行的 catch 块处理了这种情况:
catch (error) {
logError(error)
yield* yieldMissingToolResultBlocks(assistantMessages, errorMessage)
yield createAssistantAPIErrorMessage({ content: errorMessage })
return { reason: 'model_error', error }
}七、总结
Claude Code 的工具调用循环是一个精心设计的、多层次的系统,它将 LLM 的推理能力与外部工具的执行能力无缝衔接。让我们回顾一下整个循环的核心要点:
- 循环驱动:由模型是否生成
tool_useblock 驱动,而非stop_reason - Schema 约束:Zod Schema 既用于 API 参数描述,也用于客户端严格验证
- 流式优化:
StreamingToolExecutor让工具在模型还在生成响应时就开始执行 - 并发控制:通过
isConcurrencySafe智能分组,安全地并行执行独立工具 - 结果压缩:多层级的大小限制(单个工具 + 消息级聚合)防止上下文爆炸
- 优雅终止:支持轮次限制、预算限制、用户中断等多种终止条件
从源码层面看,这个循环的每一行代码都体现了工程上的深思熟虑——从防御性编程("模型不擅长生成有效输入")到性能优化(流式执行),从资源管理(结果持久化)到用户体验(权限控制)。
理解了这个循环,你就掌握了 Claude Code 作为 AI Agent 的核心工作原理。下一篇文章,我们将深入探讨 query.ts 中更底层的 API 调用与流式处理机制。
参考源码位置汇总
| 源码文件 | 关键行号范围 | 说明 |
|---|---|---|
src/QueryEngine.ts | 第 180-420 行 | QueryEngine 类定义与 submitMessage 入口 |
src/QueryEngine.ts | 第 620-900 行 | 主循环消息处理与状态管理 |
src/QueryEngine.ts | 第 980-1020 行 | 终止条件检查(预算、轮次) |
src/query.ts | 第 550-850 行 | 核心循环:API 调用与 tool_use 解析 |
src/query.ts | 第 950-1060 行 | 循环终止与恢复逻辑 |
src/Tool.ts | 第 340-490 行 | Tool 类型定义与工具查找函数 |
src/services/tools/toolOrchestration.ts | 第 1-120 行 | 工具并发/串行调度 |
src/services/tools/StreamingToolExecutor.ts | 第 1-220 行 | 流式工具执行器 |
src/services/tools/toolExecution.ts | 第 275-500 行 | Tool Call 解析与权限检查 |
src/services/tools/toolExecution.ts | 第 650-720 行 | Zod 参数验证 |
src/utils/toolErrors.ts | 第 1-110 行 | 错误格式化与 Zod 错误转换 |
src/utils/toolResultStorage.ts | 第 1-140 行 | 结果持久化阈值与工具 |
src/utils/toolResultStorage.ts | 第 180-550 行 | 消息级预算强制执行 |