从 19 篇架构文档出发,本文试图回答一个比“Claude Code 有哪些功能”更重要的问题:一套成熟的 agentic 系统,应该如何组织模型、工具、安全、上下文和多 Agent 协作?
一句话内核:Claude Code 是一个由 async generator 串联起来的“思考 → 行动 → 观察”循环。它围绕一份完备的
Tool插件契约展开,每一次工具调用都要穿过“校验 → 权限 → hook → 执行”的安全闸门。
这套架构可以先用四个数字概括:19 篇架构文档、8 条贯穿全局的设计决策、10 多条恢复或终止路径,以及 Tool、Skill、MCP 三条扩展通道。
1. 心智模型:一个 Agent 系统的心脏
Claude Code 是 Claude 官方 CLI 的开源可读源码,约有 1,900 个文件、51 万行代码,以 Bun 和 TypeScript 构建,终端 UI 使用 React 与 Ink。整套架构围绕三个核心抽象展开:异步生成器做脊柱、消息数组做历史、工具契约做能力边界。
三层同心圆
系统可以理解为三层嵌套结构:
flowchart TB
subgraph L1
direction TB
title1["会话/回合编排<br/>QueryEngine.submitMessage()"]
desc1["跨回合状态 · 组装 system prompt + 上下文<br/>翻译成对外 SDK 消息"]
subgraph L2
direction TB
title2["Agent 主循环 · query.ts queryLoop()<br/>—— 脊柱"]
desc2["一次 while 迭代 = 一次模型调用<br/>+ 它请求的整批工具"]
subgraph L3
direction TB
title3["模型 + 工具<br/>callModel / StreamingToolExecutor"]
desc3["流式 API 调用 · 工具的调度/权限/执行"]
end
end
end
desc1 -.->|"依赖注入接缝:callModel / autocompact / uuid 全可注入"| desc2
最外层负责跨回合状态、system prompt 与上下文组装,以及对外 SDK 消息的翻译;中间层是系统脊柱,一次 while 迭代对应一次模型调用及其请求的一批工具;最内层完成流式 API 调用、工具调度、权限校验和实际执行。
每一层都是 AsyncGenerator<Message>,外层用 yield* 将内层消息流接出来。这样既避免了回调式控制流,又获得天然背压,让模型输出、工具执行结果和 SDK 消息沿同一条管道流动。callModel、autocompact 和 uuid 等 I/O 边界还可以被注入,形成清晰的测试接缝。
万物皆消息
系统几乎不区分“对话历史”和“状态”。绝大多数需要跨回合保存的内容,都会编码为历史中的一条消息。历史始终保持为一维 Message[],因此可以统一切片、压缩和持久化。
| 内容 | 编码方式 | 出处文档 |
|---|---|---|
| 工具执行结果 | tool_result 类型的 user 消息,并携带 tool_use_id |
01 · 主循环 |
| 子 Agent 完成结果 | 注入的 <task-notification> user 消息 |
11 · 多 Agent 编排 |
| CLAUDE.md、Git 信息与记忆 | 包在 <system-reminder> 中的 user 消息,并标记 isMeta |
13 / 14 / 16 |
| 上下文压缩后的摘要 | 一条合成的 user 消息 | 15 · 压缩 |
| 跨回合控制簿记 | 不落盘的可变状态,如 turnCount、readFileState |
03 · 状态持久化 |
八条贯穿全局的设计决策
把各个子系统摊开,会发现同一组原则反复出现。它们比任何一个具体实现都更接近 Claude Code “为什么好用”的答案。
- Async generator 作脊柱。 每层都是
AsyncGenerator<Message>,通过yield*组合,共用一条消息管道,并获得天然背压。 - 循环加显式状态,避免递归。 跨迭代状态放入类型化对象,用
transition字段记录系统为什么继续运行。 - 只使用一个明确的退出信号。 判断本轮是否仍有
tool_use,即needsFollowUp,而不是依赖脆弱的stop_reason。 - 万物皆消息。
tool_result、子 Agent 结果与记忆都是注入的消息,历史始终线性且可切片。 - 默认 fail-closed。 并发、只读、权限和分类器投影等安全相关默认值都站在更保守的一侧。
- 在 I/O 边界做依赖注入。
callModel、autocompact、uuid都可注入,同时明确区分不可变配置快照与可变状态。 - 对 prompt 缓存保持“偏执”。 工具排序、动态边界哨兵、Agent 列表 attachment 和跨轮冻结的落盘决策,都是为了稳定缓存前缀。
- 先扣住可恢复错误。 413、输出触顶等错误在确认无法恢复之前,不向消费者暴露。
最容易被低估的一点:这套代码中大量复杂度都在服务两件看不见的事——prompt 缓存命中率与上下文预算。功能逻辑往往几十行就能写完,真正的工程量在于如何稳定缓存前缀,以及如何在不丢关键信息的前提下压缩 token。要认真构建 Agent,就应该从第一天把它们当成一等公民。
2. 主循环:思考、行动、观察的引擎
主循环是整个系统的地基:组装上下文、调用模型、收集 tool_use、执行工具、拼接历史,然后进入下一轮。它使用 while (true) 而不是递归,跨迭代状态统一放在一个 State 对象中。
单回合控制流
flowchart TD
A["① 准备消息窗口<br/>tool-result 预算 / micro-compact / autocompact"] --> B["② 流式调用模型<br/>deps.callModel · for await 消费"]
B --> C["③ 边流边收 tool_use block<br/>只要有一个 → needsFollowUp = true"]
C --> D{needsFollowUp?}
D -->|否:模型不再要工具| E["return completed<br/>循环终止"]
D -->|是| F["④ 执行工具<br/>产出 tool_result(本质是 user 消息)"]
F --> G["⑤ 拼下一回合历史<br/>[...本轮消息, ...assistant, ...toolResults]"]
G -->|turnCount++| A
唯一的退出信号是:本轮模型是否还请求工具。 这个判据由实际内容驱动,与 API 元数据解耦,比检查 stop_reason 更稳健。
恢复机制:可恢复错误一律先“扣住”
主循环把重试与恢复实现为多种特殊的 continue,并将原因写入 State.transition;终止则收敛为十余种携带 reason 的 return。
| 机制 | 触发条件 | 处理方式 | 关键细节 |
|---|---|---|---|
网络重试 withRetry |
429、529、401 或连接错误 | 冷却后重试,用户无感 | 禁用 SDK 自带重试,改用自实现的分级策略 |
| 模型 fallback | 连续出现 529 过载且存在备用模型 | 抛出 FallbackTriggeredError,换模型重放整段历史 |
先用 stripSignatureBlocks 清除与模型绑定的 thinking 签名 |
| 流式转非流式 fallback | 流式请求中途失败 | 退回非流式方式重试 | 给残缺 assistant 消息发送 tombstone,从 UI 和 transcript 中同时删除 |
| 413 prompt-too-long | 上下文超限 | 先做 collapse drain,再做 reactive compact | 恢复失败前不 yield,避免 SDK 看到 error 后结束会话 |
max_output_tokens |
输出达到上限,默认 cap 为 8K | 先升到 64K 重发,再注入“继续写”做多轮恢复,最多 3 次 | BQ p99 输出只有 4,911 tokens,多数请求仍使用小额度 |
| Stop hooks 阻塞 | hook 要求“未运行测试不得结束” | 将阻塞错误注入历史,再运行一轮 | hasAttemptedReactiveCompact 故意不重置,避免死循环 |
为什么一定要“扣住”错误?因为 SDK 消费者一旦看到 error 字段就可能结束会话,此时恢复循环即使仍在运行,也已经没有消费者继续监听。因此,413、max_output_tokens 和 media 等可恢复错误,必须等到确定无法恢复后才对外输出。
恢复路径还会刻意绕过 stop hooks,避免形成“error → hook 阻塞 → 重试 → error”的死亡螺旋。
跨回合状态的分层
Agent 系统的记忆分成两层:进程内的跨回合状态,也就是 QueryEngine 实例字段;以及磁盘中的持久转录文件 ~/.claude/projects/<cwd>/<sessionId>.jsonl。恢复会话时,JSONL 是唯一真相来源。
| 状态字段 | 作用域 | 是否落盘 | 恢复会话后的行为 |
|---|---|---|---|
mutableMessages |
每个 QueryEngine,跨回合 | 是,写入 JSONL | 从 JSONL 重建为 Message[] |
permissionDenials |
跨回合,只增 | 否 | 清空后重新计数 |
totalUsage |
跨回合累加 | 否 | 归零,成本从其他位置恢复 |
readFileState,LRU 100 / 25MB |
每个进程 | 否 | 从空缓存开始,“已读视图”归零 |
loadedNestedMemoryPaths |
跨回合,不淘汰 | 否 | 变为空集合,每个 CLAUDE.md 会重新注入一次 |
转录有两条写入路径:元数据类 entry 同步写入,保证立即出现在 EOF;高频消息进入按文件划分的异步写队列,每 100ms 批量 drain,并使用 0o600 权限。写入严格保序,因此 assistant 消息使用 fire-and-forget 也能保持安全。
3. 工具系统:一份契约打通全链路
一个工具在 Claude Code 中不是一个普通函数,而是一份完整契约:它要能将自身序列化为发给模型的 schema,证明输入合法,检查权限,声明并发语义,执行并返回结果,最后将结果映射为 API 所需的 tool_result。
新增工具只需要实现这份契约,编排、权限、持久化和 UI 都由面向接口的通用代码处理。
buildTool:fail-closed 的默认值
buildTool(def) 会补齐默认方法,将工具定义扩展成完整契约。默认值刻意保守:
| 键 | 默认值 | 语义 | 谁必须覆盖 |
|---|---|---|---|
isEnabled |
() => true |
工具启用 | 需要按 feature flag 或平台关闭的工具 |
isConcurrencySafe |
() => false |
默认不可并发 | 只读或幂等工具,如 Glob |
isReadOnly |
() => false |
默认假设会写 | 只读工具 |
isDestructive |
() => false |
默认非破坏性 | 删除、覆盖或发送类工具 |
checkPermissions |
→ allow |
下放给通用权限系统 | 文件或命令类安全工具 |
toAutoClassifierInput |
() => '' |
分类器跳过 | 安全相关工具,否则分类器无法感知它 |
工具池:为 prompt 缓存保持“偏执”
从一组 Tool 对象到发给 API 的 tools 数组,中间有一条围绕 prompt 缓存设计的流水线。工具块位于服务端 position 2,任意一个字节发生变化,都可能击穿约 11K token 的工具块及其后的全部缓存。
这条流水线维护三条彼此关联的不变量:
- 内置工具必须构成连续且顺序稳定的前缀。 服务端将全局缓存断点放在最后一个前缀匹配的内置工具之后。分区排序让内置工具在前、MCP 工具在后,并分别保持稳定排序;
uniqBy则保证同名冲突时内置工具优先。 - 工具 schema 的字节表示必须在会话内稳定。
toolSchemaCache锁定基础 schema,叠加层只修改defer_loading和cache_control。即使 GrowthBook gate 在中途翻转,也不会扰动缓存。将inputJSONSchema纳入 cache key,还修复了 StructuredOutput 同名但 schema 不同的问题。 - 延迟加载将低频工具移出首轮请求。
isDeferredTool与 ToolSearch 的tool_reference回填机制,让模型在需要时通过一次工具往返发现工具,而不是每轮重发完整定义。
配套的 system prompt 也会被 splitSysPromptPrefix 分为 attribution、CLI 前缀、静态段和动态段四块,只有静态段标记 scope: 'global'。一个请求中的 cache breakpoint 不超过 4 个。
工具执行与并发:边流边跑
新版 StreamingToolExecutor 不会等一轮 block 全部收齐。模型每流式输出一个 block,执行器就调用 addTool;只要满足并发条件,就立即开始执行。
工具状态机很简单:
stateDiagram-v2
[*] --> queued: addTool
queued --> executing: canExecuteTool 通过
queued --> queued: canExecuteTool 不通过
executing --> completed: collectResults 结束
completed --> yielded: getCompletedResults 吐出
yielded --> [*]
但它必须维护三条不变量:并发安全的工具可以并行,非并发工具必须独占,结果按接收顺序返回。Read 始终并发安全,Write 始终独占,Bash 则根据命令是否只读决定,例如 ls 可以并发,git commit 必须独占。
其中还有两个很精妙的设计:
- sibling abort 级联。 系统使用三层 AbortController。只有 Bash 出错才触发兄弟任务级联取消,因为命令之间往往存在隐式依赖,例如
mkdir失败后后续命令已经没有意义;Read 与 WebFetch 通常彼此独立,不需要互相取消。被取消的兄弟任务会收到合成的<tool_use_error>,但不会误伤整轮。 - 双输入。
processedInput用于可观测性,允许被派生字段修改;callInput与 API 绑定,逐字对齐模型原文。之所以分离,是因为 tool result 字符串可能逐字嵌入 input 路径,修改路径会改变 transcript 和 VCR 哈希。
4. 安全闸门:deny > ask > allow
权限引擎只回答一个问题:模型请求以一组具体参数调用某个工具时,是否允许?结果只有 allow、ask 和 deny 三种。安全语义的真相不藏在注释里,而是编码在规则执行顺序中。
确定性流水线与模式后处理
权限引擎分为两层:
hasPermissionsToUseToolInner是从 1a 到 3 的纯确定性规则流水线,不访问网络、不弹窗,也不运行分类器。- 外层
hasPermissionsToUseTool再对ask结果施加非确定性模式变换,包括 dontAsk、auto 分类器和 headless 兜底。
其核心顺序可以压缩为:
flowchart TD
A["inner: 1a 整工具 deny?"] --> B{"deny?"}
B -- yes --> D["return deny (最高优先级)"]
B -- no --> C["1b 整工具 ask → 1c tool.checkPermissions()"]
C --> E{"1d-1g 工具 deny / 交互 ask / content-ask / safetyCheck?"}
E -- yes --> F["return deny / ask(bypass 也拦)"]
E -- no --> G["2a bypassPermissions? → allow"]
G --> H["2b 整工具 allow 规则 → allow"]
H --> I["3 兜底 passthrough → ask"]
三条必须牢记的不变式
deny > ask > allow。 allow 规则所在的步骤 2b,始终排在所有 deny 和 ask 规则,也就是 1a 到 1g 之后。bypassPermissions只跳过步骤 3 的兜底弹窗。 步骤 1a、1d 的 deny,以及 1e、1f、1g 的 ask 都不受 bypass 影响。- hook 的 allow 不能越权。
resolveHookPermissionDecision保证 hook allow 只能跳过弹窗,settings.json中的 deny 与 ask 规则仍然有效。这来自 inc-4788 事故的教训。
auto 模式:分级探针、分类器与熔断
auto 模式的核心矛盾,是既要支持无人值守,又不能对危险操作放水。其检查按成本从低到高排列:先执行零成本规则与白名单,再在本地重新计算 acceptEdits,最后才发起分类器 API 调用。
分类器失败时默认 fail-closed,并带有熔断机制:连续拒绝 3 次或累计拒绝 20 次后,强制回落到人工处理。
deny 侧更激进,allow 侧更保守。 allow 匹配只剥离白名单中的环境变量,避免
DOCKER_HOST=evil docker ps错误命中 allow;deny 匹配会剥离全部环境变量前缀,因此用户 denyclaude后,即使命令写成FOO=bar claude也仍会被拦截。这种不对称是 HackerOne #3543050 报告推动的刻意设计。
5. 多 Agent:隔离、回注与协作
Claude Code 的多 Agent 建立在一个非常简单的递归之上:AgentTool.call() 本身是一个工具,而它的实现又调用一次主循环 query()。真正的复杂度集中在两个问题上:上下文如何隔离,以及结果如何返回父 Agent。
默认隔离、显式共享
每次递归都会创建一份独立的 ToolUseContext。隔离逻辑集中在 createSubagentContext() 中:可变状态被克隆或清空,queryTracking.depth 增加,agentId 重新生成。构造子 Agent 工具池时还会移除 Agent 工具,将递归硬限制在深度 1。
这里最微妙的是 App 状态:异步子 Agent 中的 setAppState 是 no-op,但必须保留 setAppStateForTasks 通道,使其能够直达根 store。否则异步 Agent 启动的后台 Bash 任务无法注册和终止,最终可能变成 PPID 为 1 的僵尸进程。
结果回注:不是 await,而是通知队列
子 Agent 完成后,父 Agent 并不直接 await 它,而是通过一条全局通知队列接收结果:
sequenceDiagram
participant W as worker
participant Q as commandQueue
participant P as 父 query loop
participant M as 模型
W->>W: 收完最后一条 assistant
W->>W: finalizeAgentTool
W->>Q: enqueueAgentNotification
Note right of Q: priority: later
Note over P: 父 agent 下一轮工具调用结束
P->>Q: 按 agentId 过滤领取通知
Q-->>P: task-notification 消息
P->>M: yield 成 user 消息
这套机制依赖三个不变量:
- 先转终态,再做润色。
completeAsyncAgent必须早于 handoff 分类和 worktree 清理,避免清理过程挂起时拖住状态机。 notified同时是去重闩和逐出前置条件。 结果只通知一次,通知完成后才允许回收内存。- 使用
agentId做作用域隔离。 主线程领取undefined作用域的通知,子 Agent 只领取属于自身 ID 的通知,用户 prompt 不会泄漏到子 Agent。
协调者模式与团队模式
Claude Code 内部有两套协作模型,它们通过同一个 SendMessage 工具衔接起来。
| 维度 | 协调者模式(Coordinator) | 团队 / 蜂群(Swarm Team) |
|---|---|---|
| 主体 | main session 充当 coordinator | main session 充当 team lead |
| worker | AgentTool 启动的异步后台 Agent |
TeamCreate 建队后的进程内 teammate |
| 结果返回方式 | user 角色的 <task-notification> XML |
文件邮箱中的 idle_notification |
| worker 间寻址 | agentNameRegistry,即 name → agentId |
~/.claude/teams/{team}/inboxes/ 文件邮箱 |
SendMessage 路由 |
进程内 queue / resume | 邮箱中的 handleMessage / broadcast |
同一个 SendMessage 会根据目标进入三个命名空间:注册名或不带 @ 的 createAgentId 进入内存 task 队列;裸 teammate 名进入文件邮箱;uds: 或 bridge: 进入跨会话、跨机器的 peer 通道。to 不能包含 @ 的类型约束,在类型层面将前两个命名空间隔离开。
6. 上下文与记忆:看不见的工程量
这一部分是整套架构真正值钱的地方。功能本身往往并不复杂,工程难点在于如何稳定缓存前缀,以及如何在不丢失关键信息的前提下控制 token。
系统提示:逻辑三段,物理四块
构成 API cache-key 前缀的三部分是 systemPrompt、userContext 和 systemContext,它们在每个会话中保持稳定。
而 systemPrompt 在发送给 API 前,又会被物理切分为 attribution header、CLI prefix、static 和 dynamic 四块。SYSTEM_PROMPT_DYNAMIC_BOUNDARY 哨兵负责分割静态与动态内容,只有静态块会标记 scope: 'global',从而跨组织共享缓存。
Anthropic API 每个请求最多允许 4 个 cache breakpoint:system 侧占 1 到 2 个,tools 块占 1 个,messages 最后一条占 1 个。section 缓存和 latch 会将动态段与 beta header 固定下来;/clear 和 /compact 是仅有的重置点。
上下文压缩:四条路径
压缩路径按侵入性从轻到重排列:
| 路径 | 时机 | 机制 | 代价 |
|---|---|---|---|
| micro-compact | 每回合请求前 | 按时间清理旧 tool_result 正文,或用 cache_edits 删除 |
最轻,不调用 API,也不改变消息结构 |
| session-memory | 接近触发线时优先尝试 | 使用后台维护的 session memory 作为摘要,并裁剪旧消息 | 中等,节省一次摘要 API 调用 |
| 结构化摘要 | 接近触发线时的兜底 | 调用一次 LLM,将历史替换为九段式摘要,并重新水化不超过 5 个文件 | 较重,连续失败 3 次后熔断 |
| reactive / context-collapse | 413 后兜底 | 在运行时对超限响应执行 collapse | 实验性或最终兜底路径 |
持久记忆:唯一跨会话存活的状态
持久记忆不是数据库,而是一个文件系统目录,也就是 memdir。它遵循两条很值得迁移的原则。
第一,记忆只保存无法从当前工程状态推导的信息。 代码模式、个人偏好和踩坑教训适合进入记忆;通过 grep、Git 或 CLAUDE.md 就能重新得到的内容不应该重复保存。
第二,共享落盘,独立控制。 写入端按 UUID 游标推进,由回合末的后台 forked Agent 完成;读取端按会话字节数封顶,在回合开始时由 Sonnet 选择器召回不超过 5 条;索引注入则由 GrowthBook flag 二选一控制。三者互不阻塞。
召回侧还有三个护栏:Sonnet 选择器做语义选择而非关键词匹配,以减少假阳性;已经成功使用的工具不再召回其文档,但会保留 warnings 与 gotchas;每条记忆使用“47 天前”这类 freshness 文案,而非裸时间戳,因为自然语言更容易触发模型对陈旧性的判断。
Token、成本与落盘遵循同一条工程定律
Token 统计、成本计算与转录落盘共享同一个约束:
任何改变发往模型字节内容的决策,都必须能够被确定性重放。
Token 锚点使用最后一次真实 usage 加上后续 char / 4 粗估;成本计算设有 sessionId 守卫;落盘使用 wx 保持幂等;预算通过 seenIds 和 replacements 冻结决策;resume 则逐字节重建历史。
这些机制的共同目标,是让同一段历史无论经过多少次进程重启,都产生完全相同的 wire prefix,从而稳定 prompt 缓存命中率,也就是同时节省成本和上下文。
7. 可扩展性:渐进披露
Claude Code 的可扩展性由三条正交通道构成。它们的粒度、加载时机和定义者不同,但共享同一条不变式:能力可以无限扩展,常驻上下文只按“发现清单”线性增长。
| 通道 | 单元 | 加载与披露时机 | 常驻成本 |
|---|---|---|---|
| Tool | 一次 API 调用的原语 | 进程启动时注册,也可通过 ToolSearch 延迟加载 | 每个工具一份 JSON Schema |
| Skill | 一段 Markdown 提示词及可选引用文件 | frontmatter 常驻,正文在调用时加载 | 每个 Skill 约几十 token 的一行清单 |
| MCP | 远程 Server 暴露的 tools、prompts 和 skills | 连接握手后拉取,tools 走 deferred | 描述和 instructions 各截断为 2,048 字符 |
Skill 的三层渐进披露
Skill 将渐进披露做到了极致:
- frontmatter 常驻。 只有 name、description 和 whenToUse 三个字段计入 token,约 33 tokens。
- paths 条件延迟。 带
pathsfrontmatter 的条件 Skill,连清单都不会出现,直到相关文件被触碰。 - 正文调用时加载。
SKILL.md正文只在真正调用时进入上下文,示例约 47 tokens。
MCP Skill 来自远程,因此属于不可信输入。其 Markdown 正文中的 !\...`` shell 命令绝不能执行;只有本地可信 Skill 才允许 shell 注入。两种信任等级在同一个函数中被明确区分。
插件:把三条通道做成可开关的分发包
插件是一次打包多种组件的容器,可以同时包含 skills、hooks、MCP servers、commands 和 agents。内置插件以 @builtin 随二进制分发,但仍可开关;Marketplace 插件从 Git 拉取,解析 plugin.json,并使用 SHA 固定版本。
插件提供的 Skill 会刻意标记为 source: 'bundled' 而非 'builtin',从而继续享受清单不截断、analytics 记名等 bundled 级别的待遇。
8. 黄金法则:可迁移的设计经验
将 19 篇文档继续蒸馏,最后留下的不是某个只适用于 Claude Code 的类或函数,而是一组成熟 agentic 系统普遍适用的原则。
A. 把 prompt 缓存和上下文预算当作一等公民
从第一天起就把“如何稳定缓存前缀”和“如何压缩 token 而不丢关键信息”纳入架构,不要等功能完成后再补救。
B. 使用循环和显式状态,避免用递归承载主控制流
将每次迭代建模为清晰的状态转移,用 transition 记录继续运行的原因。这样既能避开递归深度问题,也能让恢复路径被精确测试。
C. 只保留一个不含糊的退出信号
判断是否仍存在 tool_use,比依赖 stop_reason 更稳健。由内容驱动、与 API 元数据解耦的终止判据,更容易长期维护。
D. 安全默认值必须站在最保守的一侧
默认不可并发、默认假设会写、分类器默认跳过、可恢复错误先扣住。fail-closed 的价值在于让“忘记隔离”或“忘记校验”从一种依赖纪律避免的错误,变成结构上难以发生的错误。
E. 恢复可恢复错误后,再将错误暴露给消费者
SDK 消费者看到 error 后可能立即结束会话。所有可恢复错误都应该先尝试恢复,确定无法挽救后再对外输出,同时使用熔断与守卫阻止死亡螺旋。
F. 默认隔离,显式共享
多 Agent 的子上下文统一克隆或清空。只有通过 setAppStateForTasks 等明确标记的通道才共享状态,并在退出时完成清理,不留下状态残渣。
G. 所有改变模型输入字节的决策,都必须可确定性重放
落盘使用 wx 保持幂等,预算决策跨轮冻结,resume 逐字节重建。相同历史无论在多少个进程中执行,wire prefix 都应该完全一致。
H. 使用渐进披露,让常驻成本只随清单线性增长
Tool、Skill、MCP 与插件可以不断扩展,但常驻上下文的代价应该收敛到一行清单、几段 frontmatter 或少量截断描述。
结语
Claude Code 架构的价值,不在于某一个炫技的机制,而在于一组朴素原则在 51 万行代码中被持续、严格地贯彻。
读懂它最大的收获,也许不是学会某个具体的 StreamingToolExecutor 或 splitSysPromptPrefix,而是理解一条更普遍的规律:
Agentic 系统的复杂度是守恒的。你可以选择把复杂度放进清晰的架构,也可以让它散落在无法调试的运行时里。
文档来源
本文由 docs/architecture 目录下的 19 篇架构文档提炼而成,数据与机制描述均来自这些原始文档:
00-overview.md:总览、心智模型与全局设计哲学。01-agent-loop-core、02-agent-loop-recovery、03-state-persistence:主循环、恢复与状态。04-tool-contract、05-tool-registry-schema、06-tool-execution:工具契约、工具池与执行。07-permission-engine、08-rule-matching-classifier、09-hooks:安全闸门。10-multi-agent-isolation、11-multi-agent-orchestration、12-coordinator-teams-messaging:多 Agent。13-system-prompt-cache、14-claudemd-nested-memory、15-compaction、16-persistent-memory、17-tokens-cost-spill:上下文与记忆。18-extensibility.md:Skill、MCP 与插件扩展机制。