应用到自研 Agent (AI SDK)
Agent 系统作者(AI SDK 用户):用 AI SDK 构建 agent 时,前 8 章的原则落到 8 个嵌入点的哪一行。本章以 Zapvol 为具体 case 做现状评估 + 演进方向标注。
本章定位
前 8 章讲 Claude Code 怎么做的。本章的目标不一样:
- 对外部读者:指导用 AI SDK 构建 agent 时,CC 的原则落到哪个钩子的哪一行
- 对 Zapvol 团队:以我们自己作为具体 case,现状评估 + 演进方向
两个目标通过同一套结构服务——每个嵌入点三段式:
- 最佳实践(AI SDK + CC 教我们的)
- Zapvol 现状(过 / 半过 / 缺,引源码事实)
- 演进方向(具体落地项 + 优先级)
本章不是”看 Zapvol 做得多好”的展示稿——是一份承认差距、指向下一步的路线图。
阅读假设:AI SDK v6(ai@^6),streamText / generateText / prepareStep / experimental_onToolCallFinish / stopWhen 熟悉。
先拆:什么该抄,什么该扔
不是所有 CC 模式都值得搬。抄错边界会陷入”我也搞了个 git worktree”的机制级模仿坑。
| Claude Code 的东西 | 可迁移性 | 原因 |
|---|---|---|
| Worktree / CCR / Bash sandbox | 只对 coding agent | 机制依赖 git / Anthropic SaaS / POSIX |
| CLAUDE.md 目录 walk | 部分 | 抽象为”分层 prompt 注入点”可迁移 |
| Git status / File 工具 | 只对 coding | 工具选型 |
| Async generator 主循环 | 通用 | streaming UI 基线 |
| Terminal reason 枚举 | 通用 | 不同终止走不同 UX |
| System prompt 静态/动态边界 | 通用 | cache 命中率硬红线 |
| 压缩级联(便宜→贵) | 通用原则 | 层数 N 按需 |
| MEMORY.md 索引模式 | 通用 | 无界增长的唯一 scale 路径 |
| 记忆多维分类 | 通用 | 谁写 × 存什么 × 多久 |
| Subagent 上下文防火墙 | 通用 | 含 ReAct 循环的 agent 都该有 |
| Hook vs prompt 分层 | 通用 | 过程级需求不能靠 prompt |
| Tombstone streaming 撤回 | 通用 | 任何流式 UI |
| Circuit breaker | 通用 | 任何含重试的系统 |
| Data-driven prompt 工程 | 通用(元原则) | 抄文化,不抄数字 |
核心:机制不通用,原则通用。worktree 的机制对非 coding agent 无用;但它扮演的”可弃置隔离工作区”这个角色对任何需要隔离试错的 agent 都成立。
AI SDK 生命周期的 8 个嵌入点
AI SDK v6 的 streamText 把循环藏在内部,对外只给几个钩子。所有 CC 模式都得挤进这几个点:
| 点 | 位置 | 职能 |
|---|---|---|
| A | call-site 组装 | system / messages / tools / providerOptions |
| B | prepareStep | 每步输入预处理(压缩 / 预算 / 缓存布点) |
| C | streaming 消费 | 消费 stream events |
| D | tool.execute | 工具运行处(权限 / hook / 沙箱) |
| E | experimental_onToolCallFinish | tool 结束后(microcompact 入口) |
| F | stopWhen | 继续还是停 |
| G | onFinish | 聚合 usage / 持久化 messages |
| H | post-call | session snapshot / 后台任务 |
下面每点三段式展开。
点 A · call-site 组装
最佳实践
system字面量稳定:不要把Date.now()/userName/gitStatus拼进 system string——每变一个字节整个 prompt cache 失效- 动态 context 走 messages 层:CC 把 currentDate 放 auto memory,不是 system
providerOptions.cacheControl显式布 breakpoint:AI SDK 不会替你加,必须手动标- Tool description 是 prompt 工程:每条都按”教 agent 下一步怎么走”写(见 CC Edit 工具)
maxSteps有上限但别太小:30-50 是日常合理范围
Zapvol 现状
半过 · 一个潜在 cache 风险需要 audit
- 过 · tool description 普遍按教程式写(看
filesystem.compact.ts、browser.tool.ts等),抄了 CC 的风格 - 过 · providerOptions 显式 cache control:
applyCacheControl(compacted, model, { extraBreakpointAt })每步调一次 - 半过 ·
appendSystemContext(systemPrompt, systemContext)(agent-round.ts)把动态systemContext拼到 system 末尾——systemContext如果包含任何每轮变化的字段(currentDate / gitStatus / 时间戳),整个system每轮 hash 都变,前面所有 cache breakpoint 失效。这是.claude/design/compaction-redesign.md未列但值得审查的点 - 半过 ·
BUDGET_RATIO = 0.2(compaction/config.ts:15)vs 文档说的 0.8 vs CC 的 ~93%——正在审查(redesign §2),不是 Point A 的问题但影响 MVP 蓝图里的阈值讨论
演进方向
P0 · 审查 appendSystemContext 的内容组成
加日志验证 cache 稳定性:
// 在 applyCacheControl 前加
log.info("cache.system_hash", {
hash: createHash("sha256").update(fullSystemPrompt).digest("hex").slice(0, 16),
step: steps.length,
})
跑一个 10 步 task 看 hash:
- 全程不变 → 过 当前设计没问题
- 每步变化 → 缺 必须重构:把动态部分(currentDate / ctx 信息)从
systemContext移到 messages 的头部 user message
P1 · 把 systemContext 里的内容按稳定性分层
参考 CC 的 getSystemContext() + getUserContext() 分层(见 系统提示词组装)。每层 memoize,session 启动时算一次。
点 B · prepareStep(承载大部分压缩逻辑的单点)
最佳实践
- 按步分化:step 0 可以做重活(boundary filter + autocompact);后续步只做 microcompact 够了——每步都跑 autocompact 会爆成本
- Partial 返回:只写
{ messages },不写{ messages, system, tools, toolChoice }——未改字段不要写出来,避免意外 cache 破坏 - AbortSignal 手动传进压缩 LLM:
prepareStep签名没有 abortSignal,要从 ctx 读 - Blocking check 在开头:撞硬顶时 return synthetic error 让模型优雅退出,不让 API 抛 413
- Hysteresis(滞后带):超 budget ≤ 5% 时不触发 activateMoreCrCandidates——保 cache 稳定性
Zapvol 现状
半过 · 功能齐但缺几个精细控制
- 过 ·
stepCompactor.apply统一调用(agent-round.ts:183)——每步都走完整压缩流水线 - 过 · abortSignal 传播:
context.abortController.signal从 context 读,传进 autocompact / summarizer 的 LLM 调用 - 过 · cache breakpoint 显式布:
applyCacheControl+extraBreakpointAt = compactedPrefixEnd - 缺 · 没有 step 0 vs step 1+ 分化:
stepCompactor.apply每步都跑同一套——可能在不需要的续步上做了重活 - 缺 · 没有 hysteresis:超 budget 1 token 就触发 activateMoreCrCandidates(redesign §P2 #6 明确列为待办)
- 缺 · 没有 blocking synthesis fallback:撞 413 才触发 reactive(但 reactive 本身也缺,见下文)
- 缺 · 没有 413 reactive handler:redesign §P1 #4 明确列为 gap
演进方向
P0 · 添加 step 0 vs step 1+ 分化
当前 stepCompactor 对首步和续步无差别处理。建议:
// packages/backend/src/agent/compaction/step-compactor.ts 内
export async function apply(
messages: UIMessage[],
options: StepCompactorOptions,
): Promise<CompactResult> {
const isFirstStep = options.stepNumber === 0 // 需从 prepareStep 传入
if (isFirstStep) {
// 首步:boundary 已 prepareInitialMessages 处理过;这里只检查是否需要 autocompact
return await maybeAutocompact(messages, options)
}
// 续步:优先读 precompact cache,避免再次跑 LLM summarize
const truncated = await truncateOldToolResults(messages, options)
if (getLastStepTotalTokens(options) > budget) {
return await maybeAutocompact(truncated, options) // 仍超才跑 autocompact
}
return { messages: truncated, compactedPrefixEnd: ... }
}
收益:续步省一次 LLM 调用(当 microcompact 能压到预算内时)。
P1 · Hysteresis + blocking synthesis fallback
参考 redesign §P2 #6:超 budget ≤ 5% 不 activateMoreCrCandidates。
参考 CC 的 blocking check(见 compaction 章节):撞硬顶时:
if (currentTokens >= HARD_CEILING) {
yield synthetic error message
return { reason: 'blocking_limit' }
}
这是一个用 synthetic message 让模型优雅退出的模式——比 API 抛 413 干净。
P1 · 413 reactive handler
redesign §P1 #4 已列。撞 413 后强制 activateMoreCrCandidates + 重试一次。
点 C · streaming 消费
最佳实践
- Tombstone 撤回:已流出的 chunk 如果因为 streaming fallback 作废,需要显式作废标记——UI 不能从客户端收回字符
- Thinking block signature 保持:跨轮复用时不能动签名字段;序列化 / 反序列化要保持字节对齐
- Usage 按分量记:
inputTokens/outputTokens/cachedInputTokens/cacheCreationInputTokens四个分量分别——cache 命中率是核心可观测指标
Zapvol 现状
半过 · Streaming retraction 缺;thinking signature 部分处理
- 过 · UI message stream 用 AI SDK v6 的
ui-message-stream协议(agent-ui-stream.ts) - 过 · Thinking block 过滤:
round-precompact.ts:249明确过滤掉 reasoning/thinking blocks 给压缩模型——因为 thinking 带原模型 signature,转发给压缩模型会校验失败。这层做得比大多数 agent 好 - 半过 · Thinking block 跨轮复用:没看到显式的 signature 字段保护(序列化时)——存进 DB 再读回来如果 JSON 字段顺序漂,会在下次提交时 API rejected
- 缺 · 没有 tombstone / streaming retraction:UI 端没有”已流出但作废”的显式标记
- 半过 · Usage 记录:
onStepFinish累加但只存totalTokens(agent-round.ts:247)——丢失 cache read / cache creation 两个关键分量
演进方向
P1 · Usage 分量化记录
改 onStepFinish:
onStepFinish: (step) => {
context.lastStepTotalTokens = step.usage.totalTokens // 当前用法
// 新加:
session.recordStepUsage({
input: step.usage.inputTokens,
output: step.usage.outputTokens,
cacheRead: step.usage.cachedInputTokens, // ← 关键
cacheCreate: step.usage.cacheCreationInputTokens, // ← 关键
})
}
不分量记等于 cache 命中率不可观测——任何 cache 优化工作都没法验证效果。
P2 · Streaming retraction 原语
如果 production 碰到过 “streaming fallback 导致 UI 残留文本” 的 bug,加 tombstone:
// agent-ui-stream.ts 加一种 data part
context.writeTransient(DataPartEvent.TOMBSTONE, { messageId, reason: "streaming_fallback" })
前端收到就删对应渲染区域。没碰到可以不做。
P2 · Thinking signature 持久化保护
如果未来要做 resume 且启用 extended thinking——序列化 messages 时要把 providerOptions.anthropic.signature 字段原样存(不 normalize JSON 字段顺序 / 不改 base64 padding)。现在 Zapvol 没到这一步,等做 extended thinking 时再处理。
点 D · tool.execute(权限 + hook 的物理位置)
最佳实践
- 权限检查在 execute 开头:不要在 prepareStep 过滤 tool 列表(模型不知道被拒,只报 schema mismatch)
- PreToolUse hook 等价物:做一个
wrapTool高阶函数,支持updatedInput(修改参数,不只是 allow/deny) - AbortSignal 传到叶子:
fetch/execFile/ DB query 都要接 signal——半截 abort 比没有更糟 - 输出截断 + offload:单工具输出不能吃光 context
- Error text 就是 prompt:教模型下一步做什么
Zapvol 现状
过 · 几乎完整——D 点 Zapvol 做得最好
- 过 · 权限系统完整:
utils/permissions/(按钮级 mode + rule + classifier + hook) - 过 · abortSignal 全链路:所有 tool execute 接 signal,传进 sandbox 的命令执行
- 过 ·
maxResultSizeChars概念存在(TOOL_TRUNCATE_CHARS = 1000) - 过 ·
ServerToolConfig封装:每个 tool 有compact / requiredPermission / renderMessage等扩展字段,编译成 AI SDK tool(正是后面”架构决策”要推荐的模式) - 过 · Error text:多数工具 error 写得像教程(看
browser.tool.ts的 error codes) - 半过 · PreToolUse hook 等价物:有 permission 但没有”hook 修改参数”能力——
updatedInput这条能力暂时没有
演进方向
P2 · 添加 tool wrapper 的 pre/post/error hook
现在权限是 tool-by-tool 手动做的。如果未来要做 “自动加 lint / 自动注入 ctx”之类的需求:
function wrapTool<I, O>(cfg: ServerToolConfig<I, O>, hooks: ToolHooks<I, O>): AiSdkTool {
return toAiSdkTool({
...cfg,
execute: async (input, ctx) => {
const pre = await hooks.preToolUse?.(input, ctx)
if (pre?.decision === "block") return { error: pre.reason }
const actualInput = pre?.updatedInput ?? input
// 跑原 execute ...
},
})
}
这是 redesign 没列的,但 CC 的 PreToolUse hook 有 updatedInput 能力,对 agent 的可扩展性很有价值。优先级不高(当前没 block 需求),但是架构上可以预留。
点 E · experimental_onToolCallFinish(microcompact 入口)
最佳实践
- Race abort:precompact 是独立 LLM 调用,用户 ESC 时必须立即中断
- 幂等:同 toolCallId 重放时短路读缓存
- 用便宜模型:主 agent Opus/Sonnet,precompact Haiku——成本 1/10
- 落盘 + cache:压缩结果 + 原文落盘,供主 agent 需要时
read_file取回
Zapvol 现状
过 · 范本级——Zapvol 的 tool-precompact.ts 几乎完全对齐 CC 的 Tier 1 microcompact
- 过 · Race abort:
raceAbort(compactor(...), abortSignal)显式 - 过 · 幂等:
readCompactCache(toolCallId)先查 - 过 · 落盘 + cache:
offloadToolData落盘 +writeCompactCache缓存结果,file-based 跨步持久 - 过 · per-tool compactor:
ServerToolConfig.compact()每工具自定义压缩策略(比 CC microcompact 的统一占位符更精细) - 半过 ·
PRECOMPACT_TRIGGER_TOKENS = 2500:redesign §P1 #3 建议降到 500-1000——让更多小工具在 finish 时压
演进方向
P1 · 降 PRECOMPACT_TRIGGER_TOKENS
redesign §P1 #3 已明确。2500 → 500-1000。收益:更多工具在 cache 安全时机被压,减少 mid-round prepareStep 的压缩 spike。
对外读者:看 tool-precompact.ts 的设计——这是 microcompact 的生产级范本,值得抄。
点 F · stopWhen
最佳实践
- 组合多个条件:
stepCountIs(N) + hasToolCall('complete') + 自定义 - 提供显式
completetool:让模型”显式声明完成”,避免无用步 - 维护业务层 TerminalReason:AI SDK 的
finishReason颗粒度太粗 - Circuit breaker:连续失败 N 次后停
Zapvol 现状
过 · F 点设计比 CC 还细——stopOnComplete 的 todos-blocking 是亮点
- 过 ·
stopOnComplete()(tools/stop-conditions.ts):不仅检查 complete tool 调用,还检查 todos 是否全 done——没 done 阻止停。这是比 CC 更精细的模式 - 过 ·
stepCountIs(maxSteps)硬上限 - 过 ·
completetool:Zapvol 定义了,在 browser-subagent 等地方被引用 - 半过 · AI SDK
finishReason颗粒度:Zapvol 存的是 “stop / length / tool-calls / error / abort”——不细分blocking_limit/permission_denied_fatal等业务级原因 - 缺 · Circuit breaker 缺:redesign §P2 #7 明确列为 gap——autocompact / precompact 连续失败没有熔断机制
演进方向
P1 · 定义 TerminalReason 枚举 + 在 onFinish 派生
export type TerminalReason =
| "done"
| "completed" // 调了 complete tool
| "aborted" // ctx.abortController.signal.aborted
| "max_steps" // stepCountIs 触发
| "blocking_limit" // 硬顶退出(需配合 Point B 的 synthesis fallback)
| "todos_incomplete" // complete 调了但 todos 没 done(当前 stopOnComplete 已实现但没 surface)
| "error"
// agent-round.ts onFinish 里派生
const terminalReason = deriveTerminalReason(result, context)
await session.setTerminalReason(terminalReason)
收益:UI 可以按 reason 显示不同提示;telemetry 可以按 reason 聚合分析。
P1 · Circuit breaker for autocompact + precompact
redesign §P2 #7 已列。consecutiveFailures >= 3 后停止尝试——不然一个 context 不可恢复的 session 会浪费大量 API 调用(CC 的教训:每天 250K 次 API 调用被浪费,见 compaction 章节)。
点 G · onFinish
最佳实践
result.response.messages必须持久化:不持久化等于 resume 不了- 事务原子:
messages和checkpoint必须同事务更新 - 多步 usage 累加:不要只记最后一步
- finishReason 分类:派生业务层 TerminalReason
Zapvol 现状
过 · 持久化做得严谨
- 过 ·
captureCompactionCheckpoint:onFinish 里把compactionCheckpoint写入 DB(agent-round.ts:266+) - 过 · Round summarizer 触发:onFinish 里异步生成
RoundSummary存回 DB(供下轮 plan-phase 用) - 过 · 多步 usage 聚合:
stepUsages累加(agent-round.ts:241+) - 半过 · messages / checkpoint 是否同事务写 没具体确认——从代码看像是分开写(需要审查)
- 半过 · TerminalReason 派生:现在记录的是 AI SDK 原始 finishReason,缺业务层映射(见 F 的演进方向)
演进方向
P1 · 审查 messages + checkpoint 的事务原子性
确认下面这段是不是在同一 DB 事务里:
await db.session.appendMessages(session.id, newMessages)
await db.session.updateCheckpoint(session.id, checkpoint)
如果不是——一个成功一个失败时,下次 resume 会读到不一致的状态(消息多了但 checkpoint 指向旧位置 → 重压缩浪费 / 或消息少了但 checkpoint 指向未存在的 index → 崩)。
P2 · 统一 Terminal reason 存储
见 F 的演进方向。
点 H · post-call(后台任务)
最佳实践
- Memory extraction 异步:提取长期记忆条目不 await,不拖尾用户
- Resume pre-compact:退出 session 时在后台预算 resume 摘要——下次 resume 即时
- PostCompact hook:外部可扩展点
Zapvol 现状
半过 · Memory extraction 有;resume pre-compact 缺
- 过 · Memory extraction:
memory/memory-extraction.ts完整实现——onFinish 里 fire-and-forget,用 Haiku 提取记忆条目,互斥于用户手动save_memory - 过 · Round summarizer:onFinish 里异步生成 RoundSummary(已压缩单轮),存回 DB 供下轮复用——这其实是轻量级的 resume pre-compact 变体
- 缺 · 完整 resume pre-compact 缺:redesign §3.3 明写 “我们需要这层” 在对齐 CC 的矩阵里。Zapvol 现在 resume 时,前几轮 summaries 已经存了(round-level),但跨整个 session 的统合摘要没有
- 缺 · PostCompact hook 缺:没有”压缩完成”的扩展点
演进方向
P1 · 加 resume pre-compact
CC 的模式:session 退出时启动后台 job,把整个历史压缩到一个全局摘要存好,下次 resume 直接读。
Zapvol 已有 round-level RoundSummary——基础件齐。缺的是session-level 统合摘要:
// agent-round.ts onFinish 末尾
if (terminalReason === "done" || terminalReason === "completed") {
void generateSessionResumeSummary(session.id, context) // 不 await
}
// 下次 A 点加载时
const summary = await session.loadResumeSummary()
if (summary) {
// 用 summary 代替 + 最近 N 轮 raw messages
} else {
// fall back 到当前逻辑:load all messages
}
P2 · PostCompact hook surface
对 Zapvol 是对外扩展性准备——如果未来做 marketplace / plugin 体系,让第三方在压缩完成时跑自定义逻辑(例如写告警、触发 workflow)。当前没这个需求可以不做。
架构决策(不是 trap,是选择题)
以下两个决策不是”点 A/B/…/H”里的战术问题,而是架构级选择。Zapvol 已经选对了,这里留作外部读者参考。
决策 1:CLAUDE.md 的抽象是”分层 prompt 注入点”
CC 的 CLAUDE.md 依赖 filesystem walk。通用 agent 没这个机制,但角色可以迁移:
分层 prompt 注入点 = {
Managed: 企业 / policy 强制规则(最高权威)
Project: 项目 / account 级规则(team-shared)
Local: 用户在此 project/account 的私有规则(not shared)
User: 用户全局偏好
Session: 本次会话动态注入
Auto: agent 自己学到的记忆
}
存储介质(DB / YAML / settings.json / markdown)不重要。分层的权威合并逻辑才是 point。
Zapvol 现状:有 ZapvolMemory 系统支持 Project / User / Auto 三层(memory-service.ts),缺 Managed 层——多租户企业场景需要这个(某租户强制”所有 agent 不许 write to production DB”)。演进方向 P2:在现有 memory-service 上加 Managed scope。
决策 2:Tool 要包一层自己的 ServerToolConfig
AI SDK 的 tool 只有 description + inputSchema + execute。CC 的 Tool 有十几个字段。要在中间加一层自己的配置,编译成 AI SDK tool:
type ServerToolConfig<I, O> = {
name: string
description: string
inputSchema: ZodSchema<I>
execute: (input: I, ctx: Ctx) => Promise<O>
// AI SDK 不认识但你内部需要的字段
compact?: (input: I, output: O, hint: CompactHint, ctx: Ctx) => Promise<CompactResult>
requiredPermission?: PermissionDescriptor
maxResultSizeChars?: number
clientRenderer?: (part: ToolUIPart) => ReactNode
}
Zapvol 现状:已经正是这个模式——ServerToolConfig 接口完整(agent/tools/ 下所有工具用)。这是对的。
按规模选复杂度
Zapvol 已经过 MVP,不是”从零开始”。下面是按成熟度扩展 feature 的指南——对外读者有用,对 Zapvol 是”接下来该上哪一档”的参考。
已完成的(基础档)
Zapvol 当前覆盖:
- 过 Point A 基础 + cache control
- 过 Point B 的
stepCompactor.apply统一调用 - 过 Point D 完整(权限 + maxResultSize + ServerToolConfig)
- 过 Point E 完整(precompact 范本级)
- 过 Point F 基础 +
stopOnComplete的 todos-blocking(比 CC 还细) - 过 Point G 基础 + round summarizer
- 过 Point H 的 memory extraction
第二阶段(1-2 周能完成,显著提升)
- Point A 的 system prompt cache audit(P0)
- Point B 的 step 0 / step 1+ 分化(P0)
- Point C 的 usage 分量化记录(P1)
- Point E 的 PRECOMPACT_TRIGGER_TOKENS 降低(P1,redesign §P1 #3)
第三阶段(基础件齐后补能力)
- Point B 的 hysteresis + blocking synthesis fallback(P1)
- Point F 的 TerminalReason 枚举 + circuit breaker(P1,redesign §P2 #7)
- Point H 的 session-level resume pre-compact(P1)
- Point G 的 messages + checkpoint 事务原子审查(P1)
暂不做(除非有真实压力)
- Point A 的企业 Managed scope(P2,多租户才需要)
- Point C 的 tombstone retraction(P2,UI 没碰到问题就不需要)
- Point D 的 pre/post tool hook 扩展(P2,没 block 需求就不急)
- Resume 的完整 PostCompact hook surface(P2,对外扩展体系才需要)
Zapvol 演进路线图(汇总表)
把散在各点的演进项汇总,按优先级排:
P0(2 周内建议先做)
| # | 点 | 改动 | 预期收益 |
|---|---|---|---|
| 1 | A | appendSystemContext cache audit + 动态内容迁到 messages | 保 prompt cache 命中率 |
| 2 | B | stepCompactor 加 “首步 vs 续步” 分化 | 续步省一次 autocompact LLM 调用 |
P1(基础件,3-4 周完成)
| # | 点 | 改动 | 相关 redesign 条目 |
|---|---|---|---|
| 3 | C | Usage 按分量(input/output/cacheRead/cacheCreate)分开记 | — |
| 4 | E | 降 PRECOMPACT_TRIGGER_TOKENS 2500 → 500-1000 | §P1 #3 |
| 5 | F | 定义 TerminalReason 枚举 + 在 onFinish 派生 | — |
| 6 | F | autocompact / precompact 的 circuit breaker | §P2 #7 |
| 7 | G | 审查 messages + checkpoint 的事务原子性 | — |
| 8 | H | Session-level resume pre-compact | §3.3(CC 有我们缺) |
| 9 | B | Hysteresis(超 budget ≤5% 不触发) | §P2 #6 |
| 10 | B | Blocking synthesis fallback + 413 reactive handler | §P1 #4 |
P2(机会档,等有压力再做)
| # | 点 | 改动 | 触发条件 |
|---|---|---|---|
| 11 | A | Managed scope in memory-service | 多租户企业客户 |
| 12 | C | Tombstone / streaming retraction | UI 反馈 streaming fallback 残留 |
| 13 | D | Tool wrapper pre/post/error hooks | 需要项目级自定义拦截 |
| 14 | H | PostCompact hook surface | 对外扩展 / plugin 体系 |
10 项自检清单
扫你的 agent codebase(Zapvol 或外部):
| # | 检查项 | Zapvol 状态 |
|---|---|---|
| 1 | system 字面量稳定?没拼 Date.now() / user context? | 半过 待 audit |
| 2 | messages 显式 providerOptions.cacheControl 布 breakpoint? | 过 |
| 3 | prepareStep 首步 vs 续步分开处理? | 缺 待改 |
| 4 | 每个 tool execute 里有 permission check? | 过 |
| 5 | Tool description 教程式? | 过 |
| 6 | Tool execute 传 abortSignal 到内部所有长耗时操作? | 过 |
| 7 | Tool 输出有大小上限 + 超限截断 + 落盘? | 过 |
| 8 | stopWhen 组合多条件(stepCountIs + hasToolCall('complete') + 自定义)? | 过 |
| 9 | onFinish 持久化 response.messages? | 过 |
| 10 | onFinish 后有异步后台任务(不 await)做 memory extraction / resume 预算? | 半过 一半(memory 有,resume 缺) |
Zapvol 总分:7 过 / 2 半过 / 1 缺——整体基础件齐全,核心改进点集中在 A / B / H。
下一步:把点连起来看状态流
本章按钩子切——每个钩子独立讲”在这里做什么”。但真实代码里 8 个钩子共享一套跨步、跨轮、跨 session 的状态:E 写的 compactCache 下一步 B 读、onStepFinish 写的 lastStepTotalTokens B 读、G 持久化的 messages 下次 A 读。
下一章 生命周期状态流 按状态流切——追踪一次对话中每条 state 怎么在 8 个点之间演化。配着读能把散点连成 pipeline。
延伸阅读
- 设计启示——本章的抽象原则版
- 生命周期状态流——按状态流切的互补视角
- Agent 运行循环——CC 自己的
query()实现 - 上下文压缩——5 级压缩完整展开
- Zapvol 参考实现:
packages/backend/src/agent/agent-round.ts、packages/backend/src/agent/compaction/、packages/backend/src/agent/memory/memory-extraction.ts - Zapvol 演进讨论稿:
.claude/design/compaction-redesign.md