现在来学习一下 Claude Code 的架构与设计思想。
一下子也看不完,准备持续记录笔记,对哪里感兴趣就先看哪;互联网上,各种 CC 源码报告充斥着无数自相矛盾的 LLM 幻觉,真是越看越红温,不如用自己的 CC 专门读几个方向的代码。
首先下载源码 anthropic-ai-claude-code-2.1.88。cli.js 是已经打包后的成品代码,是打包且混淆过的代码;cli.js.map 是成品代码与源代码对照表,里面有 sourcesContent。对 cli.js.map 遍历每个 sourcesContent[i] 就能获得源码内容。
由于我只写过 python 和 c/cpp,补充下最小的 JS/TS 项目知识:
Node 是 JavaScript 运行时,运行 Node 项目就是输入命令
node cli.js;npm 是包管理器,在node_modules管理依赖。然而,Claude Code 的运行时不是 Node 而是 Bun。Node 项目可以打包为 exe。事实上,
where.exe claude得到bin\claude.exe。claude.exe是用 Bun 打包的独立可执行文件,这就是 Bun 的单文件打包(Single-file Executable)。对于通过
irm https://claude.ai/install.ps1 | iex下载到的claude.exe,其本质是 Bun 运行时 + 打包好的 JS 代码。- 我在终端输入
claude,发生:Windows 执行 claude.exe,exe 内部的 Bun 运行时启动,Bun 引擎执行内嵌的 Claude Code JS 代码。
现在来看 src/。
Memory
在了解最小的 JS/TS 项目知识时,Claude Code 误导了我,认为自己是 Node.js 运行时。后来 CC 自己更正了记忆。

记忆文件大致如下:

对于记忆的读取,在 src\memdir\memdir.ts 的 loadMemoryPrompt()。启动时,Claude Code 把记忆目录的内容注入到系统提示里,记忆目录在 ~/.claude/projects/<sanitized-project-path>/memory/。
更正记忆时,使用的就是普通的 Write / Edit 工具写文件。
src/services/extractMemories/extractMemories.ts 是一个后台 agent,在关闭会话后异步运行。它会扫描整个对话记录,自动提取出值得记忆的内容并写入文件。然而我在 ~\.claude.json 里根本没找到 tengu_passport_quail,这可能是没有开启的功能。
总之,没有数据库,没有 RAG,就是文件。
共有 4 种记忆类型,在 src/memdir/memoryTypes.ts 中定义,如图:

src/memdir/memoryTypes.ts 中各记忆类型的 when_to_save 提示 LLM 什么时候要更新记忆。
一般来说,MEMORY.md 只放一行一行的指针链接,不放内容。指向的具体话题文件会有 frontmatter,其中的 description 会被 memoryScan.ts 扫到。
工具
工具调用入口在 src/services/tools/toolExecution.ts 的 checkPermissionsAndCallTool() 函数。完整调用链是:

每个工具是一个实现了 Tool 接口的对象,
1 | Tool = { |
然后通过 buildTool() 工厂函数填入安全默认值创建。
我之前读过 opencode 对于工具使用的源码,和这里的逻辑很相似,都是先注册对象,再填入安全默认值、自定义配置。这样的设计可以让自定义覆盖默认,默认值可以写更偏向于保守的安全措施。
在下文会提到的 Agent Loop 中,一次 while 循环里可以用多个工具,且如果可以的话会并发执行。具体来说,模型可以在一条 assistant 消息里返回多个 tool_use block。runTools() 收到整个数组后,调用 partitionToolCalls() 把它们分成 Batch。分 batch 的规则在 toolOrchestration.ts 里,isConcurrencySafe=true 的工具合并成一批并发运行,isConcurrencySafe=false 的工具单独一批串行运行。批次之间顺序严格。默认并发上限是 10,不过环境变量 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 能覆盖。
isConcurrencySafe=true 的工具例子:Read、Grep、Glob 等只读工具。isConcurrencySafe = false 的工具例子:Write、Edit 等写文件工具。
TOOL_DEFAULTS 里默认是 isConcurrencySafe = false。
源码里还读到个最激进的。当 streamingToolExecutor 启用时,工具在 API 还在流式输出时就已经开始执行,不用等整条 assistant 消息结束,让工具执行和模型输出重叠。

如果想用它,要用 anthropic 的官方账号获得 feature flag 缓存,我也不知道这个是实验性的还是说有官方账号就能用。
Agent Loop
src/query.ts 里的 queryLoop() 函数就是主循环。结构是 while(true) + 状态机。
1 | while(true) 第 N 轮 |
逻辑顺下来比 opencode 的 agentic loop 简明。其是否继续 loop 仅与是否要继续调用工具有关。
对于压缩,有对应的逻辑,稍后分析。
1 | messagesForQuery = getMessagesAfterCompactBoundary(messages) |
注意这个 snip 是实验性内容。源码文件 snipCompact.js 在这份源码里找不到,被 DCE 掉了。microcompact 和 autocompact 我在下一节里做笔记。
然后流式调用 API。如果有 tool_use,就 toolUseBlocks.push(...) 且标记 needsFollowUp = true。
检查 needsFollowUp 标记,如果没有就结束。如果有就往后走 runTools,伪代码类似:
1 | const toolUpdates = runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext) |
runTools 就是 toolExecution.ts 里的流程,校验 → 权限 → 执行 → 返回结果。
最后拼接新 state,进入下一轮 while 循环,类似:
1 | state = { |
补充,queryLoop() 被 query() 调用。query() 是 async function* 异步生成器,里面可以 await 可以 yield*, 把 queryLoop 产出的每一条消息原封不动地 yield 给外层:
1 | const terminal = yield* queryLoop(params, consumedCommandUuids) |
补充,QueryEngine.ts 的 QueryEngine 类会处理 /slash 命令、构建 system prompt 等,然后 for await query()。一个会话一个 QueryEngine 实例,One QueryEngine per conversation。
Compact
token 接近上下文限制触发 autoCompactIfNeeded 或输入 /compact 进行压缩。
在 src\services\compact\compact.ts 中,先做两步清理。stripImagesFromMessages() 把图片替换成 [image] 文字占位符,stripReinjectedAttachments() 去掉 compact 后会自动重新注入的附件。然后 microcompactMessages 清空旧工具结果内容。
注意这个清空旧工具结果内容 microCompact 上文也提到了,每一轮 query 循环里都有,不只在自动压缩里。microcompactMessages() 函数本身是一个二路分支,按优先级依次判断。不过第一个为默认关闭,第二个为 anthropic 内部的,用户用不了:
- 时间触发
maybeTimeBasedMicrocompact:距离上一条 assistant 消息的时间戳超过阈值。这是因为 Anthropic API 的 prompt cache 有时间限制,如果没超时当然最好就是不改直接用上,但超时了反正会缓存失效,不如趁此机会压缩 prompt。压缩的方法就是把可压缩的工具结果内容替换成占位字符串[Old tool result content cleared],默认只保留最近 5 条。 - 缓存编辑
cachedMicrocompactPath:向 API 发一个额外的cache_edits指令块,让服务端在自己的 prompt cache 里删掉旧工具结果。真没理解这是啥意思,没想象出来如何让服务端修改 prompt cache 的。询问大语言模型也没问出个所以然,网上也查不到
然后以 src\services\compact\prompt.ts 为 prompt,让 LLM 压缩全部对话。大意:
1 | CRITICAL: Respond with TEXT ONLY. Do NOT call any tools. |
生成摘要之后,src\services\compact\compact.ts 先标记这里发生过 compact,然后插入生成的摘要,附上最近几条消息,重新注入 CLAUDE.md 等附件。最后还留了能给用户 hook 的事件节点,用户在 settings.json 能配置这个压缩事件结束后执行的 shell 命令。
要是没读到这里,都不知道这里能设 hook。顺便看了一下官方文档,能 hook 扩展的事件点还挺多:Hooks 参考 - Claude Code Docs
源码里看到有个未开放的实验性的压缩功能:src\services\compact\sessionMemoryCompact.ts。浏览了一下发现是让 CC 在对话过程中持续在后台提取关键信息,存到一个本地文件 session_memory 自动做笔记。触发 compact 时,直接把这份现成笔记当摘要用,跳过调 LLM 生成摘要的步骤。
auto Dream
这是 src\services\autoDream\autoDream.ts 里的实验性功能。auto Dream 在对话结束后自动触发后台子 Agent,把最近多个会话的 transcript 读一遍,然后整理、更新、合并 memory 文件。三个门控:

流程如下:

搞笑
src\utils\userPromptKeywords.ts 用了一个非常长的正则表达式判断用户的 prompt 是不是在骂 CC,太难绷了。
未完。TODO:
- [x] Memory,工具调用总览,Agent Loop(CC 把它称为 query Loop),Compact
- [ ] 看几个具体的读写 tool
- [ ] fork subagent
- [ ] 明天再想