为什么 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、确认状态

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

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

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

参考

相关推荐
蜘蛛小助理1 小时前
AI 自动推导业务自动化规则实战教程|多维表格低代码自动化落地
大数据·人工智能·低代码·自动化·多维表格·蜘蛛表格
瀛川1 小时前
Agent 换了 Pod,身份怎么办?拆解 Substrate 的 Actor Identity
人工智能·云原生
人工智能AI技术1 小时前
从ChatGPT到GPT‑6 Astra:当AI开始解数学难题、自主操作软件
人工智能
财迅通Ai1 小时前
物理AI从产业叙事走向基本面重估 以SENASIC琻捷看端侧感算入口的产业价值
人工智能·senasic琻捷
Geek-Chow1 小时前
MCP 模型上下文协议:九、深入服务器 · 工具、资源与提示
人工智能
Theo_xx1 小时前
声学感知基础:Day1.常见音频类型(Chirp、FMCW、OFDM)的区别、联系和选择
人工智能·无线感知·声学感知
DisonTangor1 小时前
Qwen-Drive-1.0:首个统一 3D 感知、视觉问答与运动规划的自动驾驶视觉语言基础模型
人工智能·3d·自动驾驶
XiaoKe20262 小时前
数海云擎:以内外双向数智化锻造竞争壁垒,跑通销售全链路
大数据·人工智能·科技·软件需求
m0_587383002 小时前
全民健身解决方案软件开发:从架构设计到落地实践
人工智能·数据挖掘·系统架构·需求分析