应用到自研 Agent (AI SDK)

Agent 系统作者(AI SDK 用户):用 AI SDK 构建 agent 时,前 8 章的原则落到 8 个嵌入点的哪一行。本章以 Zapvol 为具体 case 做现状评估 + 演进方向标注。

本章定位

前 8 章讲 Claude Code 怎么做的。本章的目标不一样:

  • 外部读者:指导用 AI SDK 构建 agent 时,CC 的原则落到哪个钩子的哪一行
  • Zapvol 团队:以我们自己作为具体 case,现状评估 + 演进方向

两个目标通过同一套结构服务——每个嵌入点三段式:

  1. 最佳实践(AI SDK + CC 教我们的)
  2. Zapvol 现状(过 / 半过 / 缺,引源码事实)
  3. 演进方向(具体落地项 + 优先级)

本章不是”看 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 生命周期 · 8 个嵌入点与 CC 原则映射 每条 CC 原则落到 streamText() 生命周期的哪一步 · 左:AI SDK 钩子 · 右:对应的 CC 原则 AI SDK 生命周期 CLAUDE CODE 原则 · 注意点 streamText({ ... }) A Call-site 组装 system / messages / tools Late binding 保 cache · 5 层优先级梯度 system 必须字面量稳定 · 动态 context 推到 messages · providerOptions.cacheControl 显式布 breakpoint B prepareStep 每步输入覆盖 14 步流水线压到这里 · boundary / budget / snip / microcompact / collapse / autocompact 首步 vs 续步分开处理 · 只做 partial 返回 · 手动传 abortSignal · 顶部做 blocking check C Streaming 消费 text / tool-call 事件 Tombstone 撤回 · thinking block 签名保持 · usage 分量记录 流式输出无法客户端撤回——需要显式作废标记应对失败流 D tool.execute 工具真正运行处 权限检查 · PreToolUse hook · updatedInput · 输出截断 · error-as-prompt prepareStep 过滤工具是错的——deny 要在 execute 里返回结构化错误让模型理解 · abortSignal 传到叶子 E onToolCallFinish experimental 钩子 Tier 1 microcompact 入口 · 用便宜模型预压大 tool 输出 Race abortSignal · 幂等靠 cache · 用 Haiku 不用 Opus · 写 cache 下一步 prepareStep 读 F stopWhen 继续 or 退出 Terminal reason 分类 · stopWhen 组合: stepCountIs + hasToolCall('complete') + 自定义 finishReason 太粗——自己维护 TerminalReason 类型 · 提供显式 complete 工具 G Stream 完成 onFinish 回调 Session 持久化 · usage 聚合 · finishReason 分类 持久化 result.response.messages——不存就 resume 不了 · 4 个 usage 分量都要记 H Post-call 后台 onFinish 返回后 PostCompact · 记忆提取 · 后台 resume 预压缩 异步(stream 关闭前不 await) · resume UX 靠退出时预计算,不是运行时 MVP 优先级 先做 A / D / F / G (80% 价值) · B 阶段 2 加 · C / E / H 阶段 3 加 · 别为了完整性全上 参考实现: packages/backend/src/agent/agent-round.ts (Zapvol 生产模式) 每行右侧是"原则 + 要注意什么"——正文有代码示例

AI SDK v6 的 streamText 把循环藏在内部,对外只给几个钩子。所有 CC 模式都得挤进这几个点

位置职能
Acall-site 组装system / messages / tools / providerOptions
BprepareStep每步输入预处理(压缩 / 预算 / 缓存布点)
Cstreaming 消费消费 stream events
Dtool.execute工具运行处(权限 / hook / 沙箱)
Eexperimental_onToolCallFinishtool 结束后(microcompact 入口
FstopWhen继续还是停
GonFinish聚合 usage / 持久化 messages
Hpost-callsession snapshot / 后台任务

下面每点三段式展开。


点 A · call-site 组装

最佳实践

  1. system 字面量稳定:不要把 Date.now() / userName / gitStatus 拼进 system string——每变一个字节整个 prompt cache 失效
  2. 动态 context 走 messages 层:CC 把 currentDate 放 auto memory,不是 system
  3. providerOptions.cacheControl 显式布 breakpoint:AI SDK 不会替你加,必须手动标
  4. Tool description 是 prompt 工程:每条都按”教 agent 下一步怎么走”写(见 CC Edit 工具)
  5. maxSteps 有上限但别太小:30-50 是日常合理范围

Zapvol 现状

半过 · 一个潜在 cache 风险需要 audit

  • · tool description 普遍按教程式写(看 filesystem.compact.tsbrowser.tool.ts 等),抄了 CC 的风格
  • · providerOptions 显式 cache controlapplyCacheControl(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.2compaction/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(承载大部分压缩逻辑的单点)

最佳实践

  1. 按步分化:step 0 可以做重活(boundary filter + autocompact);后续步只做 microcompact 够了——每步都跑 autocompact 会爆成本
  2. Partial 返回:只写 { messages },不写 { messages, system, tools, toolChoice }——未改字段不要写出来,避免意外 cache 破坏
  3. AbortSignal 手动传进压缩 LLMprepareStep 签名没有 abortSignal,要从 ctx 读
  4. Blocking check 在开头:撞硬顶时 return synthetic error 让模型优雅退出,不让 API 抛 413
  5. 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 消费

最佳实践

  1. Tombstone 撤回:已流出的 chunk 如果因为 streaming fallback 作废,需要显式作废标记——UI 不能从客户端收回字符
  2. Thinking block signature 保持:跨轮复用时不能动签名字段;序列化 / 反序列化要保持字节对齐
  3. 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 累加但只存 totalTokensagent-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 的物理位置)

最佳实践

  1. 权限检查在 execute 开头:不要在 prepareStep 过滤 tool 列表(模型不知道被拒,只报 schema mismatch)
  2. PreToolUse hook 等价物:做一个 wrapTool 高阶函数,支持 updatedInput(修改参数,不只是 allow/deny)
  3. AbortSignal 传到叶子fetch / execFile / DB query 都要接 signal——半截 abort 比没有更糟
  4. 输出截断 + offload:单工具输出不能吃光 context
  5. 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 入口)

最佳实践

  1. Race abort:precompact 是独立 LLM 调用,用户 ESC 时必须立即中断
  2. 幂等:同 toolCallId 重放时短路读缓存
  3. 用便宜模型:主 agent Opus/Sonnet,precompact Haiku——成本 1/10
  4. 落盘 + cache:压缩结果 + 原文落盘,供主 agent 需要时 read_file 取回

Zapvol 现状

过 · 范本级——Zapvol 的 tool-precompact.ts 几乎完全对齐 CC 的 Tier 1 microcompact

  • · Race abortraceAbort(compactor(...), abortSignal) 显式
  • · 幂等readCompactCache(toolCallId) 先查
  • · 落盘 + cacheoffloadToolData 落盘 + writeCompactCache 缓存结果,file-based 跨步持久
  • · per-tool compactorServerToolConfig.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

最佳实践

  1. 组合多个条件stepCountIs(N) + hasToolCall('complete') + 自定义
  2. 提供显式 complete tool:让模型”显式声明完成”,避免无用步
  3. 维护业务层 TerminalReason:AI SDK 的 finishReason 颗粒度太粗
  4. Circuit breaker:连续失败 N 次后停

Zapvol 现状

过 · F 点设计比 CC 还细——stopOnComplete 的 todos-blocking 是亮点

  • · stopOnComplete()tools/stop-conditions.ts):不仅检查 complete tool 调用,还检查 todos 是否全 done——没 done 阻止停。这是比 CC 更精细的模式
  • · stepCountIs(maxSteps) 硬上限
  • · complete tool: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

最佳实践

  1. result.response.messages 必须持久化:不持久化等于 resume 不了
  2. 事务原子messagescheckpoint 必须同事务更新
  3. 多步 usage 累加:不要只记最后一步
  4. 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(后台任务)

最佳实践

  1. Memory extraction 异步:提取长期记忆条目不 await,不拖尾用户
  2. Resume pre-compact:退出 session 时在后台预算 resume 摘要——下次 resume 即时
  3. PostCompact hook:外部可扩展点

Zapvol 现状

半过 · Memory extraction 有;resume pre-compact 缺

  • · Memory extractionmemory/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 周内建议先做)

#改动预期收益
1AappendSystemContext cache audit + 动态内容迁到 messages保 prompt cache 命中率
2BstepCompactor 加 “首步 vs 续步” 分化续步省一次 autocompact LLM 调用

P1(基础件,3-4 周完成)

#改动相关 redesign 条目
3CUsage 按分量(input/output/cacheRead/cacheCreate)分开记
4EPRECOMPACT_TRIGGER_TOKENS 2500 → 500-1000§P1 #3
5F定义 TerminalReason 枚举 + 在 onFinish 派生
6Fautocompact / precompact 的 circuit breaker§P2 #7
7G审查 messages + checkpoint 的事务原子性
8HSession-level resume pre-compact§3.3(CC 有我们缺)
9BHysteresis(超 budget ≤5% 不触发)§P2 #6
10BBlocking synthesis fallback + 413 reactive handler§P1 #4

P2(机会档,等有压力再做)

#改动触发条件
11AManaged scope in memory-service多租户企业客户
12CTombstone / streaming retractionUI 反馈 streaming fallback 残留
13DTool wrapper pre/post/error hooks需要项目级自定义拦截
14HPostCompact hook surface对外扩展 / plugin 体系

10 项自检清单

扫你的 agent codebase(Zapvol 或外部):

#检查项Zapvol 状态
1system 字面量稳定?没拼 Date.now() / user context?半过 待 audit
2messages 显式 providerOptions.cacheControl 布 breakpoint?
3prepareStep 首步 vs 续步分开处理? 待改
4每个 tool execute 里有 permission check?
5Tool description 教程式?
6Tool executeabortSignal 到内部所有长耗时操作?
7Tool 输出有大小上限 + 超限截断 + 落盘?
8stopWhen 组合多条件(stepCountIs + hasToolCall('complete') + 自定义)?
9onFinish 持久化 response.messages
10onFinish 后有异步后台任务(不 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.tspackages/backend/src/agent/compaction/packages/backend/src/agent/memory/memory-extraction.ts
  • Zapvol 演进讨论稿:.claude/design/compaction-redesign.md
这页有帮助吗?