Claude Code Harness Agent 架构深度解析

Claude Code Harness Agent 架构深度解析

基于 @anthropic-ai/claude-code@2.1.88 源码逆向分析

核心目的:掌握构建 Harness Agent 所需的知识和最佳实践


达芬奇提问:直达本质

在分析 1382 个源文件之前,先问出最根本的问题:

第一性问题:什么是 Harness Agent?

"Agent 不是模型,Agent 是模型 + 循环 + 工具 + 判断。"

一个 Harness Agent 本质上回答一个问题:如何让 LLM 在现实世界中可靠地行动? 这要求解决五个子问题:

  1. 循环问题:模型何时该继续调用工具,何时该停下?(Agent Loop)
  2. 能力问题:模型能做什么,怎么做?(Tool System)
  3. 安全问题:谁决定模型能不能做?(Permission System)
  4. 记忆问题:上下文装不下怎么办?(Context Management)
  5. 可靠性问题:失败了怎么办?(Error Recovery & Retry)

Claude Code 对这五个问题给出了工程级的答案。以下逐一拆解。


一、全局架构概览

scss 复制代码
┌────────────────────────────────────────────────────────────┐
│                      User Interface                        │
│  REPL (Ink/React) │ CLI │ SDK │ Bridge (IDE/Web)           │
└──────────┬─────────────────────────────────────────────────┘
           │ UserMessage
           ▼
┌─────────────────────────────────────────────────────────────┐
│                     Agent Loop (query.ts)                   │
│  ┌───────────┐  ┌───────────┐  ┌──────────────────────────┐ │
│  │ Context   │  │ API Call  │  │ Tool Execution           │ │
│  │ Mgmt      │  │ + Stream  │  │ (Parallel/Serial)        │ │
│  │           │  │           │  │                          │ │
│  │ compact() │  │ callModel │  │ StreamingToolExecutor    │ │
│  │ snip()    │  │ withRetry │  │ runTools()               │ │
│  │ collapse()│  │ SSE parse │  │                          │ │
│  └───────────┘  └───────────┘  └───────┬──────────────────┘ │
│                                        │                    │
│  ┌─────────────────────────────────────┼──────────────────┐ │
│  │          Permission Gate            │                  │ │
│  │  Rules → Hooks → Classifier → User  │                  │ │
│  └─────────────────────────────────────┼──────────────────┘ │
│                                        │                    │
│  ┌─────────────────────────────────────┼──────────────────┐ │
│  │              Hooks System           │                  │ │
│  │  PreToolUse → PostToolUse → Stop    │                  │ │
│  │  SessionStart → FileChanged → ...   │                  │ │
│  └─────────────────────────────────────┘──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
           │                    │
           ▼                    ▼
┌──────────────────┐  ┌───────────────────────────┐
│  Claude API      │  │  External Systems         │
│  (1P/Bedrock/    │  │  - File System            │
│   Vertex/Azure)  │  │  - Shell/Process          │
│                  │  │  - MCP Servers            │
│  SSE Streaming   │  │  - LSP Servers            │
│  Retry + Backoff │  │  - Git/GitHub             │
└──────────────────┘  └───────────────────────────┘

源码规模

目录 文件数 职责
src/utils/ 564 工具函数、权限、Hook、Shell、MCP
src/components/ 389 Ink/React 终端 UI 组件
src/commands/ 207 80+ CLI 命令实现
src/tools/ 184 65+ 工具实现
src/services/ 130 API、MCP、Compact、Analytics
src/hooks/ 104 React Hooks(UI 状态)
总计 ~1382

二、Agent Loop:循环的艺术

达芬奇之问:为什么用 async generator 而不是简单的 while 循环?

答:因为 Agent Loop 同时是生产者 (产生流式事件)和消费者 (消耗工具结果)。async generator 的 yield 语义天然支持这种双向数据流,让调用方可以实时接收每一个 token、每一个工具进度,而不必等待整个 turn 结束。

2.1 核心控制流

入口src/query.ts --- 1729 行,整个 Agent 的心脏

csharp 复制代码
// 外层包装:设置初始状态
export async function* query(params): AsyncGenerator<StreamEvent | Message, Terminal> {
  yield* queryLoop(params)
}

// 内层循环:真正的 Agent Loop
async function* queryLoop(params): AsyncGenerator<...> {
  let state: State = { messages, toolUseContext, turnCount: 1, ... }
  
  while (true) {
    // ① 上下文压缩阶段
    // ② API 调用 + 流式接收阶段
    // ③ 错误恢复阶段
    // ④ 工具执行阶段
    // ⑤ 附件收集阶段(Memory、Skill 预取)
    // ⑥ 最大轮次检查
    // ⑦ 状态更新 → continue(隐式递归)
  }
}

2.2 状态机

typescript 复制代码
type State = {
  messages: Message[]                          // 完整对话历史
  toolUseContext: ToolUseContext               // 工具执行上下文(贯穿整个会话)
  autoCompactTracking: AutoCompactTrackingState // 自动压缩跟踪
  maxOutputTokensRecoveryCount: number         // 输出 token 恢复计数(最多 3 次)
  hasAttemptedReactiveCompact: boolean         // 是否已尝试反应式压缩
  pendingToolUseSummary: Promise<...>          // 待处理的工具使用摘要
  stopHookActive: boolean                      // Stop Hook 是否活跃
  turnCount: number                            // 当前轮次
  transition: Continue | undefined             // 状态转移原因
}

关键设计 :状态在 continue 站点被整体替换而非局部修改,保证了每轮迭代开始时状态的一致性。

2.3 一轮完整的 Turn

scss 复制代码
Turn N 开始
  │
  ├── [1] 上下文管理
  │   ├── applyToolResultBudget()    → 工具结果大小预算
  │   ├── snipCompact()              → 裁剪最旧消息
  │   ├── microcompact()             → API 级缓存编辑(仅 1P)
  │   ├── contextCollapse()          → 分阶段上下文折叠
  │   └── autoCompact()             → 全量摘要生成
  │
  ├── [2] API 调用
  │   ├── normalizeMessagesForAPI()  → 消息标准化
  │   ├── callModel()               → 流式 API 调用
  │   │   └── withRetry()           → 重试 + 指数退避
  │   ├── for await (message of stream)
  │   │   ├── yield 流式消息         → 实时推送给 UI
  │   │   └── StreamingToolExecutor.addTool()  → 边流边执行工具
  │   └── 收集 assistantMessages + toolUseBlocks
  │
  ├── [3] 错误恢复(如果 API 报错)
  │   ├── Prompt Too Long → contextCollapse → reactiveCompact
  │   ├── Max Output Tokens → 升级到 64k → 多轮恢复(最多 3 次)
  │   └── Media Size Error → 剥离图片 → 重试
  │
  ├── [4] 工具执行
  │   ├── StreamingToolExecutor.getRemainingResults()  // 已在流式期间启动
  │   │   或 runTools()                                 // 非流式模式
  │   ├── for await (update of toolUpdates)
  │   │   ├── yield 进度消息
  │   │   └── 收集 toolResults
  │   └── 处理 Stop Hooks(post-sampling)
  │
  ├── [5] 附件收集
  │   ├── 消耗 Memory 预取结果
  │   ├── 消耗 Skill 预取结果
  │   └── 排空命令队列
  │
  └── [6] 决策:继续还是停止?
      ├── needsFollowUp && turnCount < maxTurns → state 更新 → continue
      └── 否则 → return Terminal

2.4 流式工具执行(核心创新)

达芬奇之问:为什么不等模型说完再执行工具?

答:因为模型输出通常需要 5-30 秒。如果工具(如读文件)只需 50ms,那等模型说完再执行白白浪费了几十秒。流式工具执行让工具与模型输出并行,在模型还在输出下一个 tool_use block 时,前一个工具已经完成了。

typescript 复制代码
// StreamingToolExecutor.ts --- 核心并发控制
class StreamingToolExecutor {
  addTool(toolBlock, message) {
    this.tools.push({ block: toolBlock, status: 'pending' })
    this.processQueue()  // 立即尝试执行
  }
  
  private canExecuteTool(isConcurrencySafe: boolean): boolean {
    const executing = this.tools.filter(t => t.status === 'executing')
    return (
      executing.length === 0 ||  // 无执行中的工具
      (isConcurrencySafe && executing.every(t => t.isConcurrencySafe))  // 全部并发安全
    )
  }
}

并发策略

  • 只读工具 (Read、Glob、Grep):标记为 isConcurrencySafe,可并行执行(最多 10 个)
  • 写入工具(Edit、Write、Bash):独占执行,必须等其他工具完成
  • 并发度上限 :环境变量 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 控制

三、Tool System:能力的边界

达芬奇之问:工具是名词还是动词?

答:工具同时是声明 (schema 告诉模型"我能做什么")和执行 (call 方法真正"去做")。这种二元性决定了工具系统必须同时关注模型可见性运行时安全性

3.1 Tool 接口

typescript 复制代码
// src/Tool.ts --- 核心接口(362-695 行)
interface Tool<Input extends AnyObject, Output = unknown, P extends ToolProgressData> {
  // === 声明侧(模型看到的) ===
  name: string                          // 唯一标识符
  aliases?: string[]                    // 向后兼容名
  description(): Promise<string>        // 动态描述(可基于上下文变化)
  inputSchema: Input                    // Zod schema(类型安全)
  inputJSONSchema?: ToolInputJSONSchema // JSON Schema(给 MCP 工具用)
  searchHint?: string                   // 工具搜索关键词(3-10 词)
  shouldDefer?: boolean                 // 是否延迟加载 schema
  alwaysLoad?: boolean                  // 始终加载到 prompt
  
  // === 执行侧(运行时调用的) ===
  call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
  validateInput?(input, context): Promise<ValidationResult>
  checkPermissions(input, context): Promise<PermissionResult>
  
  // === 元数据侧(系统决策用的) ===
  isConcurrencySafe(input): boolean     // 是否可并行
  isReadOnly(input): boolean            // 是否只读
  isDestructive?(input): boolean        // 是否有破坏性
  maxResultSizeChars: number            // 结果持久化阈值
  
  // === 渲染侧(UI 显示用的) ===
  renderToolResultMessage?(output, progress, options): ReactNode
  mapToolResultToToolResultBlockParam(data, toolUseID): ToolResultBlockParam
}

3.2 工具注册与过滤

scss 复制代码
getAllBaseTools()                    // 65+ 工具全集
  │
  ├── 条件导入(feature flags)      // 功能门控
  ├── 懒加载(break circular deps) // 打破循环依赖
  │
  ▼
getTools(permissionContext)         // 按模式过滤
  │
  ├── SIMPLE 模式 → 仅 Bash/Read/Edit
  ├── deny rules → 过滤被禁工具
  │
  ▼
assembleToolPool()                  // 合并内置 + MCP 工具
  │
  ├── 去重(MCP 覆盖同名内置工具)
  ├── 按名称排序(prompt cache 稳定性)
  └── 内置工具保持连续前缀

3.3 工具执行管线

scss 复制代码
runToolUse(toolUse, assistantMessage, canUseTool, context)
  │
  ├── [1] 查找工具   → findToolByName(),检查 aliases
  ├── [2] 输入验证   → inputSchema.safeParse() → Zod 验证
  │                  → tool.validateInput()      → 语义验证
  ├── [3] 输入回填   → tool.backfillObservableInput() → 补充派生字段
  ├── [4] Pre-Hooks  → runPreToolUseHooks() → 可阻止/修改输入
  ├── [5] 权限检查   → canUseTool() → 多层权限决策
  ├── [6] 执行       → tool.call() → 带进度回调
  ├── [7] 结果映射   → mapToolResultToToolResultBlockParam()
  ├── [8] 结果持久化 → 超过阈值 → 写磁盘 → 返回预览 + 路径引用
  └── [9] Post-Hooks → runPostToolUseHooks() → 可修改输出

3.4 工具结果大小管理

javascript 复制代码
ToolResult<T> = {
  data: T                                           // 实际输出
  newMessages?: Message[]                           // 可选附加消息
  contextModifier?: (ctx) => ToolUseContext          // 上下文变更闭包
  mcpMeta?: { _meta, structuredContent }            // MCP 协议元数据
}

// 持久化策略
if (resultSize > maxResultSizeChars) {
  // 写入 ~/.claude/sessions/{sessionId}/tool-results/{toolUseId}.txt
  // 返回前 2000 字节预览 + 文件路径引用
}

四、Permission System:安全的哲学

达芬奇之问:安全和便利如何平衡?

答:Claude Code 的答案是分层判决 :先查规则、再问 Hook、再用分类器、最后问用户。每一层都可以提前终止,避免不必要的交互。这不是"要么全开要么全关",而是一个渐进式信任模型

4.1 权限模式

go 复制代码
type PermissionMode =
  | 'default'           // 每个工具都询问用户
  | 'acceptEdits'       // 自动批准编辑,其他询问
  | 'bypassPermissions' // 全部自动批准(需要用户二次确认)
  | 'dontAsk'           // 自动拒绝,不提示
  | 'plan'              // 计划模式(只分析不执行)
  | 'auto'              // ML 分类器决定(内部功能)

4.2 权限判决管线

scss 复制代码
canUseTool(tool, input, context)
  │
  ├── [1] 工具级禁止    → getDenyRuleForTool()
  │                     → 无内容匹配的全局 deny 规则
  │
  ├── [2] Pre-Hook 决策 → executePreToolHooks()
  │                     → Hook 可返回 allow/deny/ask
  │                     → Hook 可修改输入
  │
  ├── [3] 规则匹配      → checkRuleBasedPermissions()
  │   ├── allow rules  → toolMatchesRule() + 内容匹配
  │   ├── deny rules   → 同上
  │   └── ask rules    → 同上
  │
  ├── [4] 工具自检      → tool.checkPermissions()
  │                     → 每个工具的自定义权限逻辑
  │
  ├── [5] 自动模式分类器 → classifyYoloAction()(仅 Bash)
  │                     → 小模型打分:safe/risky/dangerous
  │
  └── [6] 用户交互      → 弹出权限对话框
                        → 用户可选择:允许/拒绝/始终允许/始终拒绝

4.3 权限规则来源

go 复制代码
type PermissionRuleSource =
  | 'userSettings'      // ~/.claude/settings.json
  | 'projectSettings'   // .claude/settings.json(项目级)
  | 'localSettings'     // .claude/settings.local.json
  | 'flagSettings'      // 远程功能标志
  | 'policySettings'    // 企业管理策略
  | 'cliArg'            // 命令行参数 --allow/--deny
  | 'command'           // 命令内置规则
  | 'session'           // 运行时动态添加

// 规则示例
{ source: 'userSettings', ruleBehavior: 'allow', ruleValue: { toolName: 'Bash', ruleContent: 'git *' } }
// → 允许所有以 "git " 开头的 Bash 命令

4.4 权限判决结果

rust 复制代码
type PermissionDecision =
  | { behavior: 'allow', updatedInput?, decisionReason? }
  | { behavior: 'ask',   message, suggestions?, pendingClassifierCheck? }
  | { behavior: 'deny',  message, decisionReason? }

// 判决原因追踪
type PermissionDecisionReason =
  | { type: 'rule', rule: PermissionRule }        // 规则匹配
  | { type: 'hook', hookName, reason? }           // Hook 决策
  | { type: 'classifier', classifier, reason }    // ML 分类器
  | { type: 'mode', mode: PermissionMode }        // 权限模式
  | { type: 'sandboxOverride', reason }           // 沙箱覆盖
  | { type: 'asyncAgent', reason }                // 异步代理

五、Context Management:记忆的经济学

达芬奇之问:上下文窗口满了怎么办?不是"丢掉什么",而是"保留什么最有价值"。

答:Claude Code 设计了五级压缩体系,从最轻量到最重量,像内存层次结构(L1→L2→L3→磁盘)一样逐级升级。只有低级压缩不够用时才启动高级压缩。

5.1 五级压缩体系

yaml 复制代码
Level 0: 工具结果预算(最轻量)
├── 每条消息的工具结果总字节数限制
├── 超出 → 持久化到磁盘,返回预览 + 文件引用
└── 成本:几乎为零

Level 1: Snip Compact(裁剪)
├── 从最旧的消息开始移除
├── 保留系统消息和最近 N 轮
└── 成本:丢失早期上下文

Level 2: Microcompact(API 级缓存编辑)
├── 仅 1P(Anthropic 直连)可用
├── 使用 clear_tool_uses_20250919 策略
├── 清除旧工具调用的输入/输出,保留最近的
├── 阈值:180K tokens → 目标压缩到 40K
├── 可清除的工具:Bash, Glob, Grep, Read, WebFetch, WebSearch
├── 不可清除的工具:Edit, Write, NotebookEdit
└── 成本:由 API 侧处理,无额外推理

Level 3: Context Collapse(上下文折叠)
├── 分阶段折叠粒度较高的上下文
├── 保留结构化信息,移除细节
└── 成本:中等推理开销

Level 4: Auto Compact(全量摘要)
├── 使用模型生成完整摘要
├── 最大输出预算:50K tokens
├── Skill 文件截断:每个 5K tokens,总计 25K tokens
├── 插入 SystemCompactBoundaryMessage 标记
└── 成本:一次完整 API 调用

5.2 系统提示构建

scss 复制代码
buildSystemPromptBlocks()
  │
  ├── 属性头(不缓存)           → 计费标识
  ├── 静态前缀(org 级缓存)      → CLI 系统提示模板
  ├── 工具描述(org 级缓存)      → 65+ 工具的 schema + description
  ├── 系统上下文(不缓存)        → gitStatus, cacheBreaker
  ├── 用户上下文(不缓存)        → CLAUDE.md 内容, currentDate
  └── MCP 工具描述(org 级缓存)  → 外部 MCP 工具的 schema

// 缓存策略:三段式
splitSysPromptPrefix() → [attribution(null), prefix(org), rest(org)]
// 工具排序保证内置工具连续前缀 → 最大化 prompt cache 命中率

5.3 消息标准化

scss 复制代码
normalizeMessagesForAPI(messages)
  │
  ├── 重排附件         → 上浮直到遇到工具结果或助手消息
  ├── 剥离虚拟消息     → Display-only 消息不发给 API
  ├── 错误清理         → 基于错误文本移除有问题的块(PDF、图片)
  ├── 工具引用处理     → 按 ToolSearch 开关决定保留/剥离
  ├── 连续消息合并     → 合并相邻的 user 消息(Bedrock 要求)
  └── 工具输入标准化   → 移除 API 不兼容的字段

ensureToolResultPairing(messages)
  │
  ├── 跨消息去重       → 追踪 tool_use ID 检测重复
  ├── 孤儿检测
  │   ├── 缺失 tool_result → 创建合成错误块
  │   └── 孤立 tool_result → 剥离
  └── 角色交替维护     → 保持 user-assistant-user 模式

六、Error Recovery & Retry:韧性的工程

达芬奇之问:失败是常态还是异常?

答:在分布式系统中,失败是常态。Claude Code 的设计哲学是永远不在可恢复的错误上崩溃

6.1 API 层重试

javascript 复制代码
// src/services/api/withRetry.ts
async function* withRetry(getClient, operation, options) {
  // 指数退避:500ms → 1s → 2s → 4s → 8s → 16s → 32s(上限)
  // 抖动:±25% 随机,防止惊群效应
  
  // 401/403 → 刷新凭据 → 重试
  // ECONNRESET/EPIPE → 禁用 keep-alive → 重试
  // 429/529 → 尊重 Retry-After 头 → 指数退避
  // Max Output Tokens → 调整 token 限制 → 重试
}

// 529 过载特殊处理
MAX_529_RETRIES = 3  // 最多连续 3 次 529
// 恢复路径:
// 1. 快速模式降级 → 切换标准速度模型
// 2. 模型降级 → 切换到备选模型
// 3. 持久重试 → 无人值守会话无限重试(分块等待)

6.2 查询层错误恢复

scss 复制代码
API 错误
  │
  ├── Prompt Too Long (413)
  │   ├── [1] Context Collapse Drain → 轻量级,保留粒度
  │   ├── [2] Reactive Compact → 全量摘要生成
  │   └── [3] 上浮错误 → 告知用户
  │
  ├── Max Output Tokens
  │   ├── [1] 升级到 64K tokens(如果之前是 8K)
  │   ├── [2] 多轮恢复(最多 3 次)
  │   └── [3] 上浮错误
  │
  ├── Media Size Error
  │   ├── [1] Reactive Compact(启用时)
  │   └── [2] 剥离图片 → 重试
  │
  └── Stop Hook 阻塞
      └── 将阻塞错误作为用户消息追加 → 模型自行修正

6.3 工具层错误处理

scss 复制代码
// 工具错误分类
function classifyToolError(error) {
  // TelemetrySafeError → telemetryMessage(安全的错误描述)
  // ENOENT → 文件不存在
  // Error.name → 错误类型名
  // 兜底 → 'Error'
}

// 工具失败后的 Hook
runPostToolUseFailureHooks(toolName, error, context)
// → 允许插件对失败做出反应(日志、清理、重试决策)

七、Hooks System:可扩展性的骨架

达芬奇之问:如何让系统在不修改核心代码的情况下改变行为?

答:Hook 是"观察者模式"在 Agent 架构中的终极实现。它不只是事件监听,还能改变控制流 (阻止工具执行)、修改数据流 (改变工具输入/输出)、扩展权限(自定义权限判决)。

7.1 Hook 事件完整列表

事件 触发时机 可影响
PreToolUse 工具执行前 阻止执行、修改输入、权限决策
PostToolUse 工具执行后 修改输出、追加上下文
PostToolUseFailure 工具执行失败后 清理、日志、重试
PermissionDenied 自动模式拒绝后 自定义处理
PermissionRequest 权限请求时 自动批准/拒绝、修改权限规则
SessionStart 会话开始 注入初始上下文、注册文件监视
Setup 初始化 维护任务
Stop 模型停止输出 后台任务、记忆提取
SubagentStart 子代理启动 跟踪、配置
SubagentStop 子代理结束 清理、记录
FileChanged 文件变更 按文件名 glob 过滤
CwdChanged 工作目录变更 环境变量导出
WorktreeCreate Worktree 创建 VCS 无关的隔离
WorktreeRemove Worktree 移除 清理
Elicitation MCP 用户输入请求 自定义 UI

7.2 四种 Hook 类型

rust 复制代码
// 1. Command Hook --- 最常用
{ type: 'command', command: 'npm test', timeout: 30, async: false }

// 2. Prompt Hook --- LLM 驱动
{ type: 'prompt', prompt: 'Is this safe?', model: 'haiku', timeout: 10 }

// 3. HTTP Hook --- 外部服务
{ type: 'http', url: 'https://api.example.com/check', 
  allowedEnvVars: ['API_KEY'], timeout: 5 }

// 4. Agent Hook --- 代理验证器
{ type: 'agent', prompt: 'Verify this change is safe', timeout: 60 }

7.3 Hook 执行流

scss 复制代码
executeHooks(hookInput, matchQuery, signal, timeoutMs)
  │
  ├── 信任检查      → 所有 Hook 都需要工作区信任(RCE 防御)
  ├── 匹配 Hook     → getMatchingHooks() → 按事件 + 匹配器过滤
  ├── 并行执行      → 每个 Hook 独立超时
  │   ├── Command → spawn 子进程
  │   ├── Prompt  → 调用 LLM
  │   ├── Agent   → 启动子代理
  │   ├── HTTP    → POST 请求
  │   └── Function → 调用内存回调
  ├── 解析输出      → JSON 验证
  ├── 处理结果      → permissionBehavior / blockingError
  └── 发射事件      → SDK 事件处理器

7.4 Hook 退出码语义(PreToolUse)

退出码 含义 行为
0 检查通过,隐藏 继续执行,不显示 Hook 输出
2 阻塞 停止工具执行,显示 Hook 输出给模型
其他 非阻塞错误 继续执行,记录错误

八、MCP 集成:能力的无限扩展

达芬奇之问:为什么不把所有工具都内置?

答:因为世界上的工具是无限的,而 Agent 的核心应该是协议 而非实现。MCP(Model Context Protocol)让 Agent 可以连接任何符合协议的工具服务器。

8.1 MCP 工具命名

arduino 复制代码
// 内置工具:直接名字
tool.name = 'Bash'

// MCP 工具:三段式命名
tool.name = 'mcp__<serverName>__<toolName>'
// 例:mcp__github__create_issue

// 权限规则匹配
getToolNameForPermissionCheck() → 'mcp__github__create_issue'

8.2 MCP 服务器加载

scss 复制代码
loadPluginMcpServers()
  │
  ├── 从插件 manifest 加载      → mcpServers 字段
  ├── 从 .mcp.json 加载        → 项目级配置
  ├── 从 MCPB/DXT manifest 加载 → 扩展包
  ├── 冲突解决                  → 最后加载的覆盖
  └── Schema 验证               → McpServerConfigSchema()

8.3 MCP 作为 Server(SDK 模式)

arduino 复制代码
// src/entrypoints/mcp.ts
// Claude Code 本身也可以作为 MCP Server 暴露工具
const server = new Server({ capabilities: { tools: {} } })

// ListTools → 暴露所有内置工具的 schema
// CallTool → 通过 tool.call() 执行

九、Subagent 系统:分治的智慧

达芬奇之问:一个 Agent 不够用怎么办?

答:分治法。主 Agent 可以派生子 Agent,每个子 Agent 有自己的上下文窗口、权限范围和隔离级别。

9.1 子代理类型

go 复制代码
// src/tools/AgentTool/AgentTool.tsx
input: {
  subagent_type?: string     // 指定代理类型(general-purpose, code-reviewer, ...)
  isolation?: 'worktree'     // Git worktree 隔离
  mode?: PermissionMode      // 子代理权限模式
  run_in_background?: boolean // 后台运行
}

output:
  | { type: 'completed', result }          // 同步完成
  | { type: 'async_launched', taskId }     // 后台运行
  | { type: 'teammate_spawned', agentId }  // 多代理团队成员
  | { type: 'remote_launched', sessionId } // 远程 CCR 任务

9.2 子代理上下文传递

  • Forked Agent:克隆父上下文 + 缓存(Session Memory 用)
  • Fresh Agent:零上下文,需要完整 briefing(prompt 必须自包含)
  • Worktree Agent:隔离的 Git 副本,独立工作目录

十、构建 Harness Agent 的最佳实践

从 Claude Code 的 1382 个源文件中提炼出的核心模式

10.1 架构原则

原则 Claude Code 实现 你该怎么做
Async Generator Loop query()AsyncGenerator<Event, Terminal> 用 async generator 作为 Agent Loop 的核心抽象
Streaming-First StreamingToolExecutor 边流边执行 模型输出与工具执行并行化
Layered Permission Rules → Hooks → Classifier → User 构建渐进式信任模型
Graceful Degradation 5 级压缩 + 多路径重试 永远不在可恢复错误上崩溃
Hook-Driven Extension 15+ 事件点 × 4 种 Hook 类型 用 Hook 实现行为扩展而非代码修改
Protocol over Implementation MCP 集成 核心是协议,工具是可插拔的
DI for Testability QueryDeps 注入 用依赖注入隔离外部依赖

10.2 关键设计模式

Pattern 1: Async Generator as Agent Loop
scss 复制代码
async function* agentLoop(params): AsyncGenerator<Event, Result> {
  let state = initState(params)
  while (true) {
    const response = yield* callModel(state)
    if (!response.hasToolCalls) return finalResult(state)
    const toolResults = yield* executeTools(response.toolCalls)
    state = updateState(state, response, toolResults)
  }
}

// 消费者
for await (const event of agentLoop(params)) {
  renderToUI(event)  // 实时渲染
}
Pattern 2: Tool as First-Class Object
php 复制代码
interface Tool<I, O> {
  // 声明 + 执行 + 元数据 + 渲染 四位一体
  schema: ZodSchema<I>          // 声明
  call(input: I): Promise<O>    // 执行
  isConcurrencySafe: boolean    // 元数据
  render(output: O): ReactNode  // 渲染
}
Pattern 3: Permission Pipeline
javascript 复制代码
async function checkPermission(tool, input): Promise<Decision> {
  // 1. 静态规则(O(1))
  const ruleResult = checkRules(tool, input)
  if (ruleResult) return ruleResult
  
  // 2. Hook 决策(O(hook_count))
  const hookResult = await runHooks('PreToolUse', tool, input)
  if (hookResult.decision) return hookResult.decision
  
  // 3. 工具自检(O(1))
  const toolResult = await tool.checkPermissions(input)
  if (toolResult.behavior !== 'ask') return toolResult
  
  // 4. 用户交互(阻塞)
  return await askUser(tool, input)
}
Pattern 4: Tiered Context Compression
javascript 复制代码
async function manageContext(messages, tokenCount) {
  if (tokenCount < BUDGET) return messages           // 不压缩
  
  const snipped = snipOldest(messages)              // Level 1
  if (estimateTokens(snipped) < BUDGET) return snipped
  
  const collapsed = await collapse(snipped)          // Level 2
  if (estimateTokens(collapsed) < BUDGET) return collapsed
  
  return await summarize(collapsed)                  // Level 3
}
Pattern 5: Streaming Tool Execution
kotlin 复制代码
class StreamingToolExecutor {
  // 模型还在输出时就开始执行工具
  addTool(block) {
    this.queue.push(block)
    this.tryExecuteNext()  // 立即尝试
  }
  
  private tryExecuteNext() {
    const next = this.queue.find(t => this.canExecute(t))
    if (next) {
      next.status = 'executing'
      next.promise = this.executeTool(next)
    }
  }
}

10.3 从零构建 Harness Agent 的路线图

yaml 复制代码
Phase 1: 最小可行 Agent
├── 实现 async generator agent loop
├── 实现 3 个基本工具(Read, Write, Bash)
├── 实现简单的权限检查(全部询问)
├── 连接 Claude API(流式)
└── 实现基本 REPL

Phase 2: 生产级能力
├── 添加流式工具执行
├── 实现工具并发控制
├── 添加权限规则系统
├── 实现基本上下文压缩
├── 添加重试和错误恢复
└── 实现工具结果持久化

Phase 3: 可扩展架构
├── 实现 Hook 系统(PreToolUse, PostToolUse)
├── 添加 MCP 客户端支持
├── 实现子代理系统
├── 添加 Session Memory
├── 实现多级上下文管理
└── 添加遥测和分析

Phase 4: 企业级特性
├── 多提供商支持(Bedrock, Vertex, Azure)
├── 企业权限策略
├── 团队协作特性
├── 远程执行环境
└── 审计和合规

十一、关键源文件索引

子系统 关键文件 行数 职责
Agent Loop src/query.ts 1729 主循环:上下文管理 → API 调用 → 工具执行 → 状态更新
Tool 定义 src/Tool.ts 695 Tool 接口、ToolUseContext、ToolResult
Tool 注册 src/tools.ts ~200 工具全集、过滤、池组装
Tool 执行 src/services/tools/toolExecution.ts 1800+ 完整执行管线
流式执行器 src/services/tools/StreamingToolExecutor.ts ~300 并发控制、边流边执行
工具编排 src/services/tools/toolOrchestration.ts ~200 批次划分、并行/串行执行
API 客户端 src/services/api/client.ts ~300 多提供商、认证、代理
API 调用 src/services/api/claude.ts 3237+ 消息构建、系统提示、流式调用
重试逻辑 src/services/api/withRetry.ts 822 指数退避、错误分类、模型降级
权限类型 src/types/permissions.ts ~300 权限模式、规则、决策类型
权限逻辑 src/utils/permissions/permissions.ts ~300 规则匹配、决策逻辑
Hook 类型 src/types/hooks.ts ~290 Hook 事件、回调、结果类型
Hook Schema src/schemas/hooks.ts ~213 Hook 配置 schema(command/prompt/http/agent)
Hook 执行 src/utils/hooks.ts 3500+ 核心执行引擎
上下文压缩 src/services/compact/compact.ts ~150 客户端压缩策略
微压缩 src/services/compact/apiMicrocompact.ts ~150 API 级缓存编辑
消息处理 src/utils/messages.ts 5400+ 标准化、配对、合并
MCP 集成 src/services/mcp/ ~200 MCP 客户端、类型、字符串工具
状态管理 src/state/AppStateStore.ts ~200 全局状态树
会话启动 src/utils/sessionStart.ts ~200 Hook 加载、会话初始化
配置加载 src/utils/config.ts ~250 项目/全局配置

最后的达芬奇之问:Claude Code 最深层的设计直觉是什么?

答:Agent 是一个有限状态机在无限可能性空间中的导航器。 它的每一个设计决策------async generator 的流式控制、分层权限的渐进信任、五级压缩的记忆管理、Hook 系统的行为扩展------都在回答同一个问题:如何让一个有限的系统在无限的可能性中做出可靠的选择。

这不是工程问题,这是一个哲学问题的工程解。

相关推荐
阿维的博客日记2 小时前
了解哪些其他的 Agent 设计范式?
agent
霪霖笙箫2 小时前
「JS全栈AI Agent学习」六、当AI遇到矛盾,该自己决定还是问你?—— Human-in-the-Loop
前端·面试·agent
Old Uncle Tom3 小时前
Claude Code 记忆系统架构分析
人工智能·ai·系统架构·agent
假如梵高是飞行员3 小时前
RAG技术近三年工程实践进化综述
llm·agent
HIT_Weston12 小时前
45、【Agent】【OpenCode】本地代理分析(请求&接收回调)
人工智能·agent·opencode
鬼先生_sir14 小时前
Spring AI Alibaba 1.1.2.2 完整知识点库
人工智能·ai·agent·源码解析·springai
是小蟹呀^14 小时前
【总结】LangChain中工具的使用
python·langchain·agent·tool
是小蟹呀^16 小时前
【总结】提示词工程
python·llm·prompt·agent
进击的野人19 小时前
MCP协议:让AI应用像插USB一样连接外部世界
人工智能·agent·mcp