第 16 章:后台智能框架(forked Agent 模式)

真正自主的系统,不只在被问到时才思考——它在你看向别处时也在学习。


16.1 核心问题

第 15 章的 Coding Agent 虽然完整,但它有一个根本局限:它只在用户主动提问时才运作。用户输入 → Agent 响应 → 等待下一次输入。Agent 本身不会主动做任何事。

但现实中,有很多有价值的事情可以"顺手"完成:

用户刚问完"解释一下认证流程"
  ↓
对话历史里出现了新的架构知识
  ↓  ← 这里可以做什么?
等待下一条输入

用户离开 5 分钟回来
  ↓  ← 这里可以做什么?
"欢迎回来,您在继续实现登录功能..."

Agent 连续处理了 10 个会话后
  ↓  ← 这里可以做什么?
记忆文件里出现了大量重复条目

Claude Code 对这三个问题的回答,都用同一个设计模式解决:forked Agent


16.2 原理讲解

16.2.1 forked Agent 模式

forked Agent 是一种设计模式,让主 Agent 在完成一轮工作后,"悄悄"启动一个与自己共享上下文的子 Agent 去做后台工作:

主 Agent 完成一轮(stop_reason = "end_turn")
    │
    ├─ 响应展示给用户
    │
    └─ [同时/之后] fork 子 Agent(异步,不阻塞用户输入)
            │
            ├─ 共享父 Agent 的 system prompt + messages(prompt cache 命中)
            ├─ 接收一条新的"任务指令"消息
            └─ 独立执行(写文件、调 API、整理记忆...)

关键特性:

  • 不阻塞:子 Agent 在后台运行,用户可以立即继续输入
  • Cache 共享:子 Agent 的首次 API 请求和父 Agent 使用完全相同的 system prompt + messages 前缀,命中父 Agent 的 prompt cache,接近零额外成本
  • 结果不回传:子 Agent 通过写文件或修改状态影响系统,不直接在对话中显示结果

16.2.2 CacheSafeParams:Cache 共享的关键

Anthropic API 的 prompt cache 键由以下五部分组成:

cache_key = hash(system_prompt + tools + model + messages_prefix + thinking_config)

forkedAgent.ts 定义了 CacheSafeParams,它精确携带这五个部分:

// src/utils/forkedAgent.ts
export type CacheSafeParams = {
  systemPrompt: SystemPrompt       // 与父 Agent 完全相同的 system prompt
  userContext: { [k: string]: string }
  systemContext: { [k: string]: string }
  toolUseContext: ToolUseContext    // 含 tools + model + thinkingConfig
  forkContextMessages: Message[]   // 父 Agent 的完整消息历史(作为前缀)
}

每轮结束后,saveCacheSafeParams() 保存当前参数到模块级变量。子 Agent 通过 getLastCacheSafeParams() 取出,确保参数与父 Agent 一致。

16.2.3 触发机制:postSamplingHooks

src/utils/hooks/postSamplingHooks.ts 提供了一个事件系统:在主 Agent 每轮采样完成后调用所有注册的 Hook:

// 注册 Hook(在各模块的 init 函数中调用)
registerPostSamplingHook(async (ctx: REPLHookContext) => {
  // ctx.messages = 完整对话历史
  // ctx.systemPrompt = 当前 system prompt
  // ctx.toolUseContext = 含工具列表和模型
  // ... 在这里检测条件,决定是否 fork
})

// 每轮采样后由 QueryEngine 调用
executePostSamplingHooks(messages, systemPrompt, ...)

16.2.4 四个实际应用

Claude Code 用 forked Agent 模式实现了四个后台智能系统:

① extractMemories(记忆提取)

  • 文件src/services/extractMemories/extractMemories.ts
  • 触发:每轮 stop_reason = "end_turn"(非工具调用轮)
  • 条件门:新增了 ≥ N 条消息 + 主 Agent 自身没有写过记忆
  • 任务:fork 子 Agent,分析最近 N 条消息,找出值得长期保留的事实,写入 ~/.claude/projects/<path>/memory/ 下的 Markdown 文件
  • 工具限制:仅允许 ReadGlobGrep、只读 Bash、Edit/Write(仅限记忆目录)
// 核心提示词策略(buildExtractAutoOnlyPrompt):
// "分析最近 ~N 条消息,找出值得持久化的事实,
//  第 1 轮:并行 Read 所有可能要更新的文件
//  第 2 轮:并行 Write/Edit 所有需要更改的文件
//  不要跑去验证源码,直接处理消息里的信息。"

② autoDream(记忆整合)

  • 文件src/services/autoDream/autoDream.ts
  • 触发:时间门(≥24 小时)+ 会话数门(≥5 个新会话)
  • 任务:fork 子 Agent,整合最近 N 个会话的所有记忆,去重、归类后重写
  • Lock 机制consolidationLock.ts 用文件锁防止多个进程同时整合
门控逻辑(cheapest-first):
  1. stat(lock_file).mtime → 时间是否 ≥ minHours      ← 最快,一次 stat()
  2. readdir(sessions/) → 新会话数 ≥ minSessions       ← 次快,一次 readdir
  3. try_acquire_lock() → 无其他进程在整合             ← 需要 atomic write

③ Magic Docs(文档自动更新)

  • 文件src/services/MagicDocs/magicDocs.ts
  • 触发FileReadTool 读取到含 # MAGIC DOC: [title] 首行的文件时注册监听;每轮 postSamplingHook 检查已注册的文件
  • 任务:fork 子 Agent,根据当前对话上下文,用 FileEditTool 更新文档内容
<!-- ARCHITECTURE.md 的首行 -->
# MAGIC DOC: System Architecture
_Keep this document up to date with key architectural decisions._

<!-- Agent 读取此文件后,每轮结束自动检查是否需要更新 -->

④ Speculation(推测性预执行)

  • 文件src/services/PromptSuggestion/speculation.ts
  • 触发:主 Agent 响应完毕,用户开始输入时(检测到键盘活动)
  • 任务:预测用户下一条指令,提前执行只读工具(Read/Glob/Grep/LSP)
  • 结果合并:若预测命中,把工具结果注入下一轮,节省 TTFT(首 Token 时间)
  • 安全限制:只允许只读工具,禁止任何写操作
MAX_SPECULATION_TURNS = 20
SAFE_READ_ONLY_TOOLS = { 'Read', 'Glob', 'Grep', 'ToolSearch', 'LSP', ... }
WRITE_TOOLS = { 'Edit', 'Write', 'NotebookEdit' }  ← 禁止

16.2.5 Away Summary(返回摘要)

Away Summary 不用 forked Agent,而是直接调用小模型:

// src/services/awaySummary.ts
const recent = messages.slice(-30)  // 最近 30 条消息
recent.push(createUserMessage({ content: buildAwaySummaryPrompt(memory) }))

await queryModelWithoutStreaming({
  model: getSmallFastModel(),   // 小模型,省 Token
  skipCacheWrite: true,         // 一次性查询,不写缓存
  // ...
})

触发条件:用户离开(最后活跃时间 > 阈值)再回来时,在 UI 上显示"您离开时正在做:..."的提示卡片。

16.2.6 为什么不用普通的 setTimeout?

后台任务用 forked Agent 而不是简单的 setTimeout + LLM 调用,原因是 prompt cache

普通方式(无 cache 复用):
  重新构建 system prompt  ─────────────────────────────────── 全部 Token 入 cache
  重新上传完整消息历史   ─────────────────────────────────── 全部 Token 入 cache
  API 调用开销 ≈ 一次完整对话                                ← 昂贵

forked Agent 方式(共享 parent cache):
  复用父 Agent 的 system prompt + messages prefix
  API 调用开销 ≈ 只有新增的任务指令(1 条消息)              ← 接近免费

这使得后台任务的实际 API 成本极低,可以在每轮结束后都触发而不心疼。


16.3 源码索引

文件 关键函数/类型 作用
src/utils/forkedAgent.ts CacheSafeParamsrunForkedAgent()saveCacheSafeParams()getLastCacheSafeParams() forked Agent 核心框架
src/utils/hooks/postSamplingHooks.ts registerPostSamplingHook()executePostSamplingHooks()REPLHookContext 采样后 Hook 注册与执行
src/services/extractMemories/extractMemories.ts initExtractMemories()createAutoMemCanUseTool() 每轮记忆提取
src/services/extractMemories/prompts.ts buildExtractAutoOnlyPrompt() 记忆提取子 Agent 的提示词
src/services/autoDream/autoDream.ts initAutoDream() 定期记忆整合(时间+会话双门控)
src/services/autoDream/consolidationLock.ts tryAcquireConsolidationLock()readLastConsolidatedAt() 文件锁防止并发整合
src/services/MagicDocs/magicDocs.ts detectMagicDocHeader()initMagicDocs() Magic Doc 检测与自动更新
src/services/PromptSuggestion/speculation.ts runSpeculation()SAFE_READ_ONLY_TOOLS 推测性预执行(只读工具)
src/services/awaySummary.ts generateAwaySummary() 返回摘要(小模型,无 fork)

16.4 最小化产出物

代码骨架位于 ../chapters/16/src/,参考实现位于 ../chapters/16/solution/前置条件:需要 ANTHROPIC_API_KEY 环境变量(npm start 需要)。

本章要实现什么

../chapters/16/src/forked.ts 中完成 forked Agent 框架。

接口规范(已提供,不要修改):

export interface CacheSafeParams {
  systemPrompt: string
  messages: Anthropic.MessageParam[]
  model: string
}

export type PostSamplingHook = (params: CacheSafeParams) => Promise<void>

export function registerPostSamplingHook(hook: PostSamplingHook): void
// 将 hook 追加到内部数组

export async function executePostSamplingHooks(params: CacheSafeParams): Promise<void>
// 并发执行所有 Hook,捕获每个 Hook 的错误

export async function runForkedAgent(
  client: Anthropic,
  cacheSafeParams: CacheSafeParams,
  taskMessage: string,
  label: string,
): Promise<string>
// 运行 forked 子 Agent,共享父 Agent 的 prompt cache

你需要实现

  1. registerPostSamplingHook():追加到 hooks 数组
  2. executePostSamplingHooks()Promise.all 并发执行,每个 hook 用 .catch() 捕获错误
  3. runForkedAgent()
    • 构造 messages = [...cacheSafeParams.messages, { role: 'user', content: taskMessage }]
    • 调用 client.messages.create(使用父 Agent 的 systemPrompt 和 model)
    • 提取文本内容并返回

关键约束

  • executePostSamplingHooks() 中单个 Hook 失败不能影响其他 Hook
  • runForkedAgent() 使用与父 Agent 完全相同的 systemPrompt + messages 前缀,这是命中 prompt cache 的关键

验收

cd docs/chapters/16
npm install
npm test

卡住时查看 ../chapters/16/solution/forked.ts

16.5 本章小结

forked Agent 的本质

forked Agent 不是一个新的架构概念,而是对两个已知问题的组合解:

  1. 后台任务:用异步不阻塞的方式触发附加工作
  2. Cache 共享:通过复用父 Agent 的消息前缀,让附加工作接近免费
设计约束                      解决方案
──────────────────────────────────────────────
不能阻塞用户输入            → void Promise(fire and forget)
后台 LLM 调用成本高         → 共享 prompt cache(CacheSafeParams)
后台任务可能并发            → 文件锁(consolidationLock)
后台任务需要访问工具        → 受限的 canUseTool(仅只读 + 记忆目录)
结果不直接显示给用户        → 写文件(MEMORY.md / Magic Doc)

四个系统的对比

系统 触发时机 频率 成本 结果
extractMemories 每轮 end_turn 极低(cache 共享) MEMORY.md 条目
autoDream 24h + 5 会话 极低 中(整合多文件) MEMORY.md 重写
Magic Docs 每轮(有追踪文件时) 低(cache 共享) 文档文件更新
Speculation 用户开始输入时 低(只读工具) 工具结果预加载
Away Summary 用户返回时 低(小模型) UI 提示卡片

进化路径

第 7 章   Agentic Loop ────────────────────────── 主动响应用户请求
     +
第 13 章  AgentTool ──────────────────────────── 并行执行子任务
     +
第 16 章  forked Agent ───────────────────────── 后台持续学习与优化
                                                        ↓
                                        真正自主的 Coding Agent:
                                        - 执行任务(第 7 章)
                                        - 并行分解(第 13 章)
                                        - 持续进化(第 16 章)

这是整个系列的终点:从"能响应"到"会学习",从"工具调用"到"持续智能"。


验收命令

cd chapters/16
npm install

# 启动 Agent
npx tsx src/main.ts

# 场景 A:记忆提取验收
# 输入:"我们项目用 React 18 + TypeScript 5,后端 FastAPI,数据库 PostgreSQL + Redis"
# 等待 Agent 回复后,查看记忆文件:
cat ~/.mini-agent/memory/MEMORY.md
# 预期:自动写入了技术选型相关的记忆条目

# 再次输入:"我们的测试框架用 Vitest,CI/CD 用 GitHub Actions"
# 再次查看记忆(应追加新条目):
cat ~/.mini-agent/memory/MEMORY.md

# 场景 B:Magic Doc 验收
# 输入(第一次):"请读取 src/ARCHITECTURE.md 文件"
# → Agent 读取后,后台检测到 MAGIC DOC 头,注册追踪

# 输入:"我们采用六边形架构,核心域不依赖任何框架,通过 Adapter 对接外部系统"
# 等待 Agent 回复后,查看文档是否被自动更新:
cat src/ARCHITECTURE.md
# 预期:ARCHITECTURE.md 内容已根据对话自动更新

# 重启验证记忆持久化
# 输入 /exit 退出,重新运行:
npx tsx src/main.ts
# 预期:启动时显示"已有记忆",Agent 在回答中体现对之前技术选型的记忆

进阶选修章结语

至此,整个系列的 16 章全部完成。

你从一个空白的 main.ts 出发,经历了进程 I/O、终端 UI、CLI 路由、工具引擎、流式 LLM、对话管理、Agentic Loop、权限系统、上下文注入、记忆持久化、会话存档、斜杠命令、多 Agent 调度、MCP 插件、完整集成,最终到达后台持续学习。

这不只是 Claude Code 的源码解读——这是一幅现代 AI Agent 系统的完整架构图。