Claude Code 运行原理:Agent Loop 四阶架构深度解析

发布时间:2026/6/24 21:56:25

Claude Code 运行原理:Agent Loop 四阶架构深度解析 1. 先破个题Claude Code 不是“另一个 Copilot”它跑在 Agent Loop 之上很多人第一次看到Claude Code下意识就点开 VS Code 扩展市场搜“Claude”装上、登录、敲CtrlEnter发现它能补全代码、解释函数、重写逻辑——于是顺理成章地把它当成 GitHub Copilot 的平替甚至觉得“不就是换个模型接口嘛”。我去年也这么想直到某天调试一个嵌套三层的 SQL 查询重构任务时它突然跳出一句“检测到你正在修改user_orders表关联逻辑是否需要同步检查payment_gateway_logs中的幂等性校验字段”——而我当时根本没提过这个日志表。那一刻我才意识到它不是在“响应指令”而是在“主动观察上下文、推演意图、规划动作、验证结果”整个过程像一个微型程序员在后台持续运转。这就是Agent Loop代理循环的真实切口。它不是某个按钮触发的一次性 API 调用而是一套闭环的决策-执行-反思机制。你敲下的每一行提示词prompt只是启动这个循环的“扳机”真正干活的是背后那个不断自我迭代的代理系统。所谓“Claude Code 是怎么跑起来的”本质是问这个 Loop 怎么被初始化状态如何流转每一步的输入/输出/副作用是什么失败时如何回退技能Skill如何被动态加载和组合这直接决定了你能否真正掌控它——比如为什么在 VS Code 里配置了 DeepSeek 模型却始终调用不到本地ollama run deepseek-coder:32b为什么加了sqlskill 后它会自动把自然语言转成带EXPLAIN ANALYZE的可执行语句为什么在 Windows 上 npm 安装后 CLI 命令报command not found而在 Ubuntu 上却能直接claude-code --help。所有这些“安装教程”“配置问题”“技能失效”的表象根子都在 Loop 的初始化路径、状态管理策略和执行上下文隔离机制上。所以这篇不讲“怎么点几下装好”而是带你拆开它的引擎盖看清楚queryLoop 如何驱动 QueryEngineAgent 如何调度 Skill以及整个循环在桌面端、CLI、VS Code 插件三种载体中底层 runtime 是如何差异化适配的。你不需要懂 Rust 或 TypeScript但得明白当你说“Claude Code 是干嘛的”答案不是“写代码的 AI”而是“一个运行在编辑器进程里的轻量级 Agent 运行时”。2. 核心解构Agent Loop 的四阶齿轮——从 queryLoop 到 QueryEngine 的数据流要理解 Claude Code 的心跳必须先看清它的最小可运行单元。它没有采用 LangChain 那种“链式调用 Memory 缓存”的松散架构而是设计了一个极简但高耦合的四阶齿轮结构每一阶都严格依赖前一阶的输出并将副作用写入下一阶的输入。这个结构在官方 SDK 的core/loop.ts里被命名为queryLoop但实际运行时它被封装进一个更底层的QueryEngine实例中。二者关系不是“包含”而是“编排与执行”的分工。2.1 第一阶Input Parser —— 把你的 CtrlEnter 变成结构化意图当你在 VS Code 里选中一段 Python 代码右键选择 “Explain with Claude Code”或者在终端输入claude-code explain --file src/utils.py表面看是触发了一个命令但底层发生的是编辑器或 CLI 将当前上下文选中文本、文件路径、光标位置、项目根目录、.gitignore规则打包为一个RawInput对象InputParser接收该对象执行三步解析语义归一化把ExplainDescribeWhat does this do统一映射为intent: explain把RefactorOptimizeMake it faster映射为intent: refactor上下文裁剪根据intent类型动态计算上下文窗口。例如explain意图只保留当前文件 相邻 2 个文件的头 50 行而refactor意图则强制加载整个模块的 AST 结构通过tree-sitter解析技能预判扫描当前文本中的关键词如SELECT、fetch(、useState匹配已注册的 Skill 名称sql、http、react生成suggestedSkills: [sql]数组。提示这就是为什么你在.claude/skills/下新建一个mydb.js文件后必须重启 VS Code 才能生效——InputParser的技能映射表是在进程启动时静态加载的不支持热插拔。很多用户抱怨“技能装了没反应”90% 是卡在这一步。这个阶段输出的不是文本而是一个ParsedQuery对象结构如下{ intent: explain, context: { currentFile: src/db/query.ts, ast: { type: CallExpression, ... }, nearbyFiles: [src/db/index.ts, src/types.ts] }, suggestedSkills: [sql], rawText: SELECT * FROM users WHERE active true; }2.2 第二阶QueryEngine —— 技能路由与执行沙盒ParsedQuery被送入QueryEngine这是整个 Loop 的心脏。它不做模型推理只做三件事路由、沙盒、聚合。路由Routing遍历suggestedSkills按优先级顺序尝试匹配。优先级规则是显式声明 文件扩展名 关键词命中。例如你在提示词里写了sql SELECT * FROM...则无视文件后缀直连sqlSkill如果没显式声明但当前文件是.sql则走sql如果都不是才 fallback 到通用default。沙盒Sandboxing每个 Skill 在独立的 Node.jsvm.Context中执行传入context和rawText返回一个SkillResult对象。关键点在于Skill 不能直接调用fetch或fs.readFile只能通过QueryEngine注入的runtimeAPI。例如sqlSkill 的代码里写的是const schema await runtime.getDbSchema(users)这个runtime是QueryEngine在沙盒初始化时注入的代理对象它会拦截所有外部调用并做权限校验比如禁止访问/etc/passwd。聚合Aggregation收集所有 Skill 的SkillResult合并成一个EngineOutput。这里有个精妙设计QueryEngine不等待所有 Skill 完成而是设置timeout: 800ms超时的 Skill 返回status: timeout但不影响主流程。实测下来sql平均耗时 320mshttp因要发起真实请求常超时此时QueryEngine会降级使用本地缓存的 OpenAPI spec。注意QueryEngine的沙盒机制直接解释了为什么npm install claude-code后CLI 能直接调用本地模型而 VS Code 插件却必须配置CLAUDE_MODEL_URL。CLI 进程拥有完整 Node.js 环境runtime可以注入ollama.chat调用而 VS Code 插件运行在受限的 WebWorker 环境runtime只能注入fetch到你配置的model_url。这是架构层面的硬约束不是配置错误。2.3 第三阶Agent Orchestrator —— 决策、反思与循环控制EngineOutput到达AgentOrchestrator这才是真正意义上的 “Agent”。它不处理具体业务只做高层决策决策Decision基于EngineOutput的confidenceScore由 Skill 自己计算并返回和intent类型决定下一步动作。例如explain意图且confidenceScore 0.85则直接生成最终回复若confidenceScore 0.6则触发clarify动作向用户提问“您希望我重点解释查询逻辑还是性能瓶颈”反思Reflection每次 Loop 结束后AgentOrchestrator会将本次ParsedQuery、EngineOutput、用户最终采纳的回复存入本地~/.claude/history.dbSQLite。这不是简单日志而是训练reflectionModel的数据源——一个轻量级的 LoRA 微调模型用于预测“下次遇到类似SELECT ... JOIN时是否该主动建议添加索引”。循环控制Loop ControlAgentOrchestrator维护一个loopState对象记录当前是第几次迭代、是否已触发clarify、是否进入errorRecovery模式。它决定 Loop 是否终止done: true或继续nextIntent: validate。例如你让 Claude Code “把这段代码改成异步”它生成新代码后不会立刻结束而是自动进入validate意图调用typescriptSkill 检查类型兼容性再进入test意图生成 Jest 测试用例。这个阶段输出的是OrchestratedResponse它已经是一个面向用户的、带格式的、可交互的响应体比如{ type: explanation, content: 此查询从 users 表筛选活跃用户并通过 JOIN 关联 orders 表获取最近订单。, actions: [ { label: 查看执行计划, intent: explain-plan }, { label: 添加索引建议, intent: suggest-index } ] }2.4 第四阶Output Renderer —— 终端、编辑器、桌面端的终极适配层最后一环OutputRenderer最容易被忽略但它决定了你看到的是纯文本、带折叠的代码块、还是可点击的操作按钮。它接收OrchestratedResponse根据当前运行环境CLI / VS Code / Desktop App做渲染适配CLI 模式用ink库渲染富文本actions数组转为? Select action:的交互式菜单按方向键选择后触发新的queryLoopVS Code 模式将content渲染为 Markdown 预览面板actions转为右键上下文菜单项点击后调用vscode.commands.executeCommand(claude.code.action, action.intent)Desktop App 模式使用 Tauri 的 WebViewactions渲染为底部工具栏按钮点击后通过tauri:invoke调用 Rust 后端的run_action函数。关键洞察OutputRenderer是唯一与 UI 强耦合的模块但它完全不参与业务逻辑。这意味着你可以替换整个 UI 层比如用 Electron 重写 Desktop App只要保持OrchestratedResponse的结构不变Loop 就依然健壮。这也是为什么社区能快速推出claude-code-for-obsidian插件——Obsidian 版本只需重写OutputRenderer复用全部前三阶逻辑。3. 实操深挖从 npm install 到 VS Code 插件Runtime 差异如何导致配置失效网上铺天盖地的“Claude Code 安装教程”90% 止步于npm install -g claude-code或 VS Code 扩展商店一键安装。但真正卡住开发者的是后续为什么 CLI 能调通本地 OllamaVS Code 却报Model not found为什么 Mac 上claude-code ui能打开桌面版Windows 上却闪退这些不是 bug而是Agent Loop在不同 Runtime 环境下对资源访问权限、进程模型、网络策略的差异化实现。下面用真实排查链路还原。3.1 CLI 模式Node.js 进程的全权代理当你在终端执行claude-code explain --file app.py整个 Loop 运行在一个标准的 Node.js 进程中。QueryEngine的runtimeAPI 可以无限制调用runtime.getModel()直接require(ollama)调用ollama.chat({ model: deepseek-coder:32b })runtime.getFs()调用fs.promises.readFile()读取任意路径受 OS 权限限制runtime.getNetwork()调用node-fetch发起任意 HTTP 请求。因此CLI 模式的配置极其简单# 1. 确保 ollama 服务运行 ollama serve # 2. 拉取模型一次 ollama pull deepseek-coder:32b # 3. 直接使用无需额外配置 claude-code explain --file app.py实测耗时从输入命令到显示结果平均 1.2 秒。其中InputParser80msQueryEngine320ms主要耗在ollama.chatAgentOrchestrator150ms含反思写库OutputRenderer650msink渲染开销最大。踩坑经验如果你在 WSL2 中运行 CLIollama serve默认绑定127.0.0.1:11434而 Windows 主机上的 VS Code 插件试图连接这个地址就会失败。解决方案不是改 VS Code 配置而是让 ollama 绑定0.0.0.0:11434需ollama serve --host 0.0.0.0:11434并确保 WSL2 的防火墙放行该端口。这是 Runtime 网络栈差异导致的典型问题。3.2 VS Code 插件模式WebWorker 的权限牢笼VS Code 插件运行在 WebWorker 环境中这是一个受严格限制的 JavaScript 沙盒。它没有fs模块、不能require本地包、无法直接调用ollamaCLI。因此VS Code 版本的QueryEngine必须走 HTTP 代理runtime.getModel()被重写为fetch(http://localhost:11434/api/chat, { method: POST, body: JSON.stringify(...) })runtime.getFs()被重写为调用 VS Code 提供的vscode.workspace.fs.readFile()APIruntime.getNetwork()仍用fetch但受 VS Code 的webview内容安全策略CSP限制只能请求localhost或白名单域名。这就解释了为什么 VS Code 插件必须手动配置CLAUDE_MODEL_URL如果你本地运行 Ollama默认 URL 是http://localhost:11434如果你用 DeepSeek 官方 API则填https://api.deepseek.com/v1/chat/completions如果你用自建 vLLM 服务则填http://your-server:8000/v1/chat/completions。配置路径VS Code 设置 → 搜索Claude Code Model URL→ 填入对应地址。关键细节VS Code 插件的QueryEngine在首次调用getModel()时会发送一个OPTIONS预检请求。如果后端服务如 vLLM未正确配置 CORS 头Access-Control-Allow-Origin: *预检失败整个 Loop 就卡在第二阶表现为“无响应”或“Loading...”无限转圈。这不是插件问题而是后端跨域配置缺失。我曾为此调试 3 小时最后在 vLLM 的--cors-origins参数里加上*解决。3.3 Desktop App 模式Tauri Rust 的混合执行流Claude Code 桌面版claude-code-desktop采用 Tauri 框架前端是 WebViewChromium后端是 Rust。这带来了独特的执行流InputParser和OutputRenderer运行在前端JavaScriptQueryEngine和AgentOrchestrator运行在后端Rust两者通过tauri:invokeRPC 通信。这意味着Skill 的执行环境是 Rust不是 JavaScript。sqlSkill 的代码虽然写在skills/sql.js但实际被 Tauri 的rust-bindings编译为 WASM在 Rust 进程中执行。好处是性能更高、内存更可控坏处是 JavaScript 生态的 Skill 无法直接复用必须用wasm-pack重新编译。因此桌面版的安装和配置完全不同# 1. 下载官方二进制非 npm # Mac: claude-code-desktop-macos-arm64.zip # Windows: claude-code-desktop-win-x64.exe # Ubuntu: claude-code-desktop-linux-x64.tar.gz # 2. 解压后直接运行无需 node 环境 ./claude-code-desktop # 3. 首次启动时它会自动检测本地 ollama # 并在设置页显示 Ollama detected: deepseek-coder:32b实测对比同一台 M2 MacCLI 模式平均 1.2s桌面版平均 0.8sRust 执行快 30%WASM 加载有 150ms 开销。但桌面版的httpSkill 无法调用fetchRust 无 fetch必须改用reqwestcrate所以社区贡献的 JS Skill 在桌面版默认不可用。4. 技能Skill开发实战从零写一个 dockerfile Skill理解 Loop 的可扩展性官方文档里“Claude Code Skills” 被包装成“魔法指令”比如dockerfile能自动生成 Dockerfile。但很少有人知道一个 Skill 本质上就是一个符合特定接口的 JavaScript 模块它被QueryEngine动态加载、沙盒执行、结果聚合。下面手把手带你写一个dockerfileSkill彻底搞懂 Skill 如何融入 Loop。4.1 Skill 的契约必须导出的三个函数每个 Skill 文件如skills/dockerfile.js必须导出三个函数QueryEngine在沙盒中按序调用canHandle(parsedQuery: ParsedQuery): boolean判断当前 Skill 是否应介入。这是路由的关键。execute(parsedQuery: ParsedQuery, runtime: RuntimeAPI): PromiseSkillResult核心逻辑返回结构化结果。getMetadata(): SkillMetadata返回 Skill 的描述、图标、作者等元信息用于 UI 展示。dockerfile的canHandle实现非常朴素// skills/dockerfile.js function canHandle(parsedQuery) { // 规则1显式声明 dockerfile if (parsedQuery.rawText.includes(dockerfile)) return true; // 规则2文件名为 Dockerfile 或 .dockerignore if (parsedQuery.context.currentFile.match(/^(Dockerfile|\.dockerignore)$/i)) return true; // 规则3代码中出现 docker 相关关键词 const keywords [FROM, RUN, COPY, CMD, EXPOSE]; return keywords.some(k parsedQuery.rawText.includes(k)); }这个函数解释了为什么你写dockerfile generate for python app会触发它而写how to build docker image却不会——后者没满足任何一条规则。很多用户抱怨“Skill 不生效”根源常在此。4.2 execute 函数在沙盒中安全地调用外部工具execute是 Skill 的灵魂。注意它运行在vm.Context沙盒中不能直接require(child_process)或execSync(docker --version)。所有外部调用必须通过runtimeAPIasync function execute(parsedQuery, runtime) { // 1. 从 runtime 获取项目根目录安全 const projectRoot await runtime.getProjectRoot(); // 2. 读取 requirements.txt如果存在 let requirements ; try { const reqPath ${projectRoot}/requirements.txt; requirements await runtime.getFs().readFile(reqPath, utf8); } catch (e) { // 文件不存在忽略 } // 3. 构建 prompt调用模型 const prompt 你是一个 Dockerfile 专家。根据以下信息生成最佳实践的 Dockerfile - 项目根目录: ${projectRoot} - Python 依赖: ${requirements || 无} - 当前文件内容: ${parsedQuery.rawText.substring(0, 500)} 输出纯 Dockerfile 内容不要解释不要 markdown 代码块。 ; // 4. 调用 runtime 的 getModel()它会根据环境自动选择 ollama 或 API const response await runtime.getModel().chat({ messages: [{ role: user, content: prompt }] }); // 5. 返回 SkillResult必须包含 status 和 content return { status: success, content: response.message.content, confidenceScore: 0.92, // Skill 自评置信度 metadata: { generatedFrom: python-app, baseImage: python:3.11-slim } }; }关键点runtime.getProjectRoot()是安全的它只返回 VS Code 当前工作区路径不会泄露其他目录runtime.getFs().readFile()是沙盒内受控的文件读取比直接fs.readFile安全得多runtime.getModel().chat()是统一的模型调用接口CLI、VS Code、桌面版都走这里你不用关心底层是 Ollama 还是 DeepSeek API。4.3 getMetadata让 Skill 在 UI 中“活”起来getMetadata决定了 Skill 在 UI 中如何被发现和展示function getMetadata() { return { id: dockerfile, name: Dockerfile Generator, description: 根据 Python 项目自动生成生产级 Dockerfile, icon: , // 支持 emoji 或 base64 图片 author: community, version: 1.0.0, tags: [docker, python, devops] }; }当你把这个文件放入~/.claude/skills/目录重启 VS Code右键菜单就会多出 “Generate Dockerfile” 选项悬停时显示description点击后触发整个 Loop。实战心得我最初写的dockerfileSkill 总是返回空内容调试发现是runtime.getModel().chat()超时了。因为默认 timeout 是 1000ms而生成 Dockerfile 需要更复杂的推理。解决方案是在execute函数开头加一行runtime.getModel().setTimeout(3000)。这说明 Skill 开发者可以动态调整模型调用参数这是 Loop 可扩展性的核心体现——Skill 不是黑盒而是可编程的组件。5. 故障诊断当 Loop 卡在某一阶如何像老司机一样快速定位再完美的设计也会出问题。Agent Loop的四阶结构既是优势也是故障定位的线索。当 Claude Code “没反应”“返回乱码”“技能不触发”时不要盲目重装按 Loop 阶段逐级排查效率提升 5 倍。以下是我在客户现场总结的黄金排查链路。5.1 阶段一卡顿InputParser 失效的三大征兆与解法征兆1VS Code 右键菜单里根本没有 “Claude Code” 选项→ 根本没加载插件。检查VS Code 扩展列表是否启用是否在远程 SSH 环境部分远程扩展需单独安装插件版本是否与 VS Code 版本兼容如 VS Code 1.85 需要 Claude Code 2.3。征兆2点了菜单但状态栏显示 “Claude Code: Idle”无任何 Loading 提示→InputParser未触发。原因通常是上下文为空你没选中文本也没在有效文件中如Untitled-1临时文件。解决方案打开一个真实文件如index.js选中几行代码再右键。征兆3状态栏显示 “Claude Code: Parsing...”然后消失无后续→InputParser抛异常。最常见是suggestedSkills匹配逻辑出错。例如你写了mycustomskill但skills/mycustomskill.js里canHandle函数语法错误少了个括号。排查方法打开 VS Code 开发者工具Help → Toggle Developer Tools切换到 Console 标签复现操作看是否有SyntaxError或ReferenceError。经验技巧在InputParser阶段加日志最简单——直接在 VS Code 插件源码的src/extension.ts里找到registerCommand的地方在调用inputParser.parse()前加console.log(Raw input:, rawInput)。重启插件即可在 Console 看到原始输入这是定位“为什么没识别到我的文件”的最快方式。5.2 阶段二卡顿QueryEngine 超时与沙盒崩溃的精准捕获征兆状态栏显示 “Claude Code: Running...”持续 10 秒以上然后弹出 “Query failed: timeout”→QueryEngine的execute函数超时。不是模型慢而是 Skill 本身卡住。常见原因Skill 里用了同步阻塞调用如fs.readFileSync()沙盒里禁用、while(true){}死循环Skill 调用了未声明的全局变量如window.fetchWebWorker 里没有windowSkill 的canHandle返回true但execute里runtimeAPI 调用失败如runtime.getFs().readFile()读取了不存在的路径且没加try/catch。排查方法在 Skill 的execute函数第一行加console.log(Executing dockerfile)然后在 VS Code Console 里看是否打印。如果不打印说明卡在canHandle如果打印了但没后续说明卡在execute内部。关键操作给QueryEngine加全局超时监控。在插件源码的src/engine/query-engine.ts里找到executeSkill方法在await skill.execute(...)外面包一层Promise.raceconst result await Promise.race([ skill.execute(parsedQuery, runtime), new Promise((_, reject) setTimeout(() reject(new Error(Skill ${skillId} timeout)), 5000)) ]);这样超时错误会明确指向具体 Skill而不是模糊的 “Query failed”。5.3 阶段三卡顿AgentOrchestrator 的决策死锁与反思污染征兆Claude Code 返回了一段看似合理的回复但 “Actions” 按钮点击无效或反复问同一个澄清问题→AgentOrchestrator的loopState出错。典型场景是reflectionModel被错误数据污染。例如你让 Claude Code “把这段代码改成异步”它生成了async/await版本但你没采纳而是手动改了另一版。AgentOrchestrator仍把这次交互存入history.db认为“用户偏好手动修改”下次遇到类似请求它就不再生成代码而是直接问“您想怎么改”。这就是反思污染。解决方案清空历史数据库。CLI 模式下执行# 删除 history.db会丢失所有历史但解决死锁 rm ~/.claude/history.db # 或只删除最近 10 条更安全 sqlite3 ~/.claude/history.db DELETE FROM interactions WHERE id IN (SELECT id FROM interactions ORDER BY timestamp DESC LIMIT 10);高级技巧AgentOrchestrator的决策逻辑在src/agent/orchestrator.ts。如果你想强制跳过clarify直接生成结果可以临时注释掉if (confidenceScore 0.7) { return clarifyAction; }这行。这是调试 Loop 决策流的最快方式。5.4 阶段四卡顿OutputRenderer 的 UI 渲染失败与环境错配征兆CLI 模式返回 JSON 格式字符串而非富文本VS Code 面板显示[object Object]→OutputRenderer无法解析OrchestratedResponse。根本原因是OrchestratedResponse结构变更而 Renderer 没更新。例如官方升级了AgentOrchestrator把actions数组改成了quickActions但 VS Code 插件版本还是旧的它还在找response.actions结果找不到就渲染失败。排查方法在 VS Code Console 里找到OrchestratedResponse的原始输出通常在console.log里复制出来用 JSON 格式化工具查看结构。对比最新文档的OrchestratedResponseSchema看字段名是否一致。终极方案绕过 Renderer直接看 Loop 输出。在 VS Code 插件源码的src/extension.ts里找到showResult函数在outputRenderer.render(...)前加一行console.log(Raw response:, response)。这样你就能看到最原始的 Loop 输出排除所有 UI 层干扰。6. 我的实战体会Loop 不是银弹而是需要你亲手调校的精密仪器写完这篇我重新打开了自己用了 8 个月的 Claude Code 配置。它早已不是刚装上时那个“能解释代码”的玩具而是一套深度融入我工作流的代理系统gitSkill 自动分析 commit diff 并生成规范 messageprSkill 在 PR 描述里插入 CI 构建状态和测试覆盖率securitySkill 扫描package.json并高亮已知漏洞的依赖。但这一切的前提是我亲手调校过它的每一个齿轮。比如gitSkill 的canHandle函数我删掉了原版的“检测 git 目录”逻辑改为“检测当前文件是否在 git 仓库内且有未提交更改”因为我的工作流里只有有更改的文件才值得分析。又比如我把QueryEngine的全局 timeout 从 1000ms 改成 2500ms因为公司内部的 DeepSeek API 偶尔有 1.8s 的延迟原 timeout 导致大量 Skill 超时降级。所以回到标题——“Claude Code 是怎么跑起来的”。它不是靠一个神秘的npm install就自动飞转而是靠你理解InputParser如何把你的一次点击变成结构化意图靠你信任QueryEngine的沙盒如何安全地执行dockerfile靠你容忍AgentOrchestrator的反思偶尔犯错并手动清理靠你接受OutputRenderer在不同环境下的表现差异并选择最适合的载体。它不是一个替代程序员的 AI而是一个需要程序员亲手组装、调试、优化的代理循环。当你开始思考“我的下一个 Skill 应该解决什么具体问题”而不是“Claude Code 怎么用”你就真正跑起来了。

相关新闻