大多数Agent演示都是调一个API然后干等。等API换成要40秒才应答的ERP,或者要十分钟才返回的批处理接口,这套做法就崩了:Agent挂住、请求超时、重试触发,同一张采购单被创建了两次。

打开网易新闻 查看精彩图片

更稳的做法是把"接任务"和"完成任务"拆开:先接下任务,把慢活丢进队列,让ERP回头调用我们,同时留一个轮询任务兜住那些回调永远不来的日子。示例代码用Python,配FastAPI和httpx,ERP接口是虚构的,字段名按自己的系统改。

同步等待为什么会坏

在Agent的请求里死等,会撞上三个问题:

  • 超时。负载均衡器和客户端远在慢系统干完活之前就放弃了。
  • 重试没有保护。客户端对超时调用重试,同一个副作用可能被执行两次。
  • 容量浪费。一个worker卡在一次慢调用上,就没法服务别人。

解法就是把"接受任务"和"完成任务"拆成两件事。

方案的形状

客户端带着Idempotency-Key请求头发来任务,立刻拿到202 Accepted。后台worker把作业提交给ERP,同时带上我们的回调地址。ERP干完后调用/webhooks/erp,并附上HMAC签名。另有一个对账任务轮询ERP,专门捞那些等太久的作业,防止webhook在半路丢了。

演示里状态放在字典里。生产环境要换成数据库,否则一重启任务就没了。

代码骨架大致是这样:定义ERP_URL、CALLBACK_URL、WEBHOOK_SECRET三个环境变量,维护三张映射表——task_id到状态、幂等键到task_id、ERP作业id到task_id,再加一个asyncio队列。应用启动时用lifespan拉起worker和对账两个协程,退出时取消。

安全地接受任务

幂等键是让重试变得无害的关键。同一个键第二次到达,直接返回原来那个任务,而不是新建一个。创建任务的接口返回202,生成task_id,写入状态为queued的记录,把task_id塞进队列。查询接口按task_id取状态,取不到就返回404。

worker做什么

worker提交作业,并记下ERP返回的job_id。提交时把我们自己的task_id作为reference传过去,这样如果ERP支持幂等提交,重试提交也不会造出重复作业。提交成功后任务状态改为waiting_for_erp。提交抛HTTP错误时,attempts加一,按次数决定是否重排。

webhook与轮询,不是二选一

webhook给你速度和便宜的流量,轮询给你确定性。两个都用上,大约多写二十行代码,就能消掉一整类凌晨三点爬起来查的问题。对账任务定期扫一遍状态仍是waiting_for_erp且已经等太久的任务,去ERP查作业状态,成功或失败就落定结果。

上生产之前还缺几样东西:持久化存储、永久失败任务的死信列表、队列深度和回调延迟的指标、取消任务的入口。另外建议加一条告警,任何任务卡在waiting_for_erp超过一小时就报出来。

回调丢了该怎么处理,是轮询、发送方重试,还是别的办法,值得在设计API那一侧时就先想清楚。