← Agent 工程 · 第 3 篇

让 Agent 能恢复:任务状态、重试与幂等

用 SQLite 演示任务状态迁移,区分对话历史、持久状态与外部副作用,处理重试造成的重复执行。

学完这一篇,你能做到

写出任务状态表,说明重试与幂等键能解决什么、不能保证什么。

能恢复的 Agent,首先是一个能保存任务状态的系统。对话历史描述说过什么,任务状态描述现在进行到哪里,二者不能互相替代。

重启之后,系统应该知道哪些步骤已确认完成、哪些结果不确定,以及下一步是否可以安全重试。

把状态写清楚

最小状态可以包含排队、运行中、等待确认、成功、失败与取消。状态迁移应有允许条件,不能让任意旧请求把已完成任务重新改成运行中。

保存任务 ID、版本、当前步骤、必要结果引用和错误类别。模型上下文可以由这些数据重新组织,不必把完整聊天记录当作唯一数据库。

对于长时间等待的任务,还需要超时或租约,避免一个已经消失的执行器永久占有任务。

用版本约束竞争执行

更新时携带预期状态和版本,通过受影响行数判断是否取得执行权。下面用内存 SQLite 展示事务与条件更新;内存数据库本身不支持跨进程恢复,真实服务需要持久存储。

import sqlite3

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE tasks (id TEXT PRIMARY KEY, state TEXT, version INTEGER)")
db.execute("INSERT INTO tasks VALUES (?, ?, ?)", ("demo-task", "queued", 0))
db.commit()

with db:
    first = db.execute(
        "UPDATE tasks SET state='running', version=version+1 "
        "WHERE id=? AND state='queued' AND version=?",
        ("demo-task", 0),
    )
assert first.rowcount == 1

with db:
    duplicate = db.execute(
        "UPDATE tasks SET state='running', version=version+1 "
        "WHERE id=? AND state='queued' AND version=?",
        ("demo-task", 0),
    )
assert duplicate.rowcount == 0
assert db.execute("SELECT state, version FROM tasks").fetchone() == ("running", 1)
print("Only one claimant advanced this state")
db.close()

这只防止同一状态被重复领取,不能保证外部操作只执行一次。数据库事务不会自动覆盖另一个服务的网络请求。

处理操作已执行、状态没保存的空隙

假设外部服务已经完成写入,本地进程却在保存结果前中断。恢复时,系统看到的仍可能是“运行中”。直接重试会制造重复动作。

为逻辑操作使用稳定的幂等键,并确认外部服务真正支持它。保存请求 ID 与结果引用,必要时查询操作状态。不能把“发送过请求”当作“操作成功”。

如果外部服务无法去重或查询,结果不确定时应进入人工确认或对账流程。不要宣称仅靠本地状态表实现了端到端恰好一次执行。

重试必须有边界

临时网络错误可以在剩余时间与次数内重试;无权限、参数错误和未确认写操作通常需要其他分支。每次重试都应复用逻辑操作标识,而不是创建一个全新的动作。

保留失败类型、尝试次数、下一次执行时间和取消状态。恢复器处理这些明确数据,不需要重新猜测整段聊天里究竟发生过什么。

下一步

为一个任务画出状态迁移表,标注每条边需要的条件。模拟一次领取后中断,再检查旧版本请求能否错误地重新领取。

接入外部写操作之前,查清它的幂等与状态查询能力,并设计“结果不确定”状态。只有这些边界清楚,长时间自主执行才有可恢复的基础。

资料来源与延伸阅读

资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。