工程实战 · 业务 Agent
学习舱 / 工程实战 / 业务 Agent

业务 Agent 实战:幂等、审批、恢复与结果核验

整理中 深度 · 分析 更新 2026-09-09

前置 工具执行、状态机

业务 Agent 实战:幂等、审批、恢复与结果核验

跟做一个只操作模拟工单的执行器,让 Agent 的「我做完了」必须经过业务状态核验。前置:工具执行机制与状态机和恢复;本文把它们落到一次有副作用的任务,不接真实资金或真实通知。

先从一次没有收到回执的操作说起

用户请助手把已经处理好的工单标记为解决,并通知申请人。助手确认操作计划,管理员批准,工具开始执行。

工单状态已经写入数据库,响应却在回程丢失。助手看见超时,决定再试一次;如果工具每次都追加变更与通知,同一个请求就会产生两份副作用。

究竟怎样知道该重试、该继续未完成步骤,还是该停下来重新审批?

把业务意图登记成持久操作,审批绑定具体载荷与数据版本,执行后对账恢复,最后从业务事实判定完成。模型提出动作,确定性程序管理承诺;网络超时只说明没收到答复,不说明事情没发生。

全景图:模型不直接决定完成状态

模拟工单:解决状态 + 通知回执,两项都核验 ① 意图登记② 绑定审批③ 执行与恢复④ 结果核验 请求键 + 载荷摘要重复不重复写业务对象、动作、版本执行前检查未变化事务 + 操作回执未知结果先对账读取工单最终状态读取通知完成标记未完成出口:未批准 / 需重审 / 部分成功 / 人工接管完成判据来自业务表,不来自模型的一句总结 →→→

图:③保存发生过什么,④核对目标是否成立;保存了执行记录不等于目标已经完成。

渐进拆解:把一张工单做成可恢复的承诺

像在服务窗口办理业务,取号不等于办好,主管签字也不等于资料永远有效。若窗口断电,下一位工作人员应根据单据查办理记录,而不是让你重办整件事。我们把这张单据落实为操作记录,但不会把数据库事务夸大成跨所有外部系统的万能保证。

① 请求键代表意图,不代表一次网络发送

客户端第一次提交时生成稳定请求键,同一意图因超时重发必须沿用它。作用域包含租户和调用主体,服务端保存载荷摘要;同键同参数返回已有操作,同键不同参数立即拒绝。不能只用自然语言全文哈希当意图标识,因为用户可能确实想连续办理两件内容相同的业务。

对模拟工单,载荷是工单号、目标状态与预期版本。数据库对请求键设唯一约束,防止两个并发请求同时登记;应用层先查再写只是优化提示,不能替代约束。幂等记录保留期限也必须覆盖允许重试的窗口,过早删除后,迟到重试可能重新触发操作。

这里的幂等指同一个业务请求重复到达,不额外改变业务结果,不要求每次 HTTP 响应字节完全一致。第一次可能返回进行中,后来返回已完成。模型可以重新组织解释文字,但不能自己重新生成请求键来规避既有操作。

② 审批批准的是具体计划,不是任意后续动作

审批页面应展示并记录以下要素:

  • 页面展示:对象、目标状态、预计副作用和数据版本。
  • 记录在案:审批人、时间、有效期以及载荷摘要。

审批入口独立鉴权,不能接受模型生成的「管理员已同意」作为凭证。计划被修改后,旧签字不能继续使用。

管理员批准解决工单时,它还是版本一;执行前客户补充信息,工单到了版本二。这就是检查和使用之间的变化。执行器应在事务内用预期版本做条件更新,更新不到一行就进入需要重审,不能先在事务外查版本、再无条件覆盖。

再次审批时展示最新数据,产生新的操作意图或明确的新审批修订,保留旧记录不擦除。工单状态已经变化不一定代表任何人犯错,它只是证明原审批的前提不再成立。权限、审批有效期和目标资源可操作性也要在实际执行前重新校验。

③ 副作用与回执如何一起留下证据

本教程把工单、操作记录和待通知项放在同一个 SQLite 数据库里。事务同时更新工单版本、把操作记为部分成功、登记待通知项。提交之前失败全部回滚,提交之后响应丢失则三者都存在;重启后查到这份记录,不再重复修改工单。

如果通知在外部服务,数据库事务无法把远程发送也包进去。常见做法是把待发送事件和业务变更一起提交,再由工作进程投递,这叫事务型发件箱。投递可能重复,下游仍需幂等键或可查询回执;发件箱保证有待办证据,不自动保证对方只收到一次。

AWS 的公开幂等实践强调客户请求标识、同键异参校验,以及记录标识与副作用的原子性。这与本例的核心约束一致。LangGraph 的中断文档则明确提醒恢复可能从节点开头重执行,因此中断前的副作用要幂等;框架的检查点不能代替业务服务的去重合同。

④ 最终核验从业务事实出发

工单已解决但通知失败,任务只能是部分成功;通知写着「已解决」但工单被其他流程重新打开,也不能宣称全部完成。核验器读取业务表中的状态、版本及对应通知标记,与预先写好的目标逐项比较,输出证据而不是重新问模型「你觉得完成了吗」。

模拟脚本要求工单版本恰好等于预期版本加一。这是故意保守的验收策略:后续合法编辑也会触发人工复核,不一定代表原操作未发生。生产系统可结合不可变业务事件标识区分「曾经成功执行」与「当前目标仍然成立」,两者都需要记录,不能混成一个成功布尔值。本例用待通知记录证明工单写入曾提交,用操作状态记录最近核验结果。

每次重复 execute 都重新核验,结果按状态落账:

  • completed:只是核验时的快照,不是永久承诺。
  • partial:通知未完成但工单仍符合时保留。
  • needs_review:目标或版本失效即写入;保守锁存——即使外部状态后来吻合,也不由旧请求自动恢复完成或重做副作用。新处理需要重新登记意图、审批,人工接管本身不在脚本内实现。
等待审批条件执行部分成功核验完成 →→→ 版本不符 → 需要重审只补通知,不重做工单④ 完成后再失效 → needs_review;旧请求不重做工单

图:部分成功是可恢复状态,不是把异常改名为成功;执行前版本冲突走重审出口,已执行后目标失效转 needs_review,不重放旧请求。

动手:运行模拟工单的故障注入

小程序读者请在电脑浏览器打开下方链接下载,随后在电脑终端运行。下载完整业务 Agent 执行实验,保存为 business-agent.py,使用 Python 3 运行。只依赖标准库,临时目录中的 SQLite 数据库在测试结束后自动清理,不连接网络,不接触真实工单、资金或消息系统。

python3 business-agent.py

本实验用固定结构化载荷替代模型计划,用显式函数调用模拟可信审批。这样读者先验证执行合同,不为随机模型输出付费。它不是完整对话 Agent,也没有实现真实审批身份系统。示例省略租户与主体键前缀,并发登记冲突目前直接报错,生产需捕获唯一键冲突后核对载荷并读回已有操作;核验函数只返回布尔值,生产应附逐项证据。这里的恢复测试是同进程重开连接读取磁盘,不是完整进程崩溃测试。接入模型时,只允许模型调用登记接口提交白名单字段,审批凭证与执行状态仍由服务控制。

第一步:观察未审批拒绝与提交后丢响应

脚本创建三张未解决工单,先对第一张提出计划,然后依次执行:

  1. 故意跳过审批直接调用执行器,断言必须拒绝。
  2. 批准后执行器完成事务,紧接着抛出模拟超时——这一步模拟「执行完成但响应没到」,不是提交前的失败。
  3. 关闭数据库连接并重新打开,从磁盘恢复;用同一个请求键再提交相同计划,返回既有部分成功状态,工单版本仍只增加一次。
  4. 再用同键换成第二张工单,断言立即拒绝同键异参。

观察这些断言比观察终端里一条「已去重」更可靠。

第二步:让通知失败,只恢复未完成部分

通知函数第一次故意抛出连接错误,核验器必须返回未完成,不能因为工单已改就提前报喜。第二次补通知后,核验器才将操作置为完成;再执行同一请求,会重新核验当前事实,仍符合才返回已完成,而不新增业务变更。

脚本中的通知只是在本地表写标记,没有真实邮件。因此这个测试证明的是状态分工与恢复路径,不证明远程通知恰好发送一次。把它换成外部服务前,要测下游收到了消息但发送方丢了回执的情况,并确定对账与人工接管规则。

第三步:审批之后改数据,检查最终状态

三张工单各测一条失效路径:

  • 工单一:另测「核验成功后再被重新打开」,分别让 verify 和 execute 首先发现失效;随后反复执行、核验及补通知都不能再报告当前 completed,工单版本和通知项数量不增加。旧请求重新审批也必须拒绝,审批冲突分支则继续保留 needs_reapproval。
  • 工单二:批准后被另一流程改到版本二,执行器的条件更新失败,进入 needs_reapproval;工单仍未解决,也没有待通知项。
  • 工单三:先完成业务更新和模拟通知,再被外部流程重新打开;最终核验必须失败并持久化 needs_review。

预期最后输出以 PASS duplicate, lost-response, stale-approval, partial, final-state, restart 开头。测试中捕获的是明确注入的预期异常,其他异常仍然向外抛出,不能用一个总的异常捕获把所有结果打印成通过。

下面是本节的实验验收表,每行都对应一个可执行断言。

故障场景必须观察的事实不接受的替代证据
同请求重复到达工单版本只增加一次模型说已经去重
提交后丢响应重启后读回部分成功记录超时被当成没执行
审批后数据改变保持原状态并要求重审沿用旧审批强行覆盖
通知失败工单已改但整体未完成工具返回一次成功就收尾
最终状态被改变核验失败并持久化 needs_review,旧请求不重放已发送通知就算成功

不看源码复现一个变体:把第一张工单的通知一直设为失败,确认任务一直不能成为已完成,同时工单版本没有再次增加。再给重试设置最大次数和截止时间,超过限制输出人工接管,而不是无限循环。此变体的持久调度器不在原脚本内,需要读者自行实现并复验。

边界与取舍:不把一个本地事务说成全局恰好一次

有副作用不代表必须用 Agent。若动作、顺序和条件都固定,确定性工作流更容易验证;模型的价值是理解非结构化请求与提出计划,而不是接管所有状态转移。一个安全的业务 Agent 往往把最不确定的解释与最确定的写操作严格分开。

外部服务既无幂等键又无查询能力时,超时后可能无法自动确定结果。此时应进入结果未知,停止盲目重试,带上请求标识和现场交给人工核对。自动恢复能力有边界,承认不知道比重复造成不可逆副作用更重要。

补偿也不是时间倒流。工单可以重新打开,但已发送的消息通常不能真正撤销,只能发送更正。跨系统部分成功要预先说明哪些操作可补偿、谁授权补偿、补偿失败怎么处理,不把「失败就回滚」写成一句没有实现的口号。

本文示例没有覆盖并发压力、真实身份鉴权、审批过期、操作记录保留期和跨服务投递。SQLite 事务能说明原子性,但单进程测试不证明分布式部署正确;上线前要在实际数据库和下游服务上增加崩溃恢复与并发重放测试。过程追踪复用可观测性,部署与接管流程复用生产化。

自测

同一业务请求重复到达和执行后响应丢失,怎样避免重复副作用?

稳定请求键绑定载荷摘要,同键同参读已有记录,同键异参拒绝;业务写入与操作回执在可覆盖的事务范围内共同提交。响应丢失先查持久记录,外部系统另需幂等合同或对账能力,不能把超时当成尚未执行。

审批后工单数据发生变化,为什么不能直接沿用批准结果?

审批绑定的是当时对象、动作与版本。执行时用预期版本做事务内条件更新,版本不符转重审,展示新资料而非强行覆盖;审批身份、权限和有效期也要在真实系统重新核验。

工单已改但通知失败时,恢复和最终核验分别做什么?

持久记录保留部分成功,恢复只补未完成通知,不重做工单写入;核验器读取工单状态、版本和对应通知结果,两项都符合才完成。后续流程若重新打开工单,verify 或重复 execute 会写入并保留 needs_review,通知记录保留历史执行证据;旧请求不再自动完成或重放工单副作用。

什么时候必须停止自动重试,模拟实验没有证明什么?

缺少幂等或对账能力、结果未知且副作用不可安全重放时应转人工;超过重试预算也须停止。实验仅验证本地事务、持久恢复与故障分支,没有真实模型、真实通知、真实资金、审批身份和分布式并发保证。

参考与延伸

  • Making retries safe with idempotent APIs,公开工程文档,发布日期未知,核验日期 2026-09-08:请求标识、参数不匹配与原子性。
  • LangGraph Interrupts,持续更新文档,发布日期未知,核验日期 2026-09-08:恢复重执行、中断与幂等副作用,不是业务恰好一次的保证。
  • 权限机制讲通用允许与拒绝;Agent 评估接收本例的故障场景作为回归题;模型选型负责比较计划生成候选,而不是代替业务核验。
本节点大纲与自测题(4 个子点 · 4 问)

大纲

  • 模拟工单的意图登记、幂等键与同键异参拒绝
  • 审批绑定载荷与版本,执行前条件校验及重审
  • 事务回执、执行后响应丢失、持久恢复与部分成功
  • 重复请求、审批后数据变化、通知失败及最终状态核验;真实与模拟边界

自测——答不上来就是缺口

  • 同一业务请求重复到达和执行后响应丢失,怎样避免重复副作用?
  • 审批后工单数据发生变化,为什么不能直接沿用批准结果?
  • 工单已改但通知失败时,恢复和最终核验分别做什么?
  • 什么时候必须停止自动重试,模拟实验没有证明什么?