用持久化状态快照恢复 Agent 任务
文章以邮箱验证流程说明,Agent 在上下文耗尽、工具超时或进程重启后,仅凭最后一条消息难以可靠续跑。建议保存精简、明确且持久化的操作状态,区分执行意图和已完成结果。
完整中文译文
一个 LLM Agent 可以在一次对话中完成邮箱验证。问题出现在执行时间超出可用上下文、某个工具耗时过长,或者进程重启时。如果 Agent 只保留最后一条消息,它就不知道该重新查询 inbox、请求另一个验证码,还是结束本次执行。
解决办法不是保存整段对话,而是创建一个用于恢复执行的 snapshot:用精简、明确且持久化的结构,表示恢复时真正需要的状态。这种模式能让自动化流程更可预测,并减少因重试而重复产生验证证据的情况。
问题:Agent 的上下文不是存储系统
模型的上下文用于推理,但它不是数据库。上下文可能被摘要、截断,也可能在其他工具介入时发生变化。此外,Agent 的叙述经常混杂意图和结果:“我要查询 inbox”并不意味着查询已经完成。
一个典型流程中存在多个风险点:
- 为某次执行预留一个一次性邮箱账户。
- 应用请求发送验证邮件。
- 工具查询邮箱,并返回部分结果。
- Agent 在确认验证码之前重启。
重启后,简单粗糙的策略会回到第二步。这可能导致再次发送邮件、消耗另一个验证码,或者把响应关联到错误的执行。如果 workflow 本身不保存状态,仅靠一个能帮助脆弱 CronJob 判断情况的信号还不够。
邮箱 snapshot 应该包含什么
snapshot 应该能在不读取完整历史记录的情况下,回答六个问题:
- 当前活跃的 run_id 是什么?
- 哪个邮箱和收件人属于本次执行?
- 最后一个已确认的动作是什么?
- 下一步允许执行什么动作?
- 还剩多少次尝试,预留何时到期?
- 哪些验证证据已经被接受?
一种精简的形式可以是:
{
"run_id": "checkout-1842",
"mailbox": "leased",
"verification": "waiting",
"attempt": 2,
"next_action": "poll_inbox",
"lease_expires_at": "2026-10-21T16:00:00Z",
"accepted_message_id": null,
"version": 7
}
如果没有必要,snapshot 不应该保存完整的邮件正文。只需保存标识符、hash,或重新获取验证证据所需的元数据。对于测试用例,一次性邮箱地址可以作为隔离机制的一部分,但恢复执行的决策必须由执行契约来定义。
来自集成系统的异常名称也值得保留,例如 temp mailid。它们有助于调查故障来源,但不应该成为接受某封邮件的判断规则。
把恢复执行设计成状态机
当 snapshot 表示一个小型状态机时,它会更有用。流程可以表示为:
LEASED -> REQUESTED -> WAITING -> RECEIVED -> VERIFIED
\-> EXPIRED
每次状态转换都必须有可观测的条件。REQUESTED 表示应用已经接受请求,并不表示邮件已经存在。RECEIVED 表示存在一封与 run_id、收件人和事件类型都匹配的邮件。只有在验证码通过校验之后,才能进入 VERIFIED。
恢复执行时,Agent 不需要解读含糊不清的历史叙述,而是读取 next_action,执行一个范围明确的操作。如果状态是 WAITING,它可以在剩余预算内查询一次。如果状态是 RECEIVED,就必须先校验验证证据,再考虑是否重新查询。如果状态是 EXPIRED,就必须结束本次执行或预留另一个邮箱,绝不能为了方便继续使用原来的邮箱。
这种职责分离也有助于隔离 Kubernetes 中的邮件故障:编排器可以重启 worker,而不会让这次重启变成一次新的业务意图。
使用幂等事件的最小实现
仅仅保存 snapshot 还不够。每次变更都必须记录为一个带版本的事件:
append(run_id, version=7, type="message_received", message_id="m-93")
如果预期版本与当前版本不再一致,服务器就会拒绝写入。这样,两个几乎同时被唤醒的 worker 就无法为同一个步骤确认不同的邮件。确认操作必须是幂等的:重复提交同一个请求,应返回相同的结果,而不是重复消耗验证证据。
实际实现时,我会拆分出三项职责:
- 规划器:决定下一步动作及其限制。
- 执行器:调用邮件工具,并返回事实。
- 存储:持久化 snapshot 和事件,并进行版本控制。
模型可以从允许的动作中进行选择,但不应该自行编造状态转换。清晰的 tool 契约能减少这样一种倾向:明明已经没有剩余尝试次数,却还要再做一次 poll。
小型 snapshot 容易检查,但需要认真设计 schema。保存完整历史记录便于审计,却会增加保留敏感数据的风险,也会拖慢恢复执行的速度。在大多数测试流程中,snapshot 加上事件 ledger,是一种合理的平衡。
- 成功恢复执行的比例;
- 每次执行的 inbox 查询次数;
- 因版本过期而被拒绝的状态转换次数;
- 从 REQUESTED 到 RECEIVED 的耗时;
- 因属于其他 run_id 而被丢弃的邮件数量。
如果每次重启后,重复查询次数都会增加,说明 snapshot 对下一步动作的描述还不够充分。如果因版本过期而被拒绝的状态转换增多,说明 worker 之间缺少协调,或者 lease 的有效期过长。
应该把完整 prompt 保存在 snapshot 中吗?
默认不应该。保存 prompt 的版本、会改变行为的参数,以及已经确认的决策即可。完整 prompt 可能包含恢复执行并不需要的数据。
如果邮件在 Agent 重启期间到达,会发生什么?
这封邮件应该作为待处理事件保留下来。下一次执行时,通过 run_id 和 message_id 进行查询,就可以把状态推进到 RECEIVED,无需再次发送邮件。
snapshot 能替代可观测性吗?
不能。它只是继续执行所需的最小输入。日志和指标用于解释每次状态转换为什么发生,并帮助发现调整不当的重试策略。
上线前的检查清单
- 每次执行是否都有 run_id,以及带过期时间的 lease?
- snapshot 是否只声明一个下一步允许执行的动作?
- 状态转换是否带有版本,并且具备幂等性?
- 接受验证证据之前,是否校验了邮件的归属?
- 重启后是否可以继续执行,而无需重新发送请求?
- 是否避免保存不必要的邮件内容?
Agent 不需要记住整段对话,也能正确行动。它需要的是一个可以验证、更新并据此恢复执行的小型状态。这个契约让邮件自动化成为一个可恢复的系统:模型负责推理下一步,基础设施负责保存那些不能丢失的信息。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。