乐于分享
好东西不私藏

从Claude Code源码看智能体驾驭(Harness)设计模式

从Claude Code源码看智能体驾驭(Harness)设计模式

引言

当我们讨论AI Agent时,通常聚焦于模型能力——推理、规划、工具调用。但一个生产级Agent系统的真正挑战不在模型,而在驾驭(Harness)——如何安全、高效、可持续地驱动模型的每一步行动。

本文从源码层面,拆解Claude Code的六大核心设计维度:

  1. 1. Query主循环:Agent的心脏——一个永不退出的while(true)
  2. 2. 工具编排:44个工具的并发执行与权限管控
  3. 3. 上下文工程:四层压缩策略保活长对话
  4. 4. 记忆系统:三层记忆架构与自动梦境整合
  5. 5. 子代理架构:Forked Agent模型与缓存共享
  6. 6. 权限沙箱:分级信任与自动分类器

读完本文,你会理解为什么Agent框架和Agent运行时是两个完全不同的工程问题。

整体架构图

一、Query主循环:永不退出的心脏

1.1 Agent Loop的本质

所有Agent系统的核心都是一个循环:用户输入 → 模型推理 → 工具执行 → 结果反馈 → 再次推理。Claude Code把这个循环实现为一个AsyncGenerator:

// query.tsexport async function* query(params: QueryParams):  AsyncGenerator<StreamEvent | Message, Terminal> {  const terminal = yield* queryLoop(params, consumedCommandUuids)  return terminal}

为什么用Generator而不是普通函数?因为Agent的响应是流式的——模型一边思考一边输出,工具一边执行一边回报进度,用户可以在任何时候中断。Generator天然支持这种”逐步产出、随时暂停”的语义。

1.2 主循环的完整生命周期

进入queryLoop后,你会看到一个经典的结构:

async function* queryLoop(params, consumedCommandUuids) {  let state: State = { messages, toolUseContext, ... }  while (true) {    // ① 上下文预算控制    let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]    messagesForQuery = await applyToolResultBudget(messagesForQuery, ...)    // ② 历史裁剪(Snip Compact)    if (feature('HISTORY_SNIP')) {      const snipResult = snipModule.snipCompactIfNeeded(messagesForQuery)      messagesForQuery = snipResult.messages    }    // ③ 微压缩(Microcompact)    const microcompactResult = await deps.microcompact(messagesForQuery, ...)    // ④ 上下文折叠(Context Collapse)    if (feature('CONTEXT_COLLAPSE')) {      const collapseResult = await contextCollapse.applyCollapsesIfNeeded(...)    }    // ⑤ 自动压缩(Auto Compact)    const { compactionResult } = await deps.autocompact(messagesForQuery, ...)    // ⑥ 调用模型(Streaming)    for await (const message of deps.callModel({ messages, systemPrompt, tools, ... })) {      yield message  // 流式产出给UI    }    // ⑦ 工具执行    for await (const update of runTools(toolUseBlocks, ...)) {      yield update.message    }    // ⑧ 循环继续或终止    if (!needsFollowUp) break    state = { ...state, messages: [...messages, ...toolResults] }  }}

这个循环的精妙之处在于每次迭代前的四层上下文压缩。在第⑤步,如果对话太长,系统会启动AutoCompact——用模型自身生成对话摘要,替换原始消息。这确保了Agent可以无限期运行,不会因为上下文溢出而崩溃。

1.3 状态管理的哲学

State对象的更新采用了不可变模式:

// Continue sites write `state = { ... }` instead of 9 separate assignmentsstate = {  ...state,  messages: [...messages, ...toolResults],  turnCount: state.turnCount + 1,  transition: 'tool_result',}

每次迭代产生新的State对象,而不是就地修改。这种不可变设计让状态追踪变得可预测——任何一个时刻,你都可以确信当前State是完整的、一致的。

1.4 熔断与恢复

主循环内置了多层熔断机制:

  • • Max Output Tokens恢复:当模型输出被截断时,自动恢复并继续
  • • 流式回退(Streaming Fallback):主模型失败时,自动切换到fallback模型
  • • 压缩熔断器:连续压缩失败3次后,停止重试(避免无限循环浪费API调用)
// 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

这个注释说明了这不是理论设计——这是从真实生产数据中得出的经验值。1,279个会话曾经陷入无限压缩循环,每天浪费25万次API调用。

二、工具编排:44个工具的并发执行引擎

工具编排与并发调度

2.1 工具接口设计

Claude Code的Tool接口可能是所有Agent系统中最完整的:

type Tool<Input, Output, P> = {  name: string  aliases?: string[]          // 工具别名(向后兼容)  searchHint?: string         // 用于ToolSearch的关键词  shouldDefer?: boolean       // 是否延迟加载  alwaysLoad?: boolean        // 是否始终加载(不受延迟影响)  maxResultSizeChars: number  // 结果大小上限  call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult>  isConcurrencySafe(input): boolean   // 是否可以并发执行  isReadOnly(input): boolean          // 是否只读  isDestructive?(input): boolean      // 是否破坏性操作  checkPermissions(input, context): Promise<PermissionResult>  validateInput?(input, context): Promise<ValidationResult>  // ... 还有10+个可选方法}

44个工具中,每个都实现了这个接口。关键设计点:

  • • :决定工具是否可以与其他工具并行执行
  • • :只读工具可以获得更宽松的权限
  • •  / :控制工具的加载策略(延迟加载减少上下文占用)
  • • :工具输出超过阈值时自动持久化到磁盘,避免上下文膨胀

2.2 分区并发调度

toolOrchestration.ts实现了一个精巧的分区调度算法:

function partitionToolCalls(toolUseMessages, toolUseContext): Batch[] {  // 将工具调用分成批次:  // 批次1: 连续的只读工具 → 并发执行  // 批次2: 写操作工具 → 串行执行  // 批次3: 又一组只读工具 → 并发执行  // ...}

这意味着如果模型同时请求读取3个文件和编辑1个文件,系统会先并发读取3个文件,然后串行执行编辑操作。最大化并行性的同时保证写入安全

最大并发数通过环境变量控制,默认10:

function getMaxToolUseConcurrency(): number {  return parseInt(process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || '', 10) || 10}

2.3 延迟工具加载(Tool Search)

Claude Code引入了ToolSearchTool——一个搜索工具的工具。不是所有44个工具都一开始就暴露给模型,有些工具(如Notebook编辑、PowerShell、Tungsten等)被标记为shouldDefer: true,只有在模型通过ToolSearch查找后才会加载完整schema。

这个设计解决了一个核心问题:工具schema太大会挤占上下文窗口。44个工具的完整JSON Schema可能占几千个Token,但大部分任务只需要5-6个工具。

2.4 流式工具执行(Streaming Tool Executor)

// StreamingToolExecutor.tsclass StreamingToolExecutor {  constructor(tools, canUseTool, toolUseContext) {    // 工具一边执行,一边流式回报结果  }  discard() {    // 流式回退时丢弃未完成的结果  }}

流式执行允许工具结果在产生的同时就传递给UI和后续处理,而不需要等待所有工具执行完毕。这对于Bash命令(可能运行几分钟)特别重要——用户可以实时看到命令输出。

三、上下文工程:四层压缩策略

Query主循环生命周期

3.1 为什么需要四层?

Claude Code的上下文管理可能是所有Agent系统中最复杂的——因为它必须支持无限长对话。一个开发者可能连续和Claude Code工作几小时,产生数百条消息和工具调用。不做上下文管理,系统会在几十轮后崩溃。

四层策略从轻到重:

层级
名称
触发条件
效果
1
Tool Result Budget
每次查询
替换过大的工具输出为文件引用
2
Snip Compact
历史过长
裁剪最旧的消息
3
Microcompact
每次查询
缓存友好的增量压缩
4
Auto Compact
上下文接近窗口上限
完整对话摘要

3.2 Tool Result Budget

工具的输出可能非常大(比如读取一个大文件、执行一个复杂的grep)。Claude Code的策略是:

messagesForQuery = await applyToolResultBudget(  messagesForQuery,  toolUseContext.contentReplacementState,  persistReplacements ?records => void recordContentReplacement(records) : undefined,  // 不持久化的工具(如FileRead)设为Infinity  new Set(tools.filter(t => !Number.isFinite(t.maxResultSizeChars)).map(t => t.name)),)

当工具输出超过maxResultSizeChars阈值时,内容被保存到磁盘文件,消息中只保留文件路径。这样模型知道结果存在,但不会被巨大的文本占满上下文。

3.3 Auto Compact:用模型压缩自己

当上下文接近模型的窗口上限时,Auto Compact启动——用一个独立的forked agent生成对话摘要:

const { compactionResult } = await deps.autocompact(messagesForQuery, toolUseContext, {  systemPrompt, userContext, systemContext, toolUseContext,  forkContextMessages: messagesForQuery,}, querySource, tracking)

关键设计:压缩本身也是一个Agent调用。系统用一个独立的Agent来阅读完整对话历史,生成精简摘要,然后用摘要替换原始消息。这意味着压缩质量等于模型的理解能力——不是简单的截断,而是真正的语义压缩。

Auto Compact还有智能的触发阈值:

export function getAutoCompactThreshold(model: string): number {  const effectiveContextWindow = getEffectiveContextWindowSize(model)  return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS  // 13,000 buffer}

不是等上下文满了才压缩,而是留出13,000个Token的缓冲区。这个值来自生产数据的分析——压缩本身需要消耗Token,缓冲区确保压缩请求不会被拒绝。

3.4 Microcompact与缓存友好

Auto Compact的代价很高(一次完整的Agent调用),Microcompact是一种更轻量的增量压缩。它的关键创新是缓存友好——修改缓存的对话前缀,而不是完全替换。

const microcompactResult = await deps.microcompact(  messagesForQuery, toolUseContext, querySource)

这意味着在Anthropic API的Prompt Caching机制下,Microcompact可以复用之前的缓存,大幅降低成本。

四、记忆系统:三层架构与自动梦境整合

三层记忆与梦境整合架构

4.1 三层记忆

Claude Code的记忆系统分为三层:

第一层:Session Memory(会话记忆)

// SessionMemory/sessionMemory.ts// 自动维护一个markdown文件,记录当前对话的关键信息// 使用forked subagent后台提取,不阻塞主对话

Session Memory在后台自动运行——每次工具调用达到一定阈值后,系统启动一个后台Agent,阅读最近的对话,提取关键信息写入markdown文件。这个文件在下次查询时作为上下文注入。

第二层:Extract Memories(持久记忆)

// extractMemories/extractMemories.ts// 从对话中提取持久记忆,写入 ~/.claude/projects/<path>/memory/// 在每个完整查询循环结束时运行(handleStopHooks)

持久记忆存储在~/.claude/projects/<path>/memory/目录下,跨会话持久化。当你下次打开Claude Code时,系统会自动加载这些记忆。

第三层:Auto Dream(梦境整合)

// autoDream/autoDream.ts// 后台记忆整合。当时间门槛和会话数量都满足时触发// 使用 /dream prompt 作为forked subagent运行

Auto Dream是最有趣的设计。它模仿人类的睡眠整合机制——当积累了足够多的会话(默认≥5个)且经过足够长的时间(默认≥24小时),系统启动一个后台Agent来整合所有会话的记忆,去除重复、提炼关键信息。

4.2 记忆提取的阈值设计

function shouldExtractMemory(messages: Message[]): boolean {  const currentTokenCount = tokenCountWithEstimation(messages)  if (!isSessionMemoryInitialized()) {    if (!hasMetInitializationThreshold(currentTokenCount)) return false    markSessionMemoryInitialized()  }  const hasMetTokenThreshold = hasMetUpdateThreshold(currentTokenCount)  const toolCallsSinceLastUpdate = countToolCallsSince(messages, lastMemoryMessageUuid)  // ...}

记忆提取不是每条消息都触发,而是有智能的阈值控制——基于Token数量和工具调用次数。只有在积累了足够的新信息后才启动提取,避免频繁的后台Agent调用。

4.3 Forked Agent模式

记忆提取和梦境整合都使用了runForkedAgent——一个关键的基础设施:

// utils/forkedAgent.tsexport async function runForkedAgent(params: {  cacheSafeParams: CacheSafeParams  messages: Message[]  ...}): Promise<{ messages: Message[]; usage: Usage }> {  // 创建一个与主对话共享缓存参数的Agent  // 确保fork的API请求能命中父对话的Prompt Cache}

CacheSafeParams确保fork出来的Agent与父Agent共享完全相同的系统提示、工具列表和消息前缀——这是Prompt Cache命中的关键条件。一次缓存命中可以节省数千个Token的计费。

五、子代理架构:多Agent协作模型

Forked Agent模型与缓存共享

5.1 五种任务类型

Claude Code的子代理系统支持5种任务类型:

类型
文件
场景
LocalAgentTask
LocalAgentTask.tsx
本地Agent子任务(最常用)
RemoteAgentTask
RemoteAgentTask.tsx
远程Agent子任务
LocalShellTask
本地Shell命令执行
DreamTask
DreamTask.ts
记忆整合梦境任务
InProcessTeammateTask
进程内Team协作

5.2 LocalAgentTask详解

LocalAgentTask是最核心的子代理实现:

type LocalAgentTaskState = {  type: 'local_agent'  agentId: string  prompt: string  selectedAgent?: AgentDefinition  agentType: string  model?: string  abortController?: AbortController  result?: AgentToolResult  progress?: AgentProgress  pendingMessages: string[]       // 待处理的消息队列  isBackgrounded: boolean         // 是否在后台运行  retain: boolean                 // UI是否正在查看  diskLoaded: boolean             // 是否已从磁盘加载  evictAfter?: number             // 驱逐时间戳}

关键设计点:

  • • pendingMessages:主Agent可以通过SendMessage工具向子Agent发送消息,这些消息被缓存在队列中,在工具轮次边界时传递给子Agent
  • • isBackgrounded:子Agent可以在后台运行,不阻塞主Agent
  • • retain:用户可以”进入”子Agent的视图,实时查看它的执行过程
  • • evictAfter:子Agent完成后不会立即销毁,而是在一段时间后被垃圾回收

5.3 进度追踪

type ProgressTracker = {  toolUseCount: number  latestInputTokens: number  cumulativeOutputTokens: number  recentActivities: ToolActivity[]}type ToolActivity = {  toolName: string  input: Record<string, unknown>  activityDescription?: string  // 如 "Reading src/foo.ts"  isSearch?: boolean  isRead?: boolean}

每个子Agent维护自己的进度追踪器,最近5个活动被保留用于UI展示。Token统计分开追踪输入和输出——因为Anthropic API的输入Token是累积的(包含所有之前的上下文),而输出Token是每次独立的。

5.4 Coordinator Mode

Claude Code还有一个Coordinator模式——一个特殊的Agent可以充当协调者,管理多个子Agent:

// coordinator/coordinatorMode.tsif (isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)) {  return asSystemPrompt([getCoordinatorSystemPrompt()])}

Coordinator模式使用独立的系统提示,专注于任务分配和结果综合,而不是直接执行任务。

六、权限沙箱:分级信任模型

6.1 权限模式

Claude Code的权限系统支持多种模式:

type PermissionMode =   | 'default'     // 每次操作都询问  | 'plan'        // 只读规划模式  | 'autoEdit'    // 自动允许编辑  | 'fullAuto'    // 全自动(需配置白名单)  // ...

6.2 工具级权限控制

每个工具都实现了checkPermissions()方法:

type Tool<Input, Output, P> = {  checkPermissions(input: Input, context: ToolUseContext): Promise<PermissionResult>  // ...}

权限检查分三层:

  1. 1. validateInput:验证输入是否合法
  2. 2. checkPermissions:判断是否需要用户确认
  3. 3. canUseTool:最终的权限决策(考虑白名单/黑名单/自动分类器)

6.3 自动分类器

在fullAuto模式下,Claude Code使用一个安全分类器来评估每个工具调用的风险:

toAutoClassifierInput(input: Input): unknown

每个工具都需要实现这个方法,将自己的输入转换为分类器能理解的紧凑表示。例如:

  • • Bash工具返回命令字符串(如 ls -la
  • • FileEdit工具返回文件路径和变更摘要
  • • FileRead工具可以返回空字符串(无安全风险,跳过分类)

6.4 Bash沙箱

Bash工具是安全风险最高的工具,有独立的沙箱机制:

// tools/BashTool/shouldUseSandbox.ts// tools/BashTool/bashSecurity.ts// tools/BashTool/bashPermissions.ts

沙箱配置包括:

  • • 命令超时(默认2分钟,最大10分钟)
  • • 后台执行支持(run_in_background参数)
  • • 破坏性命令检测和警告
  • • 路径验证(防止路径遍历攻击)

6.5 MCP权限管理

MCP(Model Context Protocol)工具的权限管理更加复杂:

// services/mcp/channelAllowlist.ts// services/mcp/channelPermissions.ts// services/mcp/channelNotification.ts

每个MCP服务器连接都有独立的权限配置,包括:

  • • 通道白名单:哪些IM通道可以触发哪些MCP工具
  • • OAuth认证:支持MCP服务器的OAuth令牌流程
  • • 用户确认:新MCP工具首次使用时需要用户确认

七、MCP集成:外部工具生态系统

7.1 MCP连接管理

Claude Code实现了完整的MCP客户端:

// services/mcp/├── MCPConnectionManager.tsx   // React Context管理MCP连接├── client.ts                  // MCP客户端├── config.ts                  // MCP配置├── auth.ts                    // OAuth认证├── types.ts                   // MCP类型定义├── normalization.ts           // 工具名称规范化├── oauthPort.ts               // OAuth端口管理├── officialRegistry.ts        // 官方MCP注册表└── xaa.ts                     // 扩展认证

MCP连接通过React Context管理,支持动态连接/断开/重连。每个MCP服务器提供一组工具,这些工具被动态注入到Agent的工具列表中。

7.2 MCP工具的延迟加载

MCP工具支持_meta['anthropic/alwaysLoad']标记——被标记的工具始终加载完整schema,未标记的工具默认延迟加载(通过ToolSearch发现)。

这个设计平衡了功能可见性和上下文效率:核心MCP工具(如代码分析)始终可见,辅助工具(如特定API集成)按需加载。

八、UI层:Ink + React的终端界面

8.1 技术选型

Claude Code使用React + Ink构建终端UI——这是一个不常见但非常聪明的选择:

  • • React:组件化、状态管理、Hooks
  • • Ink:React的终端渲染器,支持Flexbox布局

这意味着90+个React Hooks(src/hooks/目录下)都可以在终端UI中使用,包括状态管理、事件处理、副作用管理等。

8.2 关键Hooks

useCanUseTool.tsx        // 工具权限决策useBackgroundTaskNavigation  // 后台任务导航useMergedTools.ts        // 合并内置工具和MCP工具useSessionBackgrounding  // 会话后台化useSwarmInitialization   // Swarm模式初始化useSwarmPermissionPoller // Swarm权限轮询

useMergedTools是特别重要的一个——它将内置的44个工具和动态加载的MCP工具合并成一个统一的工具列表,供给Agent使用。

8.3 Bridge架构

// bridge/ — IDE集成桥接├── bridgeMain.ts         // 桥接主入口├── bridgeMessaging.ts    // 消息传递├── bridgePermissionCallbacks.ts  // 权限回调├── replBridge.tsx        // REPL桥接├── createSession.ts      // 会话创建└── remoteBridgeCore.ts   // 远程桥接核心

Bridge架构允许Claude Code与IDE(如VS Code)深度集成——在IDE中直接调用Claude Code,共享工作区、文件状态和终端。

九、系统提示工程

9.1 分层系统提示

Claude Code的系统提示构建是分层的:

function buildEffectiveSystemPrompt({  mainThreadAgentDefinition,  toolUseContext,  customSystemPrompt,  defaultSystemPrompt,  appendSystemPrompt,  overrideSystemPrompt,}): SystemPrompt {  // 优先级:Override > Coordinator > Agent > Custom > Default  // appendSystemPrompt 总是追加在最后}

系统提示有5层优先级:

  1. 1. Override:完全替换(如loop模式)
  2. 2. Coordinator:协调者专用
  3. 3. Agent:自定义Agent定义
  4. 4. Custom--system-prompt参数
  5. 5. Default:标准Claude Code提示

appendSystemPrompt总是追加在最后,不受优先级影响。

9.2 动态提示内容

系统提示不是静态字符串,而是动态构建的:

type Tool = {  prompt(options: {    getToolPermissionContext: () => Promise<ToolPermissionContext>    tools: Tools    agents: AgentDefinition[]  }): Promise<string>}

每个工具都实现了prompt()方法,在运行时动态生成自己的使用说明。这意味着工具的提示内容可以根据当前权限状态、可用Agent等因素动态调整。

十、设计模型总结:Harness的六大原则

从Claude Code的源码中,我们可以提炼出Agent Harness设计的六大核心原则:

原则一:永不信任模型输出

Claude Code的每一层设计都假设模型可能出错:

  • • validateInput验证工具输入
  • • checkPermissions检查操作权限
  • • LoopDetection防止死循环
  • • ToolResultBudget限制输出大小
  • • AutoCompact处理上下文溢出

这不是对模型的不信任,而是防御性工程——任何可能出错的地方,都要有保护机制。

原则二:流式一切

从Query主循环到工具执行到UI渲染,所有操作都是流式的。这意味着:

  • • 用户可以看到实时进度
  • • 可以随时中断任何操作
  • • 内存不会因为等待完整结果而溢出

AsyncGenerator是Claude Code最核心的抽象。

原则三:上下文是最宝贵的资源

四层压缩策略、延迟工具加载、Tool Result Budget、Tool Search——所有这些设计的核心目标都是高效利用有限的上下文窗口

在Agent系统中,上下文窗口就是内存——它决定了Agent能”记住”多少信息。Claude Code的上下文工程可以总结为一句话:用最少的Token携带最多的信息

原则四:Fork而不是Spawn

Claude Code的子代理使用runForkedAgent——fork而不是spawn。区别在于:

  • • Spawn:创建全新的Agent,从零开始
  • • Fork:复制当前Agent的状态,共享缓存

Fork的好处是缓存共享——子Agent可以复用父Agent的Prompt Cache,一次API调用可以节省数千Token。这个设计直接降低了运营成本。

原则五:分级信任

不是所有操作都需要同样的安全级别:

  • • 文件读取(只读)→ 自动允许
  • • 文件编辑(写入)→ 需要确认(或autoEdit模式)
  • • Bash命令(执行)→ 安全分类器 + 用户确认
  • • 破坏性操作(删除/覆盖)→ 强制确认

这种分级信任模型在安全性和效率之间取得了平衡。

原则六:后台处理不阻塞前台

记忆提取、梦境整合、技能发现——所有这些后台任务都不阻塞主对话流:

// Memory prefetch — runs while the model streams and tools executeusing pendingMemoryPrefetch = startRelevantMemoryPrefetch(messages, toolUseContext)// Skill discovery — runs in parallel with tool executionconst pendingSkillPrefetch = skillPrefetch?.startSkillDiscoveryPrefetch(...)

用户永远不会因为系统在后台整理记忆而等待。

结语

Claude Code的源码告诉我们:构建一个Agent很容易,但构建一个可靠的Agent运行时很难

50万行代码不是过度工程——每一行都在解决一个真实的问题。四层上下文压缩解决了”对话过长”的问题;Forked Agent解决了”子Agent成本过高”的问题;分级权限解决了”安全与效率的矛盾”。

这些不是某个特定模型的限制——它们是Agent系统本身的工程挑战。任何想构建生产级Agent的团队,都需要面对这些问题。

Claude Code给出了它的答案。而理解这些答案背后的设计决策,比记住代码本身更重要。