为什么 retry() 不是 Agent 的恢复策略

这段代码看起来很合理:

ts 复制代码
await retry(() => agent.run(task), { attempts: 3 });

直到 agent.run() 里面包含"上传图片""发邮件""发布文章"或"付款"。

第一次执行可能已经把请求送到外部平台,只是在返回响应前断线;第二次重试又执行一遍。你得到的不是高可用,而是两篇文章、两封邮件,或者两笔费用。

Agent 的恢复问题,本质上不是异常捕获,而是:如何证明已经完成了什么,以及哪些动作可以重放。

先画出 side effect fence

我会先把 workflow 的动作分四类:

ts 复制代码
type ReplayPolicy =
  | "safe"             // 纯计算,可重跑
  | "idempotent"       // 同一 key 可重复调用
  | "reconcile-first"  // 先查外部状态,再决定是否补偿
  | "never";           // 不允许自动重放

例如:

ts 复制代码
const actions = {
  outline: "safe",
  renderCover: "idempotent",
  uploadAsset: "idempotent",
  publish: "reconcile-first",
  charge: "never"
} as const;

Fence 之前的纯计算可以大胆重跑;越过 Fence 的动作必须产生可查询的外部凭证。

一次可靠工具调用至少要留下两条事件

不要只在成功后写日志。进程可能恰好死在"外部已成功、内部未记录"之间。

ts 复制代码
await events.append({
  type: "EFFECT_INTENT",
  deliveryKey,
  target: "csdn",
  requestHash
});

const result = await publisher.publish(request);

await events.append({
  type: "EFFECT_CONFIRMED",
  deliveryKey,
  externalId: result.articleId
});

如果只看到 EFFECT_INTENT,恢复器不能直接再发一次。它应该使用 deliveryKey、草稿 ID 或内容哈希去外部平台回读:已存在就补写 CONFIRMED;明确不存在才重试;无法确认则进入人工检查。

这就是 reconcile-first。

Checkpoint 不是保存内存,是保存一个可证明的阶段结论

每 10 秒序列化一次对象,不一定能恢复。你可能保存到了半个上传、未提交事务或过期签名 URL。

一个真正可用的 checkpoint 应该包含:

yaml 复制代码
runId: content-20260908-2030
lastCommittedSeq: 37
stage: visual_verified
inputVersions:
  facts: v4
  csdnDraft: v2
artifacts:
  cover: artifact://images/cover@sha256:7c1d
  body1: artifact://images/body1@sha256:2b4a
assertions:
  - images_exist
  - image_count_is_3
  - no_watermark_detected
next: csdn_prefill

恢复之前先验证引用的产物仍然存在、哈希仍匹配。通过后才能从 next 开始。

状态恢复应该发生在模型醒来之前

一种常见反模式是:把旧消息发给模型,问"继续刚才的工作"。这会把事务事实交给概率模型回忆。

更合理的顺序:

ts 复制代码
async function resume(runId: string) {
  const log = await eventStore.read(runId);
  const cp = latestVerifiedCheckpoint(log);

  await artifactStore.assert(cp.artifacts);
  await reconcilePendingEffects(log);

  return singleWriter.runExclusive(runId, async () => {
    const state = replay(log, cp.lastCommittedSeq);
    return worker.start({ state, from: cp.next });
  });
}

这里刻意把 worker.start() 放在最后。事件日志和外部系统先决定事实,模型再决定下一步做法。

为什么要 single-writer

定时调度器判断任务超时,启动恢复 worker;与此同时,原 worker 只是网络抖动,稍后又继续了。没有互斥,两边会从同一检查点向前跑。

可以用数据库租约解决:

sql 复制代码
update runs
set lease_owner = :worker,
    lease_until = now() + interval '90 seconds'
where run_id = :run
  and (lease_until is null or lease_until < now());

受影响行数为 0,就说明另一个 worker 正在执行。Google AX 的公开设计同样强调 single-writer、durable event log 和 recovery,但仓库也明确说明仍在早期开发、协议可能大幅变化;把它当作架构参考比把它当成稳定依赖更稳妥。

输入升级要分支,不能伪装成恢复

原任务基于 facts-v3 写完初稿。恢复时资料库已经到了 facts-v4。系统如果直接换新输入继续,就会出现"旧正文 + 新验收"的混合状态。

恢复有两种合法语义:

  • 保持原版本,继续同一个 run;
  • 升级输入,创建新 revision,并让受影响阶段重新验收。

不要把第二种藏在 retry 里。

小团队的最小实现

你不需要先做 Kubernetes 上的分布式 Runtime。四张表已经能覆盖大部分内容工作流:

text 复制代码
runs          当前阶段、状态、lease
events        追加式事实轨迹
checkpoints   已验收阶段与 artifact manifest
effects       delivery key、目标、外部 ID、确认状态

再配三个失败出口:RETRYABLE、NEEDS_RECONCILIATION、NEEDS_HUMAN。只有第一类允许自动重试。

这也是我们做 Tipkay 定时任务和多 AI 员工接力时的一个取舍。研究、写作、配图、排版和发布准备是不同岗位,但用户需要的是一条能继续的工作链,而不是五段随时失忆的聊天。已经完成的素材应该成为下游的稳定输入;不可确认的发布状态应该停下来回读,而不是多点一次。

一个人的生意,也能有一支专业团队。对这支团队来说,恢复能力不是"崩了再跑",而是保住已经完成的劳动,并且绝不把同一个外部动作做两次。

参考

相关推荐
蔚天灿雨3 分钟前
Agent 洗冤集录:前缀缓存 —— MCP 排序问题,如何影响模型调用的延迟与成本
人工智能·缓存·agent·harness
IT大白鼠6 分钟前
彭大帅的AI运维助手——自然语言管理 Linux 集群与网络设备——第 5 篇 · 多模态与虚拟化AI 能「看」图:多模态与虚拟化管理
linux·运维·人工智能
俊哥V10 分钟前
每日 AI 研究简报 · 2026-09-29
人工智能·ai
奕鼎竜瑆11 分钟前
MCP Connectors by Databox 新手部署与调用指南
服务器·人工智能
子非鱼eva11 分钟前
ONNX模型导出实战:PyTorch 导出 ResNet18 模型
人工智能·pytorch·python·知识图谱·onnx·昇腾知识图谱
cu14313 分钟前
细谈GM8775C的具体功能和应用
c语言·c++·人工智能·嵌入式硬件
haliu16 分钟前
【FHE 同态加密】我们如何实现同态加密推理(十四):为什么 `RESULT=PASS` 不是判据(纯 C11 · 零依赖)
人工智能·嵌入式·c·fhe·推理引擎·c11·边缘推理·同态加密推理
pjj1985418 分钟前
NLP-情感分析项目(四):训练评估 + 主函数
人工智能·深度学习·机器学习
微三云生态系统架构师-彭丹23 分钟前
微团AI红包风控与反作弊引擎:设备指纹与红包池熔断架构
人工智能·架构