Agent 动态重规划与长任务恢复
一个任务运行很久后失败,最容易想到的补救是“再试一次”或“重新生成计划”。但重复执行可能再次依赖错误事实,全量重做又会浪费已经验证的结果,甚至重复触发外部操作。可靠的恢复机制首先要回答:哪里出错、哪些状态受到影响、从哪里继续才可信。
长任务的难点包括上下文取舍,也包括工具可靠性、数据时效、持久化、并发和副作用。上下文窗口变大不会自动解决这些问题;恢复能力必须成为运行时的一部分。
1. 不同问题需要不同恢复动作
Replan 指在已有目标和执行状态的基础上修订路径。它只是纠偏的一种方式,适用于原路径、关键假设或业务条件已经失效的情况。
| 情况 | 优先动作 | 需要注意 |
|---|---|---|
| 只读接口偶发超时 | 限次重试 Retry | 保留超时和退避预算,避免内外层重复重试 |
| 写接口超时,结果未知 | 查询外部状态并对账 | 超时不等于未执行,不能直接重发 |
| 缺少决定性条件 | 澄清 Clarify | 先检查已有证据能否补齐 |
| 某数据源持续不可用但有替代 | 局部 Replan | 替代来源也要符合权限、时效与质量要求 |
| 上游事实错误,污染后继结果 | 失效传播及回滚/补偿 | 不能只改最终答案 |
| 没有授权、没有可行路径或预算耗尽 | 中断或返回部分结果 | 说明已完成、未完成及当前外部状态 |
是否需要重规划与如何生成新计划,最好分成可观测的两个决策。异常码、权限失败、重试次数、工具熔断状态等先由代码处理;业务前提是否被新证据推翻,再交给语义判断。
2. 分清失败点、根因点、回滚点和重规划起点
考虑一个假想导购案例:最终推荐都太贵,原因是系统把电子产品的历史价格偏好错误用于服装。四个位置并不相同。
| 位置 | 在例子中的含义 |
|---|---|
| 失败点 | 用户看到推荐结果并表示价格不合适 |
| 根因点 | 提取服装预算时错误复用了其他类目的偏好 |
| 回滚点 | 回到基础资料已确认、服装预算尚未确定的状态 |
| 重规划起点 | 重新确认服装预算,再决定检索范围 |
参考商品、已确认城市等可能仍能复用;错误预算、由它生成的候选集及排序应当失效。尺码或颜色偏好能否复用,也要检查其证据是否适用于本次任务,不能因为“以前存在”就默认可信。
回滚点是状态恢复边界,重规划起点是重新做决策的位置。二者可以重合,也可能需要额外回到更早节点刷新证据。最近一次通过的检查点只是候选:它可能校验不完整,也可能依赖已经过期的数据。
3. 记录依赖,才能做局部恢复
仅保存聊天记录,难以可靠定位污染范围。关键中间结果应有稳定 ID,并记录来源、时间、依赖和校验状态。
{
"artifact_id": "clothing_budget_v2",
"value": {"max": 300, "currency": "CNY"},
"status": "verified",
"evidence_refs": ["message_18"],
"depends_on": ["requirements_v4"],
"constraint_version": 4,
"checked_at": "2026-09-22T10:00:00+08:00",
"scope": {"category": "服装", "task_id": "task_12"}
}
状态可以从简单版本开始:available 表示可参考但未充分验证,verified 表示在指定条件下通过检查,invalid 表示已被否定。需要更精细恢复时增加 suspicious(待复核)与 dirty(自身未必错,但依赖失效)。这些名称是实现约定,不是统一协议。
当事实被否定时,代码沿依赖图标记受影响的后继,并阻止它们被继续消费。失效传播只覆盖实际依赖者;没有依赖的分支可保留。但任务取消、权限撤销等全局条件可能影响整个运行,应采用更高优先级的控制规则。
invalidate(fact_id, evidence):
append_event("fact_invalidated", fact_id, evidence)
set_status(fact_id, "invalid")
for item in descendants_of(fact_id):
set_status(item, "dirty")
suspend_pending_consumers(item)
已经在执行中的后继也要处理。可取消的请求发送取消信号;无法取消的请求完成后先进入隔离区,再核对版本,不能继续写入当前有效状态。
4. 从证据出发缩小恢复范围
恢复过程可以按四层展开:先重试当前动作,再替换当前步骤的路径,再回到阶段入口,最后才考虑全局重规划。层级扩大应当由失败证据驱动,而不是机械地每失败一次就多退一步。
回溯时依次检查当前动作是否执行正确、输入是否完整、上游产物是否有效、支撑事实是否过期、原假设是否成立,以及用户目标是否发生变化。某类错误已反复出现时,可以建立“错误签名—候选恢复位置”的规则,但命中规则后仍需核对当前上下文。
新计划应明确列出保留、修改、删除和新增内容。例如:
{
"base_plan_version": 3,
"new_plan_version": 4,
"trigger_evidence": ["budget_correction_18"],
"preserve": ["reference_product", "confirmed_city"],
"invalidate": ["old_budget", "candidates_v3", "ranking_v3"],
"restart_from": "confirm_clothing_budget",
"changes": ["询问预算后重新检索"],
"remaining_replan_budget": 1
}
这个差异是一份待校验提案。运行时检查版本是否仍然匹配,用户约束是否被擅自修改,是否加入了未经允许的工具或副作用,再原子地更新有效计划。模型提供的简要理由有助于审计,但并不能证明它展示了真实完整的内部推理过程。
系统还要保存尝试过的路径与失败原因,避免 A、B 两个方案来回切换。失败路径可以重新启用,但需要状态变化证据,例如依赖服务已恢复,而不是模型忘记了上一次失败。
5. 借用 Try/Confirm/Cancel 做计划预检
高成本或关键外部依赖的任务,可以在正式执行前先验证最脆弱的前提。这借用了 TCC 的阶段划分,但普通计划预检并不具备分布式事务的原子性和一致性保证。
- Try:以低成本验证工具、数据和关键假设,例如读取少量库存、检查目标记录、生成变更预览。试探应避免不可逆副作用。
- Confirm:通过后将有效证据纳入当前版本,再进行正式动作;正式写入时仍需检查权限、版本及前置条件。
- Cancel:清理试探阶段的临时资源,标记失败假设,并转向替代路径或停止。
预检成功不等于稍后执行必然成功:库存、权限、价格都可能变化。涉及资源预留、确认和释放时,需要底层服务提供对应协议、幂等键和补偿语义,不能仅靠三个同名提示词实现。
对于只读推荐,轻量探测通常已经足够。不要为了统一概念引入完整事务编排。资金或跨系统写入失败后的补偿也可能不完全可逆,系统必须记录实际完成的部分,而不是一概显示“已回滚”。
6. 长任务要分开保存状态和上下文
模型窗口只是当前决策所需信息的视图,不能成为任务状态的唯一存储。至少应持久化目标和约束版本、计划及节点状态、证据索引、产物位置、工具调用账本、失败历史和预算。
长期运行中常见的漂移包括:目标变成了另一个任务,硬约束被摘要弱化,失败动作被标为成功,旧证据继续支持新结论,或一个字段误读污染多个后续产物。检测这些问题需要对照原始事件、外部真实状态和当前计划。
可以在阶段结束、压缩上下文后、重规划前后或重要异常出现时触发上下文校验。异步校验器应记录自己读取的状态版本,返回问题和修复建议;若主任务已推进到新版本,先重新核对影响范围,不能用过期结论直接覆盖当前状态。
多加一个校验器也会增加成本和误报。确定性检查可持续运行,昂贵语义检查优先放在高影响节点。不同模块发现同一问题时应合并,避免多个修复动作并发修改同一任务。
7. 用事件与快照支持中断恢复
事件日志保存“发生了什么”,快照保存“某一时点状态是什么”。例如约束更新、工具执行完成、事实失效、检查通过和计划换版都可以成为事件。
{
"event_id": "event_204",
"task_id": "task_12",
"sequence": 204,
"type": "constraint_updated",
"expected_state_version": 18,
"payload": {"field": "budget_max", "old": 500, "new": 300},
"evidence_ref": "message_18"
}
通过最近可信快照加后续事件,可以重建内部状态。快照需要同时关联事件序号、数据结构版本和产物校验信息;关键变更持久化失败时,不应向用户宣称完成。日志中发现错误,也应追加纠正事件,保留历史用于审计,而不是抹去发生过的事实。
中断恢复至少有两种情况。等待用户补充时,保存待回答的问题与恢复位置;进程崩溃重启时,先获取任务执行权、加载持久化状态、检查进行中的调用,再恢复可执行节点。并行工作者需要租约、锁或版本比较,防止两个进程同时继续同一任务。
重放内部状态不等于重新执行外部动作。 若“创建工单”已在外部成功,但进程在记录成功前崩溃,恢复后再次调用就可能产生重复工单。执行账本应保存稳定操作 ID 和幂等键,先查询或对账未知结果;已完成动作重用回执,需要重做的动作才重新调度。具体框架的重放边界仍需核对,例如 LangGraph 的 Graph API 文档明确说明,受影响节点恢复时可能从函数起点重跑,副作用需考虑幂等。LangGraph Graph API 文档
可以用一个恢复检查表控制重启过程:先确认任务仍获准继续,再校验当前计划和约束版本;随后清点外部动作账本,核对未知结果;再验证仍要复用的产物及证据;最后重新计算就绪节点。加载快照成功只完成了其中一步,不能直接当成“任务已恢复”。
例如进程停止前已经拿到一份配送报价,重启时它可能已经过期。正确处理是保留该调用的历史记录,并把报价标记为需刷新;不应为了与旧快照一致而继续承诺旧到货时间。相反,已经成功创建的工单,即使当前窗口中没有相关消息,也应通过账本和外部回执被识别为已执行。
用户取消需要优先于普通恢复。运行时应停止派发新动作,尝试取消进行中的动作,并核对已经发生的外部影响。取消之后的晚到结果只能用于记录和对账,不应重新激活任务。若清理或补偿失败,应保存待处理项及恢复入口,使下一次处理有明确起点。
8. 分阶段交接,保留可回查的细节
长任务可以划分为需求确认、证据收集、生成、执行和验证等阶段。分阶段不必等同于多 Agent;同一个运行时也可以切换局部上下文。
交接包需要保留总目标、已完成事项、当前计划版本、仍然有效的约束、已确认事实、证据和产物索引、未解决问题及下一阶段输入。原始工具响应和失败尝试可以留在外部存储,需要时按 ID 召回。
交接后应检查接收方是否知道哪些结果已经过期、哪些动作已经执行、哪些问题仍待确认。压缩历史时尤其不能把“禁止修改接口”改成“尽量少修改”,也不能把“已提出执行方案”改成“已完成执行”。
9. 用故障注入验证恢复能力
恢复测试不能只模拟网络超时。还应覆盖:上游事实被纠正、证据过期、外部写入成功但本地记录失败、等待用户时进程重启、旧计划的并发任务晚到、补偿本身失败。
每个案例都应明确标准根因、允许保留的产物、必须失效的后继、实际外部状态和预期恢复终点。观察根因定位是否正确、恢复范围是否过大或过小、是否错误复用脏结果、是否重复副作用,以及恢复后是否真正满足目标。
恢复成功率的分母应说明是全部触发恢复的任务,还是预先标注为可恢复的故障。已有结果复用率也应只统计仍可信的结果;提高复用率不能以继续使用错误数据为代价。
对于失败路径振荡、无进展重试、重复外部操作和约束违规,应有独立计数与停止条件。可靠性体现在遇到这些情况时,系统能够保留证据、终止扩大影响,并从明确的状态继续工作。