Agent 主循环:那个 while-loop 和它的护城河

所有 AI Agent 的核心都是一个 while-loop。调模型,跑工具,把结果喂回去,再调模型。听起来简单到令人怀疑——但正是这个简单的循环,构成了今天最强编程助手最深的工程护城河。

前言

如果你去问任何一个 AI Agent 的架构师「你们的核心是什么」,他们大概率会指着一段伪代码说:「就这个 while-loop。」

然后你会觉得很失望。一个循环?一个 while(true)?这就是传说中的 AI Agent?

是的。但这就像说「火箭引擎就是个管子往后面喷火」一样——技术上正确,但完全忽略了工程实现中的复杂度。

在上一篇文章中,我们从高空俯瞰了 Claude Code 的 25 个子系统。今天,我们要深入到整个系统的绝对核心——Agent 主循环。这个循环是 Claude Code 乃至所有 Agent 系统的心脏,理解它,就理解了 Agent 工程的本质。

Agent Loop 的本质

Agent Loop 的本质

在深入源码之前,让我们先从理论层面理解 Agent Loop 的本质。

2023 年,Yao 等人在 ICLR 上发表的 ReAct 论文提出了一个关键洞察:LLM 的推理(Reasoning)和行动(Acting)应该交替进行,而不是分离。这个洞察直接催生了现代 Agent Loop 的基本范式:

while (任务未完成) {
    1. 思考(Think)—— LLM 分析当前状态,决定下一步行动
    2. 行动(Act)—— 执行一个工具调用
    3. 观察(Observe)—— 获取工具执行的结果
    4. 将观察结果反馈给 LLM
}

这个循环的精妙之处在于:每一轮迭代都在扩展 Agent 的知识。第一轮,Agent 可能只知道用户的指令;第二轮,它可能已经读取了相关文件;第三轮,它可能已经执行了搜索命令并获得了结果。每一轮的观察结果都成为下一轮推理的输入。

Claude Code 的 Agent Loop 在此基础上进行了大量工程化改造,但核心逻辑完全一致。

Claude Code 的核心循环实现

Claude Code 的核心循环实现

让我们直接看源码。以下是根据 Claude Code v2.1.88 源码还原的核心循环实现:

// agent-loop/core.ts - Agent 主循环核心实现(简化还原)
// 这是整个 Claude Code 最核心的代码,所有功能都围绕这个循环展开

interface AgentLoopState {
  messages: Message[];          // 完整的对话历史
  context: ContextWindow;       // 当前上下文窗口(经过压缩)
  turnCount: number;            // 当前轮次
  totalTokens: number;          // 累计消耗的 Token 数
  pendingToolCalls: ToolCall[]; // 等待执行的工具调用
}

class AgentLoop {
  private state: AgentLoopState;
  private config: AgentConfig;
  private tools: ToolRegistry;
  private permissions: PermissionPolicy;
  private contextManager: ContextManager;
  private apiClient: APIClient;

  async run(initialPrompt: string): Promise<AgentResult> {
    // 将用户的初始输入加入消息历史
    this.state.messages.push({
      role: 'user',
      content: initialPrompt,
    });

    // ========== 核心循环开始 ==========
    while (true) {
      try {
        // 第一步:上下文管理——确保消息不超过上下文窗口限制
        // 这是整个循环中最关键的工程决策之一
        this.state.context = await this.contextManager.compress(
          this.state.messages,
          this.config.maxContextTokens
        );

        // 第二步:调用 LLM API
        // 传入压缩后的上下文和所有可用工具的定义
        const response = await this.apiClient.createMessage({
          model: this.config.model,
          system: this.state.context.systemPrompt,
          messages: this.state.context.messages,
          tools: this.tools.getDefinitions(),  // 注册的所有工具
          max_tokens: this.config.maxOutputTokens,
          stream: true,  // 流式响应
        });

        // 第三步:处理流式响应
        // Claude 可能在一次响应中混合文本和工具调用
        const processedResponse = await this.processStream(response);

        // 第四步:将 assistant 的响应加入消息历史
        this.state.messages.push({
          role: 'assistant',
          content: processedResponse.content,
        });

        // 第五步:检查是否有工具调用需要执行
        const toolCalls = extractToolCalls(processedResponse);

        if (toolCalls.length === 0) {
          // 没有工具调用 = LLM 认为任务完成或需要用户输入
          // 将响应展示给用户,等待下一步指令
          this.displayResponse(processedResponse);
          const userInput = await this.waitForUserInput();

          if (userInput === null) {
            // 用户选择退出
            return { status: 'completed', messages: this.state.messages };
          }

          this.state.messages.push({
            role: 'user',
            content: userInput,
          });
          continue;  // 继续循环
        }

        // 第六步:执行所有工具调用
        // 注意:某些工具调用可能需要用户确认(权限系统)
        const toolResults = await this.executeToolCalls(toolCalls);

        // 第七步:将工具执行结果加入消息历史
        for (const result of toolResults) {
          this.state.messages.push({
            role: 'user',  // 工具结果以 user 消息的形式传回
            content: [{
              type: 'tool_result',
              tool_use_id: result.id,
              content: result.output,
            }],
          });
        }

        // 更新轮次计数和 Token 统计
        this.state.turnCount++;
        this.state.totalTokens += processedResponse.usage.total_tokens;

        // 第八步:安全检查——防止无限循环
        if (this.state.turnCount >= this.config.maxTurns) {
          this.displayWarning('达到最大轮次限制,自动停止');
          return { status: 'max_turns', messages: this.state.messages };
        }

      } catch (error) {
        // 错误处理:网络错误、API 限流、工具执行失败等
        const recovered = await this.handleError(error);
        if (!recovered) {
          return { status: 'error', error, messages: this.state.messages };
        }
        // 如果恢复成功,继续循环
      }
    }
    // ========== 核心循环结束 ==========
  }
}

这段代码虽然经过简化,但已经完整呈现了 Claude Code Agent Loop 的核心逻辑。让我们逐一解析其中的关键设计决策。

循环中的状态管理

循环中的状态管理

Agent Loop 的状态管理是整个系统中最微妙的部分。状态不仅包括消息历史,还包括上下文窗口、Token 计数、工具调用状态等多个维度。

// state/conversation.ts - 对话状态管理(简化还原)
// 状态管理的核心挑战:如何在有限的上下文窗口中维护尽可能多的有用信息

interface ConversationState {
  // === 消息层 ===
  fullHistory: Message[];       // 完整的消息历史(可能超过上下文窗口)
  activeContext: Message[];     // 当前活跃的上下文(在上下文窗口内)
  
  // === 工具状态层 ===
  activeTools: Map<string, ToolExecution>;  // 正在执行的工具
  completedTools: ToolResult[];             // 已完成的工具结果
  toolCallChain: ToolCallChain;             // 工具调用链(用于调试)
  
  // === 会话元数据 ===
  sessionId: string;            // 会话唯一标识
  startTime: number;            // 会话开始时间
  turnCount: number;            // 当前轮次
  tokenUsage: TokenUsage;       // Token 使用统计
  
  // === 错误恢复状态 ===
  lastCheckpoint: Checkpoint;   // 最近一次检查点(用于会话恢复)
  retryState: RetryState;       // 重试状态(避免重复失败的操作)
}

class StateManager {
  // 创建检查点——在关键操作前保存状态快照
  // 这使得会话恢复成为可能
  async checkpoint(state: ConversationState): Promise<Checkpoint> {
    const snapshot: Checkpoint = {
      id: generateId(),
      timestamp: Date.now(),
      messages: deepClone(state.fullHistory),
      metadata: {
        turnCount: state.turnCount,
        tokenUsage: state.tokenUsage,
      },
    };
    
    // 异步持久化到磁盘,不阻塞主循环
    await this.persistence.save(snapshot);
    
    return snapshot;
  }

  // 状态压缩——当消息历史过长时进行压缩
  // 这是上下文管理的核心操作
  async compress(state: ConversationState): Promise<ConversationState> {
    const tokenCount = this.countTokens(state.fullHistory);
    
    if (tokenCount <= this.config.maxContextTokens) {
      return state;  // 未超过限制,无需压缩
    }
    
    // 压缩策略:保留系统提示 + 最近 N 轮 + 关键工具结果摘要
    const compressed = await this.contextManager.compress(state.fullHistory);
    
    return {
      ...state,
      activeContext: compressed,
      // 注意:fullHistory 仍然保留完整历史,只是 activeContext 被压缩了
    };
  }
}

这里有一个重要的设计决策:fullHistory 和 activeContext 的分离。完整历史始终保留在内存中(或持久化到磁盘),但只有 activeContext 会被发送给 LLM。这种设计使得:

  1. 会话恢复成为可能——即使 activeContext 被压缩了,完整历史仍然可用
  2. 调试追踪可以回溯到任何一轮的完整状态
  3. 压缩是有损的但可逆的——如果需要,可以从 fullHistory 重新构建 activeContext

错误处理与重试

在生产环境中,Agent Loop 面临的错误类型远比想象中多样:

// agent-loop/error-handler.ts - 错误处理与重试策略(简化还原)
// 这个模块体现了「在生产环境中,一切都会出错」的工程信念

class AgentErrorHandler {
  // 错误分类——不同类型的错误需要不同的处理策略
  private classifyError(error: Error): ErrorCategory {
    if (error instanceof APIRateLimitError) {
      return { type: 'rate_limit', retryable: true, backoff: 'exponential' };
    }
    if (error instanceof APITimeoutError) {
      return { type: 'timeout', retryable: true, backoff: 'linear' };
    }
    if (error instanceof ContextWindowExceededError) {
      return { type: 'context_overflow', retryable: true, backoff: 'compress' };
    }
    if (error instanceof ToolExecutionError) {
      // 工具执行错误需要特殊处理——可能是权限问题,也可能是工具本身的 bug
      return { type: 'tool_error', retryable: this.isToolRetryable(error), backoff: 'none' };
    }
    if (error instanceof AuthenticationError) {
      return { type: 'auth', retryable: false, backoff: 'none' };
    }
    return { type: 'unknown', retryable: false, backoff: 'none' };
  }

  async handleError(error: Error, state: AgentLoopState): Promise<RecoveryResult> {
    const category = this.classifyError(error);

    switch (category.type) {
      case 'rate_limit':
        // 限流错误:等待 Retry-After 头指定的时间后重试
        const retryAfter = error.retryAfter || 60;
        await this.sleep(retryAfter * 1000);
        return { recovered: true, action: 'retry' };

      case 'context_overflow':
        // 上下文溢出:触发紧急压缩,然后重试
        state.context = await this.contextManager.emergencyCompress(
          state.messages,
          // 紧急压缩会更激进地裁剪历史
          { aggressive: true, preserveSystemPrompt: true }
        );
        return { recovered: true, action: 'retry_with_compressed_context' };

      case 'tool_error':
        // 工具错误:将错误信息作为工具结果返回给 LLM
        // 让 LLM 自己决定如何处理——这是 ReAct 模式的精髓
        const toolResult: ToolResult = {
          tool_use_id: error.toolCallId,
          content: `Error: ${error.message}`,
          is_error: true,
        };
        state.messages.push({
          role: 'user',
          content: [{ type: 'tool_result', ...toolResult }],
        });
        return { recovered: true, action: 'continue_with_error' };

      case 'auth':
        // 认证错误:无法自动恢复,需要用户介入
        this.ui.displayError('认证失败,请检查 API Key 或重新登录');
        return { recovered: false, action: 'abort' };

      default:
        // 未知错误:记录日志,尝试有限次数的重试
        if (state.retryCount < this.config.maxRetries) {
          state.retryCount++;
          await this.sleep(1000 * state.retryCount);
          return { recovered: true, action: 'retry' };
        }
        return { recovered: false, action: 'abort' };
    }
  }
}

这段代码中最值得关注的设计是工具错误的处理方式:当一个工具执行失败时,Claude Code 不会简单地重试或终止,而是将错误信息作为工具结果返回给 LLM。这体现了 ReAct 模式的一个核心优势——LLM 可以理解错误并自主决定下一步行动。比如,如果 git commit 失败因为有未暂存的更改,LLM 可能会决定先执行 git add 再重试。

代码示例:核心循环伪代码还原

为了帮助理解,让我们用更简洁的伪代码形式还原 Agent Loop 的核心逻辑:

# agent_loop_pseudocode.py - Agent 主循环伪代码
# 这段伪代码浓缩了 Claude Code Agent Loop 的核心思想
# 去掉了所有工程细节,只保留了最本质的逻辑

def agent_loop(user_message: str, tools: list[Tool]) -> str:
    """Agent 主循环 - 所有 AI Agent 的心脏"""
    
    # 初始化状态
    messages = [Message(role="user", content=user_message)]
    turn_count = 0
    
    while True:
        # ===== 第一步:上下文压缩 =====
        # 如果消息总 Token 数超过上下文窗口限制,进行压缩
        # 压缩策略包括:消息裁剪、工具结果截断、历史摘要等
        if count_tokens(messages) > MAX_CONTEXT_TOKENS:
            messages = context_manager.compress(messages)
        
        # ===== 第二步:调用 LLM =====
        # 将消息历史和工具定义发送给 Claude API
        # 使用流式响应以提供实时反馈
        response = claude_api.create_message(
            messages=messages,
            tools=tools,           # 所有可用工具的定义
            system=SYSTEM_PROMPT,  # 系统提示词
            stream=True,           # 流式响应
        )
        
        # ===== 第三步:解析响应 =====
        # Claude 的响应可能包含文本和工具调用的混合
        text_content = extract_text(response)
        tool_calls = extract_tool_calls(response)
        
        # 将 assistant 的响应加入消息历史
        messages.append(Message(role="assistant", content=response.content))
        
        # ===== 第四步:判断是否需要继续 =====
        if not tool_calls:
            # 没有工具调用 = LLM 认为任务完成
            # 展示响应,等待用户输入
            display(text_content)
            user_input = wait_for_input()
            
            if user_input is None:
                return "Session ended"
            
            messages.append(Message(role="user", content=user_input))
            continue  # 继续循环,处理用户的下一条消息
        
        # ===== 第五步:执行工具调用 =====
        for tool_call in tool_calls:
            # 权限检查:某些操作需要用户确认
            if needs_permission(tool_call):
                granted = ask_user_permission(tool_call)
                if not granted:
                    # 用户拒绝了操作,将拒绝信息反馈给 LLM
                    messages.append(create_tool_result(
                        tool_call, "Permission denied by user", is_error=True
                    ))
                    continue
            
            # 执行工具
            try:
                result = execute_tool(tool_call)
            except ToolError as e:
                # 工具执行失败,将错误信息反馈给 LLM
                # LLM 会理解错误并决定下一步行动
                result = create_tool_result(tool_call, str(e), is_error=True)
            
            # 将工具结果加入消息历史
            messages.append(create_tool_result(tool_call, result))
        
        # ===== 第六步:安全检查 =====
        turn_count += 1
        if turn_count >= MAX_TURNS:
            return "Reached maximum turns"
        
        # 循环回到第一步,开始下一轮

这段伪代码清晰地展示了 Agent Loop 的六个核心步骤:上下文压缩 → 调用 LLM → 解析响应 → 判断继续 → 执行工具 → 安全检查。整个循环就是这六个步骤的不断重复。

为什么 while-loop 是护城河

看到这里,你可能会问:既然 Agent Loop 的逻辑这么简单,为什么说它是护城河?

答案在于:简单的循环逻辑 × 复杂的工程实现 = 巨大的护城河

让我用一个对比表格来说明:

维度学术论文中的 Agent LoopClaude Code 的 Agent Loop
上下文管理假设无限上下文窗口五层压缩管线,动态裁剪
错误处理忽略或简单重试分类错误 + 智能恢复 + LLM 自主决策
工具执行同步、无超时异步、超时、重试、并行
状态管理内存中的简单列表持久化、检查点、会话恢复
安全控制权限系统 + 沙箱 + 审计日志
用户交互一次性输入输出多轮对话 + 实时反馈 + 打断
生产就绪度原型级别数百万用户级别

每一个「看起来简单」的步骤,在生产环境中都需要大量的工程工作

  • 「调用 LLM」:需要处理流式响应、超时重试、限流退避、模型切换
  • 「执行工具」:需要权限校验、沙箱隔离、超时控制、并行执行
  • 「将结果喂回去」:需要上下文压缩、Token 计数、消息格式转换
  • 「判断是否继续」:需要处理用户中断、最大轮次限制、错误恢复

这些工程细节的累积,构成了巨大的护城河。一个团队可以复制 Claude Code 的循环逻辑,但要复制它的工程成熟度,需要数月甚至数年的迭代。

护城河的三个层次

  1. 技术护城河:五层上下文压缩管线、智能错误恢复、流式处理优化——这些技术实现需要深入理解 LLM 的行为特性和生产环境的约束。

  2. 数据护城河:数百万用户的使用数据,使得 Anthropic 可以持续优化循环中的每一个决策点——什么时候压缩、什么时候重试、什么时候询问用户。

  3. 迭代护城河:每一个生产环境中的 bug 和 edge case,都会被转化为循环中的防御性代码。这些代码是时间的结晶,无法被简单复制。

总结

Agent 主循环是 Claude Code 乃至所有 AI Agent 系统的核心。它看起来简单——一个 while-loop,调模型,跑工具,喂结果——但在生产环境中的工程实现却极其复杂。

通过本章的源码分析,我们可以看到:

  1. Agent Loop 的本质是 ReAct 模式的工程化实现:思考 → 行动 → 观察 → 重复
  2. 状态管理是循环中最微妙的部分:fullHistory 和 activeContext 的分离是有意为之的设计
  3. 错误处理不是事后添加的功能,而是循环的核心逻辑:工具错误被反馈给 LLM 让其自主决策
  4. 简单的循环逻辑 × 复杂的工程实现 = 巨大的护城河

在下一篇文章中,我们将聚焦于 Claude Code 的启动链路——从用户输入 claude 命令到 Agent Loop 开始运行之间,发生了什么。


参考资料

  1. Claude Code v2.1.88 源码分析 — 基于 2025 年 3 月泄露的 npm 包逆向分析
  2. Yao, S. et al. (2023). “ReAct: Synergizing Reasoning and Acting in Language Models” — ICLR 2023 — Agent Loop 的理论基础
  3. Anthropic (2024). “Tool Use (Function Calling) with Claude” — https://docs.anthropic.com/en/docs/tool-use — Claude 工具调用的官方文档
  4. Shinn, N. et al. (2023). “Reflexion: Language Agents with Verbal Reinforcement Learning” — NeurIPS 2023 — Agent 错误处理与自我反思的理论基础
  5. Anthropic (2025). “Claude Code Best Practices” — https://www.anthropic.com/engineering/claude-code-best-practices — Claude Code 工程实践的官方分享

本文是「Claude Code 源码深度解析」系列的第二篇。下一篇文章将聚焦于启动链路——从 claude 命令到 Agent Loop 启动之间的完整初始化过程。


本系列覆盖 AI 大模型基础、Agent 开发、MCP 协议、Skill 开发、RAG、模型微调、部署推理 七大方向,从入门到实战的全栈内容持续更新中。

所有文章的 Markdown 源文件、可运行代码、高清配图已整理成完整资料包。

👍 点赞 + ⭐ 关注,评论区扣「1」,挨个发你领取方式 👇

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐