一张采购报价单,在系统里出现了两次。业务人员以为是供应商重复提交,查了日志才发现:智能体第一次调用创建接口时,请求已经到了服务器,单也写进去了,只是响应在返回路上超时。执行器判定失败,自动重试了一次。第二次同样成功,于是同一张报价单有了两条记录。
这不是模型「不够聪明」。模型只负责判断该做什么,真正决定「做几次」的是执行层。把工具调用当成普通函数调用,失败就重试,是这类问题最常见的来源:函数在本地执行,失败就是没执行;而业务接口在网络上执行,超时只说明「没收到结果」,不说明「没发生」。
这篇文章只讲一件事:智能体调用业务系统的写操作失败之后,按什么顺序处理。评测、提示词、权限边界不在本文范围内。文中涉及的重试、幂等与熔断做法,引自公开的工程文档与协议文本,写明月日与出处;具体参数要按客户系统的承载能力定,不套用统一数字。
一、四个想当然,最后都会变成对账问题
在把智能体接进 ERP、CRM、OA 的项目里,失败处理出问题,通常不是因为没写重试,而是因为四个想当然。
想当然一:接口报错就是没执行。超时、连接中断、网关 502,这三种情况都可能发生在请求已经被后端处理之后。此时重试一次,就可能多一条订单、多一张工单、多一笔应收。
想当然二:重试是执行器的事,模型不用管。执行器确实该管重试,但执行器必须知道这次调用属于哪一个业务意图。如果每次重试都被当成一次新任务,执行器再谨慎也无从判断。
想当然三:失败了就整段重跑。一个「下单」动作往往包含创建订单、锁库存、生成应收、发通知四步。第三步失败后整段重跑,前两步会再做一遍,第四步可能发两次通知。
想当然四:人工接管就是把对话转给客服。客服接到一句「这个单没下成功」,既不知道已经执行到哪一步,也不知道该补哪一步,只能从头查。接管如果只交接一句话,处理时间会比智能体介入之前更长。
这四个问题的共同点,是都把「调用业务系统」当成了「调用本地函数」。业务系统有状态、有副作用、有网络,失败处理必须按分布式系统的规则来,而不是按函数调用的规则来。
二、先分清失败类型,再决定要不要重试
失败处理的第一步不是重试,是分类。同样是红色报错,有的可以直接重试,有的必须先查,有的重试多少次都不会成功。下面这张表按「请求是否可能已经执行」来分,这是决定能不能重试的关键一列。
| 失败类型 | 典型信号 | 是否可能已执行 | 能否直接重试 | 处置 |
|---|---|---|---|---|
| 连接超时、响应丢失 | 客户端超时、连接被重置、读响应时中断 | 可能已执行 | 不能直接重试 | 带同一个幂等键重试,或先按业务单号查询 |
| 限流、服务暂不可用 | 429、503,通常带 Retry-After | 通常未执行 | 可以,按提示等待 | 退避后重试,设次数与总时限上限 |
| 认证过期 | 401,偶发 403 | 通常未执行 | 刷新凭据后一次 | 只刷新一次;仍失败转人工,不循环刷新 |
| 业务校验失败 | 参数缺失、状态不允许、重复单据 | 未执行 | 不能 | 把错误原文回给模型修正,或直接转人工 |
| 服务端 5xx | 500、502、504,无明确业务错误码 | 不确定 | 带幂等键可重试 | 先查执行记录;查不到再带键重试 |
| 多步动作中途失败 | 第 2 或第 3 步返回失败,前序步骤已成功 | 部分已执行 | 不能整段重跑 | 记录步骤状态,只补未完成步骤,必要时补偿 |
这张表里最容易被忽略的是最后一行。前五行处理的是单次调用,最后一行处理的是一个业务动作由多次调用组成的情况。很多项目的重试逻辑写得很完整,但仍然会重复下单,原因就是整段任务被当成了单次调用。
HTTP 协议本身已经把这件事说清楚了。RFC 9110 第 9.2.2 节的定义是:如果一个方法在多次相同请求下对服务器产生的预期效果,与单次请求相同,这个方法就是幂等的;在该规范定义的方法里,PUT、DELETE 与安全方法是幂等的。文档同时写明,客户端不应自动重试非幂等方法的请求,除非它能确认请求语义实际上是幂等的,或者能检测到原请求从未被执行。
智能体调用的「创建订单」「提交工单」「新增报价」这类接口,多数在语义上接近 POST:每调用一次就新增一条记录。它们默认不是幂等的,因此默认不能自动重试。要重试,就得先把「这一次业务意图」标出来,这就是下一节的幂等键。
三、幂等键:让同一次业务意图只落一次
幂等键不是随机数,也不是每次请求都新生成的追踪 ID。它是「同一次业务意图」的稳定标识:只要还是同一个意图,无论重试多少次,键都不变;换了一个意图,键才变。
一个可落地的组成方式是四段拼接:会话或任务 ID、业务主体(例如采购单号或客户编号)、动作名(例如创建报价单)、动作版本。四段都由执行器生成并持久化,不由模型生成。原因很直接:模型每次重试都会重新推理,生成的随机串大概率不一样,键一变,幂等就失效了。
键生成之后,需要在调用前写一条执行记录,字段至少包括:幂等键、请求参数摘要、目标系统与接口、当前状态(处理中、成功、失败)、响应摘要、创建时间与过期时间。写入时由数据库约束兜住「同一个键只有一条记录」,这样并发重试也不会同时执行两次。
并发是另一个容易漏掉的点。同一个键的第二个请求到达时,如果第一条还在处理中,正确做法是等待或直接返回「处理中」,而不是并行再发一次。这一点在人工点击与自动重试同时发生时尤其重要:操作人以为没反应又点了一次,执行器也判定超时重试了一次,两个请求同时到达后端。
保留多久,要按业务周期定,而不是越久越好。Stripe 的接口文档给出的做法可以作为参照:幂等键最长 255 个字符,建议使用 V4 UUID 或其他有足够随机性的字符串;对于 API v1,可以在键至少 24 小时后自动清理,键被清理后再次使用会被当成一个新请求;幂等层还会比较本次请求参数与原始请求是否一致,不一致就报错,避免误用。这里要读准一个细节:24 小时是「可以清理」的下限,不是「只需保留 24 小时」的承诺。跨月对账、跨季度结算的场景,保留期要覆盖到对账完成之后。
参数比较同样值得抄。只认键、不认参数,会出现「同一个键改了金额再提交」的情况,第二次被当成重试直接返回第一次的结果,业务以为改成功了,实际没有。执行记录里存参数摘要,键相同而摘要不同时直接拒绝,比事后查账便宜得多。
四、重试预算与熔断:别让重试变成第二轮故障
有了幂等键,重试才是安全的;但安全不等于无限。重试需要预算,预算要同时管三件事:次数、间隔、总时限。
次数是单次调用的上限,间隔决定给下游多少恢复时间,总时限决定一个任务最多能拖多久。只设次数不设总时限,在多层重试叠加时会出问题:一个任务调用六个工具,每个工具各自重试三次,最坏情况是十八次调用,前一次的超时还会把后面的时间挤掉。更合理的做法是把预算放在任务层,由编排器统一分配,工具层只负责上报失败类型。
间隔通常用指数退避加抖动。微软 Azure 架构中心的 Retry 指南给的原则是:瞬时故障应当预期,重试策略要与业务要求匹配;对交互式请求,重试次数少、间隔短,失败后直接给用户明确提示;对批处理,可以增加次数并使用递增延迟。文档还提醒,间隔很短、次数很多的激进重试,会让已经接近容量上限的服务进一步恶化。
退避之外还要看服务端给的提示。429 与 503 的响应可能带有 Retry-After,表示建议等待多久。忽略这个头、按自己的节奏重试,等于把服务端的保护措施绕过去。
当下游持续失败时,继续重试没有意义,应该让请求快速失败。熔断器就是做这件事的:正常时处于闭合状态,失败计数在时间窗内超过阈值就进入打开状态,后续请求立即失败;等待一段时间后进入半开状态,放少量请求试探,成功则恢复闭合,失败则重新打开。微软的 Circuit Breaker 文档还提到两个实践细节:可以为熔断器提供人工复位入口;打开状态下可以把失败请求记入日志,等服务恢复后重放。
日志的级别也要分开。Retry 指南建议把早期失败记为信息级,只有最后一次重试失败才记为错误级。否则一次下游抖动会产生大量错误日志,真正的故障反而被淹没。
还有一笔容易被忽略的成本:有些接口按调用计费。短信、地图、电子签章、发票开具,重试一次就是一次费用。对这类接口,重试预算要更紧,并且要在执行记录里单独统计「重试产生的调用量」,月底和账单对得上。
五、部分成功:把事务边界画在业务动作上
单次调用可以靠幂等键兜住,多步动作不行。一个「采购下单」在系统里可能是四步:创建采购单、锁定供应商库存、生成应付暂估、通知采购员。第三步失败时,前三步里已经有两步落了库,整段重跑会再创建一张采购单。
处理顺序取决于能不能改下游系统,通常有三条路。
第一条,把多步收敛成一个服务端业务动作。如果下游系统可以改造,最稳的做法是在服务端提供一个「下单」接口,由它在一个事务里完成四步,对外只暴露一个幂等键。智能体只调用一次,失败处理也只需要处理一次。这是成本最高、但后续维护最省的一条路。
第二条,状态机加补偿。下游不能改时,执行器要为这个业务动作维护一张步骤表,记录每一步的状态与返回。失败后不重跑整段,只补未完成的步骤;已经完成但需要撤销的步骤,走补偿动作,例如冲销暂估、释放库存。补偿动作本身也必须幂等,否则补偿也会重复。
第三条,先写草稿,最后提交。把创建与生效拆开:前几步只写草稿或暂存,允许重复覆盖;最后一步「提交生效」带幂等键,只允许成功一次。这样中途失败最多留下一张草稿,清理成本低,也不会产生正式单据。我们在报价单、工单这类场景里更常用这一条,因为它对下游改造的要求最小。
三条路都需要一个兜底:对账。按幂等键或业务单号,定期比对「执行记录」与「下游单据」,找出三类差异:有记录无单据、有单据无记录、同一业务单号多条单据。对账不是为了追责,是为了把「不确定」变成「确定」,然后再决定补写、冲销还是人工处理。
还要接受一个现实:不是所有动作都能补偿。已经开出的发票、已经发给客户的通知、已经出库的货,撤销的成本可能高于错误本身。这类动作要在设计时就被标成「不可逆」,执行前必须人工确认,而不是等失败后再想办法撤回。
六、人工接管:交接的是状态,不是一句话
自动处理有边界。连续失败、金额或数量超过阈值、涉及不可逆动作、幂等键冲突、下游返回「状态未知」,这些情况都应该停下来交给人。停下来不是失败,继续猜才是。
接管要交接五样东西,缺一样都会让处理时间变长:任务 ID 与幂等键;已经执行到哪一步、每一步的结果;失败的原始错误与分类;建议的下一步动作;原始请求与关键参数。把这五样放在一个页面里,处理人不需要再去翻三个系统的日志。
OpenAI Agents SDK 的文档把这类流程拆得比较清楚:工具可以声明自己需要审批,运行结果会把待审批项作为中断抛出;被暂停的运行可以序列化,等人批准或拒绝之后再恢复。文档还特别说明,审批面是整轮运行级别的,不限于当前智能体,经过交接或嵌套调用产生的审批同样会浮到外层。长任务场景下,文档建议把审批状态放在服务端,并处理进程崩溃后的恢复与待办任务的版本管理。
这套拆法值得借鉴的地方不是具体接口,而是三个原则。第一,审批是运行状态的一部分,不是聊天里的一句话;第二,暂停要能被序列化,进程重启后还能接着走;第三,审批范围要覆盖整条调用链,不能只拦最外层。
接管之后还要能回写。人工在业务系统里补完单据,执行器要能记录处理结果,并决定这个任务是结束、重试剩余步骤还是转成新任务。没有回写,接管就成了一次性的线下操作,下一次遇到同样问题仍然从头开始。
接管率本身也是一个指标。接管率长期偏高,说明工具设计或错误分类有问题,不该靠增加人手解决;接管率长期为零也不正常,往往意味着该停的时候没停。
七、留痕:四个问题要能回答
失败处理做完之后,要能回答四个问题:谁发起的、调了什么、传了什么、最后谁处理的。回答不了,复盘就只能靠猜。
执行记录至少要有这些字段:任务 ID、会话 ID、工具名与接口地址、幂等键、请求参数摘要、第几次尝试、开始与结束时间、耗时、状态码、错误分类、下游返回摘要、是否触发熔断、接管人与处理结果。字段不需要多,但要能串起来:从一条失败记录能追到同一个任务的所有调用,也能追到同一个人处理过的其他任务。
请求体不建议原样落库。报价单里有价格,客户资料里有联系方式,工单里有现场描述,全量保存既增加泄露面,也让保留期难以管理。更稳妥的做法是存摘要加引用:参数摘要用于判断是否同一个请求,完整内容留在原业务系统,靠单据号回查。
保留期按业务与合规要求定。财务相关动作的保留期通常长于普通操作日志,涉及个人信息的字段要脱敏。这一块没有统一答案,需要在项目开始时和客户的财务、法务一起定,而不是上线后再补。
八、交付物与验收
这类能力交付的不是一段重试代码,而是一套能查、能停、能补的机制。我们通常按六份交付物验收:
1. 工具清单与写操作分级表:逐个接口标明读或写、是否可逆、是否需要审批、是否计费。
2. 错误码映射表:把下游返回码映射成可重试、需查询、不可重试、需人工四类。
3. 幂等键规范与执行记录表:键的组成、生成位置、存储位置、保留期、并发处理方式。
4. 重试与熔断配置:任务级预算、单工具次数、退避曲线、熔断阈值与复位方式。
5. 补偿与对账脚本:步骤状态表、补偿动作清单、对账口径与差异处理流程。
6. 人工接管台与操作手册:接管页面、五样交接信息、回写方式、值班与升级规则。
验收指标建议按周记录,连续观察四到八周,以客户环境实测为准:
重复写入数。按幂等键与业务单号对账,统计同一业务意图产生多条正式单据的次数。目标不是「看起来没有」,而是对账结果为 0。
失败可归因率。所有失败调用中,能落到明确错误分类并带处置结论的比例。归不了类的失败,等于没处理。
自动恢复率。可重试类失败中,经退避重试后自动成功的比例。它反映重试预算是否合理,也反映下游稳定性。
接管处理时长与一次解决率。从进入接管到任务结束的中位时长,以及不需要二次返工的比例。接管不是越快越好,一次解决比快速关闭更有意义。
九、边界与成本
第一,不是所有下游都能做幂等。老系统没有合适的业务单号,接口也不接受外部传入的键,这时只能在执行器侧做记录与对账,重复风险无法降到零,只能用人工复核兜住。项目开始前要先把这类接口挑出来,不要等上线后才发现。
第二,重试会增加下游负载。对本身已经吃紧的系统,重试预算要收紧,必要时改成「失败即转人工」,把压力交回业务流程,而不是交给自动重试。
第三,补偿有边界。已开票、已发货、已对外通知的动作,补偿往往只是再开一张反向单据,账面上平了,客户侧的感受并没有撤回。这类动作更适合在执行前拦,而不是在执行后补。
第四,模型不承担事务语义。「只做一次」由幂等键、数据库约束与状态机共同兜住,不能靠提示词里写一句「不要重复提交」。模型可以判断该不该调用、调用哪个工具,但它无法知道上一次调用的响应是不是在路上丢了。
成本主要落在四处:下游接口改造、执行记录与幂等表、监控告警与值班、接管人力。前两项是一次性投入,后两项是持续成本。项目报价时如果把后两项漏掉,上线后的运维会觉得「怎么一直在补」。具体金额按接口数量与改造范围评估,不同系统差异很大,不适合用一个统一数字概括。
十、常见问题
智能体重试后重复下单,是模型的问题吗?
多数不是。模型只决定调用哪个工具,重试由执行器触发。重复写入通常说明执行层没有幂等键,或者键在每次重试时被重新生成。先查执行记录里同一个业务意图对应几个键,就能定位。
所有接口都需要幂等键吗?
不需要。读操作重复执行通常没有副作用,重点是写操作,尤其是新增、提交、扣减、开票这类会改变业务状态的接口。是否可逆、是否计费,也应该一起标在工具清单里。
幂等键应该由模型生成还是程序生成?
由程序生成。模型每次重试都会重新推理,生成的随机串很可能不同,键一变,幂等就失效。正确的位置是执行器在发起调用前生成并持久化,重试时复用同一个键。
重试几次比较合适?
没有通用数字。要看业务能容忍多久、下游能承受多少、接口是否计费。可落地的做法是给任务一个总时限,再给单次调用一个次数上限,两者取先到者;同时尊重服务端返回的 Retry-After。
人工接管是不是越多越安全?
不是。接管要有明确触发条件,例如连续失败、金额超阈值、不可逆动作、状态未知。接管率长期偏高,说明工具或错误分类需要改;为了「安全」把所有失败都转人工,只是把自动化的问题推迟到人力上。
附:口径与来源
1. 幂等方法的定义、PUT 与 DELETE 及安全方法属于幂等、客户端不应自动重试非幂等请求:IETF《RFC 9110: HTTP Semantics》第 9.2.2 节,2022 年 6 月发布。
2. 瞬时故障的重试策略、指数退避、激进重试会加重下游负载、早期失败记信息级日志:微软 Azure 架构中心《Retry pattern》,页面标注更新于 2025 年 12 月。
3. 熔断器的闭合、打开、半开三种状态,人工复位,打开状态下记录并重放请求,429 与 503 的提示:微软 Azure 架构中心《Circuit Breaker pattern》,页面标注 2025 年 2 月。
4. 幂等键最长 255 个字符、建议 V4 UUID、API v1 可在至少 24 小时后清理、参数不一致时报错:Stripe 接口文档《Idempotent requests》,页面口径为 2026 年 11 月访问时。
5. 工具声明审批、待审批项作为中断抛出、运行状态可序列化并恢复、审批面覆盖交接与嵌套调用、长任务把审批状态放在服务端:OpenAI Agents SDK 文档《Human-in-the-loop》,页面口径为 2026 年 11 月访问时。
6. 文中涉及的具体重试次数、超时时间、接管阈值均为工程做法示例,需按客户系统承载能力与业务时限设定;验收指标以客户环境实测为准。