Claude Code 源码拆解:一个请求的生命周期
- 大语言模型
- 5天前
- 8热度
- 0评论
在现代化的AI辅助编程工具中,Claude Code 以其流畅的交互体验和高效的代码处理能力脱颖而出。当用户在终端输入“帮我重构这个模块”并看到系统在数十秒内完成文件读取、测试运行及代码编辑时,这背后隐藏着一套极其精密的工程架构。大多数技术文章仅停留在使用层面的教程,而深入探究其内部运行机制——从入口分发到多Agent递归,从流式API调用到复杂的权限检查链——对于理解高性能CLI应用的设计哲学具有重要意义。
本文将基于对 Claude Code 源码的深度逆向工程分析,拆解一个用户请求在系统内部的完整生命周期。我们将探讨其七层架构中的关键环节,包括启动时的快速路径优化、全局状态管理的“上下文护照”机制、Query Engine 的核心调度逻辑,以及独特的五级上下文压缩策略。通过剖析这些设计选择,读者不仅能理解 Claude Code 如何实现低延迟与高可靠性,还能掌握构建复杂状态机驱动型应用的通用工程模式。这种从底层原理出发的视角,有助于开发者在面对类似的高并发、长链路异步任务时,做出更优的技术选型与架构决策。
入口分发机制:启动阶段的性能优化策略
当用户在终端执行 claude 命令并按下回车键时,系统的执行流程从 cli.tsx 文件开始。这一阶段的设计核心在于最小化启动延迟,确保用户能够迅速获得反馈。源码中实现了一个典型的“快速路径”(Fast Path)机制,专门用于处理如 --version 这类无需加载重型依赖的命令。
// cli.tsx — Fast Path:零导入,12ms 退出
if (args[0] === '--version') {
console.log(`${MACRO.VERSION} (Claude Code)`);
return;
}
// 正常路径:全部延迟加载
const { main: cliMain } = await import('../main.js');在上述代码片段中,如果检测到版本查询参数,系统直接输出版本号并立即退出进程,整个过程耗时仅约12毫秒,且没有任何模块导入开销。对于其他常规命令,系统则采用动态 import() 进行懒加载。这种设计使得交互模式、一次性执行模式以及各类子命令可以根据实际需求加载对应的代码模块,避免了启动时加载所有功能导致的内存浪费和时间损耗。
在模块加载的同时,系统并行执行了多项初始化任务,包括 MDM(移动设备管理)设置读取和 Keychain(钥匙串)认证等。这种并行化处理策略显著缩短了准备时间,因为传统的串行等待会累积各步骤的延迟。整个入口层由三个关键文件协同工作:cli.tsx 负责轻量级的入口分发,判断走快速路径还是正常路径;main.tsx 作为完整的 CLI 核心,负责参数解析和环境初始化,代码量约为4690行;而 REPL.tsx 则承载了最重的交互逻辑,包括用户输入处理和输出渲染,是项目中最大的单个文件,大小达到876KB。
这种架构体现了一个重要的设计哲学:将复杂度消化在启动层,主循环才能保持单纯。如果在主循环中处理模块是否加载、认证是否完成等边界条件,将会引入大量的条件分支,降低代码的可读性和执行效率。通过在启动阶段明确所有状态和依赖,进入主循环后的逻辑只需专注于处理业务请求,从而实现了高内聚低耦合的系统结构。
全局状态管理:构建请求的“上下文护照”
一旦命令进入系统,Claude Code 需要维护一个贯穿整个会话生命周期的全局状态,以追踪项目上下文、模型使用情况、成本消耗以及功能激活状态。这些信息被集中管理在 bootstrap/state.ts 文件中,形成了一个包含80多个字段的复杂状态对象,可以被视为每个请求的“上下文护照”。
该状态对象涵盖了多个维度的信息:会话追踪字段如 sessionId、projectRoot 和 originalCwd 确保了操作在当前项目环境中的正确性;成本与Token监控字段如 totalCostUSD、modelUsage 和 totalAPIDuration 提供了实时的资源消耗视图;Skills追踪字段如 invokedSkills 记录了已调用的技能插件;此外,还有多种 Sticky-on Latches(粘性锁存器)用于控制特定行为标志位。
其中,Sticky-on Latch 机制 是解决缓存命中率问题的关键创新。Anthropic 的 Prompt Cache 特性要求请求参数必须完全一致才能命中缓存,而 Cache Key 包含系统提示词、工具定义、Beta Headers、模型版本等多个维度。例如,当用户通过 Shift+Tab 切换 Auto Mode 时,会改变 betas 列表中的 afk-mode-2026-01-31 头部信息,导致 Cache Key 变化,进而使原本命中的50-70K token缓存失效。
为了解决这一问题,系统引入了类似数字电路中锁存器的概念:一旦某个 Beta Header 被激活,就将其状态锁定,直到用户执行 /clear 或 /compact 命令才重置。
// state.ts:226-237
afkModeHeaderLatched: boolean | null, // null → true → 保持 true
fastModeHeaderLatched: boolean | null,
cacheEditingHeaderLatched: boolean | null,
thinkingClearLatched: boolean | null,在源码实现中,激活时调用 setAfkModeHeaderLatched(true) 等方法一次性写入状态;读取时根据 latch 值构建 betasParams 数组;重置时由特定命令触发统一归零。这种设计本质上是一种工程权衡:牺牲了模式切换的即时响应灵活性,换取了极高的缓存命中率,从而大幅降低了API成本和延迟。
此外,系统还实现了精细的缓存分段机制。System Prompt 被 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记拆分为四个语义段,各自拥有独立的缓存策略。第一段计费归属头不缓存;第二段CLI静态前缀在组织级别共享;第三段全局静态内容拥有最高的缓存命中率,包含核心系统指令;第四段动态内容每轮变化,不缓存。这种分段策略使得大部分静态内容能够被高效复用,同时配合 promptCacheBreakDetection.ts 中的缓存中断检测机制,监控令牌读取量的变化,帮助开发团队快速定位导致缓存失效的操作。
Query Engine:会话管理的核心中枢
当全局状态准备就绪后,用户请求被移交给 QueryEngine.ts,这是整个系统的会话管理中枢。QueryEngine.submitMessage() 方法作为请求进入核心循环前的最后一道关口,执行了一个严谨的七步流程,确保数据的一致性和处理的有序性。
class QueryEngine {
async *submitMessage(prompt, options): AsyncGenerator
<SDKMessage> {
// 1. 构建 system prompt
// 2. 处理用户输入(slash commands、attachments、hooks)
// 3. 写入 transcript(在 API 调用之前!)
// 4. Skills 和 Plugins 加载(cache-only,不阻塞网络)
// 5. yield 系统初始化消息
// 6. 进入核心循环 for await (message of query(...))
// 7. yield 最终 result
}
}在这个流程中,第三步的设计尤为值得注意:Transcript(对话转录)在 API 调用之前写入磁盘。这一操作虽然花费4-30毫秒,但提供了关键的容错能力。如果进程在随后的 API 调用中途因用户中断(Ctrl+C)或系统崩溃而终止,下次启动时可以从磁盘恢复上下文,无需从头开始。这种“先持久化状态,再执行高风险操作”的模式,是构建高可靠性系统的重要原则。
第四步涉及 Skills 和 Plugins 的加载,采用了延迟加载闭包策略。启动时仅解析文件的 frontmatter(YAML元数据),而正文内容被闭包捕获,直到实际调用时才进行编译。这意味着即使系统中存在50个 Skill,启动时也只需读取少量的元数据,避免了大量文件编译带来的启动阻塞。真正用到的 Skill 会在调用瞬间编译,实现了启动速度与运行时性能的平衡。
此外,submitMessage() 被定义为一个 AsyncGenerator(异步生成器),使用 async * 语法声明,并通过 for await...of 进行消费。与只能 resolve 一次的 Promise 不同,AsyncGenerator 可以多次 yield 值。这一特性使得 Claude Code 能够实现流式 UI 更新,即在收到部分消息时即可向用户展示;支持通过 .return() 中断执行以响应用户取消操作;并在消费方处理较慢时自动暂停,实现背压控制,防止内存溢出。
核心循环架构:While(True) 状态机与五级压缩
Query Engine 将请求打包后,送入系统的核心心脏——query.ts 中的主循环。该循环采用 while(true) 结构而非递归调用,主要基于三个考量:避免深层递归导致的栈溢出风险;通过单一的 State 对象集中管理可变状态,避免状态分散在多个栈帧中;利用 transition 字段记录每次循环的继续原因,便于调试和测试断言。
在每次 API 调用之前,系统会检查上下文窗口的使用情况,并实施五级渐进式上下文压缩策略。这种策略旨在以最低的成本维持上下文在可用范围内,只有在前一级别不足时才升级到更高级别。
| 级别 | 方法 | 触发条件 | 成本 |
|---|---|---|---|
| L0 | ToolResultBudget | 工具返回结果超预算 | 零成本(裁剪工具输出) |
| L1 | Snip | 历史消息超阈值 | 零成本(直接截断) |
| L2 | MicroCompact | 特定工具结果可删除 | 低成本(删除已知安全的消息) |
| L3 | ContextCollapse | 折叠多轮对话 | 中等(合并对话轮次) |
| L4 | AutoCompact | 上下文窗口将耗尽 | 最重(调模型总结) |
L0: ToolResultBudget 是每轮循环必定执行的步骤。当工具返回的结果超过字符限制(单工具50,000字符,单消息200,000字符)时,系统不会直接丢弃数据,而是将其持久化到磁盘,并在消息中替换为文件路径引用。模型在后续需要时可以再次读取该文件,从而保证了信息的完整性而不占用宝贵的上下文窗口。
L2: MicroCompact 利用了 API 层面的 cache_edits 指令。它不在本地修改消息数组,而是指示 API 服务端删除旧的工具结果。这种设计的优势在于容错性:如果 API 调用失败,本地状态保持不变,避免了数据不一致的问题。
L4: AutoCompact 是最后的手段,即调用模型对对话历史进行总结。为了防止陷入“上下文满->压缩失败->上下文更满”的死循环,系统引入了熔断器机制。如果连续3次自动压缩失败,系统将停止尝试,保护系统免受无限重试的资源消耗。
这五级策略在 query.ts 中严格顺序执行,每一级的输出作为下一级的输入。这种渐进式升级确保了绝大多数请求仅经过零成本的 L0 或 L1 处理,只有极少数长对话才会触及高成本的 L4 压缩,体现了“用最低成本解决问题”的工程智慧。
流式调用与工具执行:并行化的性能魔法
在进入核心循环后,Claude Code 展现了其性能优化的另一大亮点:流式调用与工具执行的并行化。传统的 AI 应用往往采用串行流程:等待 API 完整返回 -> 提取工具调用 -> 执行工具 -> 获取结果 -> 发起下一轮 API 调用。而 Claude Code 通过 StreamingToolExecutor 实现了 API 返回与工具执行的重叠。
// 伪代码示意
while (streaming) {
const chunk = await stream.next();
if (chunk.type === 'tool_use') {
executor.addTool(chunk.toolBlock); // 注册工具,立即返回,不阻塞
}
}
// API 流结束后
const results = await executor.getRemainingResults(); // 等待所有剩余工具完成当 API 流式返回数据时,每接收到一个 tool_use 块,系统立即将其注册到执行器中并开始执行,无需等待后续数据。这种重叠执行策略显著减少了总等待时间,特别是当多个工具可以并行运行时。
为了确保并行执行的安全性,每个工具定义都包含三个关键标记:isConcurrencySafe、isReadOnly 和 isDestructive。只读工具如 FileRead 和 Glob 被标记为并发安全,可以同时执行;而写入工具如 Bash 和 FileEdit 则必须串行执行,以避免竞态条件和数据损坏。
此外,系统实现了严格的错误传播机制。如果其中一个并行工具(如 Bash 命令)执行失败,siblingAbortController 会立即中止所有其他正在运行的兄弟工具。这种做法防止了部分成功、部分失败导致的不一致状态,确保了事务的原子性。
针对 API 返回可能被截断的情况,系统设计了三层恢复机制:首先尝试提升 max_output_tokens 限制;其次压缩上下文后重试;最后注入 meta 消息引导模型从断点继续。这种多层次的容错设计,进一步增强了系统在复杂网络环境和长上下文场景下的稳定性。
设计哲学:防御性编程与可恢复性
> 能并发的绝不串行,失败路径也是主路径。
在 StreamingToolExecutor 的设计中,并发不仅仅是一种性能优化手段,更是一种容错策略。通过将 API 流式响应与工具执行逻辑重叠(Overlap),系统能够在模型生成下一个 token 的同时,异步准备或执行前一个工具调用,从而显著降低端到端延迟。这种设计假设网络波动或 API 超时是常态,因此引入了三层恢复机制来确保单次 API 截断不会导致整个会话崩溃。
第一层恢复侧重于局部重试,针对瞬时的网络抖动或临时性服务端错误进行指数退避重试;第二层涉及状态回滚与上下文重组,当检测到响应结构损坏时,系统能够利用已接收的部分数据重建请求上下文,避免从头开始;第三层则是模型降级(Fallback),在主模型不可用时自动切换到备用模型或简化模式。这种“假设一切都会失败”的思维模式,使得 Claude Code 在面对不稳定的 LLM API 时,依然能提供连贯且可靠的用户体验,将异常处理从边缘情况提升为核心架构的一部分。
第 6 章:权限系统 — 8 层安全检查链
当工具准备执行时,Claude Code 必须回答一个核心安全问题:这个操作是否安全? 传统的 AI 助手往往依赖简单的用户确认弹窗,但这不仅打断工作流,还容易让用户产生“点击疲劳”而盲目授权。Claude Code 摒弃了这种粗放的方式,构建了一套严密的 8 层安全检查链,旨在实现自动化与安全性的平衡。
8 层检查详解
- 工具类型静态分析:首先判断操作的基本性质,区分只读操作(如文件读取)与写操作(如文件修改、命令执行),为后续检查设定基准风险等级。
- Tree-sitter AST 语义解析:对于 Bash 命令,系统不使用脆弱的正则表达式,而是利用 tree-sitter 将命令解析为抽象语法树(AST)。这使得系统能理解 rm -rf /tmp/foo && cat /etc/passwd 是两个独立操作的组合,而非单一字符串,从而精准识别潜在的危险链路。
- 包装器剥离与真实命令识别:针对 npx、uv run 等常用包管理器或运行器,系统会递归剥离外层包装,提取最终执行的二进制文件或脚本。这防止了攻击者通过合法的包装器掩盖恶意的底层命令,确保权限检查针对的是真实执行体。
- 环境变量白名单过滤:并非所有环境变量都适合传递给子进程。系统维护一个严格的白名单,仅允许必要的配置变量(如 PATH、HOME)透传,阻断可能泄露敏感信息(如 API Keys)的环境变量。
- 路径沙箱与访问控制:检查目标文件路径是否位于项目根目录或允许的白名单目录下。任何试图访问 /etc、/usr 或上级目录的操作都会被立即拦截,防止目录遍历攻击。
- YOLO Classifier 动态研判:在 Auto 模式下,系统调用独立的 YOLO Classifier(基于专用 Prompt 的 LLM 实例)对上下文进行语义分析。它综合命令意图、路径敏感度和历史行为,返回 allow/deny/ask 三种决策,弥补规则引擎在复杂场景下的不足。
- 决策理由记录(Decision Reasoning):每一个权限决策都伴随详细的理由记录,包括触发的规则 ID、分类器的置信度以及关键上下文片段。这不仅用于审计,也为后续的规则优化提供数据支持。
- 运行时沙箱隔离:对于被标记为高危但仍需执行的命令,系统会在受限的沙箱环境中运行,限制其网络访问、文件系统写入权限及资源使用上限,将潜在破坏控制在最小范围。
其中,Tree-sitter AST 解析 和 包装器剥离 是技术亮点。前者赋予系统理解代码语义的能力,后者则消除了间接执行带来的安全盲区。这两层机制共同作用,使得权限系统不再局限于简单的字符串匹配,而是具备了接近人类开发者的代码理解能力。
AFK Mode:智能分类器的权衡
用户可以选择 Auto Mode(又称 AFK Mode,Away From Keyboard)以摆脱频繁的确认干扰。然而,这并不意味着放弃安全管控。该模式的核心在于 YOLO Classifier,它是一个专门用于安全判定的轻量级 LLM 调用。
每次工具调用前,Classifier 接收完整的上下文快照,包括工具类型、命令内容、文件路径及环境变量。它并非执行命令,而是进行元认知推理,判断该操作在当前语境下的风险等级。虽然这引入了一次额外的 API 调用延迟,但换来了远超静态规则引擎的灵活性与准确性。对于低风险操作(如读取配置文件),它自动放行;对于高风险操作(如删除数据库),它依然强制要求用户确认;对于模糊地带,则采取保守策略询问用户。
两个容易被忽视的防御机制
除了显式的检查链,系统还内置了两个隐式防御机制以提升健壮性。规则矛盾检测(Shadowed Rule Detection) 会在加载配置时分析用户定义的权限规则。如果存在 allow Bash(git:) 和 deny Bash(git push:) 这样的冲突规则,系统会发出警告,防止因配置错误导致的安全漏洞或功能失效。
另一个机制是 拒绝追踪(Denial Tracking)。如果用户在短时间内连续拒绝同一类权限请求,系统会自动调整策略,暂时抑制该类请求的自动执行尝试,并可能主动建议用户更新权限配置。这不仅避免了“疯狂弹框”导致的用户体验恶化,也体现了系统对用户意图的动态适应能力。
权限四级模式
| 模式 | 行为特征 | 适用场景 |
|---|---|---|
| Default | 每个写操作均需用户显式确认 | 日常交互,最高安全性 |
| Auto (AFK) | YOLO Classifier 自动研判,低风险自动执行 | 长时间运行的后台任务 |
| Bypass | 跳过大部分检查,全部自动执行 | 高度隔离的 CI/CD 环境 |
| Strict | 即使读操作也需确认,禁用 Auto 模式 | 处理极高敏感度代码库 |
设计哲学
> 不是弹框,是可解释的执行链。
权限系统的核心价值不在于拦截,而在于可解释性。通过 8 层检查、决策记录、分类器判断和沙箱隔离,每一步操作都有据可查,每个决定都有理可依。这种设计将安全从一个黑盒开关转变为一个透明、可审计的工程流程。
第 7 章:附加消息 — 模型看不到的上下文
工具执行完毕后,结果需要反馈给模型以继续对话。但在 Claude Code 中,注入模型的不仅仅是工具的直接输出,还包括一系列附加消息(Attachments)。这些消息由系统自动生成,对模型可见,但对用户界面可能透明或折叠,旨在提供丰富的隐性上下文。
5 种关键附加消息
| 类型 | 作用 | 示例场景 |
|---|---|---|
| edited_text_file | 通知模型文件外部变更 | 用户在 VSCode 中手动修改了代码 |
| queued_command | 传递排队中的用户指令 | 模型执行长任务时,用户插入新 Prompt |
| task-notification | 后台异步任务完成信号 | npm install 或编译任务结束 |
| memory | 注入长期记忆检索结果 | 从 MEMORY.md 或向量库检索相关备忘 |
| skill_discovery | 动态发现可用技能 | 扫描 .claude/skills/ 目录新增技能 |
这些消息以标准的 tool_result 格式注入对话流。模型能够识别这些系统注入的信息,并据此调整后续行为。例如,收到 edited_text_file 后,模型可能会重新读取文件以获取最新状态,而不是基于旧的缓存上下文生成代码。
Drain 机制:消息路由与隔离
在多 Agent 协作场景中,Drain 机制 确保了消息的正确路由。每个 Agent 实例维护一个独立的消息队列,只消费发给自己的消息。
全局命令队列:
[cmd1: agentId=undefined] ← 主线程 Drain 消费
[cmd2: agentId="agent-A"] ← 子 Agent A Drain 消费
[cmd3: agentId="agent-B"] ← 子 Agent B Drain 消费这种设计防止了消息污染。例如,子 Agent 通常只关注 task-notification 类型的消息,即使用户在主线程发送了新的 Prompt,子 Agent 也会忽略,从而避免上下文混乱。这种基于标识的消息隔离是多 Agent 系统稳定运行的基石。
扩展点的收敛设计:Skill → Command → Tool
这是架构设计中极具洞察力的一环。尽管外部生态中存在 Skills、MCP(Model Context Protocol)、Plugins 等多种扩展形式,但在 Claude Code 内部,它们最终收敛为两种核心对象:Command 和 Tool。
关键路径解析:
- Skills 路径:Skills 从磁盘加载后,本质上被注册为 Command(类型为 prompt,来源为 skills)。它们不直接暴露给模型,而是通过一个名为 SkillTool 的桥接工具间接调用。当模型调用 SkillTool({ skill: "commit" }) 时,运行时查找对应的 Command 并展开执行。
- MCP 路径:MCP 工具在运行时从外部服务器动态拉取定义,直接包装为实现 Tool 接口的 MCPTool 对象,并标记 isMcp: true 以应用特定的权限策略。
- 最终序列化:无论来源如何,所有 Tool 最终通过 toolToAPISchema() 统一序列化为 BetaToolUnion[] 格式,这是模型 API 调用时看到的标准 Tools 参数。
为什么 Command 和 Tool 要分开?
| 维度 | Command | Tool |
|---|---|---|
| 面向对象 | CLI 用户 / 内部调度器 | LLM 模型 |
| 输入结构 | 自由文本或简单参数 | 严格 JSON Schema |
| 执行方式 | 由调度器同步/异步执行 | 自包含执行逻辑 (call()) |
| 权限检查 | 通常绕过或简化 | 经过完整 8 层检查链 |
| 典型示例 | /compact, /model | BashTool, FileEditTool |
Command 服务于人类用户或内部流程,强调灵活性;Tool 服务于模型,强调结构化与安全性。SkillTool 作为桥梁,既满足了模型对标准 Tool 接口的需求,又复用了 Command 的执行逻辑,实现了内外生态的完美解耦。
设计哲学
> 外部热闹(MCP/Skills/Plugins),内部只有两种对象:Command 和 Tool。
理解这一收敛设计,就能看透复杂扩展生态背后的简洁本质。无论未来出现多少种新的集成方式,内核只需维护 Command 和 Tool 两条路径。SkillTool 是桥,toolToAPISchema() 是出口,这种高内聚低耦合的设计极大降低了系统的维护复杂度。
第 8 章:多 Agent — 递归的边界
当任务复杂到需要并行处理或上下文隔离时,Claude Code 会启动子 Agent。但与传统的微服务或独立进程不同,这里的子 Agent 并非物理隔离的新进程,而是逻辑上的递归调用。
子 Agent 的本质:递归调用 query()
源码分析显示(runAgent.ts),子 Agent 的本质是一个递归调用的 query() AsyncGenerator 实例。
子 Agent = 递归调用 query() 的 AsyncGenerator 实例父 Agent 将当前的执行栈压入一层,创建一个新的 Generator 实例来处理子任务。这意味着子 Agent 共享父进程的内存空间、配置和环境变量,但拥有独立的上下文状态和消息队列。这种设计极大地降低了启动开销,使得创建子 Agent 几乎零成本。
6 种子 Agent 类型
系统定义了 6 种不同类型的子 Agent,分别针对代码分析、测试执行、后台编译等特定场景优化。每种类型预设了不同的资源限制、超时时间和权限范围,确保子任务在可控的边界内运行。
Fork Agent:上下文继承与递归防护
Fork Agent 是一种特殊的子 Agent,具备三个关键特性:
- 上下文继承:自动继承父 Agent 的文件状态、对话历史和环境变量,确保子任务拥有足够的背景信息。
- 输出隔离:子 Agent 的输出不会直接混入主对话流,而是通过特定通道返回,父 Agent 可选择性地汇总或展示。
- 递归防护:这是最关键的安全机制。系统维护一个递归深度计数器,防止模型陷入“创建 Agent 来创建 Agent”的死循环。一旦超过阈值,强制终止并报错。
Mailbox 通信:基于文件系统的锁机制
父子 Agent 之间的通信不依赖内存消息队列,而是基于文件系统的 Mailbox 机制。
消息以文件形式投递,并利用 proper-lockfile 库实现原子读写,防止并发冲突。这种设计虽然看似原始,但在跨进程、跨语言甚至分布式场景下具有极高的通用性和可靠性。12+ 种消息类型覆盖了任务通知、权限请求、结果返回等全生命周期事件。
权限上收:统一的用户交互界面
子 Agent 严禁直接向用户提问。如果子任务需要用户确认(如执行危险命令),它必须将请求通过 Mailbox 上收(Escalate) 到父 Agent。
子 Agent → 权限请求 → Mailbox → 父 Agent → 用户确认 → Mailbox → 子 Agent这种权限上收机制保证了用户界面的统一性。用户始终只与一个“主窗口”交互,不会面对多个同时弹出的确认对话框,极大地提升了 UX 的一致性和可控性。
Agent Swarm:三种后端执行策略
为了支持并行任务,Claude Code 提供了三种后端执行策略:
| 后端 | 实现方式 | 特点 |
|---|---|---|
| TmuxBackend | tmux split-pane + send-keys | 最高隔离性,每个子 Agent 独立终端 Pane,适合长期运行任务 |
| ITermBackend | macOS iTerm 脚本控制 | macOS 专属,提供图形化终端集成体验 |
| InProcessBackend | 同进程 AsyncGenerator | 最轻量,无额外进程开销,适合快速并行查询 |
Tmux 方案虽然“重”,但提供了操作系统级别的进程隔离和信号处理,是生产环境中最稳健的选择。
横向对比:Task vs Actor
| 维度 | Claude Code | LangGraph | AutoGen |
|---|---|---|---|
| Agent 本质 | 递归 query(),同进程任务单元 | 图节点,由编排引擎驱动 | 独立进程/线程 Actor |
| 通信方式 | 文件系统 Mailbox + 锁 | 内存 State 对象传递 | 消息队列/网络协议 |
| 权限模型 | 上收到父 Agent,统一审批 | 无内建细粒度权限 | 各 Agent 独立管理 |
| 上下文共享 | 继承父 Agent 文件状态 | 共享 State Graph | 显式消息传递 |
| 隔离方式 | Tmux Pane / 逻辑隔离 | 无物理隔离 | 进程级隔离 |
Claude Code 的选择体现了其核心哲学:把 Agent 当作 Task 而不是 Actor。它不追求 Agent 的完全自主性,而是追求任务的可控性、可追溯性和安全性。先有 Task,再有 Agent,顺序不可颠倒。
设计哲学
> 多 Agent = 任务系统,先是 Task,才是智能体。
在这种视角下,Mailbox 是任务通信总线,权限上收是任务审批流,Tmux 是任务隔离沙箱。这种设计使得多 Agent 系统不再是不可控的黑盒,而是一个结构化、可管理的任务执行引擎。
第 9 章:三层可观测性 — 用户看不到的记录
在复杂的执行生命周期中,每一步操作都被详细记录。用户看到的是简化的进度提示,而内部运行着一套三层独立但互补的可观测体系。
L1: Analytics Events — 业务事件层
代码任意位置调用 logEvent("tengu_tool_use", {...}),事件经过队列缓冲后路由至两个后端:
- Datadog:接收采样后的事件,用于实时监控和报警。敏感字段(如文件路径、未脱敏数据)被剥离,仅保留 PROTO* 以外的元数据。
- 1P BigQuery:接收完整 Payload,包含特权列。用于长期的产品分析、用户行为研究和模型效果评估。
这种双管道隔离机制确保了隐私合规:即使 Datadog 配置出错,也不会泄露敏感的用户数据。每个事件自动携带模型版本、会话 ID、环境上下文及进程指标,为数据分析提供丰富维度。
L2: OpenTelemetry — 分布式追踪层
Claude Code 定义了 6 种核心 Span 类型,构建了嵌套的追踪层级:
| Span 类型 | 含义 |
|---|---|
| interaction | 用户请求到 Claude 回复的完整周期(Root Span) |
| llm_request | 单次 LLM API 调用 |
| tool | 工具注册与权限检查阶段 |
| tool.blocked_on_user | 等待用户确认权限的耗时 |
| tool.execution | 工具实际执行的耗时 |
| hook | 前置/后置 Hook 执行耗时 |
系统使用两个独立的 AsyncLocalStorage 实例:interactionContext 管理交互级上下文,toolContext 管理工具级上下文。这种分离允许工具内部的子 Span 独立于主交互流进行追踪,避免了上下文污染。孤儿 Span 设有 30 分钟 TTL 自动清理机制,结合 WeakRef 确保内存安全。
L3: Diagnostic Tracking — IDE 诊断反馈
这是 Claude Code 独有的闭环反馈机制。在编辑文件前后,系统会自动捕获 IDE 的 LSP 诊断信息:
编辑文件前 → beforeFileEdited() → 获取诊断基线
编辑文件后 → getNewDiagnostics() → 对比基线 → 过滤出新诊断新发现的类型错误或 Lint 报错会被注入下一轮 LLM 上下文。这使得模型能够“感知”到自己的修改引入了 Bug,并有机会在下一轮对话中自动修复。这种自我修正闭环极大提升了代码生成的可用性。
用户看到的 vs 内部记录的
| 用户看到 | 内部记录 |
|---|---|
| 读取 3 个文件 | tengu_session_file_read ×3,含扩展名、读取方式 |
| 运行测试 | tengu_tool_use + completed,含耗时、成功状态 |
| Auto Mode 切换 | tengu_auto_mode_decision,含分类器置信度 |
| 模型 Fallback | 记录原始模型与降级模型的切换轨迹 |
设计哲学
> 用户看到的越少,内部记录的越多——但敏感数据必须分层隔离。
三层系统各司其职:L1 关注业务价值,L2 关注性能瓶颈,L3 关注代码质量。PROTO* 机制的本质是最小权限原则在数据层面的体现:记录一切,但让不同的消费者只看到他们有权看到的内容。
第 10 章:全局设计哲学 — 一张表看懂 Claude Code
回顾整个架构,Claude Code 的设计原则可以浓缩为以下表格:
| 设计原则 | 一句话概括 | 体现章节 |
|---|---|---|
| 复杂度前置 | 启动层确定边界,主循环保持纯净 | 第 1 章:Fast Path、动态 Import |
| 状态机心智 | 非函数调用,而是 While-True + 状态压缩 | 第 4 章:Query Loop |
| 工具制度化 | Tool 是带 Schema/Permission 的运行时对象 | 第 5 章:StreamingToolExecutor |
| 权限可解释 | 非简单弹框,而是 8 层执行链 | 第 6 章:AST 解析、决策记录 |
| 失败即主路径 | 三层恢复、Fallback、Reactive Compact | 第 5 章:容错机制 |
| 内外收敛 | 外部生态多样,内部仅 Command/Tool | 第 7 章:扩展点收敛 |
| 多 Agent = 任务 | 递归 Query,先是 Task,才是 Agent | 第 8 章:Mailbox、权限上收 |
| 分层可观测 | 三套独立系统,数据隔离 | 第 9 章:Analytics/OTEL/Diagnostic |
贯穿始终的核心哲学是:
> 假设一切都会失败,但让失败变得可恢复、可追踪、可解释。
Transcript 的先写后调实现了可恢复,三层容错实现了可恢复,Transition 字段实现了可追踪,Decision Reason 实现了可解释。这不是因为代码质量差,而是因为 LLM 系统本质上是不确定的。在这样的环境中,“假设一切正常”才是最危险的设计。
第 11 章:带走什么 — 可复用的工程 Pattern
最后,我们从 Claude Code 中提取出四个可复用的工程 Pattern,供构建 LLM 应用时参考:
Pattern 1:Sticky-on Latch — 缓存保护范式
问题:用户操作改变了缓存 Key,导致高频缓存失效。 方案:将影响缓存 Key 的参数锁定(Latch),直到显式重置点(如 /clear)才释放。 适用场景:CDN 缓存策略、数据库查询缓存、前端状态管理。这避免了因微小参数变化导致的缓存雪崩。
Pattern 2:While(True) 状态机 — 替代递归
问题:递归实现 Agent 循环导致栈溢出、状态分散、调试困难。 方案:使用 while(true) 循环 + 集中式 State 对象 + Transition 字段。 优势:栈深度恒定,状态集中管理,易于断言中间状态,日志清晰。对于超过 10 轮的 Agent 交互,这是更稳健的选择。
Pattern 3:流式穿插执行 — Pipeline 优化
问题:串行执行导致 API 等待时间长。 方案:在 Pipeline 的任意两个阶段之间,只要数据依赖允许,就重叠执行。 原理:类似 CPU 指令流水线。在 LLM 生成 Token 的同时,异步准备或执行前一个工具调用。适用于 ETL、CI/CD 及任何多阶段处理系统。
Pattern 4:权限上收 — 多 Agent UX 原则
问题:多个 Agent 同时向用户提问,造成交互混乱。 方案:子 Agent 禁止直接交互,所有用户决策请求必须上收到父 Agent 统一处理。 价值:无论后端有多少个 Agent 在并行工作,对用户而言,始终只有一个统一的交互界面。这是多 Agent 系统用户体验的黄金法则。