检测 stop_reason === 'tool_use':何时该执行工具
在 Claude Code 的查询循环中,每次 API 响应结束后,系统需要做一个关键判断:Claude 只是在思考/回答,还是它想要调用工具? 这个判断是 ReAct 循环的核心开关。
stop_reason 的所有可能值
Claude API 的每条 assistant message 都带有一个 stop_reason 字段,表明 Claude 停止生成的原因:
| stop_reason | 含义 |
|---|---|
end_turn | 正常结束:Claude 认为任务完成,不需要工具 |
tool_use | 工具调用:Claude 请求执行一个或多个工具 |
max_tokens | 输出 token 达到上限,被截断 |
stop_sequence | 遇到了预设的停止序列 |
pause_turn | 暂停(用于多轮任务编排) |
model_context_window_exceeded | 模型上下文窗口超出 |
refusal | 模型拒绝响应(安全策略触发) |
timeout | 响应超时 |
其中,end_turn 和 tool_use 是正常会话流程中最常见的两种。
源码中的检测逻辑
这是 query.ts 中关键的一段:
// source/src/query.ts,第 551-558 行
const toolUseBlocks: ToolUseBlock[] = []
// NOTE: stop_reason === 'tool_use' is unreliable
// Set during streaming whenever a tool_use block arrives
// — the sole loop-exit signal.
let needsFollowUp = false
注意这里的注释:stop_reason === 'tool_use' 是不可靠的。Claude Code 没有依赖 stop_reason 本身,而是在流式响应过程中实时检测 tool_use 内容块的出现。
实时检测:流式过程中标记
// source/src/query.ts,第 826-845 行
if (message.type === 'assistant') {
assistantMessages.push(message)
// 从 assistant message 中提取所有 tool_use blocks
const msgToolUseBlocks = message.message.content.filter(
content => content.type === 'tool_use',
) as ToolUseBlock[]
if (msgToolUseBlocks.length > 0) {
// 累积本轮所有 tool_use 请求
toolUseBlocks.push(...msgToolUseBlocks)
// 标记:本轮需要执行工具并继续循环
needsFollowUp = true
}
// 如果启用了流式工具执行,立即开始执行(不等流式响应结束)
if (streamingToolExecutor && !abortController.signal.aborted) {
for (const toolBlock of msgToolUseBlocks) {
streamingToolExecutor.addTool(toolBlock, message)
}
}
}
Claude 的一次响应可以包含多个 tool_use blocks,每个对应一个工具调用请求。这些 blocks 被收集到 toolUseBlocks 数组,后续统一交给 runTools() 处理。
流结束后的分支判断
// source/src/query.ts,第 1062 行
if (!needsFollowUp) {
// ---- 路径 A:Claude 回答完毕,不需要工具 ----
// 检查是否有 stop hooks、token budget 等特殊情况
// 最终返回 { reason: 'completed' }
return { reason: 'completed' }
}
// ---- 路径 B:需要执行工具 ----
// 进入 runTools() 执行工具,然后追加 tool_result,继续下一轮循环
needsFollowUp 是整个判断逻辑的核心变量。它只在流式响应期间发现 tool_use block 时才设为 true,不依赖最终的 stop_reason 字段。
为什么不用 stop_reason?
原始代码注释(第 554 行)写道:
stop_reason === 'tool_use'is unreliable -- it's not always set correctly.
这是一个来自实战经验的注释。在某些情况下(尤其是流式响应的边缘情况、某些 API 代理层、或模型内部的特殊状态),stop_reason 可能不准确。而通过检测消息内容中是否实际出现了 tool_use block,可以得到更可靠的判断。
这是"以数据为准,而非以状态字段为准"的设计原则。
从 tool_use Block 提取调用信息
每个 ToolUseBlock 包含执行工具所需的全部信息:
// Anthropic SDK 类型定义
interface ToolUseBlock {
type: 'tool_use'
id: string // 工具调用 ID(如 "toolu_01ABC...")
// 用于匹配后续的 tool_result
name: string // 工具名称(如 "Read", "Bash", "Write")
input: unknown // 工具参数(JSON 对象,根据工具 schema 解析)
}
id 字段极为关键:每个 tool_use 请求必须有一个对应的 tool_result 响应,且通过 tool_use_id 字段匹配。如果工具执行失败或被中断,也必须生成一条带有 is_error: true 的 tool_result,否则 API 会报错(对话历史中出现了"无结果的工具调用")。
多工具调用的场景
Claude 可以在一次响应中请求多个工具:
// 典型的多工具 assistant message 内容
[
{ type: "text", text: "我需要查看几个文件。" },
{
type: "tool_use",
id: "toolu_01",
name: "Read",
input: { file_path: "src/index.ts" }
},
{
type: "tool_use",
id: "toolu_02",
name: "Read",
input: { file_path: "src/utils.ts" }
}
]
这两个 Read 工具调用会被同时放入 toolUseBlocks 数组,然后在 runTools() 中按照并行/串行规则决定是同时执行还是依次执行。
工具调度决策
toolUseBlocks 收集完成后,调度决策发生在:
// source/src/query.ts,第 1380-1408 行
const toolUpdates = streamingToolExecutor
? streamingToolExecutor.getRemainingResults() // 流式执行路径(已在流过程中启动)
: runTools( // 普通执行路径
toolUseBlocks,
assistantMessages,
canUseTool,
toolUseContext,
)
for await (const update of toolUpdates) {
if (update.message) {
yield update.message // yield tool_result 消息给 UI
// 同时收集到 toolResults 数组,用于下一轮 API 调用
toolResults.push(
...normalizeMessagesForAPI([update.message], tools).filter(m => m.type === 'user')
)
}
}
这里有两条路径:
- 流式工具执行(
StreamingToolExecutor):在 API 流式响应的同时就开始执行工具,不等响应完全结束。目标是减少端到端延迟。 - 标准工具执行(
runTools):等响应完整后,再统一执行所有工具。
关于 runTools() 内部的并行/串行决策逻辑,将在下一篇文章详细介绍。
中断场景的处理
如果用户在流式响应期间按下 Ctrl+C(中断),系统需要确保所有已接收到的 tool_use blocks 都有对应的 tool_result:
// source/src/query.ts,第 1015-1029 行
if (toolUseContext.abortController.signal.aborted) {
if (streamingToolExecutor) {
// 让执行器生成"中断"的合成 tool_result
for await (const update of streamingToolExecutor.getRemainingResults()) {
if (update.message) yield update.message
}
} else {
// 为每个未执行的 tool_use 生成错误 tool_result
yield* yieldMissingToolResultBlocks(
assistantMessages,
'Interrupted by user',
)
}
return { reason: 'aborted_streaming' }
}
yieldMissingToolResultBlocks() 函数(query.ts 第 123 行)遍历所有已接收的 assistant messages,为每个 tool_use block 生成一条 is_error: true 的 tool_result,确保对话历史的配对完整性。
小结
needsFollowUp 这个简单的布尔值,承载了整个 ReAct 循环的关键决策:
false→ Claude 已完成,退出循环true→ Claude 需要工具,继续循环
它的检测方式(实时监测流式内容而非依赖 stop_reason)体现了 Claude Code 在工程实践中对可靠性的重视。