别让 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 的部分岗位也可通过定时任务继续推进;接入企业流程时,同样要为每次运行保留去重、快照和关键操作确认。

相关推荐
admin and root5 小时前
「AI安全篇」实战AntiDebug自动化JS逆向加解密MCP
javascript·人工智能·网络安全·自动化·漏洞挖掘·cnvd·src赏金
wengad7 小时前
从 wangEditor 迁到 wangEditor-next
前端·javascript·vue.js
行者-全栈开发8 小时前
【前端/JS框架】CVE-2026-3854:GitHub Enterprise Server X-Stat注入XSS漏洞深度剖析与紧急修复指南
前端·javascript·github
怕浪猫10 小时前
2026年金九银十,我面了22个前端
前端·javascript·面试
前端的日常10 小时前
一个人+AI做情侣食谱小程序,30天纯赚
前端·javascript·后端
qq_3168377511 小时前
阿里云百炼 fun-asr-realtime uniapp vue3 调用
javascript·vue.js·uni-app
默_笙11 小时前
🚊 别再让 AI 的记忆写两遍了:PostgreSQL 把关系查询和语义检索装进一张表
前端·javascript
lzhdim11 小时前
C#不为人知的10个魔法特性:资深开发者也会震惊的底层奥秘
java·前端·javascript·算法·c#
只睡四小时13 小时前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
喵个咪15 小时前
GoWind Admin|风行 — 开箱即用的企业级全栈中后台框架:脚本系统实战
javascript·后端·go