长生命周期 Agent 真正贵的,往往不是模型推理本身,而是“状态怎么存、怎么恢复”。
一旦一个 Agent 跑上几十轮、几百轮,再叠加消息历史、文件上下文、人工介入和可恢复性,传统的“每一步都存全量快照”就会开始失血。
LangChain 这篇最新文章讲的就是这个问题:Delta Channels。
它不是在模型层做文章,而是在运行时层把状态检查点的存储方式改成“先记增量,定期落全量快照”,把长会话下的 checkpoint 成本压下来。
这篇文章我用更直白的方式拆一遍:
- 为什么全量快照在长会话里会炸
- DeltaChannel 到底改了哪一层
reducer为什么要满足 batching-invariantsnapshot_frequency怎么影响恢复延迟和存储成本- 这套机制为什么对 coding agent 特别有用
先说结论
LangGraph 早期的默认检查点模型很简单:每一步都把完整 state 存下来。
对短任务没问题,但对长任务会出现两个典型问题:
- 状态越来越大,写入越来越慢
- 历史检查点越来越多,总存储量近似按二次增长
Delta Channels 的思路更像日志系统:
- 每一步只写本步新增的差异
- 每隔
K步写一次完整快照 - 恢复时只需要回放最近一次快照之后的少量增量
结果就是:
- 存储从“越跑越贵”变成“更接近线性增长”
- 恢复延迟被
K约束住,不会无限放大 - 现有线程可以直接升级,不需要数据迁移
问题出在哪
Deep Agents 这类系统的 state 不是一个简单对象,通常至少包含两类会持续增长的数据:
messages:消息历史files:文件和上下文片段
这两类字段有一个共同点:都是追加型的。
也就是说,前面步骤产生的数据不会被后面覆盖,而是会一直累积下去。
如果每一步都把整个 state 重新序列化一遍,成本会这样增长:
- 第 1 步写 1 份
- 第 2 步写 2 份
- 第 100 步写 100 份
把所有 checkpoint 加总后,总量就不再是线性,而是接近 1 + 2 + ... + N 这种二次级别的膨胀。
flowchart LR A[Step 1] --> B[Full snapshot 1] C[Step 2] --> D[Full snapshot 2] E[Step 3] --> F[Full snapshot 3] G[Step N] --> H[Full snapshot N] B --> S[(Storage)] D --> S F --> S H --> S S --- X[每一步都存全量]
对于只跑几步的 Agent,这点开销不明显。
但对于一个会持续工作半小时、几个小时,甚至跨多个交互轮次的 coding agent,这种增长会很快变成系统成本。
DeltaChannel 怎么做
DeltaChannel 的关键变化就一句话:检查点不再存“整块状态”,而是存“本步增量”。
它的机制可以理解成两层:
- 普通步骤:只写本步新增的 delta
- 周期性步骤:每隔
snapshot_frequency = K步写一次完整快照
这样做的好处很直接:
- 绝大多数步骤写入更小
- 恢复时不用从头回放所有历史,只要回到最近快照
- 写入成本和恢复成本之间可以通过
K做平衡
sequenceDiagram participant Agent as Agent Runtime participant Ch as DeltaChannel participant St as Checkpointer Agent->>Ch: step 1 updates Ch->>St: delta write Agent->>Ch: step 2 updates Ch->>St: delta write Agent->>Ch: step K updates Ch->>St: full snapshot Agent->>Ch: step K+1 updates Ch->>St: delta write
文章里给出的 benchmark 很直观:
- 200 turn 的 coding agent 场景下,全量快照累计到约
5.3 GB - Delta Channels 压到约
129 MB - 也就是大约
41x的减少
这不是“省一点点”的优化,而是运行时结构级别的变化。
这个 API 最容易踩坑的点
如果你自己定义 DeltaChannel,最重要的不是“怎么写 reducer”,而是reducer 是否满足 batching-invariant。
旧模型里,reducer 常常是“接收旧值和单个新值,然后合并”:
def reducer(existing, update):
return existing + update
DeltaChannel 的新约束更强:它会把一段时间内累积的所有 writes 一次性传给 reducer。
所以 reducer 必须满足:
reducer(reducer(state, [w1, w2]), [w3, w4]) == reducer(state, [w1, w2, w3, w4])
也就是说,你不能依赖“分几次喂进来”这件事改变最终结果。
如果你的 reducer 不是这种性质,delta 和 full snapshot 的结果就可能分叉,而且这种 bug 往往只会在跨快照边界的长会话里出现,特别阴。
一个更容易理解的类比
把 reducer 想成“把一叠便签装进档案袋”的工人:
- 旧模型:每次来一张便签,就装一次
- 新模型:可能一次给你一沓便签,你要保证按一沓处理,和一张一张处理,最后归档结果一致
如果你处理顺序敏感,档案就会乱。
snapshot_frequency 不是越大越好
snapshot_frequency 决定多久写一次完整快照。
它其实是在做一个很现实的工程权衡:
K小:快照更频繁,恢复更快,但写入更重K大:写入更省,但恢复时需要回放更多 delta
LangChain 在 Deep Agents 里默认用了 50。
这个值不是数学常数,而是工程折中:既避免恢复过长,又把存储成本压到可接受范围。
可以把它理解成:
- 存储成本和恢复延迟,是一对跷跷板
snapshot_frequency就是调平衡的旋钮
为什么对 Agent 特别关键
很多人讨论 Agent 时只盯着“模型会不会推理”,但真正落地时,长运行、可恢复、可观测、可中断才是决定生产可用性的核心。
Delta Channels 解决的就是这条链路里的基础设施问题:
- Agent 跑久了,状态还能不能稳住
- 人工介入后,能不能无损恢复
- 文件上下文越来越多时,存储会不会爆
- 运行时开销会不会把收益吃掉
这也是为什么它对 coding agent、research agent、长任务工作流特别有价值。
这些场景的状态不是“一个答案”,而是“一个会持续生长的工作现场”。
如果你要自己实现类似方案
可以把设计目标压成三条:
- 写入路径尽量只记录增量
- 恢复路径必须有界,不能无上限回放
- reducer 必须保证 batch 合并前后语义一致
如果你在做自己的 Agent runtime,可以先按下面这个思路拆:
| 目标 | 设计选择 |
|---|---|
| 降低写入量 | 记录 delta,而不是每步全量快照 |
| 控制恢复成本 | 每 K 步写一次完整快照 |
| 保证一致性 | reducer 满足 batching-invariant |
| 平滑升级 | 兼容旧线程的存量状态 |
这套思路不只适用于 LangGraph。
只要你的系统里存在“持续增长的追加型 state”,都可以考虑类似的 checkpoint 分层策略。
一个最小化理解图
flowchart TB
A[追加型 state<br/>messages / files] --> B[Delta writes]
B --> C{到达 K 步?}
C -- 否 --> D[继续累积增量]
C -- 是 --> E[写入完整快照]
E --> F[恢复时从最近快照回放少量增量]
D --> F
这张图其实就概括了 Delta Channels 的本质:
- 平时轻量写
- 定期做校准点
- 恢复时只补最近一段历史
总结
Delta Channels 不是一个“更聪明的提示词技巧”,而是 Agent runtime 层的存储策略升级。
它的价值很实际:让长生命周期 Agent 的状态检查点从二次增长,变成更接近线性、可控、可恢复的工程形态。
如果你在做的系统也有这些特征:
- 状态会持续追加
- 会话会很长
- 需要断点恢复
- 需要人工介入
那就值得认真看这种“增量检查点 + 周期快照”的设计。
参考资料:Delta Channels: How We’re Evolving our Runtime for Long-Running Agents