AI agent Async Callback 与 Webhook 收敛:异步外部结果返回后怎样挂回正确 Run

HTMLPAGE 团队
20 分钟阅读

AI agent 一旦依赖异步 OCR、转码、审批回调或第三方 Webhook,最大的风险就不再是调用失败,而是结果回来了却挂错 Run。本文讲清 correlation id、pending state 与回调对账。

#AI agent #async callback #webhook #工程实践

很多 AI agent 的执行链都不再是同步闭环。OCR、文档解析、异步审批、第三方处理结果、Webhook 通知,这些步骤往往要过几秒、几十秒甚至更久才回来。难点不在“有没有收到回调”,而在“收到以后,系统能不能确定这条结果应该挂回哪一次 Run、哪一个 Step、当前还算不算有效”。

如果没有这层收敛设计,异步回调很容易变成系统里最难排查的一类错误:结果回来了,但挂错上下文、重复入账,或者因为 Run 已结束而悄悄丢失。

建议先结合 AI agent Checkpoint 与断点恢复AI agent Run Ledger 审计模型AI agent Worker Lease 与心跳机制AI agent 沙箱环境设计 一起看。

先给结论:异步结果不能当成“新请求”,而要当成“旧 Run 的后续事件”

问题正确处理
回调属于谁用 correlation id 关联原 run
Run 还活着吗检查 pending state 与有效期
回调重复到达怎么办幂等写入与事件去重
Run 已结束怎么办进入 reconcile 或 orphan queue

如果系统把回调当成一个独立新请求,就会丢掉最关键的上下文。

一、发出异步请求时就要生成 callback contract

更稳的方式是把异步调用发出去之前,先记录一份等待合同:

{
  "runId": "run_123",
  "stepId": "ocr_parse_01",
  "callbackToken": "cb_8f2a",
  "expectedEvent": "ocr.completed",
  "expiresAt": "2026-05-10T10:10:00Z"
}

这样外部结果回来时,系统不是“猜测这是什么”,而是去匹配一份已经存在的等待记录。

如果链路更复杂,这份 contract 最好再带幂等和恢复边界:

{
  "dedupWindowMs": 86400000,
  "resumePolicy": "resume_after_reconcile",
  "orphanAfterMs": 600000,
  "callbackAttemptKey": "cb_8f2a:ocr.completed"
}

这样系统对重复回调、迟到回调和恢复时机的处理会更一致,而不是由各个 handler 临时决定。

二、Pending state 要显式存在,不要靠内存等待

异步等待最容易犯的错,是在 worker 里 await 外部结果,假设线程一直活着。真实系统应该把等待写成状态:

  • waiting_callback
  • waiting_external_approval
  • callback_received_pending_reconcile

这会让系统在 worker 重启、Lease 回收或人工查询时,都能明确知道当前 Run 停在什么外部依赖上。

进一步说,回调等待最好也有显式状态迁移:

状态含义下一步
waiting_callback已发请求,等待结果等待或超时挂起
callback_validating已收到回调,正在验签和匹配通过后进入 reconcile
callback_reconciled已完成挂回和落账恢复执行或转人工
orphaned_callback找不到有效 run进入 orphan queue

这张表的价值在于,系统不只知道“回调来过了”,还知道它现在处于哪一个收敛阶段。

三、回调匹配至少要验证 4 个条件

回调不是拿到 token 就能直接继续执行。更安全的验证通常包括:

检查项目的
callbackToken / correlationId找对原 Run
event type防止把错误结果挂错步骤
signature / source verification防止伪造 Webhook
expiresAt / run status防止过期回调继续推进旧流程

只有这几层都成立,系统才应该继续进入后续状态。

四、回调收敛不是简单更新状态,还要做 reconcile

一个更完整的回调处理流程通常是:

  1. 收到 Webhook
  2. 校验来源与 correlation id
  3. 读取等待中的 run / step
  4. 生成 callback artifact 或结果快照
  5. 写入 ledger
  6. 决定继续执行、转人工还是进入 orphan queue

“reconcile” 这个动作很重要,因为回调带回来的不一定是可以直接继续的最终结果,也可能只是一个需要二次解析的中间事实。

很多团队会在这里再补一份 reconciliation record,明确这次回调究竟影响了哪一次恢复:

{
  "runId": "run_123",
  "callbackToken": "cb_8f2a",
  "matchedStepId": "ocr_parse_01",
  "reconcileOutcome": "resume",
  "artifactCreated": "callback_result_07"
}

这会比只写一条“Webhook 处理成功”有价值得多,因为它能把异步结果真正挂回具体 run 和具体产物。

五、重复回调和孤儿回调都要单独处理

现实里常见两类脏事件:

  • 重复回调:第三方因为网络问题重复推送
  • 孤儿回调:Run 已关闭,结果才回来

这两类都不应该直接报错丢弃。一个更健康的处理方式是:

  • 重复回调:命中幂等键,记录 duplicate received 后忽略副作用
  • 孤儿回调:进入 orphan queue,等待人工对账或补挂

否则排查时你只会看到“系统偶尔没反应”,却不知道结果其实已经来过。

六、上线后要看 callback 链路完整率,而不是只看 Webhook 成功率

建议至少记录:

指标用途
callback matched ratio看有多少回调能正确挂回原 Run
duplicate callback rate看外部系统是否经常重复推送
orphan callback count看是否存在 Run 关闭后的迟到结果
callback replay blocked ratio看重复回调是否真的被幂等挡住
expired callback rejected count看迟到结果是否常越过有效期
callback to resume latency看回调回来后多久真正继续执行

Webhook 200 只能说明你收到了请求,不代表系统真的把它接回了正确流程。

七、失败案例:OCR 结果回来了,但被挂到最新 Run 上

某个文档解析 agent 用 documentId 作为唯一关联键。用户短时间内对同一文档发起了两次处理请求,第二次 Run 启动后,第一次 OCR 结果才返回。系统按 documentId 把结果挂到了最新 Run,导致后续校验全部基于错误版本继续执行。

修复后,团队把回调 contract 改成 runId + stepId + callbackToken 三重关联,并为迟到结果单独进入 reconcile 队列,问题才真正消失。

八、Async Callback 与 Webhook 收敛 Checklist

  • 发出异步请求时,是否先写 callback contract
  • Run 是否显式进入 waiting callback 状态,而不是靠内存等待
  • callback contract 是否包含 dedupWindow、resumePolicy 和 orphanAfter
  • 回调是否校验 token、event type、signature 和有效期
  • 是否定义 waiting / validating / reconciled / orphaned 的状态流
  • 收敛流程是否包含 reconcile,而不是直接改状态
  • 是否为重复回调与孤儿回调设计单独处理路径
  • 是否落 reconciliation record 而不是只记“处理成功”
  • 是否监控 matched ratio、orphan count 和 resume latency
  • 审计时能否把回调结果还原到原始 Run 和 Step

结语

AI agent 的异步回调问题,从来不只是“Webhook 能不能收得到”,而是结果回来以后,系统还能不能把它准确、安全地挂回原来的执行链。只有 callback contract、pending state 和 reconcile 共同成立,异步依赖才不会把运行系统撕成两半。

延伸阅读: