第1章:开篇 —— Claude Code Harness 总览
约 10 分钟 · 更新于 2026-09-01
第1章:开篇 —— Claude Code Harness 总览
本章回答三个问题:这份源码是怎么来的、Claude Code 到底是个什么形状的系统、以及它和前三个 harness 的根本分歧在哪。看完这一章你应该能接受本教程的一个判断:Claude Code 的架构主线不是"agent 循环",是"上下文预算"。
一、先说这份源码是怎么来的
读一个闭源产品的架构,通常只有三条路:读文档、看行为、反编译。这三条都不太行——文档讲用法不讲实现,行为只能看到表面,反编译出来的是被压缩过的名字(nJT、Chq 这种)。
Claude Code 提供了第四条路:npm 包里带 sourcemap,而且 sourcemap 里带 sourcesContent。
sourcesContent 是 Source Map v3 规范里的一个可选字段,用来存放原始源文件的完整文本,作用是让浏览器 DevTools 在没有源文件的情况下也能显示原始代码。它是为调试体验存在的,但副作用是:只要构建时没关掉它,产物里就带着一份原始源码。
于是 claude-code-deep-dive 这个项目做的事情很简单——把 cli.js.map 解析开,把 sourcesContent 里的每一份文本按 sources 里的路径写回磁盘。得到的就是本教程读的 extracted-source/。
这份源码有多"原始"?看几个证据:
证据一:注释里有内部 PR 编号和实验代号。
ts
// @[MODEL LAUNCH]: capy v8 assertiveness counterweight (PR #24302)
// — un-gate once validated on external via A/B
[源码 src/constants/prompts.ts:224]
证据二:注释里有生产事故的数据。
ts
// Stop trying autocompact after this many consecutive failures.
// BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272)
// in a single session, wasting ~250K API calls/day globally.
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
[源码 src/services/compact/autoCompact.ts:66]
证据三:有开发者写给自己看的段子。
ts
/**
* The rules of thinking are lengthy and fortuitous. They require plenty of thinking
* of most long duration and deep meditation for a wizard to wrap one's noggin around.
* ...
* Heed these rules well, young wizard. For they are the rules of thinking, and
* the rules of thinking are the rules of the universe. If ye does not heed these
* rules, ye will be punished with an entire day of debugging and hair pulling.
*/
[源码 src/query.ts:151]
(那三条"思考规则"本身是正经的:thinking block 必须在 max_thinking_length > 0 的请求里、不能是最后一个 block、必须在整条助手轨迹里保持完整。这段注释保护的是一类真实的 400 错误。)
证据四:有原作者也没想清楚的 TODO。
ts
//TODO: no need to set toolUseContext.messages during set-up since it is updated here
[源码 src/query.ts:545]
这份源码不是官方发布物
它是从公开 npm 包的调试产物里还原出来的,不是 Anthropic 公开的开源仓库。本教程当它是技术研究材料:只读、只做架构分析、不做任何"这段代码可以拿去用"的建议。同时它没有 git 历史,所以本教程不做时间线考古——不像 Codex 教程那样能追"这个功能什么时候进的仓库"。
它的年代:提示词里的前沿模型常量写着 Claude Opus 4.6,模型 ID 表只到 claude-opus-4-6 / claude-sonnet-4-6 / claude-haiku-4-5-20251001 [源码 src/constants/prompts.ts:118-125]。也就是 Claude 5 家族之前。快照的 git 提交时间是 2026-03-31。
本机运行的是 2.1.241(npm 上当时最新 2.1.245)。教程里凡是本机可验证的地方都会补一条 [实测]——最有说服力的一次交叉验证在第 6 章:快照源码里有一批 *_delta 附件类型,本机 48 个会话的 transcript 里实打实数出了 130 条 deferred_tools_delta、100 条 mcp_instructions_delta、38 条 agent_listing_delta。设计在源码里,痕迹在磁盘上,能对上。
二、Claude Code 是什么形状的系统
先看顶层目录 [源码 src/]:
| 目录 | 文件数 | 干什么 |
|---|
| utils/ | 564 | 底层共用能力(权限、钩子、会话存储、上下文计算都在这) |
| components/ | 389 | TUI 组件(React + Ink) |
| commands/ | 207 | 斜杠命令 |
| tools/ | 184 | 42 个工具目录 |
| services/ | 130 | 运行时服务(api / mcp / compact / plugins / analytics / lsp) |
| hooks/ | 104 | React hooks,不是钩子系统(钩子系统在 utils/hooks.ts) |
| ink/ | 96 | 终端渲染层 |
| bridge/ cli/ constants/ skills/ … | 各 10–30 | 入口、常量、技能加载 |
一眼看过去的第一个判断:这不是一个 CLI 包装器,是一个平台。CLI 界面(components/ + ink/,485 个文件)只占四分之一,剩下四分之三是运行时。
第二个判断来自入口层 [源码 src/entrypoints/]:
text
cli.tsx — 终端 REPL
init.ts — 初始化流程
mcp.ts — 把自己当成一个 MCP 服务器跑
sdk/ — 给 SDK 消费者的类型与协议
agentSdkTypes.ts — Agent SDK 的公共类型
sandboxTypes.ts — 沙箱配置类型
同一个 agent runtime,服务终端、IDE 插件、桌面 App、Web、SDK、以及"被别人当 MCP server 调用"这六种消费方式。这一点和 Codex 的思路是一致的(core 不知道前端是谁),但实现路径不同:Codex 用的是一份提交/事件队列协议,Claude Code 用的是一个 async generator——query() 吐出的消息流就是所有前端共同消费的东西(第 3 章详述)。
第三个判断来自一个不起眼的数字:89 个编译期特性开关。
ts
const reactiveCompact = feature('REACTIVE_COMPACT')
? (require('./services/compact/reactiveCompact.js') as typeof import('...'))
: null
[源码 src/query.ts:15]
feature() 来自 bun:bundle,是构建期常量折叠——不是运行时 if,是让打包器把整个分支连同被 require 的模块一起从产物里删掉。源码里到处是这样的注释:
feature() only works in if/ternary conditions (bun:bundle tree-shaking constraint), so the collapse check is nested rather than composed.
(feature() 只在 if / 三元表达式里生效——这是 bun:bundle 做 tree-shaking 的约束——所以 collapse 那个判断是嵌套写的而不是组合写的。)
[源码 src/query.ts:796]
为了让打包器能删掉死代码,代码的写法本身被扭曲了。 这是本教程里反复出现的一类现象:产物体积和 token 成本反过来塑造源码结构。
同一套机制还有第二个用途——process.env.USER_TYPE === 'ant' 也是构建期 --define:
ts
// DCE: `process.env.USER_TYPE === 'ant'` is build-time --define. It MUST be
// inlined at each callsite (not hoisted to a const) so the bundler can
// constant-fold it to `false` in external builds and eliminate the branch.
[源码 src/constants/prompts.ts:617]
也就是说:Anthropic 内部版和外部版是从同一份源码构建出的两个不同产物,内部版多一批提示词段落、多几个工具、多几种权限模式。本教程会明确标出哪些东西外部用户根本拿不到。
三、根本分歧:这个系统在优化什么
到这里可以给出本教程的核心判断了。
Pi 优化的是核心的简单性。dsh 优化的是能力的可替换性。Codex 优化的是产品的完备性。
Claude Code 优化的是 token 成本与上下文占用。
这不是一句概括,是能在源码里逐条指认的组织原则。举五个例子,它们分别来自五个完全不同的子系统:
3.1 系统提示词里有一根哨兵
ts
/**
* Boundary marker separating static (cross-org cacheable) content from dynamic content.
* Everything BEFORE this marker in the system prompt array can use scope: 'global'.
* Everything AFTER contains user/session-specific content and should not be cached.
*
* WARNING: Do not remove or reorder this marker without updating cache logic in:
* - src/utils/api.ts (splitSysPromptPrefix)
* - src/services/api/claude.ts (buildSystemPromptBlocks)
*/
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
[源码 src/constants/prompts.ts:105]
边界之前的内容可以用 scope: 'global' 缓存——注意这个词,不是"这个用户的缓存",是跨用户的全局缓存。所以那份静态提示词对所有人必须逐字节相同,于是任何一个"根据当前会话情况决定要不要加这句话"的段落,都必须挪到边界之后。第 5 章会看到一整套为此设计的注册表。
3.2 给 section 加"每轮重算"能力的函数叫 DANGEROUS
ts
/**
* Create a volatile system prompt section that recomputes every turn.
* This WILL break the prompt cache when the value changes.
* Requires a reason explaining why cache-breaking is necessary.
*/
export function DANGEROUS_uncachedSystemPromptSection(
name: string,
compute: ComputeFn,
_reason: string,
): SystemPromptSection {
return { name, compute, cacheBreak: true }
}
[源码 src/constants/systemPromptSections.ts:29]
注意第三个参数 _reason——它有下划线前缀,说明代码里根本不用它。它存在的唯一目的是逼调用者在代码里写一句人话解释为什么这里必须破坏缓存。全仓只有一处用了它:
ts
DANGEROUS_uncachedSystemPromptSection(
'mcp_instructions',
() => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
'MCP servers connect/disconnect between turns',
)
[源码 src/constants/prompts.ts:513]
而且看那个三元——一旦 delta 机制启用,它连这一处都不要了,MCP 说明改走消息尾部的附件(第 6 章)。
3.3 Agent 列表从工具描述里搬走了
ts
/**
* The dynamic agent list was ~10.2% of fleet cache_creation tokens: MCP async
* connect, /reload-plugins, or permission-mode changes mutate the list →
* description changes → full tool-schema cache bust.
*/
export function shouldInjectAgentListInMessages(): boolean { … }
[源码 src/tools/AgentTool/prompt.ts:53]
一个功能被重构的理由,是它占了全机队 10.2% 的缓存创建 token。 因为工具描述属于请求的靠前部分,它一变,后面全部重新缓存。解法是让 Agent 工具的描述变成一句静态的"可用 agent 类型见 <system-reminder> 消息",真列表挪到消息尾部去。
你现在就能看到这个设计
如果你在 Claude Code 里,往上翻本会话的系统提示词——Agent 工具的描述里那句 Available agent types are listed in <system-reminder> messages in the conversation. 就是这段代码的输出。
3.4 fork 子进程共享父进程的缓存
ts
/** Placeholder text used for all tool_result blocks in the fork prefix.
* Must be identical across all fork children for prompt cache sharing. */
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
[源码 src/tools/AgentTool/forkSubagent.ts:89]
fork 出来的子 agent 会继承父进程的完整对话,但父进程那条 assistant 消息里的每个 tool_use 都还没有结果。于是给每一个都填上同一个占位字符串,让所有 fork 子进程的请求前缀逐字节一致,只有最后一个 text block(各自的指令)不同。
模型侧的提示词里也把这件事直接告诉了模型:
Forks are cheap because they share your prompt cache. Don't set model on a fork — a different model can't reuse the parent's cache.
(fork 很便宜,因为它们共享你的 prompt cache。不要给 fork 指定 model——换个模型就没法复用父进程的缓存了。)
[源码 src/tools/AgentTool/prompt.ts:89]
3.5 连"3 天前保存"这种字样都要预先算好
ts
/**
* Pre-computed header string (age + path prefix). Computed once
* at attachment-creation time so the rendered bytes are stable
* across turns — recomputing memoryAge(mtimeMs) at render time
* calls Date.now(), so "saved 3 days ago" becomes "saved 4 days
* ago" across turns → different bytes → prompt cache bust.
*/
header?: string
[源码 src/utils/attachments.ts:508]
一条记忆附件的头部写着"3 天前保存"。如果每次渲染时重算,跨过午夜它会变成"4 天前",字节变了,缓存断了。所以创建时算一次,之后一直用那个字符串。
这五个例子来自提示词、注册表、工具描述、多 agent、附件渲染五个互不相干的子系统,但它们是同一句话的五个变体:不要让不该变的东西变。
可迁移的判断 ①
如果你的系统里有"前缀缓存"这类机制,就把"这个改动会不会动到前缀"提升成代码评审的一个固定问题,并且在类型系统或 API 命名上留下痕迹。
Claude Code 的做法是三件事:(a) 在数据里插一根哨兵常量当边界,(b) 把危险操作的函数名前缀成 DANGEROUS_,(c) 强制传一个代码里根本不读的理由字符串。第三件事是最聪明的——它把"解释清楚"这件事从代码评审的社会规范变成了编译器要求。
四、四个 harness 的一句话画像
| 组织原则 | 最能代表它的一处设计 |
|---|
| Pi | 减法 | 核心只有 4 个包,MCP / 子 agent / 权限弹窗明确不做,缺的用扩展补 |
| dsh | 一切皆插件 | 连 agent loop 本身都是配置文件里的一行 |
| Codex | 工程完备 | 四个操作系统四套沙箱实现,外加一个用模型当判官的安全层 |
| Claude Code | 上下文经济学 | 系统提示词里插一根字符串哨兵,把缓存边界写进数据本身 |
需要说清楚的是:这四条不是优劣排序,是不同约束下的不同解。
- Pi 是个库,用户是开发者,所以它可以说"这个我不做,你自己扩展"。
- dsh 要支持多种部署形态和厂商替换,所以它把一切做成接缝。
- Codex 是要装进一个静态二进制、跑在几百万台开发机上的产品,所以它必须自己解决每个操作系统的沙箱。
- Claude Code 的 token 直接计费给 Anthropic 自己(订阅制),机队规模到了"一个功能省 10% 缓存创建 token 就值得专门重构"的量级,所以缓存经济学成了第一性问题。
约束不同,最优解就不同。第 15 章会把这四行展开成完整对照,并说清哪些能抄、哪些抄了会死。
五、本教程的三条主线
读后面 14 章时,建议带着这三条线:
主线一:缓存前缀是不可侵犯的。
出现在第 4、5、6、12 章。任何"看起来绕远路"的设计,先问一句是不是为了保前缀。
主线二:上下文是有预算的,而且预算要分层花。
出现在第 6、7、8 章。技能列表拿走上下文窗口的 1%(SKILL_BUDGET_CONTEXT_PERCENT = 0.01);工具 schema 超过 10% 就自动转成延迟加载;压缩有五层,从最便宜的开始试。
主线三:好行为要写进 prompt 和 runtime 规则,不能指望模型即兴发挥。
出现在第 5、9、11、13 章。从"不要过度抽象"到"每个 PASS 必须附带 Command run 块",从 PreToolUse 钩子能改写输入到验证 agent 不许自己给自己判 PARTIAL。
六、动手复核
bash
# 0. 拿到源码
git clone https://github.com/tvytlx/claude-code-deep-dive.git
cd claude-code-deep-dive/extracted-source
# 1. 规模
find src -type f | wc -l # 1902
find src -type f | xargs cat | wc -l # 512685
# 2. 顶层目录按文件数排序
for d in src/*/; do echo -e "$(find $d -type f | wc -l)\t$d"; done | sort -rn
# 3. 89 个编译期开关
grep -rho "feature('[A-Z_0-9]*')" src/ | sort -u | wc -l
# 4. 本章引用的五处「缓存经济学」证据
grep -n 'SYSTEM_PROMPT_DYNAMIC_BOUNDARY' src/constants/prompts.ts
grep -n 'DANGEROUS_uncached' src/constants/systemPromptSections.ts
grep -n '10.2% of fleet cache_creation' src/tools/AgentTool/prompt.ts
grep -n 'FORK_PLACEHOLDER_RESULT' src/tools/AgentTool/forkSubagent.ts
grep -n 'saved 4 days' src/utils/attachments.ts
# 5. 快照的年代
grep -n 'FRONTIER_MODEL_NAME\|CLAUDE_4_5_OR_4_6_MODEL_IDS' -A5 src/constants/prompts.ts | head -20
本机侧(对照 2.1.241):
bash
claude --version
ls ~/.claude/projects/ # 每个项目一个目录
ls ~/.claude/projects/*/ | head # <sessionId>.jsonl + 同名子目录
七、总结
- 这份源码来自 npm 包 sourcemap 的 sourcesContent 字段——不是反编译,是带注释、带 TODO、带内部 PR 编号的原始 TypeScript。保真度接近读开源仓库,代价是没有 git 历史
- Claude Code 是平台不是 CLI:六种入口共用一个 runtime,UI 只占四分之一的代码量
- 89 个 feature() 是构建期常量折叠,内部版和外部版是同一份源码的两个产物;为了让 tree-shaking 生效,代码写法本身被约束了
- 组织原则是上下文经济学:提示词哨兵、DANGEROUS_ 前缀、agent 列表搬家、fork 共享缓存、附件头部预计算——五个子系统、同一句话
- 四个 harness 的差异来自约束差异,不是水平差异
下一章往下走一层:这 1902 个文件是怎么组织的,89 个开关怎么把一份源码变成两个产品,以及运行时在磁盘上留下什么。
- README-教程总览
- 第2章-工程骨架-一个npm包里的AgentOS
- 第4章-模型调用与缓存经济学 —— 本章第三节那五个例子的统一解释在这里
- Codex 教程第 1 章 —— 工程化路线的对照
- Pi 教程第 1 章 —— 减法路线的对照
- dsh 教程第 1 章 —— 全插件化路线的对照