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 的本质。
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 v2.1.88 源码还原的核心循环实现:
// agent-loop/core.ts - Agent 主循环核心实现(简化还原)// 这是整个 Claude Code 最核心的代码,所有功能都围绕这个循环展开interfaceAgentLoopState{messages:Message[];// 完整的对话历史context:ContextWindow;// 当前上下文窗口(经过压缩)turnCount:number;// 当前轮次totalTokens:number;// 累计消耗的 Token 数pendingToolCalls:ToolCall[];// 等待执行的工具调用}classAgentLoop{privatestate:AgentLoopState;privateconfig:AgentConfig;privatetools:ToolRegistry;privatepermissions:PermissionPolicy;privatecontextManager:ContextManager;privateapiClient:APIClient;asyncrun(initialPrompt:string):Promise<AgentResult>{// 将用户的初始输入加入消息历史this.state.messages.push({role:'user',content:initialPrompt,});// ========== 核心循环开始 ==========while(true){try{// 第一步:上下文管理——确保消息不超过上下文窗口限制// 这是整个循环中最关键的工程决策之一this.state.context=awaitthis.contextManager.compress(this.state.messages,this.config.maxContextTokens);// 第二步:调用 LLM API// 传入压缩后的上下文和所有可用工具的定义constresponse=awaitthis.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 可能在一次响应中混合文本和工具调用constprocessedResponse=awaitthis.processStream(response);// 第四步:将 assistant 的响应加入消息历史this.state.messages.push({role:'assistant',content:processedResponse.content,});// 第五步:检查是否有工具调用需要执行consttoolCalls=extractToolCalls(processedResponse);if(toolCalls.length===0){// 没有工具调用 = LLM 认为任务完成或需要用户输入// 将响应展示给用户,等待下一步指令this.displayResponse(processedResponse);constuserInput=awaitthis.waitForUserInput();if(userInput===null){// 用户选择退出return{status:'completed',messages:this.state.messages};}this.state.messages.push({role:'user',content:userInput,});continue;// 继续循环}// 第六步:执行所有工具调用// 注意:某些工具调用可能需要用户确认(权限系统)consttoolResults=awaitthis.executeToolCalls(toolCalls);// 第七步:将工具执行结果加入消息历史for(constresultoftoolResults){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 限流、工具执行失败等constrecovered=awaitthis.handleError(error);if(!recovered){return{status:'error',error,messages:this.state.messages};}// 如果恢复成功,继续循环}}// ========== 核心循环结束 ==========}}这段代码虽然经过简化,但已经完整呈现了 Claude Code Agent Loop 的核心逻辑。让我们逐一解析其中的关键设计决策。
循环中的状态管理
Agent Loop 的状态管理是整个系统中最微妙的部分。状态不仅包括消息历史,还包括上下文窗口、Token 计数、工具调用状态等多个维度。
// state/conversation.ts - 对话状态管理(简化还原)// 状态管理的核心挑战:如何在有限的上下文窗口中维护尽可能多的有用信息interfaceConversationState{// === 消息层 ===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;// 重试状态(避免重复失败的操作)}classStateManager{// 创建检查点——在关键操作前保存状态快照// 这使得会话恢复成为可能asynccheckpoint(state:ConversationState):Promise<Checkpoint>{constsnapshot:Checkpoint={id:generateId(),timestamp:Date.now(),messages:deepClone(state.fullHistory),metadata:{turnCount:state.turnCount,tokenUsage:state.tokenUsage,},};// 异步持久化到磁盘,不阻塞主循环awaitthis.persistence.save(snapshot);returnsnapshot;}// 状态压缩——当消息历史过长时进行压缩// 这是上下文管理的核心操作asynccompress(state:ConversationState):Promise<ConversationState>{consttokenCount=this.countTokens(state.fullHistory);if(tokenCount<=this.config.maxContextTokens){returnstate;// 未超过限制,无需压缩}// 压缩策略:保留系统提示 + 最近 N 轮 + 关键工具结果摘要constcompressed=awaitthis.contextManager.compress(state.fullHistory);return{...state,activeContext:compressed,// 注意:fullHistory 仍然保留完整历史,只是 activeContext 被压缩了};}}这里有一个重要的设计决策:fullHistory 和 activeContext 的分离。完整历史始终保留在内存中(或持久化到磁盘),但只有 activeContext 会被发送给 LLM。这种设计使得:
- 会话恢复成为可能——即使 activeContext 被压缩了,完整历史仍然可用
- 调试追踪可以回溯到任何一轮的完整状态
- 压缩是有损的但可逆的——如果需要,可以从 fullHistory 重新构建 activeContext
错误处理与重试
在生产环境中,Agent Loop 面临的错误类型远比想象中多样:
// agent-loop/error-handler.ts - 错误处理与重试策略(简化还原)// 这个模块体现了「在生产环境中,一切都会出错」的工程信念classAgentErrorHandler{// 错误分类——不同类型的错误需要不同的处理策略privateclassifyError(error:Error):ErrorCategory{if(errorinstanceofAPIRateLimitError){return{type:'rate_limit',retryable:true,backoff:'exponential'};}if(errorinstanceofAPITimeoutError){return{type:'timeout',retryable:true,backoff:'linear'};}if(errorinstanceofContextWindowExceededError){return{type:'context_overflow',retryable:true,backoff:'compress'};}if(errorinstanceofToolExecutionError){// 工具执行错误需要特殊处理——可能是权限问题,也可能是工具本身的 bugreturn{type:'tool_error',retryable:this.isToolRetryable(error),backoff:'none'};}if(errorinstanceofAuthenticationError){return{type:'auth',retryable:false,backoff:'none'};}return{type:'unknown',retryable:false,backoff:'none'};}asynchandleError(error:Error,state:AgentLoopState):Promise<RecoveryResult>{constcategory=this.classifyError(error);switch(category.type){case'rate_limit':// 限流错误:等待 Retry-After 头指定的时间后重试constretryAfter=error.retryAfter||60;awaitthis.sleep(retryAfter*1000);return{recovered:true,action:'retry'};case'context_overflow':// 上下文溢出:触发紧急压缩,然后重试state.context=awaitthis.contextManager.emergencyCompress(state.messages,// 紧急压缩会更激进地裁剪历史{aggressive:true,preserveSystemPrompt:true});return{recovered:true,action:'retry_with_compressed_context'};case'tool_error':// 工具错误:将错误信息作为工具结果返回给 LLM// 让 LLM 自己决定如何处理——这是 ReAct 模式的精髓consttoolResult: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++;awaitthis.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 的核心思想# 去掉了所有工程细节,只保留了最本质的逻辑defagent_loop(user_message:str,tools:list[Tool])->str:"""Agent 主循环 - 所有 AI Agent 的心脏"""# 初始化状态messages=[Message(role="user",content=user_message)]turn_count=0whileTrue:# ===== 第一步:上下文压缩 =====# 如果消息总 Token 数超过上下文窗口限制,进行压缩# 压缩策略包括:消息裁剪、工具结果截断、历史摘要等ifcount_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))# ===== 第四步:判断是否需要继续 =====ifnottool_calls:# 没有工具调用 = LLM 认为任务完成# 展示响应,等待用户输入display(text_content)user_input=wait_for_input()ifuser_inputisNone:return"Session ended"messages.append(Message(role="user",content=user_input))continue# 继续循环,处理用户的下一条消息# ===== 第五步:执行工具调用 =====fortool_callintool_calls:# 权限检查:某些操作需要用户确认ifneeds_permission(tool_call):granted=ask_user_permission(tool_call)ifnotgranted:# 用户拒绝了操作,将拒绝信息反馈给 LLMmessages.append(create_tool_result(tool_call,"Permission denied by user",is_error=True))continue# 执行工具try:result=execute_tool(tool_call)exceptToolErrorase:# 工具执行失败,将错误信息反馈给 LLM# LLM 会理解错误并决定下一步行动result=create_tool_result(tool_call,str(e),is_error=True)# 将工具结果加入消息历史messages.append(create_tool_result(tool_call,result))# ===== 第六步:安全检查 =====turn_count+=1ifturn_count>=MAX_TURNS:return"Reached maximum turns"# 循环回到第一步,开始下一轮这段伪代码清晰地展示了 Agent Loop 的六个核心步骤:上下文压缩 → 调用 LLM → 解析响应 → 判断继续 → 执行工具 → 安全检查。整个循环就是这六个步骤的不断重复。
为什么 while-loop 是护城河
看到这里,你可能会问:既然 Agent Loop 的逻辑这么简单,为什么说它是护城河?
答案在于:简单的循环逻辑 × 复杂的工程实现 = 巨大的护城河。
让我用一个对比表格来说明:
| 维度 | 学术论文中的 Agent Loop | Claude Code 的 Agent Loop |
|---|---|---|
| 上下文管理 | 假设无限上下文窗口 | 五层压缩管线,动态裁剪 |
| 错误处理 | 忽略或简单重试 | 分类错误 + 智能恢复 + LLM 自主决策 |
| 工具执行 | 同步、无超时 | 异步、超时、重试、并行 |
| 状态管理 | 内存中的简单列表 | 持久化、检查点、会话恢复 |
| 安全控制 | 无 | 权限系统 + 沙箱 + 审计日志 |
| 用户交互 | 一次性输入输出 | 多轮对话 + 实时反馈 + 打断 |
| 生产就绪度 | 原型级别 | 数百万用户级别 |
每一个「看起来简单」的步骤,在生产环境中都需要大量的工程工作:
- 「调用 LLM」:需要处理流式响应、超时重试、限流退避、模型切换
- 「执行工具」:需要权限校验、沙箱隔离、超时控制、并行执行
- 「将结果喂回去」:需要上下文压缩、Token 计数、消息格式转换
- 「判断是否继续」:需要处理用户中断、最大轮次限制、错误恢复
这些工程细节的累积,构成了巨大的护城河。一个团队可以复制 Claude Code 的循环逻辑,但要复制它的工程成熟度,需要数月甚至数年的迭代。
护城河的三个层次
技术护城河:五层上下文压缩管线、智能错误恢复、流式处理优化——这些技术实现需要深入理解 LLM 的行为特性和生产环境的约束。
数据护城河:数百万用户的使用数据,使得 Anthropic 可以持续优化循环中的每一个决策点——什么时候压缩、什么时候重试、什么时候询问用户。
迭代护城河:每一个生产环境中的 bug 和 edge case,都会被转化为循环中的防御性代码。这些代码是时间的结晶,无法被简单复制。
总结
Agent 主循环是 Claude Code 乃至所有 AI Agent 系统的核心。它看起来简单——一个 while-loop,调模型,跑工具,喂结果——但在生产环境中的工程实现却极其复杂。
通过本章的源码分析,我们可以看到:
- Agent Loop 的本质是 ReAct 模式的工程化实现:思考 → 行动 → 观察 → 重复
- 状态管理是循环中最微妙的部分:fullHistory 和 activeContext 的分离是有意为之的设计
- 错误处理不是事后添加的功能,而是循环的核心逻辑:工具错误被反馈给 LLM 让其自主决策
- 简单的循环逻辑 × 复杂的工程实现 = 巨大的护城河
在下一篇文章中,我们将聚焦于 Claude Code 的启动链路——从用户输入claude命令到 Agent Loop 开始运行之间,发生了什么。
参考资料
- Claude Code v2.1.88 源码分析— 基于 2025 年 3 月泄露的 npm 包逆向分析
- Yao, S. et al. (2023). “ReAct: Synergizing Reasoning and Acting in Language Models”— ICLR 2023 — Agent Loop 的理论基础
- Anthropic (2024). “Tool Use (Function Calling) with Claude”— https://docs.anthropic.com/en/docs/tool-use — Claude 工具调用的官方文档
- Shinn, N. et al. (2023). “Reflexion: Language Agents with Verbal Reinforcement Learning”— NeurIPS 2023 — Agent 错误处理与自我反思的理论基础
- 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」,挨个发你领取方式 👇