Claude Code 源码深度解析(二):系统主循环与 Agent Loop 实现声明:📝 作者:甜城瑞庄的核桃(ZMJ)原创学习笔记,欢迎分享,但请保留作者信息及原文链接哦~系列说明:本文是"Claude Code 源码深度解析"系列的第二篇,深度解析 Claude Code 最核心的模块——系统主循环(Agent Loop)。涵盖双层生成器架构、QueryEngine 会话生命周期管理、query() 核心状态机、流式工具并行执行、错误扣留策略等关键技术点。适合有 TypeScript/Node.js 基础,对 Agent 工程感兴趣的 AI 工程师。一、Agent Loop 的本质:一次交互的全景流程理解 Claude Code 系统主循环,必须先理解一次完整交互的全貌。当用户输入一条消息,Claude Code 执行以下核心流程:用户输入 → 上下文组装 → 模型决策 → 工具执行 → 结果注入 → 继续/停止这个循环不断重复,直到模型决定不再调用工具——返回纯文本响应为止。这就是 Agent Loop(代理循环)的本质。与普通的"问答式"大模型调用相比,Agent Loop 的核心差异在于:维度普通 LLM 调用Agent Loop调用次数一次多次(直到任务完成)工具执行无自主调用 60+ 工具上下文管理简单拼接多级压缩 + 裁剪错误处理直接暴露透明恢复,用户无感知状态维护无跨轮次持久化二、双层生成器架构:分离关注点的工程智慧Claude Code 的查询系统采用双层生成器架构,这是整个主循环设计的核心骨架。2.1 架构全景┌─────────────────────────────────────────────────────────┐ │ QueryEngine │ │ (src/QueryEngine.ts, 1295行) │ │ │ │ processUserInput() │ │ submitMessage() ──────────────────────────────────► │ │ │ │ │ query() 调用 │ └──────────────────────────┼──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ query() │ │ (src/query.ts, 1729行) │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 单次迭代 │ │ │ │ 消息规范化 + 压缩 │ │ │ │ │ │ │ │ │ API 流式调用 │ │ │ │ │ │ │ │ │ 工具并行执行 │ │ │ │ │ │ │ │ │ 继续/终止? ──继续──► 下一次迭代 │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘2.2 两层的职责边界维度QueryEnginequery()作用域对话全生命周期单次查询循环状态持久化(mutableMessages、usage)循环内(State 对象每次迭代重新赋值)预算追踪USD/轮次检查,结构化输出重试Task Budget 跨压缩结转,Token 预算续写恢复策略权限拒绝、孤儿权限PTL 排水/压缩、max_output_tokens 升级/重试为什么分两层?会话管理和查询执行的关注点完全不同:QueryEngine 关心的是"用户说了什么、花了多少钱、这轮结果是否成功"query() 关心的是"消息是否需要压缩、API 返回了什么、工具执行是否成功、是否需要恢复"双层分离使每层代码更聚焦,单元测试时可以独立 mock 另一层,降低耦合复杂度。三、QueryEngine:会话生命周期管理src/QueryEngine.ts(1295 行)是对话的外壳,驱动一次完整的用户交互。3.1 完整配置参数解析// src/QueryEngine.tsexporttypeQueryEngineConfig={cwd:string// 工具执行的工作目录tools:Tools// 可用工具集(66+ 内置工具)commands:Command[]// 斜杠命令(/compact, /memory, /clear 等)mcpClients:MCPServerConnection[]// 活跃的 MCP 服务端连接agents:AgentDefinition[]// 自定义 Agent 定义(来自 .claude/agents/)canUseTool:CanUseToolFn// 权限判定函数(多层防御)getAppState:()=AppState// 读取 UI 状态setAppState:(f:(prev:AppState)=AppState)=void// Zustand 式不可变更新// 可选配置initialMessages?:Message[]// 会话恢复时的初始消息readFileCache:FileStateCache// 文件状态缓存(去重读取)customSystemPrompt?:string// 完全覆盖系统提示词appendSystemPrompt?:string// 追加到系统提示词末尾userSpecifiedModel?:string// 模型覆盖(如 claude-sonnet)fallbackModel?:string// 错误时降级模型thinkingConfig?:ThinkingConfig// 扩展思考配置maxTurns?:number// 最大工具调用轮次(安全限制)maxBudgetUsd?:number// USD 成本上限taskBudget?:{total:number}// API 侧 Token 预算jsonSchema?:Recordstring,unknown// 结构化输出 JSON Schemaverbose?:boolean// 详细调试日志abortController?:AbortController// 取消控制器orphanedPermission?:OrphanedPermission// 孤儿权限处理}几个值得重点关注的设计细节:① canUseTool 的包装机制submitMessage()内部会包装这个函数,在原有权限检查基础上追踪所有权限拒绝事件。这些拒绝记录最终返回给 SDK 消费者(如桌面应用),告知哪些操作被用户拒绝了。② readFileCache 的去重策略防止模型重复读取同一文件。如果模型在第 3 轮调用 FileReadTool 读了src/query