跳到主要内容

检测 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_turntool_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: truetool_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: truetool_result,确保对话历史的配对完整性。

小结

needsFollowUp 这个简单的布尔值,承载了整个 ReAct 循环的关键决策:

  • false → Claude 已完成,退出循环
  • true → Claude 需要工具,继续循环

它的检测方式(实时监测流式内容而非依赖 stop_reason)体现了 Claude Code 在工程实践中对可靠性的重视。

📄source/src/query.tsL551-570查看源码 →
📄source/src/query.tsL826-865查看源码 →
📄source/src/query.tsL1062-1070查看源码 →