Claude Code Deep Dive

Claude Code 架构深度解析

基于泄露源码的逆向工程分析 — 从宏观架构到微观实现,全面解读 Anthropic 的本地 Agent 平台设计

1,902
源文件
513K
行代码
43
内建工具
100+
命令
85
React Hooks
6
运行形态

🗺️ 源码阅读地图:怎么用这个仓库学 Harness

Claude Code 的源码是一份难得的 harness 设计范本,但 src/ 下近两千个文件直接读极易迷路。本章给出"该读哪些文件、按什么顺序读、每个文件盯哪个函数"的导航——让你带着问题进源码,而不是大海捞针。

先认识仓库里的三层资料

这个学习仓库里有三种互补的材料,建议配合使用,而不是只盯一份:

资料是什么怎么用
src/逆向还原的 Claude Code 源码(约 1900 文件 / 513K 行)唯一真相来源。按下方路线定向跳读,切忌从头通读
analysis/29 篇分模块的深度笔记(Markdown)读源码前先扫对应笔记建立预期,再回源码核对
本文档架构全景 + 工程心法(你正在看的这份)建立心智模型与术语索引,当作"地图册"
黄金读法:本文档建立「地图」 → analysis/ 笔记建立「预期」 → src/ 源码「对照验证」。三者来回印证,比单看任何一份都快得多。

三条难度递进的阅读路线

按你的目标选一条线,务必从绿线开始。每条线都能独立读完、独立收获,不必一次吃透全部。下图自上而下即推荐阅读顺序:

🟢 跑通最小闭环 · 半天 🟡 抗崩与长任务 · 进阶 🔴 平台化扩展 · 高级 query.ts① 主循环 while(true) Tool.ts② buildTool 安全旗标 toolOrchestration③ 工具调度执行链 claude.ts④ stream() 流式输出 autoCompact⑤ 预算闸门 + 熔断 PTL 截断⑥ 超长头部截断兜底 session JSONL⑦ resume 修复链 memdir/⑧ 压缩后复灌状态 prompt boundary⑨ 静态/动态缓存边界 services/mcp⑩ 外部工具接入 swarm/⑪ forkSubagent 协作 sandbox/⑫ 来源决定能力
路线选择:只想搞懂"agent 凭什么能自主干活"→ 走完 🟢 绿线 即可;要做能扛真实任务的 harness → 加 🟡 黄线;要做平台 / 多 agent → 再上 🔴 红线

这条线的唯一目标:亲手看清 一个 LLM 是怎么被"循环 + 工具"变成 agent 的。读完你应能复述出主循环的每一步。

打开文件聚焦符号带着这个问题读详解
src/query.tsquery()while 循环tool_use 如何变成下一轮的输入?循环靠什么终止?Harness
src/Tool.tsbuildTool()三个安全旗标为何默认全 false?忘记声明会怎样?工具系统
services/tools/toolOrchestration.tsrunTools / partitionToolCalls一轮多个工具调用,如何决定并发还是串行?执行内核
services/api/claude.tsstream()模型输出怎么逐 token 流出来、又怎么被收集成动作?执行内核
读完自测:不看代码,能否画出"组装上下文→问模型→收集 tool_use→执行→回灌 tool_result→再循环"这张图?能画出来,绿线就通关了。

闭环能跑通只是 demo。真实任务一长,必然撞上"上下文溢出"和"进程崩溃"两座大山。这条线看 Claude Code 怎么把硬错误降级成软兜底。

打开文件聚焦符号带着这个问题读详解
src/context/(compact 相关)autoCompactIfNeeded()上下文逼近上限时怎么不崩?熔断器在防什么活锁?Context
同上(PTL 处理)prompt-too-long 重试压缩都不够时,最后一道兜底怎么截?Context
会话存储(JSONL)append-only / resume 修复链崩溃后怎么续聊?孤儿 tool_result 怎么补?会话存储
memdir/ / compact.tscreatePostCompactFileAttachments压缩之后模型为什么不会"失忆"忘掉打开过的文件?Memory

当你的 harness 要省钱、要接外部工具、要多 agent 协作、要在不可信环境里安全运行时,读这条线。它是"工程化的天花板"。

打开文件聚焦符号带着这个问题读详解
Prompt 管理相关buildEffectiveSystemPrompt / boundary静态前缀如何切出来命中 prompt cache 省钱?Prompt
services/mcp/wrapFetchWithTimeout / 工具去重外部工具如何接入、同名时谁优先、怎么防雪崩?MCP
multi-agent / swarm/forkSubagent / Coordinator子 agent 如何复用父字节?多 agent 怎么通信协作?Multi-Agent
sandbox / skillsexecuteShellCommandsInPrompt / loadedFrom同一份 Shell 能力为何对本地放开、对远程切断?Sandbox

读源码前,强烈建议先把下面这段 能真正 node 跑通的最小 harness 跑一遍——它把绿线的概念浓缩成约 60 行可执行代码。跑通它,再去读 query.ts,你会发现"Claude Code 的内核不过是这段代码的工业级版本"。

mini-harness.mjs — 可直接运行(填入 API key 即可)
// 运行:ANTHROPIC_API_KEY=sk-... node mini-harness.mjs "算 (12+5)*3"
const API = 'https://api.anthropic.com/v1/messages'
const KEY = process.env.ANTHROPIC_API_KEY

// ① 工具表:每个工具 = schema + 执行函数 + fail-closed 安全旗标
const tools = {
  calculator: {
    schema: { name: 'calculator', description: '计算一个算术表达式',
      input_schema: { type: 'object', properties: { expr: { type: 'string' } }, required: ['expr'] } },
    readOnly: true,                       // 默认应为 false;这里显式声明只读
    call: ({ expr }) => String(eval(expr)),   // 仅教学,生产勿用 eval
  },
}

// ② 朴素上下文兜底:超阈值就丢最早几轮(生产应换成 autoCompact)
function trim(messages, max = 40) {
  return messages.length <= max ? messages : messages.slice(-max)
}

async function callModel(messages) {
  const res = await fetch(API, {
    method: 'POST',
    headers: { 'x-api-key': KEY, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },
    body: JSON.stringify({
      model: '<在此填入模型名>', max_tokens: 1024,
      tools: Object.values(tools).map(t => t.schema),
      messages,
    }),
  })
  return res.json()
}

// ③ harness 主循环:问模型 → 执行工具 → 回灌 → 再问
async function run(userInput) {
  let messages = [{ role: 'user', content: userInput }]
  while (true) {
    const reply = await callModel(trim(messages))
    messages.push({ role: 'assistant', content: reply.content })

    const calls = reply.content.filter(b => b.type === 'tool_use')
    if (calls.length === 0) {                  // 模型不再点工具 = 任务完成
      console.log(reply.content.find(b => b.type === 'text')?.text)
      break
    }
    const results = []
    for (const c of calls) {
      const tool = tools[c.name]
      let content
      try {
        content = tool ? tool.call(c.input) : `未知工具: ${c.name}`   // fail-closed:未知不放行
      } catch (e) {
        content = `工具出错: ${e.message}`                  // 结构化回灌,不炸循环
      }
      results.push({ type: 'tool_result', tool_use_id: c.id, content })
    }
    messages.push({ role: 'user', content: results })       // 关键:结果以 user 角色回灌
  }
}

run(process.argv[2] ?? 'hello')
这 60 行里藏着 harness 的全部骨架:① 工具表(schema + 执行 + 安全旗标)、② 上下文兜底、③ "问→执行→回灌"主循环。后续每读懂一个 Claude Code 的机制,都可以回来问自己:"它在这段代码的哪一步加了什么加固?"——这就是把范本变成自己能力的最快路径。
三个常见的读源码误区:从头通读——近两千文件会劝退,永远带着一条路线和一个问题去读;② 只读不跑——先跑通上面的最小 harness 再读源码,抽象立刻落地;③ 纠结实现细节——先抓"它解决什么问题、用什么策略",具体函数实现是第二层,别一上来就陷进 diff。

🏗️ 六层分层架构

Claude Code 不是单纯的命令行聊天程序,而是一套本地 Agent 平台。采用六层分层架构,每一层职责清晰、接口明确。

Layer 1

CLI 引导层

多入口分流器,早期快路径分流,支持 6 种运行形态

cli.tsxmain.tsx
Layer 2

TUI/REPL 交互层

基于 Ink (React) 构建的终端 UI 工作台,消息区+输入区双中枢

REPL.tsxMessages.tsx
Layer 3

Query/Agent 执行内核

统一执行主循环,AsyncGenerator 流式输出,所有形态共用

query.tsQueryEngine.ts
Layer 4

Tool/Permission 层

43 内建工具 + MCP 工具池,内建权限(fail-closed 默认安全)

Tool.tstools/
Layer 5

Memory/Persistence 层

四层记忆体系,文件化可审计,分层 compact 策略

memdir/compact.ts
Layer 6

MCP/Remote/Swarm 扩展层

MCP 协议集成、远程连接、多 Agent 协作平台化扩展

services/mcp/swarm/

核心架构全景图

CLI 引导层 cli.tsx → 快路径分流 · main.tsx(编排中心) REPL · headless · SDK · MCP · bridge · remote 初始化层(能力装配) init.ts(trust 前)+ setup.ts(trust 后) commands + tools + MCP + agents + skills 控制面 / 命令层 commands.ts · 100+ slash cmds 斜杠命令 · 快捷指令 TUI / REPL 层 App → REPL → Messages PromptInput(输入编排) ⚡ 执行内核 query.ts → AsyncGenerator<StreamEvent> QueryEngine.ts · 统一主循环 · 所有形态共用 Tool / Perm 层 43 内建工具 buildTool() fail-closed 默认安全 MCP 工具去重 Memory / Persist 4 层 Memory .md 文件化 4 种 compact append-only JSONL 扩展层 MCP 4 种传输 Multi-Agent Swarm / Teammate Skills / Plugin tool_result 回流 · 闭环

🎯 Agent Harness 剖析

Harness(执行骨架)是把"一个会聊天的 LLM"改造成"能自主完成任务的 Agent"的脚手架——它不改模型权重,只在模型外面包一圈循环、工具桥、上下文治理与恢复逻辑。本节从最小可用 harness 出发,逐层叠加 Claude Code 的生产级加固,并给出"自己动手搭 harness"的心智模型。

src/query.tssrc/QueryEngine.tssrc/services/tools/toolOrchestration.tssrc/services/api/claude.ts

什么是 Agent Harness

模型 API 本身是一个无状态的 text → text 函数:给它一段对话,它吐回一段文本。它不会自己读文件、不会自己跑命令、记不住上一次说过什么。Harness 就是补齐这些缺口的那层"外骨骼"——它把模型放进一个循环里,给它一双手(工具)、一段记忆(上下文/持久化)、一套护栏(权限/沙箱),从而让"预测下一个 token"演化成"自主推进任务"。

一句话:Agent = LLM + Harness。模型负责"想",harness 负责"把想变成可执行、可观测、可恢复的行动",并把行动结果再喂回给模型形成闭环。

Harness 的五大支柱

支柱解决的问题Claude Code 落点
① 循环 Loop单次请求不够,需多轮"思考→行动→观察"query()while(true) AsyncGenerator
② 工具桥 Tools让模型能影响真实世界(读写文件、跑命令)Tool 协议 + runTools() 调度
③ 上下文 Context在有限窗口里塞下历史+工具结果不溢出预算预留 + autoCompact + PTL 兜底
④ 持久化 State跨崩溃/退出/设备续聊,长期记忆append-only JSONL transcript + Memory
⑤ 护栏 Safety自主行动不能失控、不能被劫持fail-closed 权限 + 沙箱 + trust 边界

最小可用 harness:30 行心智模型

剥掉所有工程加固,一个能跑的 agent harness 其实只需要三件事:(1) 把工具描述塞进请求 → (2) 解析模型吐出的 tool_use → (3) 执行后把 tool_result 回灌,再循环。理解了这个骨架,后面 Claude Code 的一切复杂度都只是在它之上"加固"。

最小 harness 骨架(教学伪代码)
async function* miniHarness(userInput, tools) {
  let messages = [{ role: 'user', content: userInput }]

  while (true) {
    // ① 带着"工具说明书"调用模型(流式)
    const reply = await callModel({ messages, tools: describe(tools) })
    messages.push(reply)
    yield reply

    // ② 模型没要调工具 → 它认为任务完成,退出循环
    const calls = reply.content.filter(b => b.type === 'tool_use')
    if (calls.length === 0) break

    // ③ 执行工具,把结果作为 user 消息回灌 → 下一轮模型就"看见"了观察结果
    for (const c of calls) {
      const result = await tools[c.name].call(c.input)
      messages.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: c.id, content: result }] })
    }
  }
}
这就是 agent 的全部秘密:没有魔法,只有一个"模型 ↔ 工具"之间反复倒手 messages 的循环。tool_result 永远以 user 角色回灌——在模型眼里,"工具执行结果"和"用户说的话"是同一种输入。Claude Code 的 query.ts 正是这个骨架的工业级版本。

单轮生命周期:一次 turn 里到底发生了什么

把上面循环的"一圈"放大,就是 harness 的单轮生命周期。Claude Code 在每个环节都插入了真实的工程处理(函数名为源码真实命名):

上下文组装normalizeMessagesForAPI() · 去重/格式化/注入 memory/挂回附件 预算闸门autoCompactIfNeeded() · 逼近窗口−13k buffer 先压缩 调用模型claude.ts.stream() · 静态前缀命中缓存,逐 token yield 收集动作extractToolUseBlocks() · 无 tool_use → 本轮即终轮,退出循环 有 tool_use 分批调度partitionToolCalls() · 连续只读并发,遇写操作收口串行 执行工具校验→hook→权限→tool.call()→tool_result · 失败短路回灌 结果回流messages=[...toolResults] · contextModifier 批末应用 → 落盘 回到 ① 进入下一轮
关键直觉:harness 的每一轮都是"组装上下文 → 问模型 → 模型要么收尾要么点工具 → 执行并观察 → 把观察写回上下文"。终止条件极其简单——模型不再请求工具,就认为任务完成。没有外部的"任务完成判定器",是否继续完全由模型自己在每一轮决定。

最小 harness 能跑 demo,但放到真实生产环境会处处崩。下表把"最小做法"与"Claude Code 做法"逐项对照——这七层差距,正是 harness 工程化的全部价值所在

关注点最小 harnessClaude Code 生产级做法
成本每轮重发全部 prompt静态前缀切 DYNAMIC_BOUNDARY,命中 prompt cache;subagent forkSubagent 复用父字节
上下文溢出撞墙直接 400 报错三层纵深:预算预留 → autoCompact 主动压缩 → PTL 头部截断兜底
工具并发串行慢,或并发踩踏partitionToolCallsisConcurrencySafe 分批,只读并发、写操作独占
安全模型说啥跑啥fail-closed 权限 + PreToolUse hook + 沙箱 + trust 两阶段初始化
崩溃恢复内存态丢失即归零append-only JSONL + resume 修复链(孤儿 tool_result 重挂)
失忆压缩后忘记打开过的文件压缩后 createPostCompactFileAttachments 复灌关键状态
可观测黑盒,出错难查--dump-prompts JSONL 落盘 + 结构化错误回灌 + 全程 transcript
学习路径建议:先用最小 harness 跑通"模型↔工具"闭环,再按"成本→上下文→并发→安全→恢复"的顺序逐项加固。每一层都能独立理解、独立验证,对应本文档后续各章节深入。

Claude Code 把一切"模型想做的事"都收敛成同一种事件——tool_use → tool_result。这让 query() 内核只需理解一种交互模型,复杂度被牢牢钉在工具层内部:

看似不同的行为在 harness 里都是
改文件 / 跑命令普通工具调用(FileEditTool / BashTool)
向用户提问一个工具(AskUserQuestionTool):渲染选项卡,用户作答即 tool_result
派生子 agent一个工具(AgentTool):子 query 的输出汇总成 tool_result
记笔记 / 管任务工具(MemoryTool / TodoTool),写盘后回灌
统一性的威力:因为"问人""派活""动手"全是工具调用,harness 主循环无需为任何特殊行为开后门。想扩展能力?写一个新 Tool 即可,循环本身一行都不用改——这是 harness 可扩展性的根基。

若你想基于任意模型 API 复刻一套自己的 agent harness,按这份清单逐项落地即可(难度从低到高):

Level 1 — 跑通闭环(半天)

  • 实现 while 循环:调模型 → 解析 tool_use → 执行 → 回灌 tool_result。
  • 用 AsyncGenerator/流式接口对外吐事件,方便 UI 与 SDK 共用同一内核。
  • 终止条件:模型本轮无 tool_use 即结束。

Level 2 — 工具协议与安全默认(1~2 天)

  • 定义统一 Tool 接口:inputSchema(Zod) + call() + 安全旗标。
  • fail-closedisConcurrencySafe / isReadOnly 默认 false,忘记声明=最安全档。
  • 工具入参先 schema 校验,失败把错误回灌让模型自纠,而非抛异常退出。

Level 3 — 上下文与持久化(2~3 天)

  • token 预算:有效窗口 = 模型窗口 − 输出预留 − 压缩缓冲,触顶前主动压缩。
  • transcript 用 append-only 落盘,崩溃可 resume;压缩后复灌关键文件状态。
  • system prompt 切静态/动态边界,让静态前缀命中 provider 的 prompt cache。

Level 4 — 护栏与扩展(按需)

  • 权限判定(allow/ask/deny)+ PreToolUse hook 拦截点 + 命令沙箱。
  • 并发分批调度;subagent 派生;MCP 等外部工具接入(注意同名时内建优先)。
最容易踩的坑:① 把 tool_result 用错角色(必须是 user 侧)导致模型无法消费;② 不做上下文预算,长任务必然撞窗口;③ 工具抛异常直接炸循环,而非结构化回灌——这三点是新手 harness 最常见的"跑两步就死"原因。

🚀 程序入口与启动链路

多入口系统设计,cli.tsx 作为早期分流器,main.tsx 是编排中心,支持 6 种运行形态。

src/entrypoints/cli.tsxsrc/main.tsxsrc/entrypoints/init.tssrc/setup.ts

设计思路

Claude Code 的入口采用分流器模式。CLI 入口点做大量快路径判断,将不同运行需求分流到各自路径,使同一个二进制包支持多种运行形态:

  • REPL 模式 — 默认交互式终端对话循环
  • Headless/Print — 一次性查询无 UI(--print
  • SDK 模式 — 被外部程序调用的 API
  • MCP Server — 作为 MCP 服务端运行
  • Bridge 模式 — IDE 集成桥接(VS Code)
  • Remote/Daemon — 后台运行或远程连接
src/entrypoints/cli.tsx — 入口分流器
async function main() {
  const argv = parseArgs(process.argv)

  // 快路径:版本信息
  if (argv['--version']) { console.log(version); process.exit(0) }

  // 快路径:导出 system prompt
  if (argv['--dump-system-prompt']) { await dumpSystemPrompt(); process.exit(0) }

  // 快路径:远程控制 / 守护进程
  if (argv['remote-control']) { return runRemoteControl(argv) }
  if (argv['daemon'] || argv['bg']) { return runDaemonOrBackground(argv) }

  // 默认路径:进入 main.tsx 编排中心
  await import('./main.tsx').then(m => m.main(argv))
}

六种运行形态对照

同一份二进制,靠 cli.tsx 的早期 argv 嗅探分流到完全不同的运行时。每种形态消费 query() 内核的方式不同,但内核本身完全复用。

形态触发条件入口/运行时UI
REPL默认(无特殊 flag)main.tsx → launchRepl()Ink 全屏 TUI
Headless / Print-p / --printQueryEngine 一次性消费流纯 stdout
SDK被库调用(程序内嵌)导出 query() AsyncGenerator无(调用方接管)
MCP Servermcp servemcp.ts → 暴露工具/资源无(JSON-RPC)
BridgeIDE 扩展拉起bridgeMain.ts ↔ VS Code宿主 IDE
Remote / Daemonremote-control / daemon / bg后台守护 + 远端 ingress可附着
分流器哲学:所有"轻量、不需要进 React 渲染"的需求(版本号、dump prompt、远程控制)都在 cli.tsx 内被快路径短路,避免无谓加载庞大的 TUI 依赖树,显著加快冷启动。

初始化被刻意切成两段,以 工作目录是否可信(trust) 为分界。任何会读取/执行项目内容(自定义命令、project agents、本地 MCP、skills)的装配都必须等到用户确认 trust 之后才能跑——这是防止"打开恶意仓库即被 RCE"的第一道闸。

init.ts — trust 之前(无副作用环境准备) • 解析 CWD / 检测 git 根 • 加载全局 settings(~/.claude/settings.json) • 环境 / 终端能力探测 • 弹出 trust 对话(首次进入某目录) 用户确认可信 setup.ts — trust 之后(装配会"执行项目内容"的能力) getCommands() 注册 100+ slash 命令 assembleToolPool() 43 内建工具 + 去重 getMcpToolsAndResources() 连接本地/远程 MCP getAgentDefinitions() 加载 .claude/agents/ initBundledSkills() File / Bundled / MCP 技能 launchRepl() 进入 TUI 主循环 ⚠ trust 前绝不触碰项目内可执行配置
安全边界:trust 前阶段绝不触碰项目内的可执行配置。把"读全局配置"与"执行项目配置"分到两个阶段,是 Claude Code 信任模型的工程落点。

setup 装配出的能力不是散落各处,而是收敛到一个 AppState 总线,再通过 React Context 下发给整个 TUI。它既是"装配产物的容器",也是"运行期可变状态的单一事实源"。

字段族内容消费方
能力池tools / commands / agents / skills / mcpClientsquery 内核、命令层
会话状态messages、sessionId、transcript 句柄Messages 渲染、持久化
权限/模式permissionMode、allowed/denied 规则权限层、Bash 沙箱
统计token 用量、cost、上下文占用状态栏、autoCompact
运行配置model、betas、feature flags 快照API 调用、prompt 组装
1进程启动:cli.tsx → 快路径分流 / main.tsx 2初始化 init.ts:trust-前(环境检测/CWD/全局 settings) 3能力装配 setup.ts getCommands()→ 注册 100+ 命令 assembleToolPool()→ 43 内建工具 + MCP 去重 getMcpToolsAndResources()→ MCP 连接 initBundledSkills()→ 技能初始化 4REPL 启动:launchRepl() → App + REPL.tsx → PromptInput 5用户输入:query.ts → normalizeMessages → claude.ts API 6工具执行:runTools() → partition → 并发/串行 → tool.call()
关键设计点:初始化分为 trust-前/trust-后两阶段。Trust-前执行无需用户交互、无副作用的操作;Trust-后操作需确认工作目录可信后才装配会执行项目内容的能力。

Query/Agent 执行内核

统一执行内核是整个系统的心脏。所有运行形态共用同一套 query() 主循环,输出 AsyncGenerator<StreamEvent>。

src/query.tssrc/QueryEngine.tssrc/services/api/claude.ts

为什么选择 AsyncGenerator?

  • 统一消费接口 — 不管内部多少轮工具调用,外部都是 for-await-of
  • 天然流式 — 每个 token、每个工具结果都可即时 yield
  • 可组合 — subagent 的 query 可被父 agent 消费
  • 背压控制 — 消费方不拉取,生产方自然暂停
src/query.ts — 核心执行循环
export async function* query(
  userMessages: Message[],
  systemPrompt: SystemPrompt,
  toolUseContext: ToolUseContext,
  deps: QueryDeps,
): AsyncGenerator<StreamEvent> {
  let messages = userMessages

  while (true) {
    // 1. 消息规范化(去重、格式化、截断)
    const apiMessages = normalizeMessagesForAPI(messages)

    // 2. 调用模型 API(流式输出)
    for await (const event of deps.claudeApi.stream(apiMessages, systemPrompt)) {
      yield event
    }

    // 3. 提取 tool_use blocks
    const toolUseBlocks = extractToolUseBlocks(messages)
    if (toolUseBlocks.length === 0) break  // 无工具调用 → 结束

    // 4. 执行工具(按并发安全性分批)
    const { concurrent, serial } = partitionToolCalls(toolUseBlocks)
    const results = await runToolsConcurrently(concurrent)
    results.push(...await runToolsSerially(serial))

    // 5. 结果追加并检查会话治理
    messages = [...messages, ...toolResultsToMessages(results)]
    if (shouldCompact(messages)) await compactConversation(messages)
  }
}
属性含义执行方式
isConcurrencySafe = true只读工具,可并发Promise.all
isConcurrencySafe = false有副作用,需串行逐个 await
isReadOnly = true纯只读跳过权限检查
安全默认值:buildTool() 采用 fail-closed — 默认不并发、默认非只读、默认需权限。
  • text_delta — 模型输出文本片段(逐 token)
  • tool_use_begin — 开始执行工具调用
  • tool_result — 工具执行完成
  • thinking — 模型思考过程(extended thinking)
  • message_complete — 一轮消息完整输出
  • error / usage — 错误事件 / token 统计

一轮模型输出可能携带多个 tool_use。内核不会一股脑并发,而是按工具自报的 isConcurrencySafe 把调用切成"可并发块"与"必须串行块",在保证副作用安全的前提下尽量榨取并行度。

src/query.ts — partitionToolCalls
function partitionToolCalls(blocks: ToolUseBlock[]) {
  const groups: ToolUseBlock[][] = []
  let current: ToolUseBlock[] = []
  for (const block of blocks) {
    const tool = findTool(block.name)
    // 只读且并发安全 → 可与前面的并发块合并
    if (tool?.isConcurrencySafe) {
      current.push(block)
    } else {
      // 遇到有副作用的工具 → 先收口当前并发块,再单独成组
      if (current.length) groups.push(current), current = []
      groups.push([block])  // 串行块(独占一组)
    }
  }
  if (current.length) groups.push(current)
  return groups  // 组内并发,组间串行,保持原始顺序
}
属性含义执行方式
isConcurrencySafe = true只读工具,可并发组内 Promise.all
isConcurrencySafe = false有副作用,需串行独占一组,逐个 await
isReadOnly = true纯只读权限可快速放行
顺序保真:分组同时保留模型给出的原始调用顺序——并发只发生在连续的只读工具之间,一旦插入写操作就强制收口,杜绝"读到半写状态"的竞态。

工具入参往往是模型流式吐出的 JSON。执行器不必等整段 tool_use 完整到达——它维护一个状态机,在入参 JSON 累积可解析时即开始预热,并把工具执行产生的 contextModifier(如对后续轮次的上下文修改)延迟到本组执行完毕后统一应用,避免并发组内相互踩踏。

流式入参累积 JSON 可解析? 继续等待 delta Zod 校验 + backfill 缺省字段 并发组内并行 tool.call() 收集 contextModifier(暂不应用) 本组完成 → 顺序合并 → tool_result 回灌
为何延迟应用 contextModifier:若工具 A、B 并发执行且都想改上下文,立即应用会产生竞态。统一到组末按确定顺序合并,结果可复现、可审计。

🔧 工具系统与权限控制

43 个内建工具 + MCP 工具池,权限系统是主干设计而非后期补丁,采用 fail-closed 默认安全策略。

src/Tool.tssrc/tools.tssrc/tools/src/utils/permissions/

工具分类全景

类别工具示例数量
文件操作FileReadTool, FileEditTool, FileWriteTool, GlobTool~8
代码执行BashTool, NotebookTool~3
搜索定位GrepTool, FindTool, LSPTool~5
Agent 协作AgentTool, TeamCreateTool, SendMessageTool~6
外部连接WebFetchTool, MCPTool~4
Memory/会话MemoryTool, TodoTool, CompactTool~5
其他ThinkTool, ImageGenTool, SkillTool~12
src/Tool.ts — 工具接口 (fail-closed 设计)
interface ToolDefinition {
  name: string
  description: string
  inputSchema: ZodSchema

  // 安全属性 — fail-closed 默认值
  isConcurrencySafe: boolean   // 默认 false
  isReadOnly: boolean          // 默认 false
  isDestructive: boolean      // 默认 false

  checkPermissions(input): PermissionResult
  call(input, context): Promise<ToolResult>
}

// 工厂函数强制安全默认值
export function buildTool(config): Tool {
  return { isConcurrencySafe: false, isReadOnly: false, isDestructive: false, ...config }
}
模型输出 tool_use block Schema 校验 (Zod) 失败 返回错误 通过 checkPermissions() 拒绝 请求用户确认 允许 tool.call(input, context) tool_result → 追加到 messages→ 下一轮 query
Auto Mode:stripDangerousPermissionsForAutoMode() 自动剥离危险权限,确保无人值守安全。

getAllBaseTools() 收集全部内建工具,assembleToolPool() 再把 MCP 工具并入,并用 uniqBy 去重——同名时内建工具优先,防止恶意 MCP server 用同名工具劫持内建能力(如伪造一个 "Bash")。

src/tools.ts
export function assembleToolPool(permCtx, mcpTools): Tool[] {
  const builtinTools = getAllBaseTools(permCtx)
  // 内建工具排在前面 → uniqBy 保留首个 → 内建优先
  return uniqBy(
    [...builtinTools, ...mcpTools.filter(isIncluded)],
    t => t.name,
  )
}

所有工具都经 buildTool() 出厂,它先铺一层 TOOL_DEFAULTS 再覆盖。三个安全旗标默认全 false——工具作者忘记声明 = 自动落到最安全档位(不并发、可能有副作用、需要权限)。这是"安全是默认值,开放是显式选择"的设计。

src/Tool.ts — TOOL_DEFAULTS
const TOOL_DEFAULTS = {
  isConcurrencySafe: false,  // 默认串行(保守)
  isReadOnly:        false,  // 默认视为有写入
  isDestructive:     false,  // 默认非破坏(但仍需权限)
}

export function buildTool<I, O>(config: ToolConfig<I, O>): Tool {
  return { ...TOOL_DEFAULTS, ...config }  // 显式声明才能放开
}

从模型给出 tool_use 到产出 tool_result,每个工具都要走完一条固定的责任链。任何一步失败都会"短路"为一条结构化错误结果回灌给模型,而不是抛异常炸掉主循环。

Zod schema 校验入参类型/必填校验(失败 → 结构化报错) validateInput()工具自定义业务校验(如文件是否存在) backfill 缺省字段补全可推导的省略参数(提升模型容错) PreToolUse hooks用户/插件钩子可改写或拦截(deny/ask/allow) 权限判定checkPermissions() → allow / ask 用户 / deny tool.call()真正执行,产出 ToolResult / 流式 yield
Hooks 先于权限:PreToolUse 钩子在权限判定之前运行,使企业策略/插件能在最早环节统一拦截或改写调用——比工具内部硬编码更灵活。

FileEditTool — 精确字符串替换

  • 安全档位isReadOnly=falseisConcurrencySafe=false——写操作必然独占串行组,且每次都过权限。
  • 唯一性约束old_string 必须在文件中唯一,否则拒绝,逼模型提供足够上下文,避免误改。
  • 路径沙箱:写路径须落在允许的工作区内,与 Sandbox 的 path validation 反哺联动。

AskUserQuestionTool — 工具即交互

  • 把"向用户提问"建模成一个普通工具:模型发起 tool_use,TUI 渲染选项卡,用户作答后以 tool_result 回灌。
  • 意义:人类决策被纳入同一条 query 主循环,无需为"等待用户输入"开特殊通道,统一了"调用工具"与"问人"两种行为的执行模型。
设计统一性:无论是改文件、跑命令、还是问用户,全部抽象为"工具调用 → 结果回灌",让 query 内核只需理解一种事件,复杂度被牢牢钉在工具层内部。

📜 Prompt 管理体系

System Prompt 不是一坨字符串常量,而是一套有运行时、有优先级、有缓存边界、可观测的组装系统。它决定了模型"是谁、能做什么、看到什么"。

src/services/prompt/getSystemPrompt.tsbuildEffectiveSystemPrompt.ts

六层 Prompt Runtime

最终喂给模型的 system prompt 由多层叠加而成,getSystemPrompt() 返回的是一个字符串数组(而非单串),数组元素之间用一条 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 哨兵切开"可缓存的静态段"与"每轮可能变的动态段"。

静态段(强缓存友好,命中 prompt cache) 1.核心身份 / 角色定义 2.工具使用规则(tool descriptions) 3.安全与行为准则 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 动态段(每轮可能变,不进前缀缓存) 4.环境信息 getSystemContext() OS / CWD / git / date 5.项目上下文 getUserContext() memory / rules / agents 6.运行时态 mode / permissions / 当前任务 静态段字节一致 → 命中 provider prompt cache 大幅降本;动态段置于边界后,避免污染前缀缓存
为什么要切边界:静态段在多轮对话中字节完全一致,可被 Anthropic API 的 prompt caching 命中,大幅降本提速;动态段每轮变化,放在边界之后单独处理,避免污染前缀缓存。
buildEffectiveSystemPrompt.ts — 覆盖优先级
// 同一时刻可能有多个来源想定义 system prompt,
// 按优先级"取最高者为基底",再统一追加 append 片段。
function buildEffectiveSystemPrompt(ctx): string[] {
  const base =
       ctx.override        // ① 显式 override(最高)
    ?? ctx.coordinatorPrompt // ② coordinator 模式身份
    ?? ctx.agentPrompt      // ③ subagent 专属 prompt
    ?? ctx.customPrompt     // ④ 用户自定义
    ?? getDefaultSystemPrompt(ctx)  // ⑤ 默认(兜底)

  return [...base, ...ctx.appendSections]  // append 不覆盖、只追加
}

组装单元是 systemPromptSection(),默认产出"可缓存"片段。框架另外保留一个刻意命名得很吓人的 DANGEROUS_uncachedSystemPromptSection(),用于那些每轮都必须重算、绝不能被缓存命中的内容(如带时间戳、带随机状态的片段)。

API用途缓存
systemPromptSection()常规静态/半静态片段✅ 可缓存
DANGEROUS_uncached…()必须每轮重算的易变片段❌ 强制不缓存
命名即文档:DANGEROUS_ 前缀逼迫调用者停下来思考"这段真的不能缓存吗",把性能陷阱前置到编码期暴露。
  • getSystemContext() — 与"机器/会话"相关:操作系统、当前工作目录、git 状态、日期时间、模型与权限模式。偏环境事实。
  • getUserContext() — 与"项目/人"相关:各层 memory 内容、.claude/rules/ 自定义规则、可用 agents 与 skills 清单。偏意图与约束。

两者都落在动态边界之后,因为它们会随目录切换、git 提交、memory 编辑而变化。

除主对话 prompt,系统还维护一组专项 prompt:compact 摘要、memory 提取、title 生成、coordinator 编排、quota/计费提示等,各自独立演进、独立测试。

可观测性:启动时带 --dump-prompts,框架会把每次实际发送的 system/user prompt 以 JSONL 落盘,便于逐行 diff、回归测试、排查"模型为什么这么答"。Prompt 在这里被当作一等工程产物来治理,而非黑盒。

🧠 Memory 多层记忆体系

四层 Memory 系统,所有记忆是磁盘 .md 文件,用户可直接查看编辑。这是与传统 AI 系统最大的差异化设计。

src/memdir/memdir.tssrc/memdir/paths.tssrc/memdir/findRelevantMemories.ts

四层记忆架构

层级位置作用触发
Auto/项目 Memory.claude/memory.md + MEMORY.md 索引项目规范、偏好、规则手动/Agent写入
Session Memory~/.claude/sessions/单次会话摘要记忆自动提取(forked agent)
Agent Memory三 scope(见下)跨会话长期记忆Agent 自主写入
Team Memory.claude/team/团队共享知识Swarm 协作共享

memdir 索引与安全截断

memdir.tsMEMORY.md 为入口索引,buildMemoryLines() / buildMemoryPrompt() 负责把磁盘内容拼成注入 prompt 的片段。为防止 Memory 撑爆 context,入口文件有硬限制:

常量含义
MAX_ENTRYPOINT_LINES200入口 MEMORY.md 最大行数
MAX_ENTRYPOINT_BYTES25,000 字节入口 MEMORY.md 最大字节
会话进行中… shouldExtractMemory()门槛:累计消息 token ≥ 10000 满足 runForkedAgent('memory-extraction')独立 fork · 便宜模型 · 不阻塞主流程 · 仅授予写一个文件 createMemoryFileCanUseTool(targetPath)路径白名单沙箱(锁死唯一可写文件) ~/.claude/sessions/{id}/memory.md目录 0o700 / 文件 0o600 下次 resume 自动加载
最小权限沙箱:提取用的 forked agent 不能乱写盘——createMemoryFileCanUseTool() 把可写路径锁死为那一个 memory 文件;目录权限 0o700、文件 0o600 确保只有当前用户可读写,记忆不外泄。

Agent 的长期记忆按"作用域"分三层存放,决定了"这条记忆跟着谁走、是否进版本库、是否跨项目"。

scope典型路径跟随版本库
user~/.claude/agent-memory/跟用户,跨所有项目否(全局私有)
project.claude/agent-memory/跟项目,团队共享可提交
local.claude/agent-memory.local/跟项目,但仅本机gitignore
为何三档:对应三种现实需求——"我个人的习惯"(user)、"团队约定,要进库"(project)、"本机临时笔记,别污染仓库"(local)。同一套 Memory 工具,靠 scope 参数路由到不同目录。

Agent Memory 的写入不是直接覆盖,而是经过一个 snapshot 状态机,避免"半截写入"或"重复初始化"。

状态含义动作
none尚无快照跳过加载
initialize首次建立创建目录与索引骨架
prompt-update已有快照、需增量更新读现状 → 模型增改 → 落盘
策略触发条件实现
手动 Compact用户 /compact全量压缩当前会话
自动 CompactToken 逼近窗口(autoCompactIfNeeded)压缩早期消息,保留近期
Session Memory会话结束/resume提取关键信息到文件
Micro Compact单个工具输出过长截断该工具结果

压缩会丢弃大量原始消息,因此压缩后必须"复灌"必要状态,否则模型会"失忆"——忘了打开过哪些文件、有哪些延迟工具结果:

  • createPostCompactFileAttachments() — 把压缩前打开/读过的关键文件以附件形式重新挂回。
  • getDeferredToolsDeltaAttachment() — 补回压缩窗口期间产生但尚未消费的工具增量结果。
压缩 ≠ 失忆:压缩是"换一种更省 token 的方式记住",而非删除。复灌机制保证模型在压缩后仍持有继续工作的最小必要上下文。

📋 Context 上下文窗口治理

在有限的上下文窗口里塞下"系统提示 + 历史 + 工具结果 + 输出预留",还要防止偶发的 prompt-too-long 把会话打死。这是一套带预算、带熔断、带降级重试的工程系统。

src/context.tssrc/services/compact/compact.tssrc/utils/tokens.ts

窗口预算与关键常量

有效可用窗口不是简单的"模型上限",而要扣掉给"模型输出"和"自动压缩缓冲"预留的部分。getEffectiveContextWindowSize() 负责算出真正能用来装历史的额度。

常量含义
MODEL_CONTEXT_WINDOW_DEFAULT200,000默认上下文窗口;has1mContext() 为真时升至 100 万
CAPPED_DEFAULT_MAX_TOKENS8,000常规输出上限(保守,给历史让位)
ESCALATED_MAX_TOKENS64,000需要长输出时升档
MAX_OUTPUT_TOKENS_FOR_SUMMARY20,000压缩摘要生成的输出上限
AUTOCOMPACT_BUFFER_TOKENS13,000预留缓冲,触顶前就压缩
留白哲学:系统从不"用满"窗口。AUTOCOMPACT_BUFFER_TOKENS 这层缓冲确保在真正撞墙之前就主动压缩,把"硬错误"转化为"软降级"。

每轮请求前评估占用,逼近"窗口 − 缓冲"即触发压缩。关键在于它带一个熔断器:如果压缩连续失败(如模型抽风、内容无法再缩),不会无限重试拖死会话,而是在达到 MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES 后停手并告警。

每轮 query 前 estimateTokens ≥窗口 − BUFFER ? 正常请求,继续 runCompact() 压缩成功? failures=0,继续 failures++ ≥ MAX ?是 → 熔断:停止压缩 + 告警
熔断意义:没有熔断器,一次"压缩本身也超长"的边界情况会让系统陷入"压缩→失败→再压缩"的活锁。计数器把它变成有限次尝试后的优雅降级。

即便有预算和自动压缩,仍可能因 token 估算偏差收到 API 的 prompt-too-long (PTL) 错误。此时不是直接报错退出,而是触发 truncateHeadForPTLRetry()——从历史头部砍掉一段后重试,把不可恢复的失败变成一次有损但可继续的重试。

发送请求API 返回 prompt-too-long truncateHeadForPTLRetry()从最旧消息开始截断保留 system + 近期上下文 重新发送成功则继续;仍失败则再截一段 循环重试
三层防御纵深:① 预算预留(不用满)→ ② autoCompact(主动缩)→ ③ PTL 头部截断(兜底重试)。三层从"预防"到"急救"逐级兜底,使长会话极难被单次超长打断。
  • has1mContext() 为真(特定模型/beta)时,窗口从 20 万跃升到 100 万,getEffectiveContextWindowSize() 据此重算预算。
  • 输出上限默认压在保守的 CAPPED_DEFAULT_MAX_TOKENS = 8,000,把更多额度留给历史;当任务确实需要长输出时再升到 ESCALATED_MAX_TOKENS = 64,000
  • 这种"默认保守、按需升档"的策略,避免每次请求都为极少数长输出场景白白预留大量 token。

💾 会话存储与 Resume

每个会话是一条 append-only 的 JSONL 事件流。崩溃、退出、跨机器,都能从这条流里把"会话图"重建出来——这是 Claude Code"长期协作"属性的物理基础。

src/services/sessions/loadTranscriptFile.tsresume.ts

append-only JSONL 事件流

会话不是"存一个最终状态对象",而是把每条消息按发生顺序逐行追加.jsonl。只追加、不回改,天然抗崩溃(写一半最多丢最后一行),也天然可审计(完整时间线)。

写入前由 isTranscriptMessage() 把关——只有 user / assistant / attachment / system 这几类进入持久化,而 progress 等纯 UI 瞬时事件被排除,避免污染可重放的事实流。

写入分流规则
主链消息去重后追加,保证线性主干无重复
sidechain(子 agent)保真写入,不与主链去重,保留分支全貌
远端镜像只跟主链,sidechain 不上行

会话元数据(标题、模型、用量统计等)会变,但文件是 append-only 不能改旧行。解法很"流式原生":把最新 metadata 再追加到文件尾部,读取时以"最后一条"为准。

.jsonl(append-only) … 旧消息 … {"type":"metadata","title":"旧标题",...} 早期写入 … 新消息 … {"type":"metadata","title":"新标题",...} reAppendSessionMetadata ▲ 读取时取最后一条 metadata 生效 append-only 下的"更新"不改历史 · 只追加新版本 · 读时取末值(事件溯源)
append-only 下的"更新":不修改历史、只追加新版本、读时取末值——这正是事件溯源(event sourcing)的经典手法,兼顾了不可变性与可变语义。

JSONL 是一维的行序列,但真实会话是带父子关系(工具调用、并行子 agent、分支)的图。loadTranscriptFile() 逐行解析并按 uuid / parent 关系把图重建出来,再交给 resume 修复链做一致性校正。

Lite Reader:只读尾部的轻量读取

很多场景(列出会话、取标题、判断能否 resume)并不需要读完整条流。Lite reader 用固定缓冲 LITE_READ_BUF_SIZE = 65536 字节从尾部回扫,快速拿到末尾的 metadata,避免为一个标题去解析数百 MB 的历史。

崩溃可能留下"半截"会话:有 tool_use 没对应 tool_result、并行工具结果成了孤儿、被截断的悬空引用。直接喂给 API 会被拒。Resume 前有一条修复链做体检与修补:

修复函数处理的"脏"情况
applySnipRemovals()移除被标记裁剪的片段,保持引用闭合
recoverOrphanedParallelToolResults()并行工具结果与其调用失联 → 重新挂回或补占位
checkResumeConsistency()整体一致性体检:tool_use ↔ tool_result 配对完整
韧性设计:resume 不假设磁盘上的会话是"干净"的。它默认会话可能脏,并主动修复到 API 可接受的形态——这让"随时崩、随时续"成为可信赖的能力,而非碰运气。

会话可镜像到远端(跨设备续聊)。上行写入用 Last-Uuid乐观并发控制:客户端声称"我基于这条 uuid 之后追加",若服务端发现中间已有别处写入(冲突),返回 409,客户端拉取最新后重试。

本地追加消息POST(ingress, Last-Uuid=最后已知 uuid) 200 OK 409 Conflict 写入成功更新 Last-Uuid 服务端已有更新尾部拉取增量 → 重算 Last-Uuid → 重试 重试
无锁同步:用乐观并发 + 409 重试代替分布式锁,在"多数时候无冲突"的真实场景下既简单又高效,只在真冲突时付出一次重试代价。

🔌 MCP 协议集成

Model Context Protocol — 外部能力扩展框架,统一工具命名 mcp__server__tool,支持 4 种传输协议。

src/services/mcp/client.tssrc/services/mcp/auth.tssrc/services/mcp/mcpStringUtils.ts

四种传输协议

协议场景并发
stdio本地进程(默认)3
SSE远程 HTTP 长连接20
WebSocketIDE 集成20
streamable-httpclaude.ai 代理20

核心设计决策

  • 连接 memoize — 同一 server 配置只建一次连接
  • 认证防雪崩 — 15 分钟 Auth Cache,防止并发认证风暴
  • 描述截断 — MCP 工具描述最大 2048 字符
  • IDE 白名单隔离 — 只允许少数高权限 IDE 工具推送
src/services/mcp/client.ts — 连接管理
export const connectToServer = memoize(
  async (name, serverRef) => {
    let transport
    if (serverRef.type === 'sse')
      transport = new SSEClientTransport(serverRef.url, { authProvider })
    else if (serverRef.type === 'ws')
      transport = new WebSocketTransport(serverRef.url)
    else if (serverRef.type === 'streamable-http')
      transport = new StreamableHTTPClientTransport(serverRef.url)
    else  // stdio 本地进程
      transport = new StdioClientTransport({ command, args, env })

    const client = new Client({ name: 'claude-code', version })
    await client.connect(transport)
    return { client, transport }
  },
  getServerCacheKey
)

远程传输需要给 fetch 加超时。常规做法是 AbortController + AbortSignal.timeout(),但在 Bun 运行时下该路径存在已知内存泄漏。Claude Code 改用手动 setTimeout 触发 abort、并在请求结束时 clearTimeout,绕开泄漏点。

src/services/mcp/client.ts — wrapFetchWithTimeout
function wrapFetchWithTimeout(ms: number) {
  return async (url, init) => {
    const controller = new AbortController()
    // 不用 AbortSignal.timeout() —— 在 Bun 下会泄漏
    const timer = setTimeout(() => controller.abort(), ms)
    try {
      return await fetch(url, { ...init, signal: controller.signal })
    } finally {
      clearTimeout(timer)  // 关键:及时清理,避免句柄堆积
    }
  }
}
运行时陷阱意识:同一段标准 API 在不同 JS 运行时(Node / Bun)表现不同。这里的注释直接钉死"为什么不用更优雅的写法",是对抗"后人好心重构引回 bug"的护栏。

多个工具/资源可能在同一时刻向同一个需要 OAuth 的远程 server 发起调用。若每次都独立走认证,会瞬间触发"认证风暴"。框架用一个 15 分钟有效期的 Auth Cache 收敛并发认证:首次认证后凭证缓存复用,窗口内的并发请求共享同一份凭证。

并发请求 A B C 同时需要认证同一 OAuth 远程 server Auth Cache 命中?TTL 15min 直接复用凭证 单飞一次真实认证 写入缓存(TTL 15min) 唤醒等待者(A B C 共享)

Session 过期:404 + JSON-RPC -32001

对 streamable-http 等长连接,服务端 session 可能过期。框架识别HTTP 404 叠加 JSON-RPC 错误码 -32001的组合信号,判定为"会话已失效",触发重连重建,而不是把它当普通网络错误反复重试。

描述截断与并发上限

约束目的
MAX_MCP_DESCRIPTION_LENGTH2048 字符防止恶意/啰嗦 server 撑爆 prompt
本地 stdio 并发3本地进程资源有限
远程并发20远端可承载更高并发

IDE 工具白名单

IDE(如 VS Code)经 MCP 推送的工具权限较高(能操作编辑器),因此只有少数被显式白名单的 IDE 工具才允许注入工具池,杜绝任意 IDE 端工具借通道获得高权限。

👥 Multi-Agent 多代理协作

三套并存的多 agent 模型——从简单子代理到完整团队协作,本质是一个小型 agent runtime。

src/tools/AgentTool/src/utils/swarm/src/tasks/InProcessTeammateTask.ts

三种协作模式

模式复杂度特点场景
普通 SubAgent同步/后台,独立 context单一子任务
Coordinator主线程变调度器,多 worker并发任务协调
Swarm/Teammates显式团队,Mailbox,权限回流大型项目
AgentTool — 路由分流
// 根据参数决定走 teammate 还是 subagent
if (teamName && name) {
  // Swarm/Teammate 路径
  await spawnTeammate({ name, prompt, team_name: teamName, ... })
} else {
  // 普通 subagent 路径
  await runAgent({ prompt, tools: agentTools, model })
}
TeamCreate创建 team file + task list + leader context Teammate 生命周期 1spawnTeammate()检测后端 tmux > iTerm2 > in-process 2启动 → claimTask() 认领任务原子抢占,避免重复抢工 3执行 → 使用受限工具集独立 context + 工具白名单 4SendMessage → 向 leader 报告mailbox 异步消息 5Leader inbox poller 收到决策 / 批准 / 继续派工 继续派工循环 ⚠ 拓扑约束:Teammate 不能再创建 team(防止无限嵌套)
隔离设计:每个 agent 独立 context + 工具白名单 + 权限回流机制。Model 降级:subagent 默认用便宜模型。

普通 subagent 走 runAgent();但当子任务与父任务共享同一套系统提示时,框架优先用 forkSubagent()——它复用父级 system prompt 的"完全相同字节",让子 agent 的首个请求也能命中父级已建立的 prompt cache,避免为每个子 agent 重新冷算几千 token 的前缀。

成本意识到字节级:"相同的 prompt 必须是逐字节相同"才能命中缓存。fork 而非重建,是把"省钱"做进了多 agent 派生的最底层。

进入 coordinator 模式后,主线程自身的身份被重写:system prompt 切换为 orchestrator 人格,并启用 task-notification 机制。主线程不再亲自干活,而是拆解任务、派发给 worker、汇总结果。

普通模式:主线程 = 干活的人 coordinator:主线程 = 调度的人(orchestrator prompt) 拆解任务 派发 worker worker A worker B worker C task-notification 回报进度 汇总 / 决策下一步

Swarm 是最重的一档,本质是一个小型分布式 agent runtime,落地在几个具体机制上:

机制作用
team file团队的共享事实源:成员、目标、状态
task list + claimTask()共享任务清单;teammate 主动"认领"任务,避免重复抢工
mailbox成员间异步消息收发;leader 用 inbox poller 拉取汇报
leaderPermissionBridgeteammate 无权批准的高危操作,权限请求回流给 leader 决策
claimTask — 抢占式任务认领
// teammate 从共享 task list 原子认领下一个未分配任务
async function claimTask(teamName, memberName) {
  const task = await atomicallyPickUnclaimed(teamName)
  if (!task) return null          // 无活可干 → 空闲/退出
  task.assignee = memberName        // 标记归属,防重复领取
  await persistTaskList(teamName)
  return task
}
拓扑约束:teammate 不能再创建 team(防止无限嵌套递归生成 agent 集群)。后端按 tmux > iTerm2 > in-process 顺序探测,in-process teammate 用 AsyncLocalStorage 做上下文隔离,同进程内也互不串台。

Skills 与 Plugin 扩展

Skills 是按需加载的领域知识包;Plugin 是结构化能力扩展点。共同构成可扩展的能力层。

src/skills/bundledSkills.tssrc/skills/loadSkillsDir.tssrc/plugins/builtinPlugins.ts

Skill 三种来源

  • Bundled — 内置技能 (test-driven-development, systematic-debugging 等)
  • File-based.claude/skills/ 目录下的自定义技能
  • MCP — 通过 MCP server 提供的远程技能

getSkillDirCommands() 经 memoize 缓存并并行扫描目录,把每个技能编译成一条可调用命令;技能元信息写在 Markdown 的 Frontmatter 里。

用户请求 / 模型判断需要 Skill触发技能加载 SkillTool.call(skillName)作为一次普通工具调用进入主循环 查找(bundled → directory → MCP)按优先级回退查找技能定义 加载 Skill prompt注入当前 context(inline)或 fork 独立执行 后续对话遵循 Skill 指导技能内容成为本轮起的约束与知识

技能 Markdown 头部的 Frontmatter 字段决定它"何时出现、谁能调、如何注入":

字段作用
paths条件技能:仅当工作区命中指定文件 glob 时才激活(如有 *.tf 才上 Terraform 技能)
user_invocable是否允许用户用斜杠命令直接触发
contextinline=注入当前对话;fork=开独立子 agent 执行,结果回灌
按需激活:paths 条件让技能池不会无脑全量塞进 prompt——只有与当前项目真正相关的技能才占用上下文,是另一种 context 节流。

技能/命令的 prompt 模板里可以内嵌 Shell 命令(如把 git status 的输出动态注入提示)。这能力很强,也很危险——一个恶意 MCP server 若能塞入带内嵌 Shell 的技能,就等于远程代码执行(RCE)。

防线是一个判断:仅当技能 loadedFrom !== 'mcp' 时才执行内嵌 Shell。来自远程 MCP 的技能,其内嵌命令被当作纯文本,绝不执行。

executeShellCommandsInPrompt — RCE 切断
function executeShellCommandsInPrompt(skill, prompt) {
  // 关键安全门:远程来源技能不准跑内嵌 Shell
  if (skill.loadedFrom === 'mcp') {
    return prompt  // 原样返回,命令视为普通文本
  }
  return runEmbeddedCommands(prompt)  // 仅本地/bundled 技能可执行
}
信任分级:同一份"内嵌 Shell"能力,对本地/内置技能放开、对远程 MCP 技能切断。能力是否启用取决于来源可信度,而非能力本身——这是纵深防御的典型思路。

🛡️ Sandbox 沙箱隔离

沙箱不是给 BashTool 套个壳那么简单,而是一套四层结构 + 语义翻译 + 逃逸防护 + 与权限系统双向反哺的体系。它让"自动放行命令"成为可能,又不牺牲安全底线。

src/services/sandbox/convertToSandboxRuntimeConfig.tspathValidation.ts

四层结构

职责
决策层shouldUseSandbox() 判定本次执行是否走沙箱
翻译层convertToSandboxRuntimeConfig() 把权限规则翻译成 OS 沙箱配置
执行层macOS: seatbelt profile (sandbox-exec);Linux: 容器/命名空间隔离
校验层pathValidation 双向校验路径,与沙箱白名单互相反哺
语义翻译是核心:用户写的是"允许写 src/、禁止写 settings"这类意图convertToSandboxRuntimeConfig() 把它翻译成 seatbelt 的 allow file-write* 等底层原语。用户不必懂 seatbelt 语法。

一个隐蔽的逃逸面:攻击者在工作区放一个 bare git repo,其中的 hooks(如 post-checkout)会在 git 操作时被执行——相当于绕过沙箱跑任意代码。scrubBareGitRepoFiles() 专门识别并清理这类危险文件,堵住该逃逸路径。

威胁建模深度:防的不只是"直接写危险路径",还包括"借 git 机制间接执行"。把 bare git repo 这种冷门攻击面单列防护,体现了对真实攻击链的理解。

有些路径无论用户怎么配都强制禁止写入——典型是 settings 文件与 .claude/skills。原因:若放开,模型/恶意内容就能改写自己的权限配置或注入新技能,形成提权与持久化后门。

路径策略防的是
settings 文件强制 denyWrite篡改权限规则自我提权
.claude/skills强制 denyWrite注入技能实现持久化 RCE

当命令运行在沙箱内时,autoAllowBashIfSandboxed 允许自动放行 Bash——因为沙箱已兜底。但这个"自动放行"仍然尊重用户显式的 deny / ask 规则:沙箱降低了风险,但不凌驾于用户的明确禁令之上。

反向地,isPathInSandboxWriteAllowlist() 把"沙箱允许写的路径"反哺给上层 path validation——两套机制共享同一份白名单事实,避免"沙箱说能写、权限层说不能写"的不一致。

命令请求执行 用户 deny 规则? 命中 拒绝(沙箱也不覆盖) 用户 ask 规则? 命中 询问用户 沙箱内 & autoAllow? 自动放行 常规权限判定
优先级清晰:deny > ask > 沙箱自动放行。沙箱是"降低摩擦的加速器",不是"凌驾用户意志的免死金牌"。安全与体验在这里被精确分层。

BashTool 安全模型

命令解析 危险模式检测 权限检查 sandbox 包装 执行

BashTool 是权限要求最高的工具。注意 excludedCommands(沙箱排除列表)不是安全边界——它只是性能/兼容优化(某些命令在沙箱里跑不起来而放行),真正的边界永远是权限规则 + 路径校验。

🖥️ TUI 组件体系

基于 Ink (React for Terminal) 构建的完整 TUI 组件平台,采用双中枢设计。

src/components/src/components/Messages.tsxsrc/components/PromptInput/

分层组件架构

层级职责核心文件组件数
Provider 根包装全局状态/统计App.tsx, AppState.tsx~5
会话工作台消息区+输入区装配FullscreenLayout, Messages, PromptInput~10
消息渲染虚拟列表、折叠VirtualMessageList, MessageRow~41
输入编排补全、历史、模式切换PromptInput/ (21文件)~21
能力面板权限/agent/MCP/任务permissions/, agents/, mcp/~51
设计系统终端 UI 基础件design-system/, ui/~16

双中枢设计

  • Messages.tsx — 展示已发生的事(transcript 视图转换器)
  • PromptInput.tsx — 组织下一步要做什么(输入编排中心)
原始 transcript normalizeMessages() · 规范化 UI 重排 · 展示顺序优化 read / search 折叠 hook 摘要折叠 grouped tool use 合并 brief 模式过滤 VirtualMessageList 渲染

VirtualMessageList 技术含量最高:终端高度测量 + 缓存 + IntersectionObserver + JumpHandle 搜索跳转。

💡 五大设计亮点

从工程角度看 Claude Code 最值得学习的架构设计决策。

亮点一:统一执行内核

所有 6 种运行形态共用同一套 query() 主循环。不同形态只是消费 StreamEvent 流的方式不同:REPL 渲染到终端,SDK 返回给调用方,headless 输出到 stdout。

亮点二:Memory 透明可审计

所有记忆都是 .md 文件。用户可以 catgrep、手动编辑。不存在"AI 偷偷记住了什么你不知道"的问题。四层独立开关,关哪层用户自己决定。

亮点三:权限系统是主干

不是上线后打补丁加的安全层,而是 Tool 接口设计之初就内建的。buildTool() 的 fail-closed 默认值意味着:忘记声明安全属性 = 最高安全级别

亮点四:渐进式多 Agent

三种模式(SubAgent → Coordinator → Swarm)从简到繁,小任务不需要完整 Swarm 开销,大项目可以用 Teammate 并行协作。后端自动检测(tmux > iTerm2 > in-process)。

亮点五:长会话治理体系化

四种 compact 策略不是临时应对,而是系统化的长会话管理。从 micro(单工具输出截断)到 macro(全会话压缩),覆盖了所有可能导致 context 溢出的场景。

🥚 隐藏特性与彩蛋

89 个 Feature Flag、三层隐藏命令体系、品牌人格化能力。

src/commands.tssrc/utils/undercover.ts

三重隐藏机制

源码中的能力 编译期分流 运行期分流 USER_TYPE === 'ant'→ 注入 internal only 命令 feature('...')→ 89 个编译开关 isEnabled()账号/平台/实验gate isHidden不出现在 help 同一份源码 · 千人千面编译期裁剪 + 运行期 gate 叠加,外部构建自动裁掉内部能力

隐藏命令分类

层级说明例子
内部版专属外部构建中裁掉backfillSessions, bughunter, teleport, ultraplan
隐藏但真实正式命令但不展示heapdump, rate-limit-options, thinkback-play
Stub 占位接口位保留,实现在内部good-claude, share, summary, onboarding
发现:项目有完整的品牌人格化能力 — buddy、Clawd、stickers、passes。内部命令作为一等公民维护,只是在外部构建中裁掉。

源码中约 89 处 feature('...') 编译期开关,叠加运行期 isEnabled()(依赖账号类型 / 平台 / 实验 gate),构成"同一份源码、千人千面"的能力分发。betas.ts 单独管理向 API 申报的 beta 头(如 1M context、特定能力),让实验特性可灰度、可回滚。

开关维度例子
账号类型USER_TYPE === 'ant' 注入 internal-only 命令
平台/运行时某些能力仅在特定 OS / Bun 下启用
实验 gate服务端下发的实验分桶
beta 申报betas.ts 控制 API beta header

项目内藏着一个相当完整的"虚拟伙伴"生成系统 buddy,采用 bones(确定性骨架) + soul(模型生成血肉) 双层设计:

  • bones — 用 mulberry32 这个确定性 PRNG(伪随机)生成骨架:物种、稀有度、外观参数。给定同一种子,结果完全可复现。
  • soul — 在骨架之上由模型生成个性化"灵魂"(性格、台词),让每只 buddy 独一无二。
  • 18 个 species 图鉴,抽卡按 rarity 权重分布:60 / 25 / 10 / 4 / 1(从普通到传说)。
反 canary 小心机:代号字符串用 String.fromCharCode(...) 在运行时拼装,而非明文硬编码——这是为了规避源码扫描/canary 检测,避免内部代号在静态分析中直接暴露。一个彩蛋里藏着工程上的"反指纹"意识。
  • undercover — 一种"隐身"运行模式,调整对外呈现的身份/标识。
  • Clawd — 吉祥物式品牌人格。
  • stickers / passes — 贴纸与通行证类的趣味/激励资产,作为一等公民维护。

这些在外部构建中通过 feature flag 与 USER_TYPE 裁掉,但在内部是被认真维护的功能模块——说明 Claude Code 的"产品化/品牌化"野心远超一个 CLI 工具。

💎 工程心法:从 Claude Code 学到什么

前面各章逐一拆解了 Claude Code 的源码实现。本章把这些散落的设计决策反向蒸馏成可迁移的 Agent/Harness 开发原则——每一条都标注"Claude Code 怎么做 → 为什么有效 → 你该怎么搬"。这不是源码导读,而是把别人的工程经验变成自己的设计直觉

十条可迁移心法总览

如果只带走一张表,就是这张。每条心法都能在前文找到对应的源码落点(点击侧栏对应章节深入)。

#心法Claude Code 的落点对你的意义
1单一内核,多形态复用6 种运行形态共用一个 query() AsyncGenerator先把"模型↔工具循环"做成唯一内核,UI/SDK/CLI 都只是它的消费者
2一切皆工具改文件/跑命令/问人/派子 agent 全是 tool_use不为任何特殊行为在主循环开后门,扩展=加一个 Tool
3安全是默认值buildTool() fail-closed,三旗标默认 false忘记声明=落到最安全档,而不是最危险档
4上下文是预算预留 buffer → autoCompact → PTL 截断三层防御从第一天就把窗口当有限资源管理,别等撞墙
5状态用事件流持久化append-only JSONL + resume 修复链只追加不回改,天然抗崩溃、可审计、可续聊
6失败结构化回灌每步出错短路成 tool_result,不抛异常让模型"看见错误并自纠",而非让循环崩掉
7缓存意识到字节级静态/动态边界切割,forkSubagent 复用父字节相同前缀=省钱省延迟,prompt 也要做"缓存友好设计"
8终止权交给模型本轮无 tool_use 即结束,无外部判定器用极简的终止条件,避免脆弱的"任务完成检测器"
9信任分级 + 纵深防御trust 两阶段、来源决定能力、沙箱兜底能力是否启用取决于来源可信度,单点失守不致命
10渐进式复杂度SubAgent → Coordinator → Swarm 三档小任务不付大架构的税,复杂度按需引入
一句话总纲:Claude Code 的所有"复杂",本质都是在一个极简内核(模型↔工具循环)之上,围绕成本、上下文、安全、韧性四个维度逐层加固。学它,不是抄它的代码,而是学它"在哪里加固、为什么这样加固"的判断力。

把十条心法按"维度"重组,能看清 Claude Code 的设计是沿着四条主线系统推进的,而非零散技巧的堆砌。

维度一 · 结构:用统一抽象压住复杂度

  • 单一内核:所有运行形态共用 query()。你的 agent 也应只有一个权威主循环,其余都是它的"皮肤"。
  • 一切皆工具:把"问人""派活""记笔记"都建模成工具调用。主循环只认 tool_use → tool_result 一种事件,新能力靠加 Tool 扩展,循环零改动。
  • 流式 AsyncGenerator:内核对外吐事件流,UI 实时渲染、SDK 程序化消费、headless 落 stdout——同一份内核,三种消费姿势。

维度二 · 成本:把省钱做进每一层

  • 静态/动态边界:system prompt 切出"逐字节不变的静态前缀"命中 provider 缓存;易变内容放边界之后。
  • fork 而非重建:派生 subagent 时复用父 prompt 的相同字节,让子 agent 首请求也命中缓存。
  • 输出档位按需升降:默认压低 max_tokens 把额度留给历史,需要长输出时再升档,避免每请求白白预留。

维度三 · 韧性:默认"会崩",主动兜底

  • 三层上下文防御:预算预留(不用满)→ autoCompact(主动压缩,带熔断器防活锁)→ PTL 头部截断(兜底重试)。把"硬错误"逐级降级为"软降级"。
  • append-only + 修复链:会话是只追加的事件流;resume 默认磁盘上的会话是"脏"的,主动修补孤儿 tool_result 到 API 可接受形态。
  • 压缩≠失忆:压缩后用 createPostCompactFileAttachments 复灌关键文件状态——"换一种更省 token 的方式记住",而非删除。

维度四 · 安全:信任分级的纵深防御

  • fail-closed 默认:安全属性默认全 false,开放是显式选择。
  • trust 两阶段:读全局配置(无副作用)与执行项目配置(有副作用)分到 trust 前/后两阶段,防"打开恶意仓库即 RCE"。
  • 来源决定能力:同一份内嵌 Shell 能力,对本地技能放开、对远程 MCP 切断;沙箱自动放行仍尊重用户 deny/ask。安全不是单点开关,而是分层叠加。

从 Claude Code 的设计可以反推出一组"新手 harness 常踩、它刻意规避"的反模式。对照自查,能省下大量返工。

反模式(别这么做)后果Claude Code 的正解
tool_result 用错角色(非 user 侧)模型无法消费工具结果,对话错乱工具结果一律以 user 角色回灌
工具抛异常直接冒泡一次工具失败炸掉整个主循环每步失败短路成结构化错误回灌,循环继续
不做上下文预算,用满才管长任务必撞窗口,直接 400预留 buffer + 主动压缩 + 兜底截断
为"问用户""派子任务"开特殊分支主循环越来越臃肿、难维护统一建模为工具调用,循环零特例
每轮重发全量 prompt缓存全失效,成本与延迟暴涨静态前缀稳定、命中 prompt cache
把状态只存在内存里崩溃/退出即归零,无法续聊append-only 事件流落盘,可 resume
安全属性默认开放(fail-open)新工具忘记声明=最危险档fail-closed,忘记声明=最安全档
压缩后不复灌状态模型"失忆",忘了打开过哪些文件复灌关键文件/延迟工具结果
一上来就上多 agent/Swarm简单任务背负巨大协调开销渐进式:能用单 agent 绝不上 Swarm

把心法转成有序的施工计划。严格按此顺序推进——每一阶段都能独立跑通、独立验证,不必等全部做完才见效。

阶段 0闭环(必做 · 半天)while 循环:调模型→解析 tool_use→执行→回灌;终止=无 tool_use 阶段 1工具协议 + 安全默认(1~2 天)统一 Tool 接口 + fail-closed;入参 Zod 校验,失败结构化回灌 阶段 2上下文治理(2~3 天)token 预算(窗口−输出预留−压缩缓冲);触顶前压缩 + 复灌 阶段 3持久化与恢复(2~3 天)append-only 事件流落盘;resume 修复链补孤儿 tool_result 阶段 4成本优化(按需)prompt 静态/动态切边界命中 cache;subagent fork 复用父字节 阶段 5护栏与扩展(按需)权限 allow/ask/deny + hook + 沙箱;并发分批;渐进多 agent 优先级心法:先跑通(0~1)→ 防崩(2~3)→ 锦上添花(4~5)不要在阶段 0 还没跑通时就优化缓存
优先级心法:先做"不做就跑不起来"的(阶段 0~1),再做"不做长任务必崩"的(阶段 2~3),最后才是"锦上添花"的成本与扩展(阶段 4~5)。不要在阶段 0 还没跑通时就去优化缓存——这是最常见的过早优化陷阱。

在你给自己的 harness 加任何新能力之前,用这份清单过一遍,能避开 90% 的架构债。

结构

  • 这个新能力能否表达成"一个 Tool"?如果要改主循环,先停下来想想是不是抽象错了。
  • 主循环是否仍然只认 tool_use → tool_result 一种事件?

安全

  • 新工具的并发/只读/破坏性旗标是否显式声明?默认是否落在最安全档?
  • 这个能力会不会因"来源不可信"而被滥用(如远程注入)?是否需要按来源分级?

上下文与成本

  • 这个工具的输出会不会很大?是否需要截断/摘要,避免一次撑爆窗口?
  • 我新增的 prompt 内容放在静态段还是动态段?会不会打破前缀缓存?

韧性

  • 这一步失败时,是结构化回灌让模型自纠,还是会炸掉主循环?
  • 崩溃后从事件流恢复,这个能力产生的状态能否被正确重建?
终极判据:一个好的 harness 改动,应该让"主循环更稳定、能力更可插拔、失败更可恢复",而不是让主循环知道更多特例。每次你想往主循环里塞 if,都该警惕——Claude Code 的优雅,正源于它几乎从不这么做。

📖 术语速查表

harness 领域的术语散落在各章,这里集中一句话解释 + 跳转到详述章节。读源码或本文档时遇到不认识的词,回这里查。

核心循环

术语一句话解释详见
Harness模型外面那层"执行骨架":循环 + 工具桥 + 上下文 + 持久化 + 护栏,把会聊天的 LLM 变成能干活的 agentHarness
tool_use / tool_result模型请求调用工具的块 / 工具执行结果回灌的块;后者一律以 user 角色回灌Harness
AsyncGenerator主循环的载体:边执行边 yield 事件,让 UI/SDK/headless 共用同一内核执行内核
partitionToolCalls把一轮的多个工具调用按"并发安全"切成可并发块与必须串行块执行内核
contextModifier工具对后续轮次上下文的修改,延迟到本组执行完毕统一应用,避免并发踩踏执行内核

上下文与成本

术语一句话解释详见
预算预留 buffer有效窗口 = 模型窗口 − 输出预留 − 压缩缓冲;从不把窗口用满Context
autoCompact上下文逼近上限时自动压缩历史;带熔断器,连续失败就停手防活锁Context
PTL(prompt-too-long)压缩仍超长时的最后兜底:头部截断后重试请求Context
静态/动态边界system prompt 切出"逐字节不变的静态前缀"以命中 provider 缓存,易变内容放边界之后Prompt
prompt cacheprovider 对相同前缀的缓存命中,省钱省延迟;harness 要做"缓存友好设计"Prompt
forkSubagent派生子 agent 时复用父 prompt 的相同字节,让子 agent 首请求也命中缓存Multi-Agent

持久化与韧性

术语一句话解释详见
append-only JSONL会话记为只追加、不回改的事件流,天然抗崩溃、可审计、可续聊会话存储
transcript完整的会话事件记录流,resume 与可观测的基础会话存储
resume 修复链重连时默认磁盘会话是"脏"的,主动修补孤儿 tool_result 到 API 可接受形态会话存储
结构化回灌工具失败时短路成错误 tool_result 让模型自纠,而非抛异常炸循环Harness
createPostCompactFileAttachments压缩后复灌关键文件状态,让模型"换更省 token 的方式记住"而非失忆Memory

安全与扩展

术语一句话解释详见
fail-closed安全旗标默认全 false(不并发/非只读/需权限),开放是显式选择,忘记声明=最安全档工具系统
buildTool工具工厂函数,统一注入 fail-closed 默认值与 schema 校验工具系统
trust 两阶段trust 前只读全局配置(无副作用),trust 后才执行项目配置,防"打开恶意仓库即 RCE"程序入口
来源决定能力同一能力按来源可信度分级开放:本地放开、远程切断(loadedFromSkills
沙箱 sandbox命令执行的隔离兜底层,仍尊重用户 deny/askSandbox
MCP外部工具接入协议,统一命名 mcp__server__tool,同名时内建工具优先MCP
SubAgent / Coordinator / Swarm渐进式多 agent 三档:小任务不付大架构的税,复杂度按需引入Multi-Agent

📝 最终总结

一句话定义

Claude Code 是一个"能力极强、平台感很重、长期协作属性明显"的本地代码 Agent 系统,而不是一个一次性问答工具。

三维度核心特征

  • 多入口 — CLI / REPL / SDK / MCP / Bridge / Remote
  • 多层次 — 命令 → 执行内核 → 工具 → 权限 → Memory → 扩展
  • 多形态协作 — 单agent / subagent / background / teammate / swarm

架构核心优势

  • 统一的 query / agent / tool / permission 内核
  • 文件化、可审计、分层的 memory 系统
  • Local-first,但可扩展到 remote / bridge / swarm
给 Agent 开发者的临别赠言:读懂 Claude Code 的最大价值,不在于复制它 513K 行代码,而在于内化它的判断力——把一个极简的"模型↔工具循环",沿着成本、上下文、安全、韧性四个维度系统加固。当你自己搭 harness 时,每个设计抉择都可以回到这四个问题:它更省钱了吗?它扛得住长上下文吗?它失守时安全吗?它崩溃后能恢复吗?详见 💎 工程心法 章节。