Claude code 源码精读 之 智能体循环

Agentic Loop

Agentic Loop(智能体循环),核心实现在 src/query.ts 的 queryLoop() 异步生成器函数。它是一个 while(true) 无限循环,每次迭代代表一次”思考→行动→观察”周期。

queryLoop() 的每次迭代(src/query.ts 中 while(true) 主循环)包含以下阶段

阶段 1:上下文预处理(Pre-Processing Pipeline)

messagesForQuery(原始消息)
applyToolResultBudget()    — 工具结果预算截断(按 maxResultSizeChars)
snipCompactIfNeeded()      — 历史 Snip 压缩(HISTORY_SNIP feature)
microcompact()             — 微压缩(工具结果摘要)
applyCollapsesIfNeeded()   — 上下文折叠(CONTEXT_COLLAPSE feature)
autocompact()              — 自动压缩(超出阈值时触发)
messagesForQuery(处理后的消息)→ 发往 API

每个步骤的输出是下一步的输入,形成串行管道。Snip 和 Microcompact 的释放 token 数会传递给 autocompact 的阈值计算(snipTokensFreed),避免重复压缩。

applyToolResultBudget在 query() 调用前对消息中的工具结果应用聚合预算限制,,确保单个 API 用户消息中所有工具结果的总大小不超过预设阈值。将过大的新工具结果持久化到磁盘,替换为预览消息。

应用场景

场景一:大型文件读取

FileReadTool 读取 100KB 文件
→ 超过 DEFAULT_MAX_RESULT_SIZE_CHARS (50,000 字符)
→ persistToolResult 保存到磁盘
→ 替换为预览 + 文件路径消息
→ 模型只看到 2KB 预览

场景二:并行工具结果聚合

用户消息包含 3 个工具结果:30K + 40K + 50K = 120K
→ 超过每消息预算限制(默认 100K)
→ 选择最大的 50K 结果进行持久化
→ 总大小降为 70K,低于预算

详细分析见文章。

Claude code源码精读之上下文压缩

阶段 2:流式 API 调用(Streaming Loop)

deps.callModel() 发起流式请求(src/query.ts 中 attemptWithFallback 循环内),返回一个 AsyncGenerator。在流式过程中:

AssistantMessage 被收集到 assistantMessages[] 数组

tool_use 块 被提取到 toolUseBlocks[],设置 needsFollowUp = true

StreamingToolExecutor 在流式过程中就开始并行执行工具(不等流结束)

可恢复的错误(prompt-too-long、max-output-tokens)被暂扣(withheld),先尝试恢复

错误即时恢复:工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略

 const message of deps.callModel({
            messages: prependUserContext(messagesForQuery, userContext),
            systemPrompt: fullSystemPrompt,
            thinkingConfig: toolUseContext.options.thinkingConfig,
            tools: toolUseContext.options.tools,
            signal: toolUseContext.abortController.signal,

 ......  
 
   const msgToolUseBlocks = (
                Array.isArray(assistantMessage.message?.content)
                  ? assistantMessage.message.content
                  : []
              ).filter(
                (content: { type: string }) => content.type === 'tool_use',
              ) as ToolUseBlock[]

  .....
  
  if (
                streamingToolExecutor &&
                !toolUseContext.abortController.signal.aborted
              ) {
                for (const toolBlock of msgToolUseBlocks) {
                  streamingToolExecutor.addTool(toolBlock, assistantMessage)
                }
              }
            }

            if (
              streamingToolExecutor &&
              !toolUseContext.abortController.signal.aborted
            ) {
              for (const result of streamingToolExecutor.getCompletedResults()) {
                if (result.message) {
                  yield result.message
                  toolResults.push(
                    ...normalizeMessagesForAPI(
                      [result.message],
                      toolUseContext.options.tools,
                    ).filter(_ => _.type === 'user'),
                  )
                }
              }
            }

systemPrompt 是一个结构化的指令集合,它:

  • 定义AI的角色和行为规范(Claude Code 助手)
  • 提供环境上下文(工作目录、Git状态、操作系统等)
  • 指导工具使用策略(核心工具 vs 延迟工具、权限模式等)
  • 包含任务执行原则(代码风格、安全注意事项、确认机制等)
  • 支持动态扩展(通过 appendSystemPrompt、MCP指令、记忆机制等)
//  QueryEngine.ts
// 1. 获取默认提示部分(来自 getSystemPrompt)
const { defaultSystemPrompt, userContext, systemContext } = await fetchSystemPromptParts({...})

// 2. 组装最终 systemPrompt(可能包含自定义提示、记忆机制、追加提示等)
const systemPrompt = asSystemPrompt([
  ...(customPrompt !== undefined ? [customPrompt] : defaultSystemPrompt),
  ...(memoryMechanicsPrompt ? [memoryMechanicsPrompt] : []),
  ...(appendSystemPrompt ? [appendSystemPrompt] : []),
])

默认的 systemPrompt(来自 getSystemPrompt)包含多个章节,例如:

1. 简介部分(getSimpleIntroSection)

You are an interactive agent that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.

<网络安全风险提示...>

2. 系统部分(getSimpleSystemSection)

# System
• All text you output outside of tool use is displayed to the user...
• Tools are executed in a user-selected permission mode...
• Your tool list has two categories: core tools (Read, Edit, Write, Bash...)...
• IMPORTANT — tool priority: When a task can be done by a core tool, use that core tool directly...
• Tool results and user messages may include <system-reminder> or other tags...
• The system will automatically compress prior messages in your conversation...

3. 任务执行部分(getSimpleDoingTasksSection)

# Doing tasks
• The user will primarily request you to perform software engineering tasks...
• You are highly capable and often allow users to complete ambitious tasks...
• Default to helping. Decline a request only when helping would create a concrete...
• In general, do not propose changes to code you haven't read...
• Do not create files unless they're absolutely necessary for achieving your goal...

4. 环境信息(computeSimpleEnvInfo)

# Environment
You have been invoked in the following environment:
• Primary working directory: /home/user/projects/claude-code
• Is a git repository: Yes
• Platform: win32
• Shell: Command Prompt (cmd.exe) (use Unix shell syntax, not Windows...)
• OS Version: Windows 11 Pro 10.0.22631
• You are powered by the model named Claude 3.5 Sonnet...
• Assistant knowledge cutoff is August 2025.

5. 工具使用指南(getUsingYourToolsSection)

# Using your tools
• Break down and manage your work with the TaskCreate tool...
• Prefer dedicated tools over Bash when possible...

6. 其他动态章节

  • MCP服务器指令(如果连接了MCP服务器)
  • 临时目录说明(getScratchpadInstructions)
  • 函数结果清理说明(getFunctionResultClearingSection)
  • 令牌预算提示(如果启用 TOKEN_BUDGET feature)
  • Brief工具说明(如果启用 KAIROS feature)

阶段 3:工具执行(Tool Execution)

如果 needsFollowUp 为 true,循环不会终止,而是执行工具:

// 两种工具执行器(互斥)
const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()  // 流式:获取已完成的+等待中的
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)
for await (const update of toolUpdates) {
      if (update.message) {
        yield update.message

        if (
          update.message.type === 'attachment' &&
          update.message.attachment!.type === 'hook_stopped_continuation'
        ) {
          shouldPreventContinuation = true
        }

        toolResults.push(
          ...normalizeMessagesForAPI(
            [update.message],
            toolUseContext.options.tools,
          ).filter(_ => _.type === 'user'),
        )
      }
      if (update.newContext) {
        updatedToolUseContext = {
          ...update.newContext,
          queryTracking,
        }
      }
    }
    queryCheckpoint('query_tool_execution_end')

工具结果通过 normalizeMessagesForAPI() 标准化后,与原始消息合并,进入下一轮循环迭代。

阶段 4:终止或继续

每次迭代结束时,根据条件决定 return(终止)或 continue(继续)

终止条件(循环结束,返回 Terminal)

当以下任意情况发生时,循环会通过 return 语句结束,并返回一个包含 reason 的终止对象:

序号

终止原因

触发场景

代码位置(约)

1

blocking_limit

上下文长度达到硬性阻塞限制(且未启用自动压缩/上下文折叠等恢复机制)

第829行

2

image_error

图像尺寸或调整大小错误

第1223行

3

model_error

模型API调用抛出错误(非恢复性错误)

第1242行

4

aborted_streaming

用户在模型流式输出期间中断(Ctrl+C

第1323行

5

prompt_too_long / image_error

提示过长或媒体大小错误,且无法通过上下文折叠或响应式压缩恢复

第1447、1454行

6

stop_hook_prevented

停止钩子(stop hook)明确要求停止继续

第1554行

7

completed

正常完成:没有待处理的工具调用(needsFollowUp === false),且未触发其他错误/恢复流程

第1632行

8

aborted_tools

用户在工具执行期间中断

第1794行

9

hook_stopped

工具执行期间的钩子指示停止继续

第1799行

10

max_turns

达到 maxTurns 参数设置的最大轮次限制

第2024行

继续条件(循环进入下一轮迭代)

当以下任意情况发生时,循环会通过 continue 语句(或隐式地通过更新 state 后回到 while 顶部)进入下一轮迭代:

序号

继续原因

触发场景

代码位置(约)

1

模型回退(fallback)

当前模型触发回退(如容量不足),切换到备用模型并重试同一请求

第1193行

2

上下文折叠耗尽重试

提示过长错误后,通过“上下文折叠”机制释放已暂存的折叠内容,并重试请求

第1387行

3

响应式压缩重试

提示过长或媒体大小错误后,通过响应式压缩生成摘要,并重试请求

第1437行

4

最大输出令牌升级

达到输出令牌限制后,将 max_output_tokens 从默认值升级到更高值(如64k)并重试

第1492行

5

最大输出令牌恢复

输出令牌限制后,向对话中插入一条“继续”的元消息,让模型接着上次中断的地方继续

第1523行

6

停止钩子阻塞

停止钩子返回阻塞性错误(如“需要用户确认”),将这些错误作为消息插入后继续

第1580行

7

令牌预算继续

令牌预算机制判断当前轮次还可继续,插入 nudging 消息后继续

第1615行

8

工具调用(默认继续)

最常见的继续场景:当模型返回 tool_use 块(needsFollowUp === true)时,执行完所有工具后,将工具结果作为用户消息追加,并更新 state,循环继续处理下一轮模型调用

第2040行(隐式)

核心逻辑流程图

一个完整的迭代示例

用户:“帮我找到项目里所有未使用的导入语句,然后删掉它们”

迭代 1: 思考→行动
  预处理管道: applyToolResultBudget → snipCompact(HISTORY_SNIP feature) → microcompact → applyCollapses(CONTEXT_COLLAPSE feature) → autocompact
    → 上下文很短,无需压缩
  API 调用: 返回 tool_use(Glob, "**/*.ts")
  工具执行: 返回 42 个文件路径
  → needsFollowUp = true
  → transition: { reason: 'next_turn' }, continue

迭代 2: 思考→行动
  预处理管道: 42 个文件结果仍在预算内
  API 调用: 返回 tool_use(Grep, "import.*from")
  工具执行: 在 15 个文件中找到 120 条 import
  → needsFollowUp = true
  → transition: { reason: 'next_turn' }, continue

迭代 3: 思考→行动(多轮)
  预处理管道: 120 条 Grep 结果触发 microcompact → 摘要化
  API 调用: 返回 3 个 tool_use(FileEdit, ...)
  工具执行: 删除 5 条未使用导入
  → needsFollowUp = true
  → transition: { reason: 'next_turn' }, continue

迭代 4: 总结
  API 调用: 返回纯文本"已清理 3 个文件中的 5 条未使用导入"
  → needsFollowUp = false
  → Stop hooks 通过
  → Token Budget 检查通过(如果启用)
  → return { reason: 'completed' }

更多推荐