多会话运行时 II — durable 会话与跨会话恢复
多会话运行时 II — durable 会话与跨会话恢复
日期: 2026-09-29 阶段: Phase 4 - 自建 Agent 平台×求职冲刺 标签: #durable-session #checkpoint #cross-session-resume
核心问题
Day 106 解决了会话之间「不串扰」。今天解决会话自己的「不丢失」——durable 会话:
- 会话怎么跨进程/跨天存活? AML 调查不是一次 request 跑完的——分析师查到一半下班,第二天接着查;HITL 卡在「等合规官批准 SAR」要等数小时。会话状态必须在 compute 被回收后持久化,再来时断点续跑。
- 怎么复用 P2 的 checkpointMachine? P2 的
checkpointMachine.ts已经实现了「每步落 checkpoint + 崩溃后从最近 checkpoint 续跑 + 幂等重放」——但它是单跑、内存内的。今天把它升到会话级、可跨会话恢复:checkpoint 以(tenantId, sessionId)寻址持久化,kill 掉「进程」后从存储读回 checkpoint 续跑。 - 托管平台怎么做? 对照 Google Agent Engine Sessions + Memory Bank(2025-12-16 GA),看「短期会话历史」与「长期跨会话记忆」是两层不同的东西——这是自建时最易混淆的设计点。
对 AML:durable 会话是可审计性的载体——每一步状态快照都是调查链路的证据,监管要求「每决策可追溯」(Day 98 复查的 FIS 口径),而可追溯的前提是状态被持久化、可重放、不因崩溃丢失。
关键内容
A. 会话持久化状态机:从单跑 checkpoint 到 durable 会话
P2 的 CheckpointMachine<S> 核心语义(checkpointMachine.ts):每步是纯幂等 reducer,每步成功后落一个 Checkpoint<S> = { meta: {completedSteps, lastStepId}, state, version },崩溃时已落盘 checkpoint 不回滚,resumeFrom(cp) 从外部传入的 checkpoint 续跑。注释里已经点明它的局限:
"真实持久化需要后端(LangGraph 把 checkpoint 写进 Postgres/Redis/DynamoDB 等 durable store,并用 thread_id 寻址);这里 checkpoint 只在内存里,进程/标签页一关即失。"
Day 107 要补的正是这个「thread_id 寻址 + durable store」。设计上不改 CheckpointMachine,而是在它外面包一层会话持久化适配器——这是关键架构决策:让纯状态机保持纯,持久化是正交关注点。
// 会话持久化适配器:把 CheckpointMachine 的内存 checkpoint
// 以 (tenantId, sessionId) 为 thread key 落到 durable store,支持跨会话恢复
class DurableSession<S> {
constructor(
private readonly key: SessionKey, // (tenantId, sessionId),Day 106
private readonly store: CheckpointStore, // 持久化后端(教学装置=localStorage 模拟)
private readonly steps: Step<S>[],
private readonly initial: S,
) {}
// 推进一步并持久化——崩溃发生在任意位置,下次 resume() 都能续上
step(): RunResult<S> {
const cp = this.store.read(this.key) ?? bootstrapCheckpoint(this.initial)
const machine = createCheckpointMachine(this.steps, this.initial)
const result = machine.resumeFrom(cp) // 从持久化 checkpoint 续跑
this.store.write(this.key, result.checkpoint) // 落盘新 checkpoint(durable 写)
return result
}
// 模拟"进程重启":丢掉一切内存态,只靠 store 里的 checkpoint 恢复
static resumeAfterCrash<S>(key, store, steps, initial): DurableSession<S> {
const cp = store.read(key)
assert(cp !== null, `no durable checkpoint for ${key.sessionId}`)
// 注意:不重放已完成步骤——completedSteps 之前的步骤不再执行(幂等续跑)
return new DurableSession(key, store, steps, initial)
}
}
关键不变量(继承自 P2,会话级强化):
- 幂等续跑:
resumeFrom从completedSteps开始,已完成步骤不重做。若崩溃发生在「reducer 算完但 checkpoint 没落盘」(P2 的phase: 'after'),恢复后会重做该步——所以 reducer 必须幂等(at-least-once 下重放不双记账)。AML 场景:重做「拉取制裁名单匹配」无副作用 OK,但「提交 SAR 到监管系统」必须幂等键去重。 - 单调版本号:每落一次 checkpoint,
version++。可断言「恢复点确实前进了」,也是乐观并发控制的基础(防两个 tab 同时写同一会话覆盖彼此)。 - thread key = (tenantId, sessionId):与 Day 106 命名空间同键,持久化天然继承隔离。
B. 对照 Google Agent Engine:Sessions ≠ Memory Bank
Google Vertex AI Agent Engine 于 2025-12-16 将 Sessions 与 Memory Bank 一起 GA(Ivan Nardini/Google Cloud 公告:「Sessions & Memory Bank are now GA」+ 7 个新区域 + 降价)。最重要的设计洞察:它把「durable 会话」拆成了两个正交的东西:
| 维度 | Agent Engine Sessions | Agent Engine Memory Bank |
|---|---|---|
| 存的是 | 单会话内的时序对话历史(短期) | 跨会话的长期事实/偏好(长期) |
| 生命周期 | 一个 session 内 | 跨多个 session、跨天/跨周 |
| 寻址 | session_id | scope(如 user_id) |
| 读写时机 | 每轮对话追加 | 后台异步抽取 + 检索注入 |
| 类比 P2 | checkpoint(状态快照) | 比 checkpoint 更高层(提炼出的知识) |
| ADK 接口 | VertexAiSessionService | VertexAiMemoryBankService |
Memory Bank 的机制(Google Cloud Blog,基于 ACL 2025 接收论文 arXiv 2503.08026)三步:
- 抽取:「Using Gemini models, Memory Bank can analyze a user's conversation history... to extract key facts, preferences, and context」——异步后台跑,不阻塞会话主链路。
- 整合(consolidation):新信息进来时「Memory Bank (using Gemini) can consolidate it with existing memories, resolving contradictions and keeping the memories up to date」——去重 + 矛盾消解,不是简单 append。
- 检索注入:新会话开始时「retrieve... a simple retrieval of all facts or a more advanced similarity search (using embeddings)」——按相关度注入,避免「lost in the middle / context rot」。
反直觉洞察①(durable 会话不等于「把对话历史全存下来」):自建时最容易犯的错——把整个会话历史当 durable state 一股脑持久化,然后新会话全量塞回上下文。这恰恰触发 "lost in the middle" 和 "context rot",长会话质量反而崩。Google 的拆分给的答案:短期对话历史(Sessions)与长期提炼记忆(Memory Bank)是两套存储、两种生命周期。durable 的是「状态机 checkpoint」,不是「原始对话流」;跨会话带回的应是提炼后的事实,不是原始 transcript。
这对自建的指导:P2 的 checkpoint 对应 Sessions 层(单会话的可恢复状态);而 P2 的 src/agent/memory/(pinnedFactsStore.ts / summarizer.ts / contextBuilder.ts)对应 Memory Bank 层(提炼 + 跨会话)。Day 107 主攻 Sessions 层的 durable,Memory Bank 层 P2 已有骨架,今天只点明分层。
C. kill→恢复演示:durable 的「可证伪」验证
durable 是个强断言——「无论何时崩,恢复后结果不变」。要让它可证伪(作品集能演示),必须设计一个 kill→恢复演示,状态机如下:
[durable 会话 kill→恢复演示]
会话 5 步:fetch→sanction→risk→narrative→submit
│
┌──────────────────┼──────────────────────────────┐
▼ ▼ ▼
step1 ✓ step2 ✓ ◄── 在 step3 "before" 注入 kill
落 cp(v1) 落 cp(v2) (reducer 未执行)
│
▼ 【模拟进程死亡:丢弃所有内存态】
store 里只剩 cp(v2)
│
▼ resumeAfterCrash(key, store)
从 cp(v2) 读回 → completedSteps=2
│
▼ 续跑 step3→4→5
executed = [step3, step4, step5] ◄── step1/2 不重做
│
▼
最终 state === 无崩溃直跑的 state ◄── durable 正确性断言
对照「崩在 phase:'after'」(reducer 算完、checkpoint 没落盘):恢复后 executed 会多一个重做的 step3([step3, step3', step4, step5]),但因 reducer 幂等,最终 state 仍相同。这两个用例一起证明 durable 的两种崩溃位置都安全——这正是 P2 CrashSpec 的 before/after 双相设计的会话级复用。
量化对照(自建教学装置 vs 三家托管的 durable 会话能力):
| 能力 | 自建教学装置 | Foundry Sessions | Agent Engine Sessions | AgentCore Memory |
|---|---|---|---|---|
| 单会话状态持久化 | localStorage 模拟 | $HOME/files,15min 回收持久 | 托管,GA 2025-12 | 短期 events |
| 跨进程恢复 | resumeFrom(读回 cp) | stateful resume | session 跨天恢复 | — |
| 跨会话长期记忆 | pinnedFactsStore(P2) | — | Memory Bank(GA) | 长期 records(episodic GA) |
| 幂等重放保证 | ✅(reducer 纯函数) | 平台托管 | 平台托管 | 平台托管 |
| 最长会话/保留 | 无限(内存/storage) | 30 天删除 | 持久 | 可配 |
| 成本 | 0 | 计费(Day 108) | $0.0864/vCPU-h + $0.25/千 events | $0.25/千 events |
D. 跨会话恢复的一致性陷阱:并发写与版本冲突
durable 会话开了「同一会话可被多次恢复」的口子,就引入了并发写风险:分析师在两个浏览器 tab 打开同一 sessionId,各自推进、各自写 checkpoint,后写的覆盖先写的——丢更新(lost update)。P2 的单调 version 字段在这里发挥第二个作用(除了「断言前进」)——做乐观并发控制:
写 checkpoint 前:read 当前 store 里的 version
if (store.version !== myBaseVersion) → 冲突!拒绝写,要求重新 resume
else → CAS 写入 version+1
这是 P2 day34/day36(LangGraph checkpoint / durable resume)埋下的伏笔的会话级兑现。AML 场景的合规含义:同一调查不能被两条分叉的状态污染——否则审计链路出现「同一 caseId 两个矛盾的 SAR 草稿」,监管无法判定哪个是真。乐观锁保证「同一会话的状态线性化」。
反直觉洞察②(durable 让「恢复」变多,反而需要更强的并发控制):直觉上 durable=更可靠。但「可恢复」意味着「可被多次、多处恢复」,并发面反而变大了。不加版本号乐观锁的 durable 会话,比不可恢复的单跑更容易出数据不一致。durable 与一致性是一对必须同时设计的孪生关注点,不能只做前者。
设计要点/决策表
| 要点 | 决策 | 理由 |
|---|---|---|
| 持久化方式 | 适配器包裹 CheckpointMachine,不改状态机本身 | 持久化是正交关注点,保持纯状态机可测 |
| thread key | (tenantId, sessionId),同 Day 106 命名空间 | 持久化继承隔离,零额外隔离逻辑 |
| 短期 vs 长期 | checkpoint=Sessions 层;pinnedFacts/summarizer=Memory Bank 层 | 对标 Agent Engine 双层,避免全量 transcript 回灌 |
| 幂等续跑 | reducer 纯幂等;提交类操作加幂等键 | at-least-once 重放不双记账(继承 P2) |
| 并发控制 | 单调 version + 乐观锁(CAS) | 防多 tab 恢复致丢更新/分叉污染 |
| 演示可证伪 | kill→恢复演示,断言 state 与直跑相同 | durable 是强断言,必须能演示证伪 |
对本项目的落地
- 新建
src/agent/durable/durableSession.ts:实现 A 节DurableSession<S>与CheckpointStore接口(read/write(key, cp),教学装置版用内存 Map / localStorage 模拟 durable store)。复用src/agent/durable/checkpointMachine.ts的createCheckpointMachine/resumeFrom/RunResult,一行不改状态机,只在外面包持久化 + 乐观锁(用Checkpoint.version做 CAS)。thread key 用 Day 106 的SessionKey。 - 新建
src/agent/durable/__tests__/durableSession.test.ts:实现 C 节 kill→恢复演示——5 步会话,在 step3 注入injectCrash(2, 'before'),丢弃内存态,resumeAfterCrash读回 store 续跑,断言 (1)executed不含 step1/step2(不重做)、(2) 最终 state === 无崩溃直跑 state;再加一个phase:'after'用例断言重做但幂等。D 节并发用例:两个 session 实例基于同一 baseVersion 写,断言后写被乐观锁拒绝。 - DurableExecutionPanel 升级预留:
src/components/agent-arch/DurableExecutionPanel.tsx(P2 已建,演示单跑 durable)扩一个「跨会话恢复」模式——展示 store 里的 checkpoint 列表、kill 按钮、resume 按钮,让作品集能点击演示 durable 会话恢复,而非只跑单次崩溃恢复。 - 诚实标注:
durableSession.ts头注——本模块 store 为内存/localStorage 模拟,真 durable 需 Postgres/Redis/DynamoDB(同 checkpointMachine 头注口径);Sessions/Memory Bank 双层对标 Agent Engine(2025-12-16 GA),本装置只实现 Sessions 层 durable,Memory Bank 层见 P2src/agent/memory/。金额纪律:会话态含金额一律整数分,提交类 reducer 须带幂等键。
参考资料
- Google Cloud / Ivan Nardini — Vertex AI Agent Engine GA 公告:Sessions & Memory Bank now GA(2025-12-16);7 新区域;降价;Code Execution 一并 GA(X/公告 2025-12)
- Google Cloud Blog — Vertex AI Memory Bank in public preview:Gemini 异步抽取 facts/preferences;consolidation 消解矛盾;similarity search(embeddings)检索注入;按 scope(user_id)组织;防 lost-in-the-middle/context rot(基于 ACL 2025 论文 arXiv 2503.08026)
- Vertex AI Agent Engine 文档 — Sessions(VertexAiSessionService)/ Memory Bank(VertexAiMemoryBankService):短期对话历史 vs 长期跨会话记忆;ADK 集成;$0.0864/vCPU-h + $0.25/千 events(2026-01-28 起计费)
- arXiv 2503.08026 — Memory Bank 底层方法(topic-based 记忆学习与召回,ACL 2025 接收)
- 本仓库
src/agent/durable/checkpointMachine.ts(resumeFrom/CrashSpec before·after/version 单调/deepClone)、src/agent/memory/{pinnedFactsStore,summarizer,contextBuilder}.ts(Memory Bank 层对标)(2026-06)
SOTA 检查 (2026-06-11)
- 「短期会话 + 长期记忆」双层是 2026-06 托管平台共识:Agent Engine(Sessions+Memory Bank,GA 2025-12-16)、AgentCore(短期 events + 长期 records,episodic memory GA)、Foundry(会话 $HOME 持久 + Memory 层 preview)三家都把 durable 拆成两层。自建照此分层是当前最佳实践,本日 WebSearch 未见反例。
- Memory Bank 的 LLM 抽取/整合是活跃方向:用 Gemini 异步抽取 + 矛盾消解(而非规则去重)是 2025-2026 新范式(ACL 2025 论文打底);竞品 AgentCore 的 episodic memory、mem0/Zep 等开源方案口径类似。说明「记忆」正从「存 transcript」演进到「LLM 提炼的结构化事实」,自建
summarizer.ts方向正确。 - Agent Engine 计费起点 2026-01-28:Sessions/Memory Bank GA 时免费至 2026-01-28,之后 $0.25/千 events——本笔记成本对照基于 2026-06 口径,执行当周须重新确认价格(Day 108 详展)。
- 过时认知警示:「durable = 把会话历史全持久化再全量回灌」过时——会触发 context rot;正确做法是 checkpoint(状态)+ 提炼记忆(事实)分层。另:「durable=更可靠,无需额外一致性设计」过时——D 节证明可恢复放大了并发面,必须配乐观锁。
- 待跟踪:Foundry Memory 层 2026-06 仍 preview(vs Agent Engine/AgentCore 已 GA),其 durable 会话与长期记忆的 GA 时间待定,转 GA 后回填 C 节量化对照表。