别让 Agent 被 Webhook 叫醒就开工:实现一个幂等启动层

别让 Agent 被 Webhook 叫醒就开工:实现一个幂等启动层

很多 Agent 系统把调度器的回调直接接到执行器:Cron 到点、Webhook 进来、邮件匹配,就立即创建会话并调用工具。Demo 很顺,生产环境却很快出现重复发信、重复建单和"重试覆盖新数据"。

问题不在 Agent 会不会做,而在系统没有区分三件事:信号到达、运行尝试、业务任务成立。

官方对 Grok Automations 的描述很值得借鉴:自动化可以由日程或邮件触发,每次运行是新的请求,沿用相同指令并读取当前数据。工程上,这意味着"职责定义"与"运行实例"必须分层。

先定义三个对象

ts 复制代码
type TriggerEnvelope = {
  source: "cron" | "webhook" | "manual" | "agent";
  eventType: string;
  subjectId: string;
  occurredAt: string;
  traceId: string;
  payloadRef: string;
};

type ContextSnapshot = {
  instructionVersion: string;
  dataAsOf: string;
  permissionVersion: string;
  inputRefs: string[];
  timezone: string;
};

type Job = {
  id: string;
  responsibilityId: string;
  idempotencyKey: string;
  trigger: TriggerEnvelope;
  snapshot: ContextSnapshot;
  state: "ready" | "running" | "done" | "duplicate" | "expired" | "blocked";
};

TriggerEnvelope 只描述发生了什么;ContextSnapshot 冻结"本次运行看到的世界";Job 才是被允许执行的工作。

幂等键必须面向业务

不要直接使用队列消息 ID。Webhook 提供方重试时可能换 ID,多个入口也可能描述同一业务变化。

ts 复制代码
function jobKey(r: string, t: TriggerEnvelope, s: ContextSnapshot) {
  const bucket = t.eventType === "daily_brief"
    ? s.dataAsOf.slice(0, 10)
    : t.subjectId;

  return sha256([
    r,
    t.eventType,
    bucket,
    s.instructionVersion,
  ].join(":"));
}

对于 PR 检查,subjectId 最好包含最新 commit SHA;对于日报,日期和时区必须进入计算;对于订单处理,订单号通常已经足够。幂等范围由业务决定,不是由中间件决定。

在事务里完成"判重 + 领取"

ts 复制代码
async function startJob(input: StartInput) {
  const trigger = normalize(input);
  const snapshot = await freezeContext(trigger);
  const key = jobKey(input.responsibilityId, trigger, snapshot);

  return db.transaction(async tx => {
    const old = await tx.job.findByKey(key);
    if (old?.state === "done" || old?.state === "running") {
      return { state: "duplicate", jobId: old.id };
    }

    const gate = await evaluateStartGate(trigger, snapshot);
    if (!gate.ok) return tx.job.insert({ key, state: gate.state, reason: gate.reason });

    return tx.job.insertWithLease({ key, state: "running", leaseSeconds: 300 });
  });
}

唯一索引仍然必不可少。应用层先查只能优化体验,数据库唯一约束才是并发下最后的防线。

上下文快照不要只存 URL

Agent 运行时间越长,数据越可能改变。建议为关键输入保存内容哈希、对象版本或查询截止时间。这样重放时可以选择:

  • 严格重放:使用原版本输入复现决策;
  • 重新执行:创建新 Job,显式使用最新数据;
  • 继续运行:只有输入版本兼容时才恢复旧租约。

把"重试"和"重新执行"分开,是避免旧任务覆盖新状态的关键。

启动闸门应返回状态,不只抛异常

至少保留 duplicate、expired、blocked 三类非成功状态。重复事件通常不需要报警;过期日报可以直接跳过;缺少凭据或涉及不可逆外部动作才需要人工介入。结构化状态能让队列策略和通知策略分别处理,而不是所有问题都进入同一条错误通道。

小结

一个可靠的 Agent 启动层只做四件事:归一化触发、冻结上下文、计算业务幂等键、通过开工闸门。执行器因此得到的是一份可追踪、可重放、只会被领取一次的 Job,而不是一段来源不明的事件载荷。Tipkay 的部分岗位也可通过定时任务继续推进;接入企业流程时,同样要为每次运行保留去重、快照和关键操作确认。

相关推荐
ShineWinsu11 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
szp200515 小时前
Cache API 会丢掉 URL 里的 #:我的 9 个模型分块一直写在同一条缓存上
javascript·浏览器
keyipatience15 小时前
TypeScript学习(1)
前端·javascript·学习·typescript·html·css3
xifangge202516 小时前
基于 JavaScript 实现 CNC 数控切削参数与螺纹公差纯前端核算引擎
开发语言·前端·javascript
honkun616 小时前
vue 表格组件 vxe-table 修改滚动条样式,修改滚动条样式,统一宽度、颜色与自动隐藏
前端·javascript·vue.js
FRing0317 小时前
JavaScript入门到精通二十四(正则表达式、元字符、修饰符)
开发语言·前端·javascript·正则表达式
大明二代18 小时前
从 RAG 到有记忆的 Agent:Mastra Memory 与 PostgreSQL 实战
前端·javascript·人工智能
小羊没烦恼!18 小时前
C#综合揭秘——Entity Framework 并发处理详解
java·开发语言·前端·javascript·数据库·mysql·c#
OpsEye19 小时前
模型能力波动怎么办?网关侧做模型质量监控与自动切换
javascript·ai编程
西红柿炖牛腩35421 小时前
机房电脑不能联网,怎么离线把课件里的 3D 模型压小?Draco / Meshopt 参数清单
前端·javascript·3d·性能优化·大屏端