Claude Code设计与实现-第18章 设计模式与架构决策

《Claude Code 设计与实现》完整目录

第18章 设计模式与架构决策

"Good architecture is not about doing everything right the first time. It's about making decisions that are easy to change." -- Martin Fowler, Patterns of Enterprise Application Architecture

:::tip 本章要点

  • Generator 驱动的流式管道:为什么 AsyncGenerator 是 AI Agent 系统的最佳编排原语
  • 自描述工具模式:工具即对象、Schema 即文档、运行时校验三位一体
  • 多模式权限模型:从五种权限模式的设计推演出分层安全架构
  • 并行预加载与启动优化:Side-effect Imports、并行预读与特性标志死代码消除
  • 协议优先的扩展性:MCP 标准协议如何解耦 Agent 的能力与实现
  • 上下文窗口经济学:Token 预算管理、自动压缩与结果截断的协同策略
  • 安全与自由的平衡术:分层安全检查、沙箱隔离与用户信任级别
  • 构建你自己的 Agent 系统:从 Claude Code 提炼的架构原则与实现路线图 :::

在前面十七章中,我们从 CLI 启动、Query 引擎、流式处理、工具系统、权限模型、MCP 协议到终端 UI,逐一拆解了 Claude Code 的每一个核心子系统。但正如一座建筑的价值不仅在于砖瓦的品质,更在于整体的结构设计------Claude Code 的真正精妙之处,在于这些子系统背后一以贯之的设计模式与架构决策。

本章是全书的收官。我们将站在更高的抽象层次上,从 Claude Code 源码中萃取出七个可迁移的设计模式。这些模式不仅适用于 Claude Code 本身,也适用于任何需要构建 AI Agent 系统的工程团队。对于每一个模式,我们都会追问三个问题:它解决了什么问题?它在源码中如何实现?如果我要构建自己的 Agent 系统,该如何复用它?

18.1 Generator 驱动的流式管道

Claude Code 中 AsyncGenerator 贯穿了从 API 响应到 UI 渲染的完整数据流。下图展示了 Generator 管道的核心组合模式:

flowchart LR subgraph API["API 层"] Stream["streamAssistant()\nAsyncGenerator"] end subgraph Query["查询引擎"] QueryLoop["queryLoop()\nAsyncGenerator"] end subgraph Tools["工具编排"] RunTools["runTools()\nAsyncGenerator"] RunTools --> Concurrent["runToolsConcurrently()\nyield* all(generators)"] RunTools --> Serial["runToolsSerially()\nyield* each generator"] end subgraph Hooks["Hook 管道"] PreHook["runPreToolUseHooks()\nAsyncGenerator"] PostHook["runPostToolUseHooks()\nAsyncGenerator"] end subgraph UI["UI 消费"] ForAwait["for await (const event of queryLoop)\n实时渲染"] end Stream -->|"yield*"| QueryLoop QueryLoop -->|"yield*"| RunTools RunTools -->|"yield*"| PreHook PreHook -->|"yield*"| PostHook QueryLoop -->|"yield"| UI style Stream fill:#e3f2fd style UI fill:#e8f5e9

18.1.1 模式描述与动机

在任何 AI Agent 系统中,最核心的架构挑战是如何编排一个多轮、流式、可中断的执行循环。这个循环需要同时满足以下约束:

  1. 流式输出:模型的响应是逐 token 流出的,UI 需要实时渲染
  2. 工具穿插:模型可能在响应中途请求调用工具,工具执行完毕后循环继续
  3. 可中断性:用户随时可能按下 Ctrl+C 中断当前操作
  4. 错误恢复:API 错误、Token 超限、模型降级都需要在循环内处理
  5. 多消费者:同一个循环的输出需要同时送给 UI 渲染、SDK 回调、日志系统

传统的异步编程范式在面对这些约束时往往力不从心。回调地狱(Callback Hell)让控制流碎片化;Promise 链无法表达"暂停并等待外部输入"的语义;RxJS 的 Observable 虽然强大,但引入了沉重的概念负担和运行时依赖。

Claude Code 选择了 JavaScript 的 AsyncGenerator 作为核心编排原语。这个选择看似简单,却深刻地影响了整个系统的架构风格。

18.1.2 在 Claude Code 中的应用

Claude Code 的核心查询循环 query() 函数就是一个异步生成器:

typescript 复制代码
// 文件:src/query.ts

export async function* query(
  params: QueryParams,
): AsyncGenerator<
  | StreamEvent
  | RequestStartEvent
  | Message
  | TombstoneMessage
  | ToolUseSummaryMessage,
  Terminal
> {
  const consumedCommandUuids: string[] = []
  const terminal = yield* queryLoop(params, consumedCommandUuids)
  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed')
  }
  return terminal
}

这个函数的返回类型 AsyncGenerator<YieldType, ReturnType> 精确地编码了两层信息:YieldType 是循环过程中流出的中间产物(流式事件、消息、墓碑标记),ReturnType 是循环结束时的终态(成功、错误、中断的原因)。

核心的 queryLoop 是一个 while(true) 循环,通过 yield 将中间产物推送给消费者:

typescript 复制代码
// 文件:src/query.ts

async function* queryLoop(
  params: QueryParams,
  consumedCommandUuids: string[],
): AsyncGenerator<...> {
  let state: State = {
    messages: params.messages,
    toolUseContext: params.toolUseContext,
    autoCompactTracking: undefined,
    maxOutputTokensRecoveryCount: 0,
    hasAttemptedReactiveCompact: false,
    // ...
  }

  while (true) {
    let { toolUseContext } = state

    yield { type: 'stream_request_start' }

    // 1. 消息预处理流水线
    // 2. API 调用与流式接收
    for await (const message of deps.callModel({...})) {
      yield yieldMessage  // 逐条推送给消费者
    }

    // 3. 工具执行
    // 4. 停止条件判断
    // 5. state 转移 -> continue 或 return
  }
}

这个模式在工具编排层同样得到了一致的应用。toolOrchestration.ts 中的 runTools 函数也是一个异步生成器:

typescript 复制代码
// 文件:src/services/tools/toolOrchestration.ts

export async function* runTools(
  toolUseMessages: ToolUseBlock[],
  assistantMessages: AssistantMessage[],
  canUseTool: CanUseToolFn,
  toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
  let currentContext = toolUseContext
  for (const { isConcurrencySafe, blocks } of partitionToolCalls(
    toolUseMessages, currentContext,
  )) {
    if (isConcurrencySafe) {
      for await (const update of runToolsConcurrently(blocks, ...)) {
        yield { message: update.message, newContext: currentContext }
      }
    } else {
      for await (const update of runToolsSerially(blocks, ...)) {
        yield { message: update.message, newContext: currentContext }
      }
    }
  }
}

注意这里的关键设计:yield* 运算符让生成器可以无缝嵌套。query() 通过 yield* 委托给 queryLoop()queryLoop() 内部又通过 for await...of 消费 runTools() 的输出。整个流水线从 API 响应到工具执行再到 UI 渲染,形成了一条类型安全、可组合、可中断的管道。

18.1.3 与其他方案的对比

维度 回调 Promise 链 RxJS Observable AsyncGenerator
控制流可读性 差(嵌套回调) 中(线性链) 中(操作符链) 优(同步写法)
暂停/恢复 不支持 不支持 支持(Subject) 原生支持(yield)
取消/中断 手动 AbortController unsubscribe .return()
背压控制 有(背压策略) 天然(拉取模式)
运行时依赖 大(RxJS 库) 无(语言原生)
嵌套组合 好(mergeMap 等) 优(yield*)

AsyncGenerator 的核心优势在于拉取模型 (Pull Model)。消费者调用 .next() 才会推动生产者继续执行到下一个 yield。这意味着背压控制是天然的------如果 UI 渲染跟不上模型输出的速度,生产者自动暂停。相比之下,Promise 和 Observable 都是推送模型(Push Model),需要额外的机制来处理背压。

18.1.4 可迁移性

如果你要构建自己的 AI Agent 系统,AsyncGenerator 模式值得优先采用。核心实现只需要三层:

  1. 最外层 :会话管理器消费生成器,将 yield 出的事件分发给不同的消费者
  2. 中间层:查询循环生成器,编排 API 调用、工具执行和停止判断
  3. 最内层:工具执行生成器,处理并发控制和权限检查

这三层通过 yield*for await...of 自然组合,无需任何框架依赖。唯一的前提是你的语言运行时支持异步生成器------在 JavaScript/TypeScript、Python、C# 中这已经是标准特性。

18.2 自描述工具模式

18.2.1 工具即对象 vs 类继承

在 AI Agent 系统中,"工具"是模型与外部世界交互的桥梁。工具系统的设计直接决定了 Agent 的可扩展性和安全性。

传统的工具系统设计有两种常见路径。第一种是类继承模式:定义一个 BaseTool 抽象类,每个具体工具继承它并覆盖 execute() 方法。这种方式简单直观,但存在钻石继承、难以组合、测试困难等问题。第二种是函数式模式:每个工具就是一个函数,通过装饰器或注册表来管理元数据。这种方式灵活,但元数据散落在各处,难以做静态检查。

Claude Code 走了第三条路:工具即对象 。每个工具是一个满足 Tool 接口的普通对象,通过 buildTool() 工厂函数构造:

typescript 复制代码
// 文件:src/Tool.ts

export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  readonly name: string
  readonly inputSchema: Input           // Zod schema
  call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
  description(input, options): Promise<string>
  checkPermissions(input, context): Promise<PermissionResult>
  isReadOnly(input): boolean
  isConcurrencySafe(input): boolean
  isEnabled(): boolean
  prompt(options): Promise<string>
  // ... 渲染、校验、分类等 30+ 方法
}

buildTool() 函数提供了安全的默认值,使得新工具只需要定义真正独特的部分:

typescript 复制代码
// 文件:src/Tool.ts

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?: unknown) => false,  // 假设不安全
  isReadOnly: (_input?: unknown) => false,           // 假设有写操作
  isDestructive: (_input?: unknown) => false,
  checkPermissions: (input, _ctx?) =>
    Promise.resolve({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: (_input?: unknown) => '',
  userFacingName: (_input?: unknown) => '',
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>
}

这个设计的精妙之处在于默认值的安全方向isConcurrencySafe 默认为 false(假设不安全,需要串行),isReadOnly 默认为 false(假设有写操作,需要权限检查)。这意味着如果工具作者忘记实现某个方法,系统的行为是保守的而不是危险的。这种"失败安全"(Fail-Safe)原则贯穿了整个工具系统。

18.2.2 Schema 即文档

Claude Code 的工具系统最具创新性的设计之一是 Schema 即文档 。每个工具的 inputSchema 不仅用于运行时校验,还直接作为发送给模型的 JSON Schema。模型读到这个 Schema 后,就知道如何正确调用工具------不需要额外的自然语言描述来教模型填写参数。

typescript 复制代码
// 文件:src/Tool.ts

export type ToolInputJSONSchema = {
  [x: string]: unknown
  type: 'object'
  properties?: {
    [x: string]: unknown
  }
}

export type Tool<...> = {
  readonly inputSchema: Input              // Zod schema,用于运行时校验
  readonly inputJSONSchema?: ToolInputJSONSchema  // 可选的原始 JSON Schema
  // ...
}

这意味着同一份定义同时服务于三个目的:

  1. 编译时 :TypeScript 通过 z.infer<Input> 推导出精确的输入类型
  2. 运行时 :Zod 的 .safeParse() 对模型输出进行校验
  3. 提示时:Schema 被转换为 JSON Schema 发送给 API,告诉模型参数的名称、类型和约束

18.2.3 运行时校验

工具执行的安全链条是:Schema 校验 -> validateInput() -> checkPermissions() -> call()。每一环都有明确的职责:

typescript 复制代码
// 文件:src/Tool.ts

export type Tool<...> = {
  // 第一道关卡:Schema 校验(由框架自动执行)
  readonly inputSchema: Input

  // 第二道关卡:业务逻辑校验(工具自己定义)
  validateInput?(input, context): Promise<ValidationResult>

  // 第三道关卡:权限检查(通用权限系统 + 工具自定义规则)
  checkPermissions(input, context): Promise<PermissionResult>

  // 第四道关卡:实际执行
  call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
}

ValidationResult 的设计也值得注意------它明确区分了"校验通过"和"校验失败及原因":

typescript 复制代码
export type ValidationResult =
  | { result: true }
  | { result: false; message: string; errorCode: number }

校验失败的消息会被直接返回给模型,让模型了解为什么它的工具调用被拒绝,从而在下一轮中修正参数。这种"让模型从错误中学习"的设计理念,是 Agent 系统特有的。

18.2.4 可迁移性

自描述工具模式可以用以下清单迁移到任何 Agent 系统:

  1. 定义工具接口 :至少包含 nameinputSchemacallcheckPermissions 四个字段
  2. 使用 Schema 库:Zod(TypeScript)、Pydantic(Python)、JSON Schema(通用)
  3. 提供安全默认值:所有布尔方法默认返回保守值(不安全、非只读、需要权限)
  4. 工厂函数而非继承buildTool() 模式比抽象基类更灵活,更易测试
  5. 四层校验链:Schema -> 业务校验 -> 权限 -> 执行,每层职责单一

18.3 多模式权限模型

18.3.1 五种模式的设计思想

AI Agent 的权限管理是一个根本性的设计挑战。模型需要足够的自由度来完成复杂任务,但又不能毫无约束地执行危险操作。不同的用户在不同的场景下对安全和效率有不同的偏好。

Claude Code 的解答是引入多模式权限系统。源码中定义了以下权限模式:

typescript 复制代码
// 文件:src/types/permissions.ts

export const EXTERNAL_PERMISSION_MODES = [
  'acceptEdits',       // 自动接受编辑操作,其他操作仍需确认
  'bypassPermissions', // 跳过所有权限检查(最高信任)
  'default',           // 默认模式,所有敏感操作需要用户确认
  'dontAsk',           // 不询问直接拒绝(最保守)
  'plan',              // 规划模式,只允许只读操作
] as const

export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'

这五种外部模式加上两种内部模式(auto 使用 AI 分类器自动决策,bubble 将权限请求向上冒泡到父级 Agent)形成了一个从"完全自动"到"完全手动"的连续频谱:

scss 复制代码
bypassPermissions → auto → acceptEdits → default → dontAsk → plan
    (最自由)                                              (最受限)

这个频谱的设计思想是:不同的信任级别对应不同的自动化程度 。初次使用的用户可能选择 default 模式逐一确认;熟悉系统后切换到 acceptEdits 以提升效率;在可信环境中使用 bypassPermissions 实现完全自动化。而 plan 模式则适合"我只想让 Agent 读代码、想方案,不要动任何文件"的场景。

18.3.2 静态规则 + 动态分类器的分层

权限决策的流程是分层的。hasPermissionsToUseTool 函数实现了完整的决策链:

typescript 复制代码
// 文件:src/utils/permissions/permissions.ts

export const hasPermissionsToUseTool: CanUseToolFn = async (
  tool, input, context, assistantMessage, toolUseID,
): Promise<PermissionDecision> => {
  const result = await hasPermissionsToUseToolInner(tool, input, context)

  // 在 auto 模式下重置连续拒绝计数
  if (result.behavior === 'allow') {
    // ...重置逻辑
    return result
  }

  // dontAsk 模式:将 'ask' 转换为 'deny'
  if (result.behavior === 'ask') {
    const appState = context.getAppState()
    if (appState.toolPermissionContext.mode === 'dontAsk') {
      return {
        behavior: 'deny',
        decisionReason: { type: 'mode', mode: 'dontAsk' },
        message: DONT_ASK_REJECT_MESSAGE(tool.name),
      }
    }
    // auto 模式:使用 AI 分类器判断
    // ...
  }
}

决策的流转经过三个层次:

  1. 静态规则层 :首先检查用户配置的 alwaysAllow / alwaysDeny / alwaysAsk 规则。这些规则来自 settings.json,支持通配符匹配(如 Bash(git *) 允许所有 git 命令)
  2. 模式转换层 :根据当前权限模式对结果进行转换(dontAskask 转为 denybypassPermissions 将所有操作转为 allow
  3. 动态分类器层 :在 auto 模式下,如果静态规则无法决定,则调用 AI 分类器分析操作的安全性

这种分层设计的关键优势在于可预测性 。静态规则的行为是确定性的,用户可以精确控制。只有在用户选择了 auto 模式时,才会引入概率性的 AI 分类器。这让用户始终拥有最终的控制权。

18.3.3 useCanUseTool 的调度架构

hooks/useCanUseTool.tsx 中,权限检查被进一步分发到不同的处理器:

typescript 复制代码
// 文件:src/hooks/useCanUseTool.tsx

function useCanUseTool(setToolUseConfirmQueue, setToolPermissionContext) {
  return async (tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision) =>
    new Promise(resolve => {
      const ctx = createPermissionContext(
        tool, input, toolUseContext, assistantMessage,
        toolUseID, setToolPermissionContext,
        createPermissionQueueOps(setToolUseConfirmQueue)
      );

      const decisionPromise = forceDecision !== undefined
        ? Promise.resolve(forceDecision)
        : hasPermissionsToUseTool(tool, input, toolUseContext, assistantMessage, toolUseID);

      return decisionPromise.then(async result => {
        if (result.behavior === "allow") {
          resolve(ctx.buildAllow(result.updatedInput ?? input, {...}));
          return;
        }
        // 'ask' 行为需要分发到不同的 UI 处理器
        switch (result.behavior) {
          case "deny":
            resolve(result);
            return;
          case "ask":
            // 1. 先检查 Coordinator 模式
            const coordinatorDecision = await handleCoordinatorPermission({...});
            // 2. 再检查 Swarm Worker 模式
            const swarmDecision = await handleSwarmWorkerPermission({...});
            // 3. 最后回退到交互式 UI 提示
            await handleInteractivePermission({...});
        }
      });
    });
}

这里展现了三种权限处理路径的优先级链:Coordinator 处理器(多 Agent 协调场景) -> Swarm Worker 处理器(团队模式子 Agent) -> 交互式处理器(用户直接确认)。每一层都可以做出最终决定,或者将请求传递给下一层。

18.3.4 可迁移性

多模式权限模型的核心可迁移原则:

  1. 设计权限频谱:从最自由到最保守,至少提供三种模式(全自动/需确认/只读)
  2. 静态优先于动态:确定性的规则匹配应该先于概率性的 AI 分类器
  3. 失败方向安全:默认拒绝,需要显式允许
  4. 权限决策可追溯 :记录每次决策的原因(decisionReason),用于审计和调试
  5. 模式可运行时切换:用户应该能在会话中途改变安全级别

18.4 并行预加载与启动优化

18.4.1 Side-effect Imports

Claude Code 的启动时间直接影响用户体验。main.tsx 文件的开头展示了一种精巧的优化策略------Side-effect Imports

typescript 复制代码
// 文件:src/main.tsx

// 这些副作用必须在所有其他 import 之前运行:
// 1. profileCheckpoint 在重型模块加载开始前标记入口时间
// 2. startMdmRawRead 启动 MDM 子进程(plutil/reg query),
//    使其与下面约 135ms 的 import 并行执行
// 3. startKeychainPrefetch 并行启动两个 macOS keychain 读取
//    (OAuth + 遗留 API key),否则 isRemoteManagedSettingsEligible()
//    会通过同步 spawn 按顺序读取它们(每次 macOS 启动约 65ms)
import { profileCheckpoint, profileReport } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();

import { ensureKeychainPrefetchCompleted, startKeychainPrefetch }
  from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();

这段代码的关键在于 import 的顺序即执行的顺序。JavaScript 模块系统保证 import 语句按出现顺序同步执行。通过将副作用触发器放在文件最顶部,在后续 135ms 的其他模块加载期间,MDM 子进程和 Keychain 读取已经在并行执行了。当后续代码真正需要这些数据时,它们很可能已经完成。

18.4.2 并行预读

同样的并行策略也出现在查询循环内部。query.ts 在每轮循环开始时启动相关记忆的预获取:

typescript 复制代码
// 文件:src/query.ts

// 每次用户输入只触发一次 ------ 提示词在循环迭代间不变
// 消费点使用 poll 方式检查(永不阻塞)
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
  state.messages,
  state.toolUseContext,
)

这里的 using 关键字(TC39 Explicit Resource Management 提案)确保了预获取资源在生成器退出(无论正常返回还是异常)时自动清理。startRelevantMemoryPrefetch 在后台启动一个 side-query 来搜索相关记忆,而主循环继续执行 API 调用和工具编排。当循环到达需要记忆数据的位置时,通过 settledAt 检查预获取是否完成------如果已完成则直接使用结果,否则跳过(不阻塞)。

Skill 发现也采用了同样的预获取模式:

typescript 复制代码
// 文件:src/query.ts

// Skill 发现预获取 ------ 每轮迭代触发
// 在模型流式输出和工具执行期间并行运行;
// 在工具执行后与记忆预获取一起消费
const pendingSkillPrefetch = skillPrefetch?.startSkillDiscoveryPrefetch(
  null, messages, toolUseContext,
)

18.4.3 特性标志死代码消除

Claude Code 使用 Bun 的构建时特性标志系统来实现条件编译:

typescript 复制代码
// 文件:src/query.ts

import { feature } from 'bun:bundle'

const reactiveCompact = feature('REACTIVE_COMPACT')
  ? (require('./services/compact/reactiveCompact.js') as typeof import(...))
  : null

const contextCollapse = feature('CONTEXT_COLLAPSE')
  ? (require('./services/contextCollapse/index.js') as typeof import(...))
  : null

const snipModule = feature('HISTORY_SNIP')
  ? (require('./services/compact/snipCompact.js') as typeof import(...))
  : null

这种模式的工作原理是:在构建时,feature('REACTIVE_COMPACT') 会被替换为 truefalse 常量。当结果为 false 时,打包器会将整个 require(...) 分支作为死代码消除。这意味着:

  1. 构建产物更小:未启用的特性代码不会出现在最终产物中
  2. 运行时零开销:不需要在运行时检查特性标志
  3. 条件导入安全 :使用 require() 而非 import 避免了 ES module 的静态分析限制

整个 query.ts 中有超过 15 处 feature() 调用,覆盖了 Reactive Compact、Context Collapse、History Snip、Token Budget、Cached Microcompact 等实验性特性。这使得团队可以安全地在生产环境中进行 A/B 测试,而不影响不参与实验的用户。

18.4.4 可迁移性

启动优化的三个可迁移策略:

  1. 识别可并行的 I/O:启动阶段的子进程调用、密钥读取、配置加载通常可以并行化。关键是在代码最早期就触发它们
  2. 预获取而非按需获取:对于查询循环中可能需要的数据,在循环开始时异步预获取,到达消费点时检查是否就绪
  3. 构建时特性消除:如果你的 Agent 有多个部署配置,使用构建时特性标志让每个配置只包含需要的代码

18.5 协议优先的扩展性

18.5.1 MCP 作为标准协议

Claude Code 的工具系统是封闭的------所有内置工具都在编译时确定。但现实世界的需求是开放的------用户可能需要连接数据库、调用内部 API、操作特定的开发工具。如何在封闭系统中实现开放扩展?

答案是 Model Context Protocol (MCP)。MCP 定义了一套标准化的协议,让外部服务能够以统一的方式向 Agent 暴露工具、资源和提示词。Claude Code 通过 MCP 客户端连接到任意数量的外部服务器:

typescript 复制代码
// 文件:src/services/mcp/types.ts

export const TransportSchema = lazySchema(() =>
  z.enum(['stdio', 'sse', 'sse-ide', 'http', 'ws', 'sdk']),
)

// 支持六种传输协议,覆盖从本地子进程到远程 HTTP 的所有场景

MCP 服务器提供的工具会被动态注册到 Claude Code 的工具系统中,与内置工具共享完全相同的执行流水线------包括权限检查、并发控制、结果渲染和错误处理。对模型来说,MCP 工具和内置工具没有任何区别。

18.5.2 协议而非 API 的设计哲学

MCP 的核心设计哲学是协议优先 而非API 优先。两者的区别在于:

  • API 优先:Agent 定义一个 SDK,工具开发者实现这个 SDK 的接口。工具与 Agent 紧耦合
  • 协议优先:Agent 和工具都实现同一个开放协议。工具与 Agent 解耦,一个工具服务器可以被多个 Agent 使用

在 Claude Code 的实现中,MCP 工具通过 mcpInfo 字段标识其来源,但在功能层面完全融入工具系统:

typescript 复制代码
// 文件:src/Tool.ts

export type Tool<...> = {
  // MCP 工具特有字段
  isMcp?: boolean
  mcpInfo?: { serverName: string; toolName: string }
  // MCP 工具可以指定永不延迟加载
  readonly alwaysLoad?: boolean

  // 以下所有方法,MCP 工具和内置工具共享同一套接口
  call(...): Promise<ToolResult<Output>>
  checkPermissions(...): Promise<PermissionResult>
  // ...
}

MCP 服务器配置支持多层作用域(local、user、project、enterprise、managed),每一层都有独立的权限策略和过滤机制。这种分层设计让企业能够在组织级别管理 MCP 服务器白名单,同时允许开发者在项目级别添加自己的工具。

18.5.3 可迁移性

协议优先扩展的实践建议:

  1. 选择或设计开放协议:MCP 是一个好的起点,它已经有完善的规范和 SDK
  2. 内外一致:外部工具应该和内置工具经过完全相同的执行管道(校验、权限、监控)
  3. 传输无关:协议层与传输层分离,支持 stdio(本地)和 HTTP/WebSocket(远程)
  4. 分层配置:支持用户级、项目级、组织级的工具配置,高层级可以覆盖或限制低层级

18.6 上下文窗口经济学

18.6.1 Token 预算管理

大语言模型的上下文窗口是有限资源。在长时间的 Agent 对话中,消息历史会不断增长,最终触及上下文窗口的上限。如何在有限的 Token 预算内最大化有效信息密度,是 Agent 系统的核心经济学问题。

Claude Code 通过多层机制管理 Token 预算:

typescript 复制代码
// 文件:src/services/compact/autoCompact.ts

// 保留这么多 Token 给压缩操作的输出
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000

export function getEffectiveContextWindowSize(model: string): number {
  const reservedTokensForSummary = Math.min(
    getMaxOutputTokensForModel(model),
    MAX_OUTPUT_TOKENS_FOR_SUMMARY,
  )
  let contextWindow = getContextWindowForModel(model, getSdkBetas())
  return contextWindow - reservedTokensForSummary
}

这个函数计算了"有效上下文窗口"------从总窗口中预留出压缩摘要的输出空间。这意味着即使在上下文快满时,系统仍有足够的空间来执行压缩操作。

18.6.2 自动压缩策略

Claude Code 的自动压缩是一个多级流水线,在 queryLoop 的每次迭代中按顺序执行:

scss 复制代码
消息历史 → Snip(历史修剪) → Microcompact(微压缩) → Context Collapse(上下文折叠) → Autocompact(全量压缩)

每一级处理都独立运作,可以组合使用:

typescript 复制代码
// 文件:src/query.ts

// 第一级:Snip ------ 裁剪最早的历史消息
if (feature('HISTORY_SNIP')) {
  const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
  messagesForQuery = snipResult.messages
  snipTokensFreed = snipResult.tokensFreed
}

// 第二级:Microcompact ------ 对单条消息进行压缩
const microcompactResult = await deps.microcompact(
  messagesForQuery, toolUseContext, querySource,
)
messagesForQuery = microcompactResult.messages

// 第三级:Context Collapse ------ 折叠已完成的上下文段
if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
  const collapseResult = await contextCollapse.applyCollapsesIfNeeded(
    messagesForQuery, toolUseContext, querySource,
  )
  messagesForQuery = collapseResult.messages
}

// 第四级:Autocompact ------ 全量对话压缩
const { compactionResult } = await deps.autocompact(
  messagesForQuery, toolUseContext, {...}, querySource, tracking,
)

这个流水线的设计遵循了先轻后重的原则。Snip 和 Microcompact 是轻量操作,不需要额外的 API 调用。Context Collapse 介于轻重之间。只有当前三级无法将 Token 数压到阈值以下时,才触发需要额外 API 调用的 Autocompact。

18.6.3 结果截断与持久化

工具执行结果是 Token 消耗的另一个大户。Claude Code 通过 maxResultSizeChars 字段和 toolResultStorage 系统来控制结果大小:

typescript 复制代码
// 文件:src/Tool.ts

export type Tool<...> = {
  /**
   * 工具结果超过此字符数时,会被持久化到磁盘。
   * 设为 Infinity 的工具(如 Read)的输出永远不会被持久化。
   */
  maxResultSizeChars: number
  // ...
}
typescript 复制代码
// 文件:src/query.ts

// 对每条消息的聚合工具结果大小施加预算约束
// 在 Microcompact 之前运行
messagesForQuery = await applyToolResultBudget(
  messagesForQuery,
  toolUseContext.contentReplacementState,
  persistReplacements ? records => void recordContentReplacement(...) : undefined,
  new Set(
    toolUseContext.options.tools
      .filter(t => !Number.isFinite(t.maxResultSizeChars))
      .map(t => t.name),
  ),
)

当工具结果超过阈值时,完整内容被写入临时文件,而上下文中只保留预览和文件路径。模型如果需要完整内容,可以使用 Read 工具重新读取。这种"按需深入"的策略,在不损失信息可达性的前提下,大幅减少了上下文的 Token 占用。

18.6.4 可迁移性

上下文窗口经济学的核心策略:

  1. 预留压缩空间:有效窗口 = 总窗口 - 压缩输出预留 - 系统提示词预留
  2. 多级压缩流水线:从轻量到重量,只在必要时才触发昂贵操作
  3. 大结果外置:超过阈值的工具结果存磁盘,上下文中只保留摘要和引用
  4. 估算而非精确计数:Token 精确计数需要分词器调用,成本高。使用快速估算(如字符数 / 4)做日常判断,只在触发压缩时精确计数
  5. 追踪压缩历史:记录每次压缩前后的 Token 数,用于调优阈值和监控异常

18.7 安全与自由的平衡术

Claude Code 的安全体系采用分层防御策略,每一层独立工作但又相互补强。下图展示了从用户输入到实际执行的安全检查层次:

flowchart TB Input["工具调用请求"] --> L1["第 1 层: Schema 校验\nZod 类型检查"] L1 --> L2["第 2 层: 业务校验\nvalidateInput()"] L2 --> L3["第 3 层: 静态规则\nallow / deny / ask"] L3 --> L4["第 4 层: 动态分类器\nBash 安全分析\nTranscript AI 评估"] L4 --> L5["第 5 层: 用户审批\n交互式权限对话"] L5 --> L6["第 6 层: 沙箱隔离\nOS 级文件/网络限制"] L6 --> Execute["安全执行"] L1 -->|"校验失败"| Reject1["拒绝: 类型错误"] L2 -->|"业务不合法"| Reject2["拒绝: 无效操作"] L3 -->|"deny 规则"| Reject3["拒绝: 规则禁止"] L4 -->|"shouldBlock"| Reject4["拒绝: 安全风险"] L5 -->|"用户拒绝"| Reject5["拒绝: 用户否决"] L6 -->|"沙箱违规"| Reject6["拒绝: 隔离限制"]

18.7.1 分层安全检查

Claude Code 的安全体系不是单一的"允许/拒绝"开关,而是一个多层次的纵深防御体系。每个工具调用需要通过以下检查链:

scss 复制代码
模型输出 tool_use
    |
    v
[1] Schema 校验:inputSchema.safeParse()
    |
    v
[2] 业务校验:tool.validateInput()
    |
    v
[3] 静态权限规则:alwaysAllow / alwaysDeny / alwaysAsk 匹配
    |
    v
[4] 模式转换:根据 PermissionMode 转换决策
    |
    v
[5] 动态分类器:auto 模式下的 AI 安全分类(可选)
    |
    v
[6] 用户确认:交互式权限对话框(如果需要)
    |
    v
[7] 沙箱执行:在受限环境中运行(如果启用)
    |
    v
实际执行

每一层都有明确的职责,且每一层都可以独立拒绝请求。这种纵深防御意味着即使某一层出现问题(比如 AI 分类器误判),后面的层仍然可以捕获危险操作。

18.7.2 沙箱隔离

对于 Bash 命令等高风险操作,Claude Code 提供了沙箱执行环境。沙箱系统通过 @anthropic-ai/sandbox-runtime 包实现,由 sandbox-adapter.ts 进行 CLI 级别的集成:

typescript 复制代码
// 文件:src/utils/sandbox/sandbox-adapter.ts

// 适配层:将 @anthropic-ai/sandbox-runtime 与 Claude CLI 的
// 设置系统、工具集成和附加功能桥接

import {
  SandboxManager as BaseSandboxManager,
  SandboxRuntimeConfigSchema,
  SandboxViolationStore,
} from '@anthropic-ai/sandbox-runtime'

沙箱提供了三个维度的隔离:

  1. 文件系统隔离:限制读写路径,防止越权访问敏感目录
  2. 网络隔离:限制可访问的主机和端口,防止数据外泄
  3. 进程隔离:限制可执行的命令,防止危险操作
typescript 复制代码
// 沙箱配置类型(来自 @anthropic-ai/sandbox-runtime)
type SandboxRuntimeConfig = {
  fsRead: FsReadRestrictionConfig      // 文件读取限制
  fsWrite: FsWriteRestrictionConfig    // 文件写入限制
  network: NetworkRestrictionConfig    // 网络访问限制
  ignoreViolations: IgnoreViolationsConfig  // 违规忽略策略
}

沙箱的配置同样支持多层级继承------企业策略可以强制启用沙箱并设置最严格的基线,项目配置可以在基线之上适度放宽,但不能突破企业基线的限制。

18.7.3 用户信任级别

Claude Code 的安全模型隐含了一个信任层次:

markdown 复制代码
企业管理员(设置全局策略) > 组织管理员(设置 MCP 白名单)
  > 用户(选择权限模式) > 项目配置(设置项目级规则)
    > AI Agent(在规则框架内行动)

高信任级别的规则可以覆盖低信任级别。例如:

  • 企业策略的 alwaysDeny 规则不能被用户的 alwaysAllow 规则覆盖
  • 用户选择的 bypassPermissions 模式在企业环境下可能被降级为 auto
  • 项目级的 MCP 服务器配置需要用户显式同意才能启用

这种分层信任模型确保了每个参与方都在自己的权限范围内行动,没有任何单一层面的决策可以绕过上层的安全策略。

18.7.4 可迁移性

安全架构的可迁移原则:

  1. 纵深防御:至少三层检查(输入校验 -> 权限判断 -> 执行隔离),任一层可独立拒绝
  2. 默认安全:未知操作默认拒绝,显式允许需要理由
  3. 信任可升级不可降级:用户可以增加自己的自由度(选择更宽松的模式),但不能突破管理员设定的上限
  4. 可审计:每次权限决策都记录决策源(静态规则/分类器/用户确认),便于事后追溯
  5. 沙箱作为最后防线:即使所有前置检查都通过了,高风险操作仍在沙箱中执行

18.8 构建你自己的 Agent 系统

18.8.1 从 Claude Code 学到的架构原则清单

通过前面七个模式的分析,我们可以提炼出以下架构原则。这些原则按照从最基础到最高层的顺序排列:

原则一:流式优先(Streaming-First)

不要把流式处理当作"高级功能"后期添加。从第一行代码开始就用 AsyncGenerator 或类似的流式原语。一旦核心循环是基于 Promise 的批量调用,后期改造为流式的成本极其高昂。

原则二:工具作为一等公民(Tools as First-Class Citizens)

工具不是"插件"或"扩展",它是 Agent 的核心能力载体。工具的定义应该包含执行逻辑、权限声明、Schema 定义和 UI 渲染------所有这些都在同一个对象上。不要将这些关注点拆散到不同的注册表中。

原则三:安全内建而非外挂(Security Built-In, Not Bolted-On)

权限检查应该嵌入工具执行的核心流水线,而不是作为可选的中间件。canUseToolcall() 的前置条件,而不是一个可以被绕过的装饰器。

原则四:协议解耦(Protocol-Based Decoupling)

Agent 的核心循环不应该知道工具的具体实现。通过标准协议(如 MCP),任何满足协议的服务都可以成为 Agent 的工具提供者。

原则五:上下文感知(Context-Aware)

Agent 必须主动管理上下文窗口,而不是等到溢出时才报错。多级压缩、大结果外置、Token 预算追踪都应该是查询循环的内在组成部分。

原则六:渐进式信任(Progressive Trust)

用户应该能够从最保守的模式开始,逐步增加 Agent 的自主权。权限模式的频谱设计让这种渐进式信任成为可能。

原则七:可观测性(Observability)

Agent 的每一个决策------调用了什么工具、为什么被允许或拒绝、消耗了多少 Token、是否触发了压缩------都应该可追踪、可记录、可回放。

下图以架构全景的视角展示了七大设计原则之间的层次关系,从最底层的流式管道到最上层的可观测性:

flowchart TB subgraph L1["基础层"] Streaming["流式优先\nAsyncGenerator 管道"] Tools["工具一等公民\n自描述 Tool 类型"] end subgraph L2["安全层"] Security["安全内建\n权限嵌入执行管线"] Trust["渐进式信任\n多模式权限频谱"] end subgraph L3["扩展层"] Protocol["协议解耦\nMCP 标准协议"] Context["上下文感知\n多级压缩策略"] end subgraph L4["可见层"] Observe["可观测性\n决策追踪 + 遥测"] end L1 --> L2 L2 --> L3 L3 --> L4 Streaming ---|"驱动"| Tools Tools ---|"保护"| Security Security ---|"扩展"| Protocol Protocol ---|"管理"| Context Context ---|"监控"| Observe Trust ---|"控制"| Protocol

18.8.2 建议的实现顺序

如果你从零开始构建一个 AI Agent 系统,以下是建议的实现顺序。每一步都建立在前一步的基础上:

第一阶段:核心循环(1-2 周)

  1. 实现 query() 异步生成器:while(true) + yield 流式事件
  2. 实现基础的 Tool 接口:name + inputSchema + call()
  3. 实现最简单的工具:文件读取和 Shell 命令执行
  4. 接通 API 调用:消息发送 -> 流式接收 -> 工具调用检测 -> 工具执行 -> 继续循环

此时你已经有了一个可以工作的 Agent,虽然没有权限控制和上下文管理。

第二阶段:安全与权限(1-2 周)

  1. 实现 checkPermissions() 接口:从 { behavior: 'allow' } 开始
  2. 添加静态权限规则:alwaysAllow / alwaysDeny 配置
  3. 实现权限模式切换:至少三种模式(自动/确认/只读)
  4. 添加输入校验:validateInput() 在权限检查之前运行

第三阶段:上下文管理(1-2 周)

  1. 实现 Token 计数(估算即可,不需要精确分词)
  2. 实现自动压缩:检测 Token 超限 -> fork 压缩 Agent -> 替换消息历史
  3. 添加工具结果截断:超过阈值的结果存磁盘,保留摘要
  4. 实现消息预处理流水线:标准化 + 去重 + 压缩

第四阶段:扩展性(1-2 周)

  1. 接入 MCP 客户端:让用户通过配置文件添加外部工具
  2. 实现工具延迟加载:太多工具时只发送名称,模型按需搜索
  3. 添加并发控制:只读工具并行执行,写操作串行
  4. 实现沙箱执行:至少对 Shell 命令提供文件系统隔离

18.8.3 常见陷阱

陷阱一:过早优化提示词

许多团队花大量时间优化系统提示词,试图让模型"更聪明地"调用工具。但 Claude Code 的经验表明,好的工具设计比好的提示词更重要。如果你的 Bash 工具的 Schema 和描述足够清晰,模型自然知道如何使用它。

陷阱二:忽视取消语义

Agent 循环中的取消不是简单的"停止"。你需要处理:正在运行的工具如何停止?已发出但未收到结果的 API 请求如何处理?部分执行的文件编辑如何回滚?Claude Code 在 StreamingToolExecutor 中通过 siblingAbortController 精心处理了这些边界情况。

陷阱三:同步权限检查

权限检查必须是异步的,因为它可能涉及用户交互(等待用户点击"允许")、网络请求(查询企业策略服务器)或 AI 分类(调用安全分类器模型)。将权限检查设计为同步函数会堵死这些扩展路径。

陷阱四:忽视 Token 经济

在原型阶段,对话长度很短,Token 管理似乎无关紧要。但在生产环境中,一个复杂任务可能涉及数十轮工具调用,每轮都往上下文中添加大量内容。如果没有压缩机制,Agent 会在第 10 轮就撞上上下文窗口的天花板。从第一天就考虑 Token 预算管理。

陷阱五:单一安全层

只依赖一层安全检查(比如只有权限规则,或只有沙箱)是危险的。任何单一机制都可能有盲区或被绕过。Claude Code 的七层检查链看似冗余,但每一层都在实际运行中捕获过其他层遗漏的安全问题。

18.9 架构全景图

以下是 Claude Code 七大设计模式在系统中的位置和交互关系:

lua 复制代码
                        +----------------------------------+
                        |       用户输入 / SDK 调用         |
                        +----------------------------------+
                                       |
                        +----------------------------------+
                        |     并行预加载与启动优化 (18.4)    |
                        |  Side-effect Imports / Prefetch   |
                        +----------------------------------+
                                       |
                        +----------------------------------+
                        |   Generator 驱动的流式管道 (18.1)  |
                        |      query() AsyncGenerator       |
                        |           while(true)             |
                        |               |                   |
                        |    +----------+-----------+       |
                        |    |                      |       |
                        |  API 流式调用      工具执行管道     |
                        |    |               |              |
                        |    |    +----------+----------+   |
                        |    |    |          |          |   |
                        |    |  Schema    权限检查    沙箱   |
                        |    |  校验     (18.3/18.7)  执行  |
                        |    |  (18.2)                      |
                        +----------------------------------+
                                       |
                   +-------------------+-------------------+
                   |                                       |
    +------------------------------+       +-------------------------------+
    |  上下文窗口经济学 (18.6)      |       |  协议优先的扩展性 (18.5)       |
    |  Token 预算 / 多级压缩        |       |  MCP 协议 / 外部工具集成       |
    |  结果截断与持久化             |       |  多层作用域配置                |
    +------------------------------+       +-------------------------------+
                   |                                       |
                   +-------------------+-------------------+
                                       |
                        +----------------------------------+
                        |     自描述工具模式 (18.2)          |
                        |  buildTool() / Schema 即文档      |
                        |  内置工具 + MCP 工具统一接口       |
                        +----------------------------------+

这七个模式并非孤立存在,而是相互支撑、相互约束。Generator 管道是骨架,工具模式是血肉,权限模型是免疫系统,上下文管理是新陈代谢,协议扩展是神经系统,启动优化是心肺功能,安全体系是皮肤和骨骼。缺少任何一个,系统都无法健康运行。

18.10 小结

本章从 Claude Code 源码中萃取了七个可迁移的设计模式:

Generator 驱动的流式管道 让多轮、流式、可中断的 Agent 循环成为可能。AsyncGenerator 的拉取模型天然提供了背压控制,yield* 的嵌套组合让复杂的工具编排保持了代码的可读性。

自描述工具模式通过"工具即对象"和"Schema 即文档"的设计,让工具定义同时服务于编译时类型检查、运行时参数校验和模型 API 调用三个场景,消除了冗余和不一致。

多模式权限模型在"完全自动"和"完全手动"之间设计了连续频谱,配合静态规则和动态分类器的分层架构,让不同信任级别的用户都能找到合适的安全-效率平衡点。

并行预加载与启动优化展示了如何在 JavaScript 模块系统的约束下,通过 Side-effect Imports 和预获取策略最大化启动阶段的并行度;构建时特性标志则从根本上消除了未使用功能的运行时开销。

协议优先的扩展性通过 MCP 标准协议实现了 Agent 能力的开放扩展,同时保持了内外工具的统一执行管道,避免了"内置工具是一等公民,外部工具是二等公民"的常见问题。

上下文窗口经济学提供了从 Token 预算管理到多级压缩流水线的完整策略,确保 Agent 能够在有限的上下文空间中维持长时间的多轮对话。

安全与自由的平衡术通过七层纵深防御、分层信任模型和沙箱隔离,在保障安全的前提下最大化了 Agent 的自主能力。

这些模式的共同主题是在约束中寻找自由。AsyncGenerator 在同步写法的约束中获得了异步和流式的自由;权限模型在安全的约束中为用户提供了效率的自由;上下文管理在 Token 预算的约束中为 Agent 提供了长对话的自由。正是对这些约束的深刻理解和精心设计,让 Claude Code 成为了一个同时兼具能力、安全和可扩展性的 AI Agent 系统。

对于正在构建或计划构建自己的 Agent 系统的工程师来说,这些模式不是需要照搬的模板,而是需要理解的原则。每个系统都有自己的约束和权衡------但问题的结构是相似的。理解 Claude Code 如何回答这些问题,将帮助你更好地回答自己面对的问题。

这也是我们这本书的终点。从第 1 章的"为什么需要 Claude Code"到第 18 章的"如何构建你自己的 Agent 系统",我们完成了一次从使用者到建设者的旅程。源码是最好的老师------但前提是你知道该往哪里看。希望这本书为你提供了那张地图。

相关推荐
杨艺韬9 小时前
Claude Code设计与实现-第10章 Bash 安全与沙箱
agent
杨艺韬9 小时前
Claude Code设计与实现-第14章 多 Agent 协调与 Swarm
agent
杨艺韬9 小时前
Claude Code设计与实现-第9章 多模式权限模型
agent
杨艺韬9 小时前
Claude Code设计与实现-第12章 IDE Bridge 通信架构
agent
杨艺韬9 小时前
Claude Code设计与实现-第16章 上下文管理与自动压缩
agent
杨艺韬9 小时前
Claude Code设计与实现-第11章 MCP 协议集成
agent
杨艺韬9 小时前
Claude Code设计与实现-第6章 工具类型系统设计
agent
杨艺韬9 小时前
Claude Code设计与实现-第1章 为什么需要理解 Claude Code
agent
杨艺韬9 小时前
Claude Code设计与实现-前言
agent