我读了Claude Code泄露的51万行源码,发现Anthropic在造一个"AI操作系统"

0 阅读19分钟

一场意外的代码泄露,让我们第一次看清了这头野兽的全貌。

事情的起因

2026年3月31日,安全研究者 Chaofan Shou 在推上发了一条不起眼的帖子:

"Anthropic 发布到 npm 的 Claude Code 包里,source map 没有被剥离。"

翻译成人话:Claude Code 的完整 TypeScript 源码,51.2万行,1903个文件,就这样裸奔在公网上。

这种事放在任何一家公司都是一场小型事故。但对我们这些对 AI 工具感兴趣的人来说,这是一份意外的圣诞礼物。

我当然不可能在几小时内把51万行代码读完。所以我带着三个问题去翻这份代码:

  1. Claude Code 和其他 AI 编程工具,到底有什么本质区别
  2. 为什么它写代码的"手感"就是比别人好?
  3. 51万行代码里,到底藏着什么不为人知的东西

读完之后,我的第一反应是——

这不是一个 AI 编程助手。这是一个操作系统。

先讲一个故事:如果你要雇一个远程程序员

在进入源码之前,我想先讲清楚一件事:为什么市面上的 AI 编程工具"感觉"不一样?

想象你雇了一个远程程序员,要给他远程访问你电脑的权限。不同公司的方案,代表了完全不同的哲学:

Cursor 的方案:让程序员坐你旁边,每次他要执行命令,你都得看一眼、点个"允许"。简单、透明,但你得一直盯着,稍微走开就不行。

GitHub Copilot Agent 的方案:给他一台全新的虚拟机,让他在里面随便折腾,搞完了把代码提交上来,你审核后再合并。隔离得很干净,但他永远用不了你本地的环境、你的配置、你的 API key。

Claude Code 的方案:直接让他用你的电脑——但给他配了一套极其精密的安检系统。他能做什么、不能做什么、哪些操作需要你点头、哪些可以自己来、甚至他想执行 rm -rf 都要过9道关卡。

这就是三种完全不同的安全哲学。

为什么 Anthropic 选了最难的那条路?因为只有这样,AI 才能真正用你的终端、你的环境、你的配置来干活。这才是"替你写代码",而不是"在一个无菌室里给你写一段代码然后复制粘贴过来"。

代价是什么?51万行代码。

你以为的 Claude Code,和真实的 Claude Code

大多数人以为 AI 编程工具的工作原理大概是这样的:

用户输入 → 调用 LLM API → 返回结果 → 显示给用户

Claude Code 实际上是这样的:

用户输入
  → 动态组装 7 层系统提示词
  → 注入 Git 状态、项目约定、历史记忆
  → 42 个工具各自附带独立使用手册
  → LLM 决定调用哪个工具
  → 9 层安全审查(AST解析、ML分类器、沙箱检查...)
  → 权限竞争解析(本地键盘/IDE/Hook/AI分类器 同时竞争)
  → 200ms 防误触延迟
  → 执行工具
  → 结果流式返回
  → 上下文接近极限?→ 三层压缩启动
  → 需要并行?→ 生成子 Agent 蜂群
  → 循环直到任务完成

这不是一个"聊天机器人加几个插件",这是一条工业级的生产流水线。

接下来,我们逐层拆开。

第一层:提示词不是"写"出来的,是"编译"出来的

很多人学 AI 开发,第一件事就是研究"怎么写好 prompt"。但如果你打开 Claude Code 的 src/constants/prompts.ts,你会发现这个问题在 Anthropic 这里根本不存在——因为他们压根不是"写"提示词的。

他们是在组装提示词。

export async function getSystemPrompt(
  tools: Tools,
  model: string,
  additionalWorkingDirectories?: string[],
  mcpClients?: MCPServerConnection[],
): Promise<string[]> {
  return [
    // --- 静态部分(可缓存)---
    getSimpleIntroSection(outputStyleConfig),
    getSimpleSystemSection(),
    getSimpleDoingTasksSection(),
    getActionsSection(),
    getUsingYourToolsSection(enabledTools),
    getSimpleToneAndStyleSection(),
    getOutputEfficiencySection(),

    // === 缓存边界 ===
    ...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),

    // --- 动态部分(每次不同)---
    ...resolvedDynamicSections,
  ].filter(s => s !== null)
}

注意到 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 这个东西了吗?

这是一条缓存分界线。分界线上面的内容是静态的,可以被 Claude API 缓存,不重复计费;分界线下面是动态的——你当前的 Git 分支、你项目里的 CLAUDE.md 配置、你之前告诉它的习惯偏好……每次对话都不一样。

这个设计的高明之处在于:

  • 省钱:静态部分走缓存,不重复计费
  • 提速:缓存命中直接跳过这些 token 的处理
  • 灵活:动态部分让每次对话都能感知当前环境

Anthropic 把提示词当成了编译器的输出来管理。静态部分是"编译后的二进制",动态部分是"运行时参数"。这是工程化的提示词管理,不是手工艺。

每个工具都有自己的"员工手册"

更让我惊讶的是:每个工具目录下都有一个 prompt.ts 文件——这是专门写给 LLM 看的使用规范。

看看 BashTool 的(光这一个文件就有约 370 行):

Git Safety Protocol:
- NEVER update the git config
- NEVER run destructive git commands (push --force, reset --hard, 
  checkout .) unless the user explicitly requests
- NEVER skip hooks (--no-verify) unless the user explicitly requests
- CRITICAL: Always create NEW commits rather than amending

这不是写给人看的文档,是写给 AI 看的行为准则。每次 Claude Code 启动时,这些规则都会被注入到系统提示词里。

这就是为什么 Claude Code 不会擅自 git push --force,而某些工具会——不是模型更聪明,是提示词里已经把规矩讲清楚了

内部版本和你用的不一样

代码里大量出现这种分支:

const minimalUniquenessHint =
  process.env.USER_TYPE === 'ant'
    ? '\n- Use the smallest old_string that's clearly unique'
    : ''

ant 就是 Anthropic 内部员工。他们的版本有更详细的代码风格指引("不写注释,除非 WHY 不明显")、更激进的输出策略("倒金字塔写作法"),以及一些仍在 A/B 测试的实验功能,比如 Verification AgentExplore & Plan Agent

这说明什么?Anthropic 自己就是 Claude Code 最重度的用户。他们在用自己的产品来开发自己的产品,然后把踩过的坑直接写进提示词。

第二层:42个工具,但你只看到了冰山一角

打开 src/tools.ts,可以看到工具注册中心:

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    BashTool,
    FileReadTool, FileEditTool, FileWriteTool,
    GlobTool, GrepTool,
    WebFetchTool, WebSearchTool,
    TodoWriteTool, NotebookEditTool,
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
    // ... 还有更多条件加载的工具
  ]
}

42个工具,但大部分你从来没有直接见过。因为很多工具是延迟加载的——只有当 LLM 真正需要时,才通过 ToolSearchTool 按需注入。

为什么要这么麻烦?

因为每多一个工具,系统提示词就要多一段描述,token 就多花一份钱。你只是想让它帮你改一行代码,它没必要加载"定时任务调度器"和"团队协作管理器"。

还有一个彩蛋:

if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
  const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
  return filterToolsByDenyRules(simpleTools, permissionContext)
}

设置 CLAUDE_CODE_SIMPLE=true,Claude Code 就只剩三个工具:Bash、读文件、改文件。这是给极简主义者留的后门,也是给网络条件差或者 token 预算紧张的用户留的逃生通道。

"fail-closed":宁可过度保守,也不漏掉风险

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?) => false,    // 默认:不并发安全
  isReadOnly: (_input?) => false,            // 默认:会写入
  isDestructive: (_input?) => false,
}

注意那些默认值——isConcurrencySafe 默认 falseisReadOnly 默认 false

这叫 fail-closed 设计:如果一个工具的开发者忘了声明安全属性,系统会假设它是"不安全的、会写入的"。宁可过度保守,也不漏掉一个风险。

这和很多公司的做法截然相反——很多系统的默认值是"允许",出了问题再补救。Anthropic 从一开始就选了"拒绝"作为默认值。

"先读后改"的铁律

function getPreReadInstruction(): string {
  return '\n- You must use your `Read` tool at least once in the 
  conversation before editing. This tool will error if you attempt 
  an edit without reading the file.'
}

FileEditTool 会强制检查:你有没有用 FileReadTool 读过这个文件?没读过,直接报错,不让改。

这就是为什么 Claude Code 不会"凭空写一段代码覆盖你的文件"——它被系统强制要求先理解再修改。这个约束不是靠模型的"道德感",而是写死在工具的准入逻辑里的。

第三层:记忆系统——它是怎么"认识你"的

用过 Claude Code 一段时间的人,都会有一个感受:它好像真的认识你。

你告诉它"不要在测试里 mock 数据库",下次对话它就记住了。你告诉它"我是后端工程师,React 新手",它解释前端代码时就会用后端的类比。你的项目里有什么奇怪的约定,它一旦学到了就不会违反。

这背后是一个完整的记忆系统,而且这个系统本身也是用 AI 驱动的。

用 AI 来检索记忆

const SELECT_MEMORIES_SYSTEM_PROMPT = 
  `You are selecting memories that will be useful to Claude Code.
   Return a list of filenames for the memories that will clearly 
   be useful (up to 5).
   - If you are unsure if a memory will be useful, do not include it.
   - If a list of recently-used tools is provided, do not select 
     memories that are usage reference for those tools. DO still 
     select memories containing warnings, gotchas, or known issues.`

Claude Code 用另一个 AI(Claude Sonnet)来决定"哪些记忆和当前对话相关"。

不是关键词匹配,不是向量数据库——而是让一个小模型快速扫描所有记忆文件的标题和描述,选出最多5个最相关的,然后把它们完整地注入到当前对话的上下文里。

策略是精确度优先于召回率——宁可漏掉一个可能有用的记忆,也不塞进一个不相关的记忆来污染上下文。

想想这意味着什么:连"该记住什么、该忘记什么"这件事,Anthropic 都没有用传统的工程方案来解决,而是直接用另一个 AI 来做判断。

KAIROS:AI 在"做梦"

这是整份源码里最让我觉得科幻的部分。

代码里有一个叫 KAIROS 的特性标志。在这个模式下,长会话中的记忆不是存储在结构化文件里,而是存在按日期追加的原始日志里。然后,有一个叫 /dream 的技能会在低活跃期(也就是"夜间")运行,把这些原始日志蒸馏成结构化的主题文件:

logs/2026/03/2026-03-30.md   ← 今天的原始记录
         ↓ /dream 蒸馏
memory/user_preferences.md   ← 结构化的用户偏好
memory/project_context.md    ← 结构化的项目背景

AI 在"睡觉"的时候整理记忆。

这已经不是工程了,这是仿生学。人类的大脑在睡眠期间会把短期记忆转化为长期记忆,海马体会在 REM 睡眠中"重播"白天的经历并完成整合。KAIROS 做的是同一件事。

我不知道这个功能现在是否已经在生产环境启用,但光是"有人在认真设计这个"这件事本身,就足够令人震惊。

第四层:它不是一个 Agent,是一群

当你让 Claude Code 处理一个复杂任务时,它可能悄悄做了你完全不知道的事:

// AgentTool 的输入 schema
z.object({
  description: z.string().describe('A short (3-5 word) description'),
  prompt: z.string().describe('The task for the agent to perform'),
  subagent_type: z.string().optional(),
  model: z.enum(['sonnet', 'opus', 'haiku']).optional(),
  run_in_background: z.boolean().optional(),
})

它生成了一个子 Agent——一个专门负责某个子任务的小 Claude,在后台悄悄运行。

而且子 Agent 有严格的"自我认知"注入,防止它递归地再生成更多子 Agent:

export function buildChildMessage(directive: string): string {
  return `STOP. READ THIS FIRST.

You are a forked worker process. You are NOT the main agent.

RULES (non-negotiable):
1. Your system prompt says "default to forking." IGNORE IT — 
   that's for the parent. You ARE the fork. 
   Do NOT spawn sub-agents; execute directly.
2. Do NOT converse, ask questions, or suggest next steps
3. USE your tools directly: Bash, Read, Write, etc.
4. Keep your report under 500 words.
5. Your response MUST begin with "Scope:". No preamble.`
}

这段提示词翻译成大白话就是:你是工人,不是经理。别想着再招人,自己干活。

这个设计解决了多 Agent 系统里一个经典问题:递归爆炸。如果每个 Agent 都可以随意生成子 Agent,那很容易出现"Agent 生成 Agent 生成 Agent"的死循环,token 费用直接爆炸。通过在子 Agent 的提示词里硬编码"禁止分叉",Anthropic 在系统设计层面掐断了这个可能性。

协调器模式:AI 版的项目经理

在协调器模式下,Claude Code 变成一个纯粹的任务编排者,自己不动手,只分配任务:

Phase 1: Research     3  worker 并行搜索代码库
Phase 2: Synthesis     Agent 综合理解所有发现  
Phase 3: Implementation  2  worker 分别修改不同文件
Phase 4: Verification    1  worker 跑测试

核心原则写在代码注释里: "Parallelism is your superpower"

只读任务(搜索、分析):全部并行跑。 写文件任务:按文件分组,串行执行(避免冲突)。

这是一个非常务实的设计——并行快,但写同一个文件会冲突,所以只在安全的地方并行。

一个省钱的小细节

为了最大化子 Agent 的缓存命中率,所有 fork 子 Agent 的工具调用结果都使用相同的占位符文本:

'Fork started — processing in background'

为什么?因为 Claude API 的 prompt cache 是基于字节级前缀匹配的。如果 10 个子 Agent 的前缀字节完全一致,那么只有第一个需要"冷启动",后面 9 个直接命中缓存。

每次调用节省几美分,听起来微不足道。但如果你每天跑几千个 Agent,一个月能省下的数字就很可观了。Anthropic 在这种犄角旮旯的地方也要抠成本,足见他们对规模化成本的重视程度。

第五层:三层压缩,让对话"永不超限"

所有 LLM 都有上下文窗口限制。对话越长,历史消息越积越多,最终一定会超出限制。大多数工具的处理方式是:超了就截断。这会导致 AI 突然"失忆",之前讨论的东西它全不知道了。

Claude Code 为此设计了三层渐进式压缩:

第一层:微压缩——外科手术式清理

export async function microcompactMessages(messages, toolUseContext, querySource) {
  // 时间触发:如果上次交互已过很久,服务器缓存已冷
  const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
  if (timeBasedResult) return timeBasedResult

  // 缓存编辑路径:通过 API 的缓存编辑功能直接删除旧内容
  if (feature('CACHED_MICROCOMPACT')) {
    return await cachedMicrocompactPath(messages, querySource)
  }
}

微压缩只处理旧的工具调用结果——把"10分钟前读的那个500行文件的内容"替换成 [Old tool result content cleared]。对话的主线、你的指令、AI 的回复,全部保留。

这就像清理浏览器缓存,但只清理你不再需要的那些 tab 里的资源,而不是关掉所有 tab。

第二层:自动压缩——提前预警

当 token 消耗接近上下文窗口的 87% (窗口大小减去 13,000 的 buffer),自动压缩触发。

还有一个熔断器机制:连续 3 次压缩失败后,停止继续尝试,避免死循环。这个细节很有意思——大多数系统在设计时不会考虑"如果压缩本身失败了怎么办",但 Claude Code 考虑了。

第三层:完全压缩——AI 总结全场

当前两层都不够用时,让 AI 对整段对话生成一份摘要,然后用摘要替换掉所有历史消息。

生成摘要时有一段很严厉的前置指令:

const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. 
Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- Tool calls will be REJECTED and will waste your only turn.`

为什么要这么凶?因为如果总结过程中 AI 又去调工具,就会产生更多 token 消耗,适得其反——你是为了省 token 来总结的,结果总结过程消耗的 token 比你省的还多,这个笑话就开不下去了。

这段提示词是在对 AI 说:"你现在只有一件事,就是总结。别想别的。"

压缩后给不同内容分配的 token 预算:

  • 文件恢复:50,000 tokens
  • 每个文件上限:5,000 tokens
  • 技能内容:25,000 tokens

这些数字不是拍脑袋定的,是在"保留足够上下文继续工作"和"腾出足够空间接收新消息"之间反复调参的结果。

第六层:安全系统——9道关卡只为让你安全敲一行命令

这是整个代码库里最让我意外的部分:单是 BashTool 的安全相关代码,就有 18 个文件

一个工具,18个安全文件。

这9层审查大概是这样的:

  1. 静态词法分析:你的命令里有没有明显危险的模式?(比如 rm -rf /
  2. AST 解析:把 shell 命令解析成语法树,看命令的结构是否危险
  3. ML 分类器:用机器学习模型判断这个命令的意图
  4. 沙箱预执行:在隔离环境里先跑一遍,看会发生什么
  5. 权限检查:这个操作是否在用户的允许范围内?
  6. 上下文理解:结合当前任务,这个命令合理吗?
  7. 破坏性评估:这个操作可逆吗?(不可逆的要额外审查)
  8. 竞争解析:多个权限来源(本地、IDE、Hook、AI)同时给出判断,谁说了算?
  9. 200ms 冷却:最终执行前强制等200毫秒,防止误操作

200ms 的延迟设计尤其有意思。它不是为了性能,是为了给人类最后一次反应的机会。在机器的时间尺度上,200ms 是永恒;在人类的时间尺度上,200ms 刚好够你看清楚将要发生什么、然后决定要不要拦截。

这是一个专门为"人机协作"设计的停顿。

读完51万行代码,我真正学到了什么

AI Agent 的90%工作量在"AI"之外

51万行代码里,真正调用 LLM API 的部分可能不到5%。

其余95%是什么?

  • 安全检查(18个文件只为一个 BashTool)
  • 权限系统(allow/deny/ask/passthrough 四态决策树)
  • 上下文管理(三层压缩 + AI 记忆检索)
  • 错误恢复(熔断器、指数退避、Transcript 持久化)
  • 多 Agent 协调(蜂群编排 + 邮箱通信)
  • UI 交互(140个 React 组件 + IDE Bridge)
  • 性能优化(prompt cache 稳定性 + 启动时并行预取)

如果你正在做 AI Agent 产品,这才是你真正要解决的问题。不是模型够不够聪明——是你的脚手架够不够结实

很多人做 AI 应用的思路是:找一个好模型,写几个 prompt,调一调,上线。这条路做出来的东西,永远只是个 Demo。Claude Code 的源码告诉你:要做一个真正可用、可信任的 AI 工具,你需要的是工程,不是魔法。

提示词工程是系统工程

不是写一段漂亮的 system prompt 就完事了。Claude Code 的提示词是:

  • 7层动态组装,根据当前状态实时生成
  • 每个工具有独立的行为规范,单独维护
  • 缓存边界精确划分,静态和动态分开管理
  • 内外版本有不同的指令集,持续 A/B 测试
  • 工具顺序固定,保持缓存的字节级稳定性

这是工程化的提示词管理体系,不是某个人坐在那里"调 prompt"。

为失败而设计

Claude Code 的每一个外部依赖都有对应的失败策略:

  • 压缩失败?熔断器生效,停止重试
  • 记忆检索超时?跳过记忆,继续对话
  • 子 Agent 无响应?主 Agent 收到超时信号,继续其他任务
  • 文件读取失败?报错信息被格式化后回传给 LLM,让它决定下一步

没有哪个地方是"出错了就崩溃"。每一条错误路径都被提前设想过,并给出了降级处理方案。

这是做过生产系统的工程师才有的直觉:系统不能假设外部世界是正常的

Anthropic 在造的不是工具,是基础设施

Claude Code 的层对应操作系统的概念
42个工具系统调用
权限系统用户权限管理
技能系统应用商店
MCP 协议设备驱动
Agent 蜂群进程管理
上下文压缩内存管理
Transcript 持久化文件系统
KAIROS 记忆睡眠整合

这个类比不是牵强的——它是在设计层面就有意为之的。

Anthropic 不是在做一个"更好用的代码生成工具"。他们在做的是:以 LLM 为内核的操作系统

这个内核可以调用工具(系统调用),管理进程(子 Agent),有文件系统(Transcript),有内存管理(上下文压缩),有驱动层(MCP),甚至有自己的"睡眠与做梦"(KAIROS)。

最后说一句

51万行代码,1903个文件,18个安全文件只为一个 Bash 工具,9层审查只为让 AI 安全地帮你敲一行命令。

这是 Anthropic 对"如何让 AI 真正有用"这个问题给出的答案。

他们的答案不是"更大的模型",不是"更好的对齐",而是:建一套完整的信任体系

你不能把 AI 关在笼子里——关起来的 AI 帮不了你什么。你也不能让 AI 裸奔——裸奔的 AI 迟早给你惹麻烦。

唯一的路是:给它建一套精密的约束系统,让它在约束内自由行动。

这套约束系统的代价,是51万行代码。

而这只是 2026 年初的版本。

如果你觉得这篇有意思,可以去翻翻 Claude Code 的 npm 包,虽然现在 source map 应该已经被摘掉了——但好消息是,有人已经存了下来。