Claude Code意外公开源码,深度解析背后的技术细节!(一)
说来也很好笑,Anthropic这家可谓是AI领域的顶级玩家,居然会犯这么低级的错误,在Claude code的 v2.1.88 版本发布到公共 npm registry 时,一个 59.8 MB 的 JavaScript source map 文件(.map )被意外打包进去了。
这个.map文件包含了一个指向 未混淆的 TypeScript 源码 的引用,而这个引用指向了一个托管在 Anthropic Cloudflare R2 存储桶 中的 zip 压缩包。任何人都可以通过这个压缩包获取源码。我也是迫不及待地去下载了源码,在AI的帮助下,理清了这51万行代码所包含的Agent开发架构。
Claude Code的prompt注入并不是我们平时调用大模型API一样,直接发送给model,而是在运行时由 constants/ 目录下的模块化段落组合而成的。架构中使用了一个叫做 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 的标记,将整个 prompt 切分成两部分: 静态段落 (跨组织可缓存的内容)和 动态段落 (用户/会话特定内容)。
这一层的特点是 内容固定、体积大、不随任何会话变量改变 ,所以可以一次缓存后被所有用户共享。
角色定义与全局行为规则 —— 「你是一个交互式 CLI 工具,帮助用户完成软件工程任务」这类基础人格描述。包括安全约束(允许/拒绝哪类操作)、URL 生成限制、代码质量原则(不过度工程化、不添加未被要求的功能、只在系统边界做校验)等。
工具定义(Tool Definitions) —— 约 40 个工具的完整定义,包括每个工具的 name、描述、参数 schema、权限门控逻辑。仅基础工具定义就有 29,000 行 TypeScript。这些内容不随用户变化,是最适合缓存的大块内容。
子 Agent 的 system prompt —— Explore(代码库探索)、Plan(架构规划)等内置子 agent 都有自己的 system prompt,这些也是静态的,可以缓存。
Beta功能协商头—— constants/betas.ts 中声明了 Claude Code 与 API 协商的所有 beta 特性,包括 interleaved-thinking 、 context-1m 、 structured-outputs 、 token-efficient-tools 等。这部分内容固定,也属于静态层。
这层的内容 每次请求都可能不同 ,一旦有任何变化就会破坏缓存命中。所以 Anthropic 把它们严格隔离在 boundary 之后,不让它们污染静态层。
当前日期 —— 形如 ## currentDate Today's date is 2026-02-20 ,被显式注入进来,并附带说明”IMPORTANT: this context may or may not be relevant to your tasks.” 日期每天变,是最典型的动态注入内容。
用户的 MEMORY.md 内容 —— MEMORY.md 是一个轻量级指针索引,始终被加载进上下文,每行约 150 字符。但它的内容随用户和项目变化,所以在 boundary 之后动态注入,而非静态缓存。
工作目录与项目上下文 —— 当前路径、git 状态、检测到的语言/框架等,每个项目都不同,动态组装注入。
Undercover Mode 指令 —— 这是一个自动激活的安全层,针对公开或开源仓库激活时,会向 prompt 中注入严格的隐身指令,防止泄露内部代号和 AI 身份信息。它是条件性动态注入,只在检测到目标是公开仓库时才插入。
promptCacheBreakDetection.ts 追踪 14 个可能导致缓存失效的向量,并设有”sticky latches”(粘性锁)来防止模式切换破坏缓存。
这 14 个向量是什么?从已知分析推断,大致包括:工具列表顺序变化、beta flags 变化、模式切换(如进入 plan mode)、Undercover Mode 的激活/停用、日期变更等。每一个都是潜在的缓存杀手,系统专门建了一套检测机制来追踪它们。
这套分层设计的本质不是”prompt 工程”,而是把 token 成本作为一等公民纳入架构设计。哪些内容放缓存层、哪些内容动态注入、哪些内容明确标记为危险——每一个决策都是在用架构约束来保住缓存命中率,因为在生产规模下,缓存失效不是性能问题,是账单问题。
工具模块是整个 Claude Code 架构里最值得研究的部分——它揭示了一个 产品级Agent 如何把”模型能做什么”和”被允许做什么”这两件事分开管理。Claude code中内置了42个工具,按照功能可以分为以下几类:
文件操作类:BashTool / PowerShellTool(Shell 执行,支持可选沙箱)、FileReadTool(读文件,支持文本、图片、PDF、Notebook)、FileEditTool(局部文件编辑)、FileWriteTool(创建或覆盖文件)、GlobTool(文件模式搜索)、GrepTool(内容搜索,底层使用 ripgrep)
网络访问类:WebFetchTool(获取 URL 内容)、WebSearchTool(网络搜索)、WebBrowserTool(浏览器自动化)
Agent 编排类:AgentTool(生成子 agent)、SendMessageTool(agent 间消息传递)、TeamCreateTool / TeamDeleteTool(团队级并行)
任务管理类:TaskCreateTool、TaskGetTool、TaskListTool、TaskUpdateTool、TaskOutputTool、TaskStopTool(后台任务全生命周期管理)
开发工具类:LSPTool(Language Server Protocol 通信)、NotebookEditTool(Jupyter Notebook 编辑)、REPLTool(交互式 VM shell)
工作流控制类:EnterPlanModeTool / ExitPlanModeV2Tool(Plan 模式切换)、CronCreateTool(定时触发)、RemoteTriggerTool(远程触发)、SleepTool(等待/主动模式)
扩展类:SkillTool(执行用户自定义 skill)、MCPTool(MCP server 调用)、BriefTool(上传/摘要文件到 claude.ai)、SyntheticOutputTool(结构化输出)
但是工具多并不意味着Token消耗就一定会多。Claude code的一次工具加载流程大概是这样的:
启动阶段
├── 并行预取:MDM + Keychain + GrowthBook flags
└── 懒加载:重模块按需 dynamic import()
每次 API 调用
├── 读取 CLAUDE.md(每轮重新注入)
├── 从 tools.ts 注册表筛选当前可用工具
│ ├── 过滤:feature flag 关闭的工具不出现
│ ├── 过滤:当前模式不允许的工具不出现
│ └── 过滤:权限不足的工具不出现
├── 序列化工具 schema(Zod 验证)
├── 组装 API 请求(工具定义附在请求体里)
└── Hook: PreToolUse 触发
↓ 模型返回 tool_use
├── Hook: PostToolUse 触发
└── 结果写回 context,进入下一轮
启动阶段做三件并行的准备工作(MDM、Keychain、GrowthBook flag),决定哪些工具在这次会话里是激活状态;进入每次对话轮次后,先重新读取 CLAUDE.md 获取最新上下文,然后从注册表里依次过滤掉 feature flag 关闭的、当前模式不允许的、权限不足的工具,剩下的才被序列化成工具 schema 附在 API 请求体里发出去;模型返回 tool_use 响应后,Hook 系统在执行前后各介入一次,最终结果写回 context 进入下一轮。
整条链路的核心逻辑只有一句话:模型每次”看到”的工具列表,是动态筛选的结果,而不是所有工具的全集。
记忆系统要解决的核心问题只有一个:长会话里模型越来越乱——上下文越堆越大,无关信息淹没关键信息,模型开始产生幻觉或做出矛盾决策。业内通常叫这个问题 “context entropy”。
Claude Code 的解法不是”存更多”,而是建了一套三层分级存储 + 一套主动维护机制。
MEMORY.md 不存任何实质内容,它只存指针——”去哪里找什么”,每行约 150 字符,始终加载在上下文里。
[auth-flow] → memdir/auth.md # OAuth 实现细节
[db-schema] → memdir/database.md # 表结构和关系
[user-prefs] → memdir/preferences.md # 用户配置偏好
为什么不直接存内容?因为一旦把所有记忆都塞进上下文,token 预算就会被历史信息占满,留给当前任务的空间越来越少。MEMORY.md 只占极少的 token,却让模型知道”完整信息在哪里”,需要的时候再去取。
Strict Write Discipline:模型只有在成功写入对应文件之后,才能更新 MEMORY.md 里的指针。这条规则防止了一个常见问题——模型在操作失败时仍然更新了索引,导致索引指向一个不存在或过期的状态,下次读取时拿到错误信息。
实际的项目知识存在一系列独立的主题文件里,放在 memdir/ 目录下,按领域分开管理。
这层的核心原则是按需拉取,不全量载入。模型在 MEMORY.md 里找到相关指针后,才去读对应的 topic file,读完用完,不会把所有主题文件同时加载进上下文。
这解决了一个实际的工程问题:一个项目可能有几十个不同领域的记忆文件,但任何一次对话通常只涉及其中两三个。全量载入是在浪费 token,分领域按需加载才是正确的做法。
过去的会话记录永远不会被完整地重新载入上下文,而是只对其进行 grep——针对特定标识符做精确检索,只把命中的片段拉出来用。
这背后的逻辑是:历史记录的绝大部分内容在当前任务里是无关的,把它全部重新载入等于把所有的噪声一起带进来。grep 的方式只取信号,不取噪声。
记忆的正确性保障:Skeptical Memory
这是整套系统里最反直觉、也最关键的一条设计:模型被明确指示,要把自己的记忆当作”hint”来对待,而不是事实。
执行任何操作之前,必须对照实际代码库或文件系统验证。记忆说”这个函数在 auth.ts 里”,模型不能直接用这个结论——它需要去实际读 auth.ts 确认,再继续。
为什么需要这条规则?因为记忆本身可能是过期的。代码变了,但 memdir 里的记录还是旧的。如果模型盲目信任记忆,就会基于错误前提执行操作,产生很难排查的 bug。把记忆降级为”参考”而不是”事实”,是在系统层面对抗记忆陈旧性问题。
上面三层解决的是”如何存”和”如何取”,autoDream 解决的是”如何保持记忆质量”。
autoDream 在用户空闲时自动运行,做三件事:
合并零散观察:把多次会话中分散积累的碎片信息整合成连贯的知识
消除逻辑矛盾:检测记忆条目之间的冲突,保留更新的版本
固化推断结论:把模糊的”可能是这样”转化为”确认是这样”,或者标记为需要验证
这个过程通过一个 fork 出来的子 agent 执行,而不是在主 agent 的上下文里运行。这个设计决策很重要——记忆维护如果在主对话流里执行,会污染主 agent 的”思维链”,让它在处理用户任务的同时还要分心做整理工作。隔离到子 agent 里,两件事互不干扰。
autoDream 对应的是 KAIROS 后台模式的基础设施,在 KAIROS 完全启用后,这个维护过程会在用户空闲的每个夜晚自动跑一遍,类似于数据库的定期 vacuum。
用户会话
│
├─ MEMORY.md(始终在上下文,只存指针)
│ │
│ └─ 按需读取 → Topic Files(memdir/)
│
├─ 历史记录(只 grep,不重读)
│
├─ Skeptical Memory(所有记忆视为 hint,执行前必须验证)
│
├─ 压缩触发(5 种策略,token 预算保持在 50K)
│
└─ [用户空闲]
└─ autoDream(fork 子 agent)
├─ 合并碎片
├─ 消除矛盾
└─ 固化结论 → 写回 Topic Files → 更新 MEMORY.md
一句话总结这套系统的设计哲学:不让模型记所有东西,让模型知道去哪里找所有东西——然后对找到的东西保持怀疑,去原处验证再用。
此外,由于篇幅有限(dazileile),还有Claude code的上下文压缩策略,子Agent编排以及一些未发布的隐藏功能,下期再讲。
最后,附上源码GitHub链接:https://github.com/instructkr/claw-code