长任务 Agent:检查点、恢复与幂等
一个 Agent 连续工作几个小时,最难处理的通常不是“怎样让模型一直说下去”,而是进程重启、工具超时或上下文被压缩之后,如何分辨哪些事情已经发生。扩大上下文和增加循环次数,不能替代对任务状态的记录。
Agent 循环讨论一次观察、行动、验证如何推进。本文把时间拉长,处理跨进程恢复与外部动作之间的边界。
把任务状态与聊天记录分开
聊天记录保存讨论过什么;可恢复状态还必须说明目标、约束、待执行动作、已经确认的结果、失败原因、剩余预算和停止条件。模型写一句“已经完成”不是完成证据:应有可重新读取的产物、工具结果或验证结论。
检查点是某一时刻可恢复的状态快照。LangGraph 的持久化文档区分单个执行线程的状态快照与跨线程的长期存储,也明确内存中的检查点会随进程退出丢失。聊天摘要可以帮助下一次模型调用理解进度,但不应是外部动作是否发生的唯一记录。
例如,一个虚构的资料整理任务包含读取文档、生成草稿、写入文件和通知用户。状态可以记录草稿文件的版本、写入动作的标识、通知是否得到服务端确认。恢复时就不必要求模型从几千行对话猜测最后一步有没有成功。
最危险的是“已经执行,但没来得及记下”
考虑通知动作:外部服务已经接收请求,客户端却在收到响应前超时。如果 Agent 因为本地没有成功记录就重新发送,用户可能收到两次通知。反过来,先把动作标记为完成再发请求,进程又可能在真正发送前崩溃,造成永久漏发。
本地快照不能和任意远端服务天然组成一个原子事务。Temporal 的活动执行模型将可能失败、重试的外部工作与工作流推进分开;具体应用仍要设计重复执行的语义。不能仅凭使用了持久化框架,就宣称所有外部副作用“恰好一次”。
幂等键标识逻辑动作,不标识尝试次数
幂等操作重复执行不会额外改变结果。例如“把文件内容设为这一版本”比“在末尾再追加一段”更容易重试。对于支持幂等键的外部接口,同一个逻辑动作的每次重试都必须使用同一个键,而不是每试一次生成新 UUID。
可以把键与任务 ID、步骤 ID 和有效负载摘要关联。同一步改了接收方或正文,就需要新的逻辑动作;复用旧键可能返回旧结果。因此接收端应能检测“同键不同参数”,而不只是静默去重。
以下是接收端的简化伪代码,展示必须由同一个事务保护的两个动作:
begin transaction
if operation_key exists:
reject if stored_payload_hash differs
return stored_result
apply the local business change
store operation_key, payload_hash, result
commit transaction
如果真正的业务变化发生在另一个服务,这段本地事务仍不够;需要对方支持幂等、根据业务标识查询状态,或使用事务 outbox 等机制衔接交付。无法确认的动作应保留为“结果未知”,而不是自动等同于失败。
恢复不是无条件重放
恢复时先读取最后确认的状态和外部产物,再决定下一步。对已经完成的生成调用,可以复用保存的结果;重新调用模型可能得到不同文本,进而改变后续动作。工作流代码、工具 schema 或任务输入发生变化时,也要明确旧检查点是否兼容。Temporal 的工作流执行文档介绍了用执行历史恢复工作流的机制;可重放计算与外部 I/O 的分离是理解这种设计的关键。
失败要按原因处理:暂时性网络错误可有限退避重试;参数错误应修正参数;权限不足不能靠更多重试解决;长期等待必须有截止时间。撤销请求应传播给正在执行的工具,同时记录已完成的副作用,不能把“停止继续工作”当成“过去什么都没发生”。
检查点与上下文压缩怎样配合
状态快照保留机器可读的进度,摘要给模型提供继续推理所需的解释,两者可以一起更新。摘要应包含目标、已接受决定、尚未验证的假设和产物位置;巨大的工具输出可留在持久存储,只把相关片段放回模型。具体上下文取舍见上下文工程。
完成条件必须依附目标。例如“草稿已写入且引用检查通过”与“用户已收到通知”是两个不同结果。如果只完成了前者,就不能用聊天里的完成标记覆盖后者。预算耗尽、等待外部输入和目标达成也应是不同状态。
最有说服力的恢复测试,是在写入前、写入后但记录前、记录后分别中断,再检查最终产物是否正确、动作是否重复、未知结果能否被查清。测试先用本地文件和假的服务,不需要为了验证恢复机制反复发送真实消息。