2026年3月31日,Anthropic 的 npm 包因一个
.map文件配置失误,将 Claude Code 完整的 TypeScript 源码暴露在公网上。约 1,900 个文件、超过 51 万行代码,就这样向所有人敞开了大门。这篇文章不讨论法律与道德边界——那是另一个话题。我们只谈:这份源码里,藏着一个怎样精心设计的 AI Agent 系统。
目录
- 系统全貌:它到底是什么?
- 入口:一切从 main.tsx 开始
- 核心引擎:query.ts 的 Agent 循环
- 上下文管理:五层过滤的精妙设计
- 工具系统:Claude 的"手"
- 容错机制:为生产环境而生的防御
- 多智能体:协调器与子 Agent
- 工程洞察:值得学习的设计决策
- 结语
1. 系统全貌:它到底是什么?
Claude Code 是 Anthropic 官方出品的终端 AI 编程助手 CLI。不同于简单的补全工具,它是一个完整的 Agentic System,能够:
- 读写文件、搜索代码库、执行 Shell 命令
- 管理 Git、创建 PR、审查代码
- 通过 MCP(Model Context Protocol)连接外部服务
- 协调多个子 Agent 并行完成复杂任务
技术栈一览:
| 层次 | 技术选型 |
|---|---|
| 运行时 | Bun(取代 Node.js,启动更快) |
| 语言 | TypeScript(严格类型,全覆盖) |
| 终端 UI | React + Ink(React 渲染到 terminal) |
| AI SDK | @anthropic-ai/sdk(流式 API) |
| 协议层 | MCP SDK(工具/资源的统一接入) |
| Schema | Zod(运行时类型校验) |
| 功能开关 | GrowthBook + Bun bundle feature flags |
代码规模与复杂度超出了大多数人对"一个 CLI 工具"的想象。
2. 入口:一切从 main.tsx 开始
src/main.tsx 的前 20 行就告诉你这个团队极其在意启动性能:
// 必须在所有 import 之前执行的副作用:
import { profileCheckpoint } from './utils/startupProfiler.js'
profileCheckpoint('main_tsx_entry') // ← 第一行就开始计时
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js'
startMdmRawRead() // ← 并行启动 MDM subprocess(plutil/reg query)
import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js'
startKeychainPrefetch() // ← 并行预取 Keychain(OAuth + API Key)
三件事并行触发,都通过注释精确说明了原因:keychain 预取能节省约 65ms 的启动时间。这种对毫秒级延迟的关注,贯穿整个代码库。
启动阶段的主要工作:
- 身份认证:OAuth 令牌校验、Bedrock/Vertex 凭证预取
- 信任检查:
checkHasTrustDialogAccepted()确保用户同意条款 - 配置加载:MDM 托管配置、远程策略限制、Feature Flag 初始化
- 工具注册:
getTools()按 feature flag 动态组装工具集 - MCP 初始化:预取官方 MCP Server 列表
- REPL 启动:
launchRepl()进入交互主循环
3. 核心引擎:query.ts 的 Agent 循环
这是整个系统最精华的部分——src/query.ts,约 1,750 行。
它的核心是一个 while(true) 无限循环,每次迭代代表一个"轮次(turn)"。用 ASCII 图描述:
用户输入
│
▼
┌─────────────────────────────────────────────────┐
│ while(true) 主循环 │
│ │
│ ① 上下文预处理(5层压缩/过滤) │
│ │ │
│ ② 调用 Claude API(流式) │
│ │ │
│ ③ 实时处理流消息 │
│ │ │
│ needsFollowUp? │
│ ├─ NO → ④ 错误恢复 / Stop Hooks / 结束 │
│ └─ YES → ⑤ 执行工具 │
│ │ │
│ ⑥ 构建下一轮消息 → 继续循环 │
└─────────────────────────────────────────────────┘
3.1 状态机设计
循环的状态通过一个不可变的 State 对象在迭代间传递:
type State = {
messages: Message[] // 完整对话历史
toolUseContext: ToolUseContext // 工具执行上下文
turnCount: number // 轮次计数
maxOutputTokensRecoveryCount: number // 截断恢复次数
hasAttemptedReactiveCompact: boolean // 防止 compact 死循环
transition: Continue | undefined // 调试:上一次为何 continue
// ...
}
transition 字段尤其值得关注——它记录了每次 continue 的原因,如 'next_turn'、'max_output_tokens_recovery'、'reactive_compact_retry' 等。这使得测试可以断言具体的恢复路径触发了,而不必检查消息内容。
3.2 流式处理与消息收集
API 以 async generator 形式流式返回消息,循环实时处理每一块:
for await (const message of deps.callModel({ messages, systemPrompt, ... })) {
// 1. 处理 fallback(模型降级)
// 2. 将 tool_use 的 input 字段 backfill 给 SDK 消费者
// 3. 判断是否要缓留(withheld)这条消息
// 4. yield 或暂存
// 5. 提取 tool_use blocks,设置 needsFollowUp = true
}
"缓留"机制是一个精妙的设计:三类可恢复的错误消息(Prompt-too-long、Media 超限、Max-output-tokens)在流式阶段被暂不传递给调用方,等待恢复逻辑判断,成功则悄无声息地重试,失败才将错误暴露出去。这避免了 SDK 消费者(如 desktop 客户端)在看到 error 字段时立即终止会话。
4. 上下文管理:五层过滤的精妙设计
LLM 的致命弱点是有限的上下文窗口。Claude Code 用五层递进的机制来管理这个问题,每层都在 API 调用前依次执行:
原始消息列表
│
▼ 层1: Tool Result 大小限制
│ applyToolResultBudget() — 截断超大工具返回值
│ 持久化替换记录,/resume 时可恢复
│
▼ 层2: Snip 压缩 [HISTORY_SNIP feature]
│ snipCompactIfNeeded() — 按策略删除旧消息
│ snipTokensFreed 传递给下层,避免误判
│
▼ 层3: Microcompact
│ 微型压缩 — 合并重复工具调用/结果
│ CACHED_MICROCOMPACT: 延迟 boundary 消息
│ 以获取真实 cache_deleted_input_tokens
│
▼ 层4: Context Collapse [CONTEXT_COLLAPSE feature]
│ 折叠历史 — Read-time projection
│ 折叠记录存 store,不修改 REPL 数组
│ 保证 /resume 后折叠状态持久
│
▼ 层5: Auto Compact
触发阈值时 fork 子 Claude 对历史做摘要
成功: 用摘要替换历史,继续当前 turn
失败: 记录 consecutiveFailures,断路器保护
↓
发送给 API 的消息
层次顺序的设计意图:
- Collapse 在 AutoCompact 之前:如果 collapse 把 token 数降到阈值以下,autocompact 就无需触发,保留细粒度历史
- Snip 在 Microcompact 之前:snip 释放的 token 数必须传给 autocompact,否则基于旧 usage 数据会误触发
- Tool Result 限制最先执行:microcompact 只认
tool_use_id,不检查内容,两者相互独立
task_budget 的跨 compact 追踪
这是一个极度细节的设计。Anthropic API 支持 task_budget(任务预算),服务端通过计数 context window 来追踪消耗。但 compact 之后,服务端只看到摘要,会低估历史消耗。
解决方案:在每次 compact 触发前,客户端快照当前的 finalContextTokensFromLastResponse,累加到 taskBudgetRemaining,下次请求时通过 remaining 字段告知服务端正确的剩余预算。
5. 工具系统:Claude 的"手"
工具系统是 Claude Code 能力边界的直接体现。所有工具实现 Tool 接口,通过 Zod schema 声明输入类型。
5.1 工具全景
核心文件操作
├── FileReadTool — 读文件,支持行范围
├── FileEditTool — 精确字符串替换(必须唯一匹配)
├── FileWriteTool — 全覆盖写入
└── GlobTool — 模式匹配文件搜索
代码搜索
├── GrepTool — 正则文本搜索
└── LSPTool — 基于 LSP 的语义搜索/跳转
Shell 执行
├── BashTool — Unix Shell 命令
└── PowerShellTool — Windows PowerShell
智能体工具
├── AgentTool — 启动子 Agent(多 Agent 协作)
├── SkillTool — 执行预定义工作流 Skill
└── TodoWriteTool — 进度追踪与任务管理
Web 能力
├── WebFetchTool — HTTP 请求/页面抓取
└── WebSearchTool — 搜索引擎查询
MCP 集成
├── MCPTool — 调用任意 MCP Server 工具
├── ListMcpResourcesTool
└── ReadMcpResourceTool
任务调度(feature-gated)
├── ScheduleCronTool — 定时任务(AGENT_TRIGGERS)
├── SleepTool — 等待/延迟(PROACTIVE/KAIROS)
└── MonitorTool — 后台监控(MONITOR_TOOL)
5.2 StreamingToolExecutor:并发工具执行
传统 Agent Loop 是串行的:模型流结束 → 执行所有工具 → 下一轮。Claude Code 实现了 StreamingToolExecutor,在模型仍在流式输出时就开始执行已确认的工具调用:
模型流式输出: [text...] [tool_use: BashTool] [text...] [tool_use: GlobTool]
│ │
StreamingToolExecutor: addTool() 立即开始执行 addTool() 立即开始执行
│ │
流结束后: getRemainingResults() 收割所有完成的结果
这将"等待工具执行"的时间隐藏在了"等待模型输出"的时间里,显著降低延迟。
5.3 工具的权限模型
工具执行前必须通过 canUseTool() 检查。权限层级:
plan模式:只读工具,不允许写操作或命令执行default模式:需要用户确认危险操作auto模式(bypassPermissions):自动批准,适合 CI/CD 场景
权限系统还内置了 DenialTrackingState——记录用户拒绝了哪些操作,避免重复询问。
6. 容错机制:为生产环境而生的防御
生产级系统的标志是当事情出错时的表现。Claude Code 有完善的错误恢复链:
6.1 模型降级(Fallback)
当主模型因高负载返回 FallbackTriggeredError 时:
if (innerError instanceof FallbackTriggeredError && fallbackModel) {
currentModel = fallbackModel
attemptWithFallback = true
// 1. Tombstone 孤立的 assistant 消息(thinking block 有签名,不能跨模型复用)
for (const msg of assistantMessages) {
yield { type: 'tombstone', message: msg }
}
// 2. 清空状态,创建新的 StreamingToolExecutor
// 3. 剥离 signature blocks(thinking block 是模型绑定的)
// 4. 告知用户:已切换到 ${fallbackModel}(warning 级别)
// 5. 重试
}
Tombstone 消息是一个有趣的设计:UI 层看到 tombstone 后,会从界面和 transcript 中移除对应消息,保证视觉一致性。
6.2 max_output_tokens 恢复(三层递进)
Claude 有时会在任务未完成时达到输出 token 上限。系统按顺序尝试三种恢复:
第1层:Token 升级
// 如果用户没有显式设置上限,先尝试用 64k 重试同一请求
if (capEnabled && maxOutputTokensOverride === undefined) {
state = { ..., maxOutputTokensOverride: ESCALATED_MAX_TOKENS }
continue // 重试,不告知用户
}
第2层:截断续写(最多 3 次)
const recoveryMessage = createUserMessage({
content: `Output token limit hit. Resume directly — no apology, no recap. ` +
`Pick up mid-thought if that is where the cut happened.`,
isMeta: true, // 隐藏消息,不显示给用户
})
state = { messages: [..., recoveryMessage], maxOutputTokensRecoveryCount: count + 1 }
continue
第3层:放弃,暴露错误
"no apology, no recap" 这句续写提示写得极妙——精确地抑制了模型在续写时的常见病:长篇大论地解释"刚才发生了什么"。
6.3 Prompt-too-long 恢复(三层递进)
当 context 过长导致 API 返回 413 时,也有分层恢复:
- Context Collapse Drain:提交暂存的折叠,便宜操作,先试
- Reactive Compact:被动触发全量摘要(
hasAttemptedReactiveCompact防止死循环) - 暴露错误,调用
executeStopFailureHooks
关键细节:这两种错误消息在流式阶段都被缓留(withheld),恢复后悄悄重试。只有恢复失败才向调用方暴露错误。这意味着用户在大多数情况下看不到中间态的错误——系统静默地解决了问题。
7. 多智能体:协调器与子 Agent
Claude Code 支持多 Agent 协作模式,架构设计颇为精妙。
7.1 AgentTool:递归的 Agent
AgentTool 允许 Claude 启动一个完整的子 Agent,子 Agent 又可以继续调用工具甚至再启动孙 Agent:
主 Agent (query.ts)
└─ AgentTool → 子 Agent (独立 query loop)
└─ AgentTool → 孙 Agent
每个子 Agent 有独立的 agentId、独立的 abort 控制器、独立的工具使用上下文。消息队列通过 agentId 作用域隔离:
- 主线程 drain
agentId === undefined的消息 - 子 Agent drain 自己
agentId的消息 - 用户 prompt 只流向主线程
7.2 Team 模式(实验性)
从工具列表可以看到 TeamCreateTool 和 TeamDeleteTool,结合 coordinatorMode.ts,这是一个多 Agent 并行执行的协调器模式:
- Coordinator 负责任务分解和调度
- 多个 Worker Agent 并行执行子任务
- 结果汇总回 Coordinator
这是一个仍在 feature flag 保护下的实验性功能,但架构雏形已经清晰可见。
7.3 Buddy 系统(有趣的彩蛋)
src/buddy/ 目录里有一个 CompanionSprite.tsx 和 sprites.ts——这是 Claude Code 的"宠物"功能,一个可以在终端中显示的像素风格伙伴角色,会在某些操作时显示动画。这个功能完全不影响核心功能,却体现了团队对产品体验的用心。
8. 工程洞察:值得学习的设计决策
通读源码后,有几个设计决策值得单独拎出来讨论。
8.1 AsyncGenerator 作为核心抽象
整个 query 函数是一个 async function*(async generator):
export async function* query(params: QueryParams):
AsyncGenerator<StreamEvent | Message | ..., Terminal>
这个选择非常巧妙:
- 自然流式传输:每个
yield立即推送给调用方,无需额外缓冲 - 背压(Backpressure):调用方消费速度控制生产速度
- 资源清理:
using pendingMemoryPrefetch = ...,无论正常退出、throw 还是.return()都能触发 dispose - 组合性:
yield*自然委托给子 generator,如queryLoop
返回类型 Terminal 提供了丰富的退出原因枚举:completed、aborted_streaming、max_turns、blocking_limit、prompt_too_long、stop_hook_prevented 等,调用方可以精确分支处理。
8.2 Feature Flag 的双轨制
代码里同时使用两套 feature flag 系统:
// 运行时 A/B 测试(GrowthBook)
const value = getFeatureValue_CACHED_MAY_BE_STALE('tengu_otk_slot_v1', false)
// 构建时消除(Bun bundle)
if (feature('CONTEXT_COLLAPSE')) {
// 此块在 feature 关闭的 build 中被完全删除
}
_CACHED_MAY_BE_STALE 后缀是一个强制的命名约定——提醒开发者这个值在流式处理的 5-30 秒内可能翻转,如果用在"缓留"和"恢复"两处必须保证一致性。mediaRecoveryEnabled 在循环入口处快照一次,正是基于这个考虑。
8.3 "不可变参数 + 可变 State" 的循环设计
// 一次性解构,循环内永不重赋值
const { systemPrompt, maxTurns, querySource } = params
// 可变 State,每次 continue 整体替换
let state: State = { ... }
while (true) {
const { messages, turnCount, ... } = state
// ...
state = { ...next } // 整体替换,7 个 continue 点一致
}
这种设计的优点:
- 意图清晰:看到
const就知道这个值绝不会变 - 避免竟态:每次迭代都是从
state全量读取,不存在"某个字段没更新"的 bug - 易于 debug:
transition字段记录了每次continue的原因,测试可验证恢复路径
8.4 工具使用摘要的延迟消费
// 工具执行完毕,异步启动摘要生成(Haiku,~1s)
const nextPendingToolUseSummary = generateToolUseSummary(...)
// 在下一轮 ——— 流结束后 ——— 才消费(流 5-30s,摘要 1s)
if (pendingToolUseSummary) {
const summary = await pendingToolUseSummary // 此时几乎已完成,无需等待
yield summary
}
这是经典的"异步计算隐藏延迟"技巧:把慢操作(即使只有 1s)藏在另一个慢操作(5-30s 的模型流)背后,用户几乎感知不到额外等待。
8.5 Memory Prefetch 的 using 析构
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
state.messages,
state.toolUseContext,
)
using 关键字(TC39 显式资源管理提案,TypeScript 5.2 起支持)确保 pendingMemoryPrefetch[Symbol.dispose]() 在所有退出路径(正常返回、throw、generator .return())上都会被调用,即使忘记 try-finally 也没问题。
9. 结语
通读这份源码,最深刻的感受不是"Claude Code 好厉害",而是:
一个真实的、面向生产的 AI Agent 系统,需要解决多少工程问题。
- 上下文窗口管理不是一个算法,而是 5 层递进的工程系统
- 错误恢复不是 try-catch,而是精心设计的状态机迁移
- 流式处理不是
await response.text(),而是涉及背压、缓留、并发执行的复杂管道 - 工具系统不是函数数组,而是带权限模型、并发执行、结果预算的完整框架
Anthropic 在 Claude Code 上的实践,代表了当前 AI Agent 工程的最高水位线之一。无论你是在构建自己的 Agent 系统,还是只是想理解这类系统的设计边界,这份意外暴露的源码都是难得的一手学习材料。
本文基于 2026-03-31 公开曝光的 Claude Code 源码快照(npm 包 source map 泄露)进行分析,仅用于技术学习与架构研究。文中所有代码引用均来自该公开快照。
如果你对某个具体模块有更深入的问题,欢迎在评论区讨论。