Browse this library

Chinese source article

2 · 状态、记忆与恢复: 长任务怎样接着做

沿着目标版本、事实状态、工作记忆、长期记忆、上下文压缩与 checkpoint, 解释长任务中哪些内容必须保存, 怎样恢复, 以及何时应当回滚.

View original Markdown ↗

一个 Agent 连续工作几百步以后, 即使仍在流畅执行, 依据的目标和事实也可能已经过期. 用户在第八十步改了约束, 文件在第一百二十步被另一个进程更新, 一次工具调用只完成了一半, 模型随后仍按照旧计划继续推进. 每一步单独看都像合理动作, 合在一起却把任务带向一个已经失效的终点.

长任务能够接着做, 依赖一份可验证、可版本化、可恢复的任务状态; 对话数量本身无法保证连续性. 这份状态至少要回答五个问题: 当前执行哪一版目标; 外部环境现在是什么样; 哪些结果已经验证; 哪些判断仍是假设; 如果当前路径失败, 可以从哪里重新开始. 工作记忆、长期记忆、外部存储、上下文压缩和 checkpoint 都服务于这五个问题, 只是它们保存的信息、生命周期与读取方式不同.

1. 先把目标也当成状态

用户目标并非一段永远不变的初始 prompt. 在真实协作中, 后续消息可能补充缺失条件, 调整优先级, 撤销旧要求, 也可能把任务换成另一个方向. 因此目标需要显式版本. 可以把第 kk 版目标写成

G(k)=(O(k),C(k),A(k),k),G^{(k)}=(O^{(k)}, C^{(k)}, A^{(k)}, k),

O(k)O^{(k)} 是要实现的结果, C(k)C^{(k)} 是约束, A(k)A^{(k)} 是验收条件, kk 是版本号. 每个计划、动作和产物都应能追溯到它依据的 G(k)G^{(k)}. 当新要求到来时, 系统先判断它是在原版本上增补、局部替换, 还是使整版目标失效, 随后生成 G(k+1)G^{(k+1)}. 旧版本仍可保留作审计依据, 但不能与新版本同时支配后续动作.

如果只把新消息追加到聊天末尾, 模型必须从越来越长的自然语言里自行推断哪一句仍有效. 冲突约束尤其麻烦: 早期要求「保留所有接口」, 后来要求「删除兼容层」, 摘要若同时保留两句, Agent 无法从文本本身知道谁覆盖谁. 2.1「用户意图演化」进一步讨论逐步披露、参数修订和任务切换怎样改变目标, 以及静态单轮 benchmark 为什么会高估系统处理演化意图的能力.

1.1. 事实状态与模型记忆必须分开

目标确定以后, 系统还要知道环境中实际发生了什么. 可以将事实状态写成

Ft=(Et,Qt,Rt),F_t=(E_t, Q_t, R_t),

EtE_t 是外部实体及其版本, 例如文件哈希、数据库 schema、远程任务状态; QtQ_t 是已经通过检查的结果; RtR_t 是仍待验证或存在冲突的记录. 这个划分看起来朴素, 却能阻止一个常见错误: 模型说「修改已完成」以后, 系统便把这句话当作修改真的存在. 正确路径是执行写入, 重新读取权威来源, 运行对应检查, 再把结果从 RtR_t 移入 QtQ_t.

事实状态还要带来源和时间. 「测试通过」必须关联测试命令、输入版本和产物版本; 文件路径必须关联哈希或修订号; 外部 API 返回值需要保留请求条件. 缺少这些信息时, 一条昨天正确的记忆会在环境变化后继续被引用. 对 Long-Horizon 系统来说, 记住一个结论与知道它现在仍然成立是两种能力.

内部认知可以比事实状态更丰富. 模型可能维护当前假设、失败原因、候选方案和下一步意图. 这些信息有助于继续推理, 却不能覆盖外部事实. 一个稳健的状态层应允许「模型认为 A」与「环境尚未验证 A」同时存在, 并在下一次规划时优先安排验证. 这样即使推理过程犯错, 错误也不会立刻取得事实资格.

2. 三种记忆各自承担什么

「记忆」经常被当作一个统一组件, 实际上至少包含三种生命周期不同的存储.

工作记忆服务当前决策. 它包含当前目标版本、正在处理的子任务、最近观测、下一步所需变量和短期假设. 工作记忆应小而新, 能直接进入模型上下文. 如果把几百步原始日志全部当作工作记忆, 注意力会被重复工具输出和过期计划占满, 推理成本也会持续增长.

长期记忆保存跨阶段仍有价值的信息, 例如稳定的用户偏好、已经验证的设计决策、关键失败及其原因、能够复用的领域知识. 写入长期记忆需要比写入工作记忆更严格的门槛. 临时猜测、仅对某个环境版本成立的结果或未经核验的自我总结, 一旦长期保存, 会在后续任务里反复制造偏差. 长期记忆必须带来源、适用范围、写入时间和失效条件.

外部存储保存模型上下文之外的权威对象与完整记录, 包括文件、数据库、事件日志、测试报告、工具返回和产物版本. 它不等于长期记忆: 数据库里可以存大量历史, 但只有经过检索和选择的部分会成为当前推理所用的记忆. 外部存储提供可追溯性和容量, 记忆策略决定何时写、写什么、怎样取回.

2.3「LiveMem 动态记忆」把注意力放在运行过程中动态写入、更新和读取记忆. 关键问题不只是召回率. 系统还要处理重复事实、互相冲突的版本、过期信息以及写入粒度. 每一步都写会形成大量近似副本, 从不更新又会让旧状态长期占据检索结果. 动态记忆需要配合目标版本和事实状态, 才能判断一条记录在当前任务里是否仍然有效.

2.1. 压缩是在重写可见状态

上下文接近窗口上限时, 最直接的动作是摘要. 但摘要会改变下一步模型能看到的证据, 因而它本身就是一次状态转移. 设原始上下文为 CtC_t, 压缩器产生

C~t=K(Ct;Gt,Ft,B),\widetilde{C}_t=\mathcal{K}(C_t;G_t,F_t,B),

GtG_t 是当前目标, FtF_t 是事实状态, BB 是压缩预算. K\mathcal{K} 不能只追求字数短. 它要优先保留仍生效的约束、已经验证的结论、未解决冲突、关键工具输出的定位信息和恢复入口. 对话中的寒暄、重复解释与失效计划可以被丢弃; 原始证据不宜彻底删除, 而应留在外部存储中供重新读取.

压缩质量要通过后续行为衡量. 一份摘要读起来顺畅, 仍可能漏掉一条决定最终结果的约束. 可以比较压缩前后系统在相同状态上的动作分布、约束遵守率和任务成功率, 也可以专门构造需要调用早期证据的检查点. 如果摘要导致系统无法解释结论来自哪里, 它就失去了恢复所需的证据链.

2.2「上下文压缩」收集并比较多条路线: 整段摘要、分块并行压缩、外部 Context Manager、程序化状态和把压缩动作纳入强化学习. 它们的共同分歧在于压缩由谁执行、何时触发、用什么信号判断信息价值. 无论采用哪条路线, 都不能把「摘要生成成功」当成任务状态已经安全迁移.

3. Checkpoint 保存的是可继续执行的边界

日志能让人回看发生了什么, checkpoint 要让系统从某个位置继续执行. 一个可恢复 checkpoint 可以表示为

Ki=(G(k),Fi,Mi,Pi,Xi),K_i=(G^{(k)},F_i,M_i,P_i,X_i),

G(k)G^{(k)} 是当时生效的目标版本, FiF_i 是已经验证的事实状态, MiM_i 是继续执行所需的最小记忆, PiP_i 是剩余计划或当前阶段, XiX_i 是外部资源版本与幂等信息. 只保存一段对话摘要通常不够, 因为它未必包含文件版本、已执行副作用、远程任务编号或哪些动作能够安全重试.

Checkpoint 的位置也有代价. 太稀疏, 故障后要重做大量步骤; 太密集, 写入开销、存储占用和版本管理都会增加. 更合理的触发点是状态发生不可忽略的变化时: 一个阶段通过验收, 即将执行不可逆操作, 外部资源版本更新, 上下文需要压缩, 或任务准备跨进程继续. 每个 checkpoint 都要有恢复测试. 仅仅把对象序列化成功, 不能证明新执行体真的能重建工具连接、读到同一环境并继续完成下一步.

副作用使恢复更复杂. 如果系统在写入数据库后崩溃, checkpoint 却停留在写入之前, 直接重放动作可能造成重复记录. 因此 XiX_i 需要保存操作标识、提交状态或幂等键. 对无法幂等重试的动作, 恢复流程应先查询外部事实, 再判断动作是否已经生效. Long-Horizon 的恢复能力最终要接受真实环境检验, 不能只在语言轨迹里恢复一句「继续」.

3.1. 回滚与恢复不是同一个动作

回滚把系统带回一个较早的可信状态, 恢复则从可信状态重新建立执行. 当错误已经污染后续产物时, 先确定最早失效点 jj, 再寻找满足全部有效约束的最近 checkpoint KiK_i, 其中 i<ji<j. 随后撤销或隔离 KiK_i 之后的副作用, 重新读取当前环境, 依据最新目标生成计划. 外部环境可能已经无法回到旧版本, 因此恢复后的 Fi′F'_i 不一定等于原来的 FiF_i; 系统需要在现实状态上重算路径.

并非所有错误都值得全量回滚. 如果一项局部产物可独立修复, 而后续步骤没有依赖它, 局部补偿成本更低. 如果错误位于依赖链上游, 后继结果即使表面通过也要重新验证. 这正是保存依赖关系的意义: 它让系统知道哪些结果被污染, 而不是凭时间顺序把后续全部删除, 或凭侥幸全部保留.

恢复完成后还要验证连续性. 当前目标版本是否正确, 事实状态是否重新读取, 已完成动作是否与外部副作用一致, 新计划是否避开原失败原因, 都需要明确检查. 恢复成功要求系统重新获得可信状态, 并从这份状态产生可验证的下一步; 进程重新运行只完成了启动.

4. 状态不变量: 每次恢复都必须重新成立的条件

前面的对象很多, 真正执行时需要一组更短的「状态不变量」把它们约束起来. 每次工具调用、上下文压缩和故障恢复后, 系统都要重新检查这些条件. 例如:

It=1[Gt=Glatest(k)]⋅1[Qt⊆Verify⁡(Et)]⋅1[Deps⁡(Pt)⊆Qt]⋅1[SideEffects⁡t 可判定],\mathcal I_t= \mathbb 1[G_t=G^{(k)}_{\text{latest}}] \cdot \mathbb 1[Q_t\subseteq \operatorname{Verify}(E_t)] \cdot \mathbb 1[\operatorname{Deps}(P_t)\subseteq Q_t] \cdot \mathbb 1[\operatorname{SideEffects}_t\ \text{可判定}],

其中第一项要求当前计划引用最新目标, 第二项要求「已完成」集合里的每个结果都能在当前环境重新验证, 第三项要求下一步的前置依赖已经满足, 第四项要求外部副作用至少能够查询其是否发生. 只有 It=1\mathcal I_t=1 时, 系统才可以继续执行. 这比保存一份越来越长的历史更严格: 历史能够解释过去, 不变量直接决定现在能否安全前进.

假设 Agent 要修改三个相互依赖的配置文件. 第一个文件已经写入, 第二个文件写到一半时进程退出, 第三个尚未处理. 如果 checkpoint 只保存「正在修改配置」, 恢复后无法判断第一步是否应重做. 若状态记录了三个文件的期望哈希、实际哈希、依赖顺序和写入事务标识, 恢复程序便可重新读取事实: 第一个标记为已验证, 第二个回到未完成, 第三个继续等待. 这里没有要求模型记住每一行操作日志, 只要求恢复后重新建立对决策有用的不变量.

不变量也能揭示「上下文足够长」为什么仍会失败. 长上下文可能完整保留旧目标、旧文件内容和早期测试结果, 模型却没有机制判断哪些记录已经失效. 信息没有丢, 有效性关系丢了. 因此状态层应把版本和依赖作为一等对象, 让检索结果带回「在什么版本上成立」, 而非只返回语义相似的文本片段.

4.1. 不确定状态: 不要把缺失观测补成事实

工具超时、网页局部加载、异步任务仍在运行时, 系统只能把结果记为未知. 用二值的成功/失败记录会诱发两种危险行为: 把超时当失败而重复执行已经生效的副作用, 或把没有报错当成功并继续依赖尚未完成的结果. 更合适的状态域至少包含

z(at)∈{未开始,执行中,已生效,明确失败,结果未知}.z(a_t)\in\{\text{未开始},\text{执行中},\text{已生效},\text{明确失败},\text{结果未知}\}.

「结果未知」必须触发查询或对账, 不能直接触发重试. 例如支付、发消息、提交远程作业这类动作, 客户端断线只说明响应没有到达, 无法推出服务端没有执行. 幂等键能让重复请求合并为同一个动作; 如果接口不支持幂等, 系统需要通过订单号、消息 ID 或作业 ID 查询真实状态. 查询仍失败时, 最安全的计划可能是暂停这一条依赖链, 同时继续不依赖该结果的其他工作.

模型对事实的置信度也不应替代这种状态分类. 「我有九成把握文件已经写入」仍然属于未知, 因为文件系统可以直接读取. 概率适合表示无法立即观测的环境假设, 可查询事实则应尽快坍缩为带证据的状态. 一个实用原则是: 能通过低成本权威读取解决的不确定性, 不交给语言模型猜; 无法读取的部分才进入假设集合, 并明确它会影响哪些后续决策.

4.2. 三篇子文组成的主线

本章的三篇子文依次处理状态变化、上下文迁移和记忆更新. 2.1「用户意图演化」处理目标怎样变, 特别是新信息到来时如何识别增补、修订和任务切换. 2.2「上下文压缩」处理可见历史怎样变短, 以及哪些内容必须穿过压缩边界. 2.3「LiveMem 动态记忆」处理运行中怎样持续写入和检索有用记忆.

把三条路线放到一起, 可以得到一条完整状态链: 目标版本决定当前约束, 事实状态保存已验证结果, 工作记忆承载眼前决策, 长期记忆保存跨阶段知识, 外部存储保留权威对象与原始证据, 压缩器重写模型的可见历史, checkpoint 划定可继续执行的边界. 任意一环缺失, 系统都可能看起来记得很多, 却无法在故障或目标变化后可靠地接着做.

后续讨论计划执行和 Agent harness 时, 这些状态必须找到具体落点. 哪个组件拥有目标版本, 谁能把结果标成已验证, 压缩由谁触发, checkpoint 与外部副作用如何对齐, 恢复后由谁重新规划. 只有这些职责能够落到实际系统, 「长程记忆」才不只是向量库、摘要或超长上下文的另一个名字.