所有文章

Claude Code 源码架构:核心价值提炼

  • Claude Code
  • Agent
  • 架构设计
  • 工程实践
文章目录

从 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 消息沿同一条管道流动。callModelautocompactuuid 等 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 · 压缩
跨回合控制簿记 不落盘的可变状态,如 turnCountreadFileState 03 · 状态持久化

八条贯穿全局的设计决策

把各个子系统摊开,会发现同一组原则反复出现。它们比任何一个具体实现都更接近 Claude Code “为什么好用”的答案。

  1. Async generator 作脊柱。 每层都是 AsyncGenerator<Message>,通过 yield* 组合,共用一条消息管道,并获得天然背压。
  2. 循环加显式状态,避免递归。 跨迭代状态放入类型化对象,用 transition 字段记录系统为什么继续运行。
  3. 只使用一个明确的退出信号。 判断本轮是否仍有 tool_use,即 needsFollowUp,而不是依赖脆弱的 stop_reason
  4. 万物皆消息。 tool_result、子 Agent 结果与记忆都是注入的消息,历史始终线性且可切片。
  5. 默认 fail-closed。 并发、只读、权限和分类器投影等安全相关默认值都站在更保守的一侧。
  6. 在 I/O 边界做依赖注入。 callModelautocompactuuid 都可注入,同时明确区分不可变配置快照与可变状态。
  7. 对 prompt 缓存保持“偏执”。 工具排序、动态边界哨兵、Agent 列表 attachment 和跨轮冻结的落盘决策,都是为了稳定缓存前缀。
  8. 先扣住可恢复错误。 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;终止则收敛为十余种携带 reasonreturn

机制 触发条件 处理方式 关键细节
网络重试 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 的工具块及其后的全部缓存。

这条流水线维护三条彼此关联的不变量:

  1. 内置工具必须构成连续且顺序稳定的前缀。 服务端将全局缓存断点放在最后一个前缀匹配的内置工具之后。分区排序让内置工具在前、MCP 工具在后,并分别保持稳定排序;uniqBy 则保证同名冲突时内置工具优先。
  2. 工具 schema 的字节表示必须在会话内稳定。 toolSchemaCache 锁定基础 schema,叠加层只修改 defer_loadingcache_control。即使 GrowthBook gate 在中途翻转,也不会扰动缓存。将 inputJSONSchema 纳入 cache key,还修复了 StructuredOutput 同名但 schema 不同的问题。
  3. 延迟加载将低频工具移出首轮请求。 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

权限引擎只回答一个问题:模型请求以一组具体参数调用某个工具时,是否允许?结果只有 allowaskdeny 三种。安全语义的真相不藏在注释里,而是编码在规则执行顺序中。

确定性流水线与模式后处理

权限引擎分为两层:

  • 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"]

三条必须牢记的不变式

  1. deny > ask > allow allow 规则所在的步骤 2b,始终排在所有 deny 和 ask 规则,也就是 1a 到 1g 之后。
  2. bypassPermissions 只跳过步骤 3 的兜底弹窗。 步骤 1a、1d 的 deny,以及 1e、1f、1g 的 ask 都不受 bypass 影响。
  3. 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 匹配会剥离全部环境变量前缀,因此用户 deny claude 后,即使命令写成 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 前缀的三部分是 systemPromptuserContextsystemContext,它们在每个会话中保持稳定。

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 保持幂等;预算通过 seenIdsreplacements 冻结决策;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 将渐进披露做到了极致:

  1. frontmatter 常驻。 只有 name、description 和 whenToUse 三个字段计入 token,约 33 tokens。
  2. paths 条件延迟。paths frontmatter 的条件 Skill,连清单都不会出现,直到相关文件被触碰。
  3. 正文调用时加载。 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 万行代码中被持续、严格地贯彻。

读懂它最大的收获,也许不是学会某个具体的 StreamingToolExecutorsplitSysPromptPrefix,而是理解一条更普遍的规律:

Agentic 系统的复杂度是守恒的。你可以选择把复杂度放进清晰的架构,也可以让它散落在无法调试的运行时里。

文档来源

本文由 docs/architecture 目录下的 19 篇架构文档提炼而成,数据与机制描述均来自这些原始文档:

  1. 00-overview.md:总览、心智模型与全局设计哲学。
  2. 01-agent-loop-core02-agent-loop-recovery03-state-persistence:主循环、恢复与状态。
  3. 04-tool-contract05-tool-registry-schema06-tool-execution:工具契约、工具池与执行。
  4. 07-permission-engine08-rule-matching-classifier09-hooks:安全闸门。
  5. 10-multi-agent-isolation11-multi-agent-orchestration12-coordinator-teams-messaging:多 Agent。
  6. 13-system-prompt-cache14-claudemd-nested-memory15-compaction16-persistent-memory17-tokens-cost-spill:上下文与记忆。
  7. 18-extensibility.md:Skill、MCP 与插件扩展机制。