第 16 章:后台智能框架(forked Agent 模式)
# 第 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`,它精确携带这五个部分:
```typescript
// 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:
```typescript
// 注册 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 文件
- **工具限制**:仅允许 `Read`、`Glob`、`Grep`、只读 Bash、`Edit`/`Write`(仅限记忆目录)
```typescript
// 核心提示词策略(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` 更新文档内容
```markdown
<!-- 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,而是直接调用小模型:
```typescript
// 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` | `CacheSafeParams`、`runForkedAgent()`、`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 框架。
**接口规范**(已提供,不要修改):
```typescript
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 的关键
### 验收
```bash
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 章)
```
这是整个系列的终点:从"能响应"到"会学习",从"工具调用"到"持续智能"。
---
## 验收命令
```bash
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 系统的完整架构图。