Claude Code 核心引擎源码深度剖析报告
第一章:架构全景与核心驱动流 (引言与基石)
在深入剖析 Claude Code 庞大的代码库之前,我们需要站在全局的系统架构师视角,俯瞰其核心 AI 调度模块(Core Engine)的全貌。Claude Code 并不是一个简单的“发送 Prompt 并打印 Response”的 CLI 脚本,而是一个基于事件驱动、支持复杂状态机跃迁、具备高度容错性和流式处理能力的智能代理内核。
本章将系统性地梳理 QueryEngine.ts、query.ts、coordinator/ 以及 assistant/ 这四大核心模块之间的协同关系,并探讨其背后的架构演进与设计模式。
1.1 模块职责边界与拓扑拓扑
在 Claude Code 的内核架构中,为了保证系统的高可扩展性和可测试性,研发团队对核心逻辑进行了严格的职责分层。
1. 职责划分矩阵
query.ts(底层通信与流式解析层): 这是整个系统中最贴近 Anthropic API 的底层网络层。它的职责极其纯粹:负责将上层结构化的消息载荷转换为 HTTP 请求,管理网络连接(如 Keep-Alive 优化),并通过 SSE(Server-Sent Events)协议解析增量流式响应。它包含了复杂的容错逻辑(如自动退避重试机制)和防截断算法,但不包含任何特定于 CLI 业务的逻辑。QueryEngine.ts(AI 调度引擎与状态中心): 如果说query.ts是网络管道,那么QueryEngine.ts就是指挥整个网络交通的大脑。它是整个事件循环(Event Loop)的核心,负责管理 LLM 会话的全生命周期。它将底层的流式数据包装为更高层次的生命周期事件(如onStreamChunk,onToolCall),并在此过程中注入全局配置、成本统计追踪(Cost Tracker)和工具沙盒权限校验。coordinator/(复杂编排与多步推理控制器): 对于简单的“一问一答”,QueryEngine已经足够。但当面临如“帮我重构某个目录下的所有文件”这样需要多步推理(Plan-and-Solve)的任务时,coordinator模块便会介入。它本质上是一个高级代理控制器,负责将宏观任务拆解为子任务树,评估当前进度,决定是继续让 LLM 推理、执行工具,还是抛出异常中断任务。assistant/(会话状态与交互策略代理): 该模块主要用于管理客户端会话上下文状态(如sessionHistory.ts维护的对话历史队列)。它不仅负责历史消息的结构化存储,还涉及到与用户交互策略的落地(如隐式上下文的推断、何时应当触发ask_user询问人类)。
2. 单向数据流与事件总线模型
为了防止模块间的循环依赖和强耦合,Core Engine 采用了单向数据流与事件总线(Event Bus)相结合的拓扑结构。
用户从 CLI 输入的指令,首先在 assistant 被格式化为上下文对象,随后流入 QueryEngine。QueryEngine 并不会直接调用网络,而是将其分发给 query.ts。query.ts 产生的增量响应不会通过回调函数地狱层层返回,而是通过 AsyncIterator 转化为可订阅的事件流(Stream Events)。上层模块(甚至包括 UI 渲染层的 Ink 组件)通过订阅这些事件来实现解耦的响应式更新。
3. 架构视角的取舍:集中式调度 vs 分布式 Actor
值得注意的是,Claude Code 并没有采用类似 Erlang 的纯分布式 Actor 模型,而是采用了一个带有状态机的集中式调度器(QueryEngine)。这种取舍是出于 CLI 运行环境的限制(通常在单进程内运行)以及对终端标准输出 (stdout/stderr) 竞态条件控制的需求。集中式引擎能够更安全地劫持控制台,并在工具调用(Tool Calling)时提供一致的沙盒环境隔离。
1.2 核心设计模式的运用
优秀的底层代码离不开经典设计模式的支撑,这使得数万行的 TypeScript 代码依然能保持极高的可读性。
1. 洋葱模型与中间件 (Middleware) 模式
在 QueryEngine.ts 中,请求发出前和响应返回后都存在大量的干预需求(如:敏感词过滤、强制 JSON Schema 校验、Token 余量检查)。为此,系统参考了 Koa 等 Web 框架的洋葱模型设计。
在发起实际的 query.ts 调用前后,系统会穿透一系列拦截器(Interceptors)。例如,executePostSamplingHooks 会在 LLM 返回结果后执行,一旦某个中间件检测到 LLM 试图输出越界的危险命令,可以直接在洋葱圈内层熔断请求,向 LLM 注入错误提示让其重新生成,而这对于最外层的 UI 是完全透明的。
2. 策略模式 (Strategy Pattern) 的多维应用
大模型技术日新月异,为了在不同的模型版本(如 Claude-3.5-Sonnet 与 Claude-3-Opus)之间无缝切换,内核大量使用了策略模式。
例如在上下文计算模块,不同模型具有不同的 Token 上限(Context Window)和分词器(Tokenizer)。通过动态注入 ModelStrategy,QueryEngine 可以在运行时无缝切换不同的上下文滑动窗口算法和 System Prompt 拼接策略,而无需修改主流程代码。
3. 响应式编程与异步可迭代对象 (Async Iterators)
在处理 LLM 的流式输出时,传统的基于回调(Callback)的方式容易引发“回调地狱”且难以处理背压(Backpressure)。
query.ts 和 QueryEngine.ts 广泛采用了 ES6 的 AsyncGenerator。底层网络流被包装为 for await (const chunk of stream) 的形式。这带来的巨大优势是:如果终端 UI 的渲染速度(或写入本地日志文件的速度)跟不上网络接收速度,JavaScript 引擎会在 await 处自然形成背压,暂停网络流的读取,有效防止了内存泄漏和缓冲区溢出 (OOM)。
1.3 核心数据结构解析
理解 Core Engine 的最后一块拼图是其在跨模块透传时使用的数据骨架。这些接口通常定义在 types/message.ts 及 entrypoints/agentSdkTypes.ts 中。
1. 底层流转对象模型
QueryRequest/MessageContext: 这是驱动整个流转的输入载体。它不仅包含了当前用户的自然语言指令,还封装了复杂的上下文环境元数据:当前的工作目录 (CWD)、环境变量、启用的工具列表 (Tools Schema)、甚至包括前几轮的终端异常堆栈。StreamEvent/SDKMessage: LLM 的响应在系统中并不是简单的字符串,而是被抽象为富文本流事件。每个 Chunk 都带有明确的类型签名,例如TextBlock,ToolUseBlock,TombstoneMessage(用于内存回收的标记位)等。
2. Context 树形结构与序列化设计
随着对话的深入,历史记录会变得异常庞大。在向 API 发送请求前,所有的历史 UserMessage、AssistantMessage、工具执行结果 ToolResultBlockParam 会被组织成一棵巨大的树形 Context 对象。
系统在构建这个 Context 时采用了一种“延迟序列化(Lazy Serialization)与紧凑边界(Compact Boundary)”策略。大段的文件读取结果或系统日志不会一直占用内存,而是以引用的方式存在,只有在真正发起 API 调用组装 JSON payload 时,或者触发 AutoCompactTrackingState 时,系统才会对这些沉重的数据块进行有损压缩或剔除,以此在智力表现与 Token 消耗之间达成微妙的平衡。
第二章:QueryEngine.ts —— AI 调度核心状态机与生命周期
在掌握了外围拓扑之后,我们必须深入剖析 QueryEngine.ts —— 这个充当了 Claude Code 绝对“大脑”角色的庞大模块。它不仅要与底层 API 模块(query.ts)进行流式通信,更要严格管控一次对话在本地执行时的所有环境状态。由于需要穿透工具权限、截断日志、应对意外退出,QueryEngine 被设计为一个极度健壮的异步状态机。
2.1 引擎的初始化与单例管理
QueryEngine 的实例化并不像一个普通类那样简单,它的构造函数需要接收一个高度复杂的配置对象(QueryEngineConfig)。
1. 依赖注入 (DI) 的高级应用
从源码中可以看出,QueryEngine 并未直接耦合任何具体的 UI 层、状态存储介质甚至特定的安全规则。它大量采用了控制反转(IoC)和依赖注入(Dependency Injection)。
例如,QueryEngineConfig 中要求注入如下关键依赖:
getAppState/setAppState: 用于读写全局应用状态(AppState)。通过剥离状态存储,QueryEngine既可以在交互式的 REPL(Read-Eval-Print Loop)中运行(其中AppState可能绑定到了 React hooks),也可以在 Headless(无头/静默后台)模式或 SDK 模式下稳定执行。canUseTool: 工具权限裁决函数。这是一种基于委托的安全机制。当 LLM 尝试执行诸如Bash或Write这样具有副作用的工具时,QueryEngine不自己做主,而是将其委托给外部注入的canUseTool函数,以便外部能够弹出二次确认(Consent UI)或比对静默安全白名单(Auto-approve policies)。
2. 配置项的动态加载与热更新
引擎初始化时(constructor),会通过 config.initialMessages 等参数建立基线状态。不仅如此,QueryEngine 具备良好的热更新基因。
例如,针对 readFileCache(文件状态缓存:为了避免重复读取同一个未修改的文件从而节省 Token),如果用户在会话期间通过外部编辑器修改了文件,文件状态的监听器可以借由 setReadFileCache 等方法进行更新。引擎并不绑定死板的静态配置,对于 customSystemPrompt 或 appendSystemPrompt(系统提示词注入),它也会在每次迭代(Turn)或调用 submitMessage 前重新评估这些动态计算属性。
3. 全局单例的安全性校验与多实例隔离策略
在 Claude Code 的 CLI 环境中,通常一个进程对应一个 QueryEngine。然而,为了防止由于异步任务导致的“幽灵并发”(即上一个任务还未结束,下一个任务又进入了事件循环),QueryEngine 通过内部严格的状态锁(例如检查上一次流式解析是否 done)以及唯一的 AbortController 来保证同一个实例在同一时刻只能执行一个主链路任务(Main Loop)。
如果是使用 Task 工具开启了后台子代理,系统实际上会通过沙盒环境隔离策略,分配一个新的或经过严格裁剪限制的 QueryEngine 实例,以确保主从逻辑的 mutableMessages(消息历史)互不污染。
2.2 核心状态机 (State Machine) 设计
驱动 QueryEngine 运作的核心方法是 async *submitMessage(...),这是一个返回 AsyncGenerator<SDKMessage> 的生成器函数。在它的执行生命周期中,隐藏着一个复杂的隐式状态机。
1. 状态树拆解与阶段跃迁
虽然没有使用显式的 enum State 变量,但在 submitMessage 方法中,代码执行的路径严格遵循以下阶段的跃迁:
- Idle (空闲): 等待新的 Prompt 输入。在此状态下,引擎处于
Date.now()记录点之前,主要进行清理工作,例如调用this.discoveredSkillNames.clear()。 - Preparing (准备与组装): 收到
submitMessage调用。此阶段主要任务是上下文对齐(Context Alignment)。调用fetchSystemPromptParts组装庞大的 System Prompt,结合当前 CWD、工具声明 Schema、 MCP 客户端列表,甚至根据hasAutoMemPathOverride()的条件判断是否注入特殊的记忆机制提示(Memory Mechanics Prompt)。此时,状态机仍在进行纯粹的本地同步/极速异步操作。 - Sending (网络握手): 此时所有的本地组装(包括强校验)完成,状态机将构建好的
processUserInputContext传递给底层query.ts暴露的接口。同时,如果注册了结构化输出(Structured Output / Synthetic Output Tool),相关的 Hook 也会在这个阶段挂载。 - Streaming (流式响应): 这是最漫长的阶段。底层网络握手成功,开始源源不断地收到 SSE chunk。引擎在此阶段使用
yield*(或包装的迭代器)将增量的TextBlock或ToolUseBlock向上层吐出,触发前端打字机效果。 - ToolExecution (工具执行拦截): 如果接收到的块是完整且解析成功的
ToolUseBlock,状态机暂时挂起网络等待,并根据注入的canUseTool检查权限。一旦放行,将在沙盒内执行真正的业务逻辑,将结果追加到mutableMessages中,并在需要时触发重试循环(反向提交到 Preparing 阶段继续询问 LLM)。 - Completed / Error (终端收敛): 遇到终端标志位(如 Stop Reason为
end_turn)或无法恢复的网络异常时,状态机流转到最终阶段,处理setSDKStatus、归档耗时统计,并将权限拒绝记录 (permissionDenials) 落库。
2. 状态跃迁的原子性保证
在异步的网络 I/O 期间,状态极容易发生竞态条件。例如,用户在看到 LLM 正在长篇大论时,又在控制台疯狂敲击按键触发了额外的回调。
QueryEngine 通过单一的事件队列和可变消息池(this.mutableMessages)来解决原子性。任何引起状态改变的动作(如 slash command /force-snip 导致的历史剪裁)必须通过 processUserInputContext.setMessages 以函数式更新 (fn => fn(prev)) 的方式进入闭包,以此保障在高度并发的环境下,数组变更的顺序和状态不会错乱。
3. 用户中断 (SIGINT / CTRL+C) 抢占式调度
在 CLI 界面下,用户按下 CTRL+C 是非常常见的行为。如果不对其进行干预,进程将直接崩溃,导致对话历史彻底丢失或临时文件损坏。
QueryEngine 中持有一个专用的 AbortController 实例。当用户触发中断时,系统不会立刻 process.exit(),而是:
- 调用
abortController.abort()。 - 底层
query.ts中的 fetch 流监听到 abort 信号,立即截断现有的 TCP 连接。 - 状态机会捕获这个特定类型的中断错误(如
AbortError),优雅地流转到 Error 状态。 - 在抛出异常给上层前,将已经收到的残缺文本安全地组装并压入
mutableMessages。随后发出一个类型为createUserInterruptionMessage()的特殊 Tombstone 消息,让 LLM 知道刚才的话被打断了。
2.3 Hook 机制与生命周期拦截器
为了保持引擎核心流程的纯粹性,QueryEngine 深度依赖一套灵活的钩子(Hooks)体系,这为以非侵入方式扩展系统功能提供了可能。
1. 关键生命周期钩子
虽然具体实现部分下放至底层的调度中,但在 QueryEngine 的编排下,形成了严密的拦截网:
onBeforeQuery(隐式): 对应于Preparing阶段,诸如headlessProfilerCheckpoint('before_getSystemPrompt')的打点。这是预处理的最佳时机,如动态重置 Token 计数器、更新工具白名单等。executePostSamplingHooks: 在一轮 API 调用完成后立刻触发,主要负责执行诸如日志上报、安全性扫描以及结构化输出的验证(例如确保返回的结果符合强制要求的 JSON Schema,如果不符合,就在这个 hook 中直接进行修复甚至在内部悄悄重试请求,无需透传给用户)。executeStopFailureHooks: 如果 LLM 输出因为超过最大 Token 限制 (max_tokens) 或其他异常停止,触发该容灾 Hook,以判断是否应当清理当前会话并执行降级逻辑。
2. 异步 Hook 的超时控制与执行链熔断机制
由于 Hook 可能包含网络请求(例如将使用量上报给远端分析服务器,或者请求一个额外的验证接口),系统不能容忍某个 Hook 永远挂起导致核心状态机死锁。
虽然当前可见代码中对这些 Hook 采取了 await 策略,但在核心框架的更底层设计中,它们都受到了全局 AbortController 及上下文超时设置的约束。任何抛出严重错误的 Hook(如非法的环境变异)都会导致执行链即时熔断,触发降级回滚。
3. 成本追踪与日志审计 (Cost Tracker & Audit)
在 QueryEngine 的实例属性中,始终维护着 this.totalUsage 变量。
这是一个典型的拦截器应用场景。每当 submitMessage 中的一轮 query 完结,不管它是流式成功结束还是发生意外被截断,只要从 API 响应头部获取到了 Token 使用量,系统都会调用 accumulateUsage 或更新 Cost Tracker,将本次交互的 Input Tokens 和 Output Tokens,结合当前模型(如 Opus 或 Sonnet)的单价,动态转化为美元成本(taskBudget)。这确保了即便在极其冗长的多 Agent 协作网络中,花费也能被被极其精确地统计,并在即将超支(Over Budget)前强制切断状态机。
第三章:query.ts —— LLM 通信底座与流式响应工程
如果说 QueryEngine 是发号施令的“大脑”,那么 query.ts 就是直接与外部世界(Anthropic 服务器)对抗的“肌肉与骨骼”。作为底层通信协议栈,它必须在极端不可靠的网络环境下,保障多模态数据与流式 JSON 的精确投递和组装。
3.1 网络传输层设计与内存优化
大语言模型会话通常具有“Request 包体巨大(附带全量历史/代码库),Response 持续时间极长(可达数分钟)”的特点,这对 Node.js 底层的 Fetch 产生了极大的压力。
1. Payload 内存逃逸控制 (dumpPromptsFetch)
每次发起请求时,携带了上万行代码的 Request Body 高达数兆(MB)。如果每次重试或轮询都实例化一个新的 Request 闭包并在内存中挂起,对于多轮长对话而言将很快导致 OOM(Out of Memory)。
在源码中可以看到对 createDumpPromptsFetch 极为精妙的运用。系统通过单例闭包代理,拦截真实的 fetch 调用。它在确保请求体最新版本可用(为了在开启 verbose 时能 dump 出最后一次的 Prompt 以供调试)的同时,强制丢弃旧轮次的 Payload 引用,将冗长会话的内存堆积问题巧妙化解。
2. 底层错误拦截与重试架构 (withRetry & FallbackTriggeredError)
query.ts 不仅仅是发起 API 请求,它内部还集成了一套智能重试与降级网关(调用自 services/api/withRetry.ts)。
当遇到 HTTP 502/529(Overloaded)或者 Rate Limit 限制时,它并没有立即向外抛出异常中断对话,而是自动执行基于指数退避(Exponential Backoff)的重试。
更进一步,它支持自动模型降级 (Model Fallback)。源码中的 FallbackTriggeredError 捕获块表明,如果高配模型(如 Claude-3-Opus)遭遇容量瓶颈拒绝服务,协议层可以自动切换到降级模型(如 Claude-3.5-Sonnet)重发请求,并通过 yield createSystemMessage(...) 抛出合成警告,让用户感知到降级发生,从而提供“不间断”的体验。
3.2 增量流式解析协议 (SSE) 深度定制
如何将服务端返回的一个个散碎的 Server-Sent Events (SSE) 字节组装成可用的工具指令,是流式响应工程的灵魂所在。
1. 结构化块解析与 StreamingToolExecutor
Anthropic API 在流式返回工具调用时,实际上是逐个字(Token)地吐出 JSON 结构(ToolUseBlock)。如果等到整个 JSON 接收完毕再执行工具,将浪费大量时间。
为此,query.ts 引入了 StreamingToolExecutor。它的作用相当于一个“流式拦截缓冲区”。当嗅探到当前流属于 tool_use 类型时,它会将增量的字符串追加进内部 Buffer。对于部分支持预处理的工具(例如:需要进行 AST 分析的慢速工具),这允许系统在工具参数尚未完全接收完毕时,就能提前嗅探意图甚至启动预热。
当遇到中止块(Abort)或网络意外断开时,streamingToolExecutor.discard() 能够安全地丢弃这些半成品的残片,防止将其注入到最终的 mutableMessages 中产生“JSON 格式不完整”的幻觉。
2. 合成消息 (Synthetic Messages) 补偿机制
在复杂的网络调度中,有时会导致消息队列不连贯。例如由于某种框架层面的 Bug 或网络阶段,LLM 成功发出了 tool_use 请求,但引擎在执行前抛出了致命异常,导致对应的 tool_result 缺失。
query.ts 非常具有防御性地使用了 yieldMissingToolResultBlocks 函数。这是一个自我修复例程,它会在即将抛出崩溃异常并退出生成器之前,主动扫描最后一句 AssistantMessage。如果发现其中存在孤立的、没有被关闭的 tool_use,它会“伪造”一个 tool_result 返回给历史队列(例如填入 Error: Interrupted by user),从而确保下一次请求时上下文结构的严密闭合,防止由于 API Schema 要求必须成对出现而导致的后续 400 Invalid Request 错误。
3.3 异步可迭代对象 (Async Iterators) 的高级应用
为了在整个应用栈中透传这种增量流,query.ts 大量使用了 ES2018 引入的 AsyncGenerator。
1. 基于 yield* 的平滑穿透
代码签名 export async function* query(...) 清晰地展示了其流式本质。
在内部执行主循环 queryLoop 时,通过 yield* 将内部深层递归的流直接打平并透传到最外层的 UI 框架(Ink),期间任何的 StreamEvent、RequestStartEvent 或 TextBlock 都无需组装成数组,极大降低了内存延迟。
2. 事件劫持与 UI 渲染背压
这种 for await ... of 的消费模式自带了天然的背压(Backpressure)属性。当终端 UI 在进行重型渲染(例如打印大面积高亮代码差异)时,如果事件循环阻塞,底层的 TCP Socket 读取会自动放缓,这使得整个系统的表现异常丝滑,既不会因为接收过快造成内存抖动,也不会因为 UI 卡顿丢失数据。
3. Tombstone 墓碑机制与幽灵中断修复
在中断处理(AbortController 触发)中,源码展示了如何向 UI 发送 TombstoneMessage ({ type: 'tombstone', message: msg })。这是一种幽灵引用的销毁机制——告诉上层状态机和 UI:“刚才流式输出给你的那条残余消息,现在作废了,请从 UI 上抹去它”,从而完美解决了中断残留文本的问题。
第四章:Prompt 组装、上下文滑动窗口与 Token 调度
在解决了底层的通信与状态机后,接下来决定 Claude 代码代理“智商”上限的,是其对 Prompt(提示词)和 Context(上下文)的管理能力。在真实项目中,工作区内可能包含数万个文件,如果不加以智能裁剪,任何模型的 Context Window(如 200K Tokens)都会被迅速撑爆。
4.1 Prompt 动态组装管线
在 QueryEngine.ts 发起请求前,会调用 fetchSystemPromptParts 组装系统提示词。
1. 模块化系统指令注入
Claude Code 的系统提示词并非一段写死的字符串,而是模块化动态拼装的。它包括:
- 基础设定 (Base Identity): 定义了它是一个 CLI 专家,应该简明扼要。
- 环境上下文 (Environment Context): 动态获取当前的操作系统类型、CWD(当前工作目录),甚至是终端颜色支持能力。
- 动态能力清单 (Tools & Capabilities): 当引入了不同的插件(如 MCP 客户端)或启用了特定的 Flag 时,相关的工具说明会被动态编译进 System Prompt 中。例如,若开启了记忆存储(Memory Directory),还会自动注入
loadMemoryPrompt()返回的专属引导协议。
2. 工具签名块剥离 (stripSignatureBlocks)
为了节省大量的 Token,对于已经被确认为历史消息的旧轮次,工具的详细 JSON Schema(签名块)是不需要被反复发送给大模型的。utils/messages.ts 中的过滤方法会在消息队列进入 API 组装之前,通过 stripSignatureBlocks 将多余的元数据扒除,只保留对话的精要内容。
4.2 智能上下文滑动窗口 (Sliding Window) 机制
当多轮对话的历史 Token 即将触及模型的物理上限时,services/compact/ 模块下的自动紧凑算法 (Auto Compact) 就会启动。
1. 紧凑边界 (Compact Boundary) 的设定
系统并不是简单粗暴地切断最古老的对话,因为这会丢失前置的任务目标(System 指令和初始的 Task 要求通常在第一轮)。
系统使用一种基于标记点的滑动窗口:createMicrocompactBoundaryMessage。每当进行一轮对话后,系统会测算当前的 API 消费 Token (tokenCountWithEstimation)。当达到预警水位线(calculateTokenWarningState)时,就会寻找一个“安全切割点”。在这个边界之前的日常问答(如试错的 Bash 报错、中间反复尝试的编辑动作)会被折叠甚至抛弃,只保留“用户初始目标”和“最近三四轮的上下文”。
2. 反应式压缩 (Reactive Compact) 与上下文坍缩 (Context Collapse)
从 query.ts 头部的特性开关可以看到 REACTIVE_COMPACT 和 CONTEXT_COLLAPSE 两个高级特性。
这意味着系统不光是在发请求前做截断,还具备在收到 API 返回的 "Prompt Too Long" 错误后,动态地进行事后抢救。系统会触发 buildPostCompactMessages 进行紧急瘦身,然后利用重试网关 (yield* yieldMissingToolResultBlocks 后继续循环) 再次发起请求,从而让用户在无感知的情况下平稳度过 Token 溢出的危机。
4.3 提示词缓存 (Prompt Caching) 优化
在高频的 CLI 交互中,每一次命令的间隔可能只有几秒钟。Anthropic 提供了 Prompt Caching 技术来极大降低重复长文本的费用,但这需要客户端做极为严苛的配合。
1. Caching 拦截与静态分层
代码历史和系统指令占据了绝大部分的 Token。为了让 API 服务器能命中缓存,必须保证这部分字符串在多次请求间绝对一致。
Claude Code 将频繁变动的状态(如当前时间戳、最后一条错误日志)与静态状态(如项目根目录下的全量代码结构索引)进行了隔离。通过在静态消息块末尾打上 CACHED_MAY_BE_STALE 类似的断点,确保前面的内容能作为长效 Cache 被 Anthropic API 重用。这种基于偏移量的动静分离设计,使得即便是带有巨大上下文的对话,后续的平均单轮成本也能缩减高达 90%。
第五章:coordinator/ —— 复杂任务编排与代理生命周期管控
简单的“一问一答”不足以解决诸如“重构整个 Auth 模块并修复测试”这样宏大且具有不确定性的任务。当用户意图庞大时,Claude Code 会通过 coordinatorMode.ts 与任务系统 (Task.ts) 转化为一个具备高阶思考与多步并行执行能力的“架构师”角色。本章深入剖析这套多阶段任务编排机制。
5.1 多阶段任务规划引擎 (Plan-And-Solve)
在 Coordinator 模式下,底层 QueryEngine 被赋予了特殊的系统提示词(System Prompt),将其心智强行锁定在 coordinator(协调者)而非底层代码编写者 (Worker)。
1. 意图解析与任务树 (Task Tree) 构建
系统强制将大型工作流切分为四个严格的阶段:Research(研究/并行) -> Synthesis(信息综合) -> Implementation(执行实现) -> Verification(独立校验)。
在这个闭环中,Coordinator 被禁止自己亲自下场运行 Bash 工具或直接读取文件,而是必须通过 Agent 工具来派发任务。这在架构上逼迫大模型在执行任何操作前,先绘制一棵清晰的“任务依赖树”(Dependency Graph)。
2. 强综合 (Synthesis) 与避免“懒惰委托”
coordinatorMode.ts 中非常精彩的一点是针对 LLM 容易产生的“懒惰委托 (Lazy Delegation)”问题进行了防御性 Prompt 设计。系统严厉警告大模型:“永远不要写‘基于你的发现,修复这个 Bug’”。
Coordinator 的核心职责是提取 Worker 返回的线索,理解并合并(Synthesize)成一份拥有确切文件名、行号以及修改目标的规范说明(Spec)。这种设计避免了错误上下文在不同 Worker 之间的级联扩散。
5.2 状态快照与断点续传机制
既然是执行动辄耗时数十分钟的超长任务,引擎随时可能遇到 CLI 意外崩溃或断电。这就需要一套健壮的状态持久化(State Persistence)系统。
1. Task 状态机与持久化定义
在 Task.ts 中,任何一个被派发出去的 Worker 都有严格的生命周期状态(pending -> running -> completed / failed / killed)。通过 isTerminalTaskStatus 守护机制,引擎能够安全地判断哪些子代理已经彻底死去,从而避免幽灵写入。任务运行时产生的冗长输出会被重定向到特定的 outputFile (日志存盘) 而非挤占主进程内存。
2. Agent 记忆快照 (Memory Snapshot) 与 WAL 机制
在 tools/AgentTool/agentMemorySnapshot.ts 源码中,呈现了一套类似数据库 Write-Ahead Log (WAL) 的机制。
整个项目级别的代理状态会被定格存放到 .claude/agent-memory-snapshots/ 目录下,并以 snapshot.json 以及 .snapshot-synced.json 作为版本校验游标。
当一个项目重新打开时(甚至在另一台机器上拉取了最新的 Git 仓库),initializeAgentMemorySnapshots 能够进行时间戳对比。若发现远端快照更新,它可以原样还原之前的思考节点和经验记忆,达到“断点续传”的奇效。
3. 错误恢复与任务回滚
如果一个子代理因为指令错误导致代码彻底改崩,Coordinator 提供了强有力的干预手段。它可以通过 TASK_STOP_TOOL_NAME 强制中止目标线程,且在分析错误后,并不盲目重试错误路径。
相反,系统提示词指导 Coordinator 选择:如果当前上下文(Context)污染严重,应当果断放弃原有的子代理实例,通过 AGENT_TOOL_NAME 重新拉起一个干净状态的空白 Worker。这种隔离式的容灾策略远比在一个长对话中不停说 "No, that's wrong, revert it" 要经济和高效得多。
5.3 并发与子代理 (Sub-Agent) 调度逻辑
Claude Code 协调器(Coordinator)不仅懂得多步推理,它的杀手锏在于:Fan-out (扇出并行) 与 Fan-in (聚合收敛) 的高并发调度。
1. 动态标识系统与寻址调度
每个被实例化的子代理,都会通过 generateTaskId 获得一个防碰撞的短标识符(如 a1b2c3d4 的 Local Agent,或带 r 前缀的 Remote Agent)。这就好比是给每个 Worker 分配了独立的端口或 PID。
当 Coordinator 决定并行派发多个任务(例如:同时检索 Auth 模块源码 和 Auth 单元测试文件)时,它在一次会话回合 (Turn) 内,会连续并发调用多次 AgentTool。
2. 伪造“人类”消息的异步唤醒 (<task-notification>)
Worker 并不运行在 Coordinator 的主事件循环内。当后台的子代理完成任务时,由于系统采用了纯文本的 Prompt 上下文通信,系统如何通知挂起的 Coordinator?
精妙的设计出现了:引擎会将子代理的运行结果封装为一个格式严格的 XML 标签 <task-notification>(包含 <task-id>, <status>, <result>, <usage> 等)。
并在主事件循环中,将这段 XML 伪装成普通人类用户 (User Role) 的输入发送给 Coordinator。这种统一入口的设计,极大简化了引擎的架构,让 Coordinator 可以像和多个人类聊天一样,自然地收集并汇总多个并行任务的回调结果。
3. 记忆继承与继续委派 (SendMessageTool)
并不是所有任务完成后都需要销毁 Worker。在 coordinatorMode.ts 中规定了 Context Overlap(上下文重合度)判定原则。
若一个 Worker 刚刚完成了特定目录的梳理,那么它的上下文中已经“温热”了相关的代码定义。此时,Coordinator 会使用 SEND_MESSAGE_TOOL_NAME,附带上 to: "agent-id",将下一步的具体修改指令直接送入该 Worker 的进程中,从而实现了对 LLM Cache 的最大化压榨和复用。
第六章:Tool Calling 的解析与执行沙盒
大语言模型与物理世界的交互枢纽,便是 Tool Calling(工具调用)。在 Claude Code 中,由于允许大模型自主执行 Bash 脚本甚至修改敏感文件,工具调用层被设计为防御级别最高、最容不得沙子的一层。本章剖析其严苛的沙盒机制。
6.1 工具清单注册与 Schema 动态生成
每一次 LLM 响应前的请求载荷中,不仅包含对话记录,还包含着它可用的“武器库”(Tools Array)。
1. 基于 TypeScript 的工具泛型体系
在 src/Tool.ts 中,核心对象 Tool 使用了与 Zod 深度绑定的泛型声明 ToolInputJSONSchema。
不同于传统的硬编码 JSON Schema 字符串,Claude Code 借助 zod/v4 的强类型能力,使得每一个被抛出的工具 Schema 都能在其源码内部被静态校验。这意味着当开发者更改了一个工具的逻辑参数,TypeScript 编译器就会自动验证生成的向 LLM 投递的说明书。
2. MCP 与动态上下文注入
并非所有工具都是静态编译在应用中的。借助于 MCP (Model Context Protocol),Claude Code 能够在运行时动态连接外部 Server 获取新工具。
在组装工具列表时,系统会调用如 getToolPermissionContext() 将隐式的上下文注入。例如:大模型调用 Edit 工具时,无需显式指明权限 Token,因为沙盒已经在外围为本次 Tool 封装了 CWD(当前路径)等强制环境变量隔离边界。
6.2 参数解析与强制校验机制
尽管 Claude 被训练得足够聪明,但在生成多级嵌套的复杂 JSON 参数时,依然可能产生“幻觉”或者类型错误(如本该传 Array 却传了 String)。
1. 流式工具参数的渐进式验证 (Streaming Parse)
在 StreamingToolExecutor.ts 的处理中,工具参数并非总是“一次性全额到账”。借助容错能力极强的底层协议,执行器具备增量反序列化的能力。如果发现不符合 Zod schema,它可以通过 safeParse 安全阻断。
2. buildSchemaNotSentHint 的智能错误修复层
如果在 toolExecution.ts 中发现了严格校验不匹配(Zod ValidationError),直接把错误抛给人类会让体验极差。
代码中引入了一个叫 buildSchemaNotSentHint 的自动修复提示。它发现如果模型是因为“没有查阅工具的完整 Schema”而导致格式错误时,会自动产生这样一条底层系统日志注入并向大模型重试请求:
“This tool's schema was not sent to the API... Without the schema in your prompt, typed parameters get emitted as strings... Load the tool first: call ToolSearchTool...” 这种通过自然语言提示(Hint)引导大模型自我修正(Self-Correction)的设计,赋予了系统极高的自愈能力。
6.3 隔离执行沙盒与超时控制
工具执行(特别是 BashTool)是安全防御的最前线,必须对其进行时间与空间上的物理隔离。
1. 子进程树与 siblingAbortController 劫持
在 StreamingToolExecutor.ts 中,可以看到它分配了一个专门的 siblingAbortController。
当执行需要 Spawn Child Process 的命令时,系统将此信号绑定到子进程上。这意味着如果发生了权限拒绝(Permission Dialog Rejection)或是用户按下了 Ctrl+C,不仅上层网络流会被切断,挂载在系统底层的 Bash 子进程也会立刻收到 SIGKILL 信号。这就避免了僵尸进程驻留。
2. 标准输出 (Stdio) 的限流与智能截断
Bash 执行可能瞬间吐出上百万行的巨量日志(如死循环打印或者 Dump 文件)。
在工具执行沙盒中,通过 Stream 拦截,不仅限制了每次运行的 timeout 超时时间上限,同时在收集 summary 时(summary.length > 40)以及后续截断中,会动态将多余的文本转换为 [Truncated...] 类似提示。这避免了恶意的超长日志在下一轮对话中瞬间耗尽(OOM)整个上下文 Token 配额。
第七章:assistant/ —— 对话策略与历史意图收敛
虽然名为 CLI 工具,但 Claude Code 最具魅力的部分在于其高度拟人化的交互策略。作为衔接底层的 query.ts 和上层 UI 的中枢,assistant 和其配套的 utils/messages.ts 等模块负责让大模型展现出资深工程师的特质:不盲目执行、善于提问、懂得踩刹车。
7.1 意图理解与主动求问 (AskUserQuestionTool)
为了防止 LLM 陷入无限的瞎猜或执行危险的“假设性修复”,系统专门设计并深度集成了 AskUserQuestionTool。
1. 反对“懒惰假设”与 AskUserQuestion 的触发条件
在系统的全局提示词 (constants/prompts.ts) 中,有着极其严苛的训诫:“Escalate to the user with AskUserQuestion only when you're genuinely stuck... not as a first response to friction.”(只有在你真正卡住时才求助,而不是一遇到摩擦就问人)。
但这并不意味着完全不提问。在计划模式 (Plan Mode) 下,或者当系统遇到歧义需求(例如:发现了两个同名的配置文件,不知修改哪一个),AskUserQuestion 允许大模型通过结构化的 JSON 表单(包含 question, header, options)向终端 UI 发起询问。
2. 结构化的多选与预览 (Preview)
AskUserQuestionTool 的实现远比纯文本输入复杂。源码显示它不仅支持单选/多选,更支持通过 preview 字段注入 HTML 或 Markdown 格式的代码差异 (Diff) 供用户在侧边栏审查。这种结构化的提问,使得 Claude 能够像一个真正的协作者一样,给出 A/B 方案让主程(人类)拍板。
3. 拦截权限拒绝与意图澄清
当人类用户拒绝了某个越界工具(如试图 rm -rf 某目录)的执行权限时,如果不对大模型加以干预,它很可能会尝试换一个类似的方法强行继续。系统提示词中明确规定:“If you do not understand why the user has denied a tool call, use the AskUserQuestionTool to ask them.”。这种将错误反馈回路闭环交还给用户的设计,是其安全策略的一环。
7.2 多轮历史收敛与死循环打破
大模型最容易暴露缺陷的地方是陷入“修改 -> 报错 -> 同位置继续修改 -> 继续报错”的死循环。
1. 记忆纠正提示 (Memory Correction Hint)
当出现明显的工具使用错误或是用户主动打断了它的动作时,系统并非简单将错误信息抛回。例如,在流式拦截或沙盒发生中止时,引擎不仅终止流,还会附加类似 withMemoryCorrectionHint 的辅助提示词。
这些特殊的 System Message 充当了“物理清醒剂”,它们被强行插入到历史 messages 队列的末尾,警告模型:“你刚才的策略彻底失败了,退后一步思考,不要重复相同的错误”。
2. Tombstone(墓碑)的降噪机制
人类在 CLI 中经常会产生手误敲击或半截命令终止的情况。如果在历史消息数组(Context Array)中原样保留这些乱码残片,将会极大地带偏模型后续的注意力 (Attention)。
正如前面第三章所述,系统发明的 TombstoneMessage (type: 'tombstone') 的作用,不仅是从 UI 上隐藏被放弃的流式生成,更是将其从发送往 Anthropic 服务器的 API Payload 中进行物理剪裁 (Pruning) 隔离,使得最终发送给大模型的上下文始终是“连贯且具有逻辑”的高质量主线。
7.3 反馈闭环与拟人化交互设定
为了在 CLI 这个纯文本、黑底白字的环境中营造“结对编程”的体验,系统在提示词注入层面下了大功夫。
1. "Report outcomes faithfully" 的真实性宣言
在系统提示词中,有一段长篇的警告:“如果测试失败了,如实说出来;如果你没有运行验证步骤,直接说没有。绝不为了制造绿色的结果而隐瞒或简化报错...”。
大模型存在先天的讨好型人格 (Sycophancy) 和“幻觉闭环”(即为了达成用户目标,假装自己执行了某命令并捏造了成功的输出)。Claude Code 通过系统级的断言,在 Prompt 层面强行压制了这种讨好,强制其只将终端沙盒反馈的确切 stdout/stderr 作为决策依据。
2. 无声的执行者 (Silent Executor)
你可能会注意到 Claude Code 很少说“好的,我这就去办”这类废话。它的系统指令写明:“Avoid giving time estimates... Focus on what needs to be done”。
这种克制的设计语言通过 assistant 模块下发,确保每一次响应都伴随着实质性的工具调用(Tool Call)或关键结论。这也极大地节省了 Output Token 的计费。
第八章:异常捕获、容错恢复与兜底策略
由于要与不稳定的网络环境、具有幻觉的 LLM 输出、以及复杂的本地操作系统交互,Claude Code 将健壮性提升到了“航空航天级”。本章剖析其在遇到灾难时如何避免崩溃。
8.1 细粒度异常分类树 (Exception Taxonomy)
在 services/api/errors.ts 中,系统并未采用简单的 try...catch,而是构建了一棵极度细化的异常树。
1. categorizeRetryableAPIError 分类器
所有的 API 错误在进入核心状态机前,都会被 categorizeRetryableAPIError 拦截并分类:
rate_limit(限流/过载): 捕获 HTTP 429 或 529,触发重试队列。ssl_cert_error(安全连接异常): 代理或证书链问题,立即熔断并提供清晰提示,防止安全穿透。connection_error(网络闪断): 触发带有抖动的指数退避重试。
2. 全局 Error Boundary
即便是遇到了未知崩溃,终端 UI (ink 框架内) 也包裹了一层 SentryErrorBoundary。这保证了即使在渲染复杂的 Git Diff 或执行深层回调时发生了 JavaScript Panic,整个 CLI 也不会被操作系统粗暴 Kill 掉,而是能够拦截异常并保存现场环境。
8.2 智能退避与 LLM 自愈策略
在遇到了确定为“可重试”的异常后,系统拥有一套高度智能的自愈网关。
1. 指数退避与自动降级 (Failover)
面对模型层的服务拒绝(如过载),withRetry 装饰器不仅控制着休眠时间,还会在 Claude 3.5 Sonnet 等模型不可用时,利用 RateLimitOptions 机制,自动或提示用户切换至负载较小的后备节点或等效模型。
2. 基于 buildSchemaNotSentHint 的大模型自修复
如果错误并非来自网络,而是由于大模型自身的逻辑幻觉导致了不可恢复的 JSON 结构错误,传统的做法是抛出 Error 结束运行。
但在 Claude Code 中,系统会将底层 Zod 的结构校验异常 (formatZodValidationError) 格式化为通俗的系统提示,包装成一段合成的历史消息,将问题直接“原路甩回给大模型”让其自行诊断并重新下发工具调用指令。
8.3 降级渲染与用户友好的兜底方案
作为一款面向开发者的生产力工具,它的最后一层防线是:当一切都崩溃时,如何尽可能保住用户的劳动成果。
1. 资源超载兜底 (RateLimitMessage)
在进行极度密集的代码阅读或超大规模的上下文替换时,很容易遇到 Token 耗尽或账单超支。此时,底层的 cost-tracker.ts 和 UI 层的 RateLimitMessage.tsx 会联动。系统会平滑地打断当前的推理任务,将状态封存,并在终端弹出可视化的菜单,允许用户选择购买额外额额度 (extra-usage) 或是中止任务。
2. 安全日志与快照转储 (Diagnostics)
面对不可恢复的核心逻辑死锁,系统提供了类似于操作系统 CoreDump 的快照诊断体系(例如记录 agentMemorySnapshot 或触发 headlessProfilerCheckpoint),并在终端友好地抛出诊断建议,防止“静默崩溃 (Silent Crash)”让用户陷入长久的困惑。
全文终。
(本报告基于对 Claude Code 最新版本核心 src/ 代码库的深层次剥离与技术架构还原。它呈现了一个顶尖 CLI 智能代理从底层流控制到顶层意图拟人化的全套工程化实践结晶。)

Comments