Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 26 章:设计哲学 — 从源码中提炼的核心原则

核心问题:Claude Code 不是一个“玩具级 demo“,而是一个日活百万级的生产系统。支撑它的不是某个天才算法,而是一套贯穿始终的设计哲学。这些哲学是什么?它们如何体现在每一行源码中?

很多开源项目的“设计文档“写得很好看,但源码和文档完全脱节。Claude Code 恰恰相反 — 它没有独立的设计文档,设计哲学就活在代码结构和注释里。本章将从数万行 TypeScript 源码中,提炼出七大核心设计原则,并用具体的代码实现来佐证每一个。


26.1 终端优先 vs IDE 插件的选择

为什么不是 VS Code 插件?

Coding Agent 最自然的载体似乎是 IDE 插件 — 用户在 VS Code 里写代码,Agent 也在那里运行。但 Claude Code 选择了一条反直觉的路:终端优先,IDE 作为可选的远程连接层。

从 main.tsx 的入口结构可以看出这个选择:

// main.tsx:1 — 入口是终端,不是 IDE
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

// main.tsx:22 — Commander.js CLI 框架
import { Command as CommanderCommand } from '@commander-js/extra-typings';

整个应用构建在 Commander.js(CLI 框架)和 Ink(React 终端 UI 框架)之上,而非任何 IDE 扩展 API。IDE 集成通过独立的 Bridge 模块实现:

终端优先的分层结构:
┌────────────────────────────────────────────┐
│           IDE (VS Code / JetBrains)        │  ← 可选层
│  通过 bridge/replBridge.ts WebSocket 连接   │
├────────────────────────────────────────────┤
│         Bridge 层 (src/bridge/)            │  ← 适配层
│  bridgeMain.ts · bridgeMessaging.ts        │
├────────────────────────────────────────────┤
│         核心层 (src/query.ts 等)            │  ← 核心引擎
│  main.tsx · QueryEngine.ts · Tool.ts       │
├────────────────────────────────────────────┤
│         终端 UI (Ink / React)              │  ← 原生界面
│  REPL.tsx · components/                    │
└────────────────────────────────────────────┘

设计决策:终端优先有三大优势:1) 零依赖 — 用户不需要安装任何 IDE,任何有 shell 的环境都能运行;2) CI/CD 友好 — 通过 claude -p "fix this bug" 可以在无 UI 环境中使用;3) 可组合 — 可以和 |、>、xargs 等 Unix 工具组合。IDE 插件只能在特定 IDE 里运行,而终端是所有开发环境的最大公约数。

Bridge 模块的结构印证了这一点 — 它是一个适配器,不是核心依赖:

// bridge/bridgeEnabled.ts:28
export function isBridgeEnabled(): boolean {
  return feature('BRIDGE_MODE')
    ? isClaudeAISubscriber() &&
        getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_bridge', false)
    : false
}

Bridge 的启用需要同时满足 feature flag 和用户订阅条件,核心功能完全不依赖它。


26.2 渐进式信任模型

从“默认拒绝“到“自动批准“的信任阶梯

Claude Code 的权限系统不是简单的“允许/拒绝“二元模型,而是一个渐进式信任阶梯。从源码中可以清晰看到这个设计:

// Tool.ts:123 — ToolPermissionContext 定义了信任等级
export type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode          // 'default' | 'plan' | 'auto' | ...
  alwaysAllowRules: ToolPermissionRulesBySource   // 已建立信任的规则
  alwaysDenyRules: ToolPermissionRulesBySource    // 黑名单规则
  alwaysAskRules: ToolPermissionRulesBySource     // 总是需要确认的规则
  isBypassPermissionsModeAvailable: boolean       // 是否允许跳过
  shouldAvoidPermissionPrompts?: boolean          // 后台 Agent 静默拒绝
  awaitAutomatedChecksBeforeDialog?: boolean      // 协调器模式
  prePlanMode?: PermissionMode                    // Plan 模式前的状态
}>

这个信任模型有四个层级:

信任阶梯:

┌─── Level 4: Bypass Mode ────────────────────────┐
│  跳过所有权限检查(需用户显式启用)               │
├─── Level 3: Auto Mode ──────────────────────────┤
│  自动批准大多数操作,仅拒绝危险操作               │
├─── Level 2: Always Allow Rules ─────────────────┤
│  用户对特定工具/模式建立了信任                    │
├─── Level 1: Default (Ask) ──────────────────────┤
│  每次写入操作都需要用户确认                       │
└─────────────────────────────────────────────────┘

每个工具都声明了自己的安全属性,供权限系统决策:

// Tool.ts:362 — Tool 接口的安全属性
export type Tool = {
  isConcurrencySafe(input): boolean  // 是否可以并行执行
  isReadOnly(input): boolean         // 是否只读
  isDestructive?(input): boolean     // 是否不可逆
  isEnabled(): boolean               // 是否启用
  validateInput?(input, context): Promise<ValidationResult>  // 输入验证
}

关键在于:这些属性不是静态标签,而是接受输入参数的函数。同一个 Bash 工具,ls 是只读的,rm -rf / 是危险的。权限系统基于具体输入做决策,而不是笼统地对待整个工具。

设计决策:渐进式信任模型解决了一个核心矛盾 — 如果 Agent 什么都要问,用户会烦;如果什么都不问,用户会怕。通过 isReadOnly() 区分读写操作,读操作默认放行、写操作默认确认,在安全和效率之间取得了平衡。用户可以通过 alwaysAllowRules 逐步放宽信任边界,而非一次性交出所有权限。


26.3 Async Generator 的全链路流式架构

为什么选择 async generator 而不是 callback/event emitter?

Claude Code 最引人注目的架构选择之一,是将 async function*(async generator)作为从 API 层到 UI 层的全链路数据通道。

// query.ts:219 — 核心查询循环返回 AsyncGenerator
export async function* query(
  params: QueryParams,
): AsyncGenerator<
  | StreamEvent
  | RequestStartEvent
  | Message
  | TombstoneMessage
  | ToolUseSummaryMessage,
  Terminal   // 返回值:终止原因
> {
  const terminal = yield* queryLoop(params, consumedCommandUuids)
  return terminal
}

这个模式贯穿了整个系统:

AsyncGenerator 全链路流式:

API (SSE)        query()          QueryEngine      UI (Ink)
   │                │                  │                │
   │ SSE events     │                  │                │
   ├──────────────→ │ yield message    │                │
   │                ├────────────────→ │ yield SDK msg  │
   │                │                  ├──────────────→ │ render
   │ tool_use block │                  │                │
   ├──────────────→ │ yield tool_use   │                │
   │                │ (同时执行工具)     │                │
   │                │ yield progress   │                │
   │                ├────────────────→ │ yield progress │
   │                │                  ├──────────────→ │ render
   │                │ yield result     │                │
   │                ├────────────────→ │ yield result   │
   │                │                  ├──────────────→ │ render

为什么不用更“传统“的模式?对比三种方案:

方案优势劣势
Callback简单直接回调地狱、难以组合、无法暂停/恢复
Event Emitter解耦发送和接收无背压、类型安全差、错误处理分散
Async Generator天然背压、保持执行上下文、类型安全学习曲线、调试稍复杂

工具执行也是 async generator — call() 函数的签名:

// Tool.ts:379 — 工具执行返回 Promise<ToolResult>
call(
  args: z.infer<Input>,
  context: ToolUseContext,
  canUseTool: CanUseToolFn,
  parentMessage: AssistantMessage,
  onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>>

注意这里用了 onProgress callback 而不是 generator — 因为工具执行是“叶子节点“,不需要全链路流式。但整个查询循环用 generator 串联,保证了一个 yield 从 API 层直通 UI 层,无需中间缓冲。

设计决策:async generator 最大的优势是天然保持执行上下文。while(true) 循环中的局部变量(state、turnCount、autoCompactTracking)在每次 yield 后自动恢复,不需要额外的状态管理。这比 event emitter 模式减少了大量的“状态恢复“代码。


26.4 依赖注入与可测试性

从 QueryDeps 看依赖注入模式

Claude Code 没有使用 Angular 式的 DI 容器或 InversifyJS,而是采用了一种极简的依赖注入模式 — 通过参数对象传递依赖,用工厂函数提供默认实现。

// query/deps.ts — 依赖接口
export type QueryDeps = {
  callModel: typeof queryModelWithStreaming  // API 调用
  microcompact: typeof microcompactMessages  // 微压缩
  autocompact: typeof autoCompactIfNeeded   // 自动压缩
  uuid: () => string                        // UUID 生成
}

// 生产环境的默认实现
export function productionDeps(): QueryDeps {
  return {
    callModel: queryModelWithStreaming,
    microcompact: microcompactMessages,
    autocompact: autoCompactIfNeeded,
    uuid: randomUUID,
  }
}

调用端通过 params.deps ?? productionDeps() 使用默认实现:

// query.ts:263 — 在 queryLoop 入口使用
const deps = params.deps ?? productionDeps()

注释说得很清楚:

// query/deps.ts:9-16 注释
// I/O dependencies for query(). Passing a `deps` override into QueryParams
// lets tests inject fakes directly instead of spyOn-per-module — the most
// common mocks (callModel, autocompact) are each spied in 6-8 test files
// today with module-import-and-spy boilerplate.
//
// Using `typeof fn` keeps signatures in sync with the real implementations
// automatically.

这个模式同样用在 QueryConfig 中 — 将环境依赖快照化:

// query/config.ts:16 — 运行时配置,一次快照
export type QueryConfig = {
  sessionId: SessionId
  gates: {
    streamingToolExecution: boolean
    emitToolUseSummaries: boolean
    isAnt: boolean
    fastModeEnabled: boolean
  }
}

export function buildQueryConfig(): QueryConfig {
  return {
    sessionId: getSessionId(),
    gates: {
      streamingToolExecution: checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
        'tengu_streaming_tool_execution2',
      ),
      // ...
    },
  }
}

设计决策:QueryConfig 的注释 (“Immutable values snapshotted once at query() entry”) 揭示了一个重要原则:将环境变量和 feature flag 在入口处快照化,避免在循环中多次读取导致不一致。config 在循环外构建一次,循环内只读取 — 这使得每一轮迭代都基于一致的配置状态。这种模式也叫做“snapshot isolation“,在并发系统中很常见。

ToolUseContext 是另一个典型的依赖注入容器:

// Tool.ts:158 — ToolUseContext 是所有工具的运行时上下文
export type ToolUseContext = {
  options: { commands, debug, tools, verbose, thinkingConfig, ... }
  abortController: AbortController
  readFileState: FileStateCache
  getAppState(): AppState
  setAppState(f: (prev: AppState) => AppState): void
  setInProgressToolUseIDs: (f: (prev: Set<string>) => Set<string>) => void
  setResponseLength: (f: (prev: number) => number) => void
  messages: Message[]
  // ...30+ 个可选依赖
}

注意 getAppState 和 setAppState 是函数而非直接引用 — 这意味着同一个上下文可以被 SubAgent 重写,指向不同的状态存储。setAppStateForTasks 的注释说明了这种灵活性:

// Tool.ts:190 — 子 Agent 的特殊状态管理
setAppStateForTasks?: (f: (prev: AppState) => AppState) => void
// Unlike setAppState, which is no-op for async agents,
// this always reaches the root store so agents at any nesting depth
// can register/clean up infrastructure that outlives a single turn.

26.5 Feature Flag 驱动的渐进式交付

bun:bundle 的编译时 dead code elimination

Claude Code 大量使用 feature() 宏来控制功能开关。与运行时 feature flag 不同,这是一个编译时机制,未启用的功能在构建时被完全移除:

// tools.ts:26 — 编译时条件导入
const SleepTool =
  feature('PROACTIVE') || feature('KAIROS')
    ? require('./tools/SleepTool/SleepTool.js').SleepTool
    : null

const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
  : null

从源码中搜集到的 feature flag 一览:

Feature Flag功能域说明
BRIDGE_MODEIDE 集成远程控制 / IDE 双向连接
COORDINATOR_MODE多 Agent协调器模式,管理 Worker Agent
KAIROS助手模式长期运行的 Assistant 模式
VOICE_MODE语音输入语音流交互
PROACTIVE主动行为主动通知 / 定时任务
AGENT_TRIGGERS触发器Cron 定时任务工具
BASH_CLASSIFIER安全Bash 命令安全分类器
TRANSCRIPT_CLASSIFIER安全对话轨迹安全分类
CONTEXT_COLLAPSE上下文上下文折叠压缩
HISTORY_SNIP上下文历史裁剪
CACHED_MICROCOMPACT缓存缓存微压缩
REACTIVE_COMPACT上下文响应式压缩
WEB_BROWSER_TOOL工具浏览器交互工具
WORKFLOW_SCRIPTS工具工作流脚本引擎
UDS_INBOX通信Unix Domain Socket 对等通信

注意源码注释中的严格规范:

// query/config.ts:14 — 解释为什么 feature() 不能放在 config 中
// Intentionally excludes feature() gates — those are tree-shaking
// boundaries and must stay inline at the guarded blocks for
// dead-code elimination.

这意味着 feature() 调用必须出现在 if/ternary 条件中,不能被抽象到变量里。这是 bun:bundle 打包器的约束 — 它只能在看到 if (feature('X')) 这种模式时才能消除 dead code。

设计决策:编译时 feature flag 比运行时 flag 有三个优势:1) 零运行时开销 — 未启用的代码完全不存在于二进制中;2) 减小包体积 — 外部用户不携带内部功能的代码;3) 隐私保护 — 内部 flag 名称不会泄漏到外部构建。代价是每个 flag 变更需要重新构建,但对 Claude Code 这种持续发布的项目来说,这不是问题。


26.6 防御性编程与多层容错

从错误恢复看工程成熟度

生产级系统的标志不是“不出错“,而是“出错后能优雅恢复“。Claude Code 的 query.ts 中有多层容错机制:

第一层:模型 fallback

// query.ts:889-951 — 模型降级
catch (innerError) {
  if (
    innerError instanceof FallbackTriggeredError &&
    fallbackModel &&
    !streamingFallbackOccured
  ) {
    // 主模型不可用时降级到备选模型
    toolUseContext.options.mainLoopModel = fallbackModel
    // 清理孤立的 tool_use blocks
    yield* yieldMissingToolResultBlocks(assistantMessages, 'Model fallback')
    // 重置所有状态
    assistantMessages.length = 0
    toolResults.length = 0
    continue
  }
  throw innerError
}

第二层:max_output_tokens 恢复

// query.ts:164 — 最多恢复 3 次
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3

当模型输出被截断时,系统注入恢复提示让模型继续生成,最多重试 3 次。

第三层:prompt-too-long 响应式压缩

// query.ts:1086-1099 — 先尝试 context collapse,再尝试 reactive compact
if (isWithheld413) {
  // 先 drain context collapses
  if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
    const drained = contextCollapse.recoverFromOverflow(...)
    if (drained.committed > 0) {
      // 继续循环
    }
  }
  // collapse 不够则做 reactive compact
}

第四层:孤立 tool_result 保护

// query.ts:123 — 确保每个 tool_use 都有对应的 tool_result
function* yieldMissingToolResultBlocks(
  assistantMessages: AssistantMessage[],
  errorMessage: string,
) {
  for (const assistantMessage of assistantMessages) {
    const toolUseBlocks = assistantMessage.message.content.filter(
      content => content.type === 'tool_use',
    ) as ToolUseBlock[]
    for (const toolUse of toolUseBlocks) {
      yield createUserMessage({
        content: [{
          type: 'tool_result',
          content: errorMessage,
          is_error: true,
          tool_use_id: toolUse.id,
        }],
      })
    }
  }
}

这个函数在每一个错误出口都会被调用 — 模型错误、用户中断、streaming fallback。它确保 API 的 tool_use/tool_result 协议不会被破坏。

设计决策:注意 yieldMissingToolResultBlocks 是一个 function*(同步 generator)而不是 async function* — 因为它只构造消息对象,不做任何 I/O。在错误恢复路径中,同步操作比异步操作更可靠。这是防御性编程的典型做法:在错误处理代码中,减少引入新的异步依赖。


26.7 状态机思维 vs 递归

循环 + 状态对象 > 递归调用

Agentic Loop 有两种实现方式:

方案 A (递归):                    方案 B (循环 + 状态):
function agentStep(state) {       while (true) {
  const result = callAPI(state)     const result = callAPI(state)
  if (result.done) return           if (result.done) break
  const toolResult = runTool()      const toolResult = runTool()
  return agentStep({                state = {
    ...state,                         ...state,
    messages: [..., toolResult],      messages: [..., toolResult],
    turnCount: state.turnCount+1      turnCount: state.turnCount+1
  })                                }
}                                 }

Claude Code 选择了方案 B,并将所有可变状态集中到一个 State 对象:

// query.ts:204 — 集中的循环状态
type State = {
  messages: Message[]
  toolUseContext: ToolUseContext
  autoCompactTracking: AutoCompactTrackingState | undefined
  maxOutputTokensRecoveryCount: number
  hasAttemptedReactiveCompact: boolean
  maxOutputTokensOverride: number | undefined
  pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
  stopHookActive: boolean | undefined
  turnCount: number
  transition: Continue | undefined  // 上一次迭代的继续原因
}

每次循环迭代开头解构、结尾重新组装:

// query.ts:307 — 循环顶部
while (true) {
  let { toolUseContext } = state
  const {
    messages, autoCompactTracking,
    maxOutputTokensRecoveryCount,
    turnCount,
    // ...
  } = state

  // ... 整个循环体 ...

  // 继续下一轮时构造新 state
  state = {
    messages: [...messages, ...assistantMessages, ...toolResults],
    toolUseContext,
    turnCount: turnCount + 1,
    transition: { reason: 'tool_use' },
    // ...
  }
}

transition 字段记录了每次 continue 的原因,这使得测试可以断言恢复路径是否正确触发:

// query.ts:215 — 注释说明
// Why the previous iteration continued. Undefined on first iteration.
// Lets tests assert recovery paths fired without inspecting message contents.
transition: Continue | undefined

设计决策:循环 + 集中状态对象比递归有三个优势:1) 栈安全 — 不会因为深度对话导致栈溢出(async generator 在每次 yield 时暂停,但递归调用不会释放栈帧);2) 状态可观察 — 所有状态集中在一个对象中,调试时一目了然;3) continue 语义清晰 — state = { ..., transition: { reason: 'recovery' } }; continue 比递归调用 agentStep(newState) 更容易理解控制流走向。


26.8 小结:设计哲学的统一性

回顾这七大设计原则,它们并非孤立存在,而是互相增强:

设计哲学关系图:

终端优先  ──→  环境无关性  ──→  CI/CD 集成
    │                              ↑
    ▼                              │
渐进式信任  ──→  权限可配置  ──→  自动化流水线
    │
    ▼
async generator ──→  全链路流式 ──→  实时 UI 响应
    │                                    ↑
    ▼                                    │
依赖注入  ──→  可测试性  ──→  快速迭代  ──┘
    │
    ▼
feature flag ──→  编译时消除 ──→  安全的渐进交付
    │
    ▼
防御性编程  ──→  多层容错  ──→  生产级可靠性
    │
    ▼
状态机思维  ──→  可观察状态  ──→  可调试/可测试

这七个原则的核心主题只有一个:在保持系统灵活性的同时,确保生产级可靠性。

Claude Code 不是一个学术项目或概念验证 — 它必须在数百万用户的终端中稳定运行,处理各种边界情况,同时还要支持快速迭代和功能实验。每一个设计选择都在这两个目标之间寻找最优解。

理解了这些设计哲学,接下来在第 27 章中,我们将把它们转化为可落地的架构指南 — 如果你想构建自己的 Coding Agent,应该从这些原则出发。