一个大脑,两个前端:大促那天,用户被退了两次款|Agentic AI 落地方法论(十六)

《Agentic AI 落地方法论》系列第十六篇。 前面讲过单个 agent 怎么设计、怎么定级;这一篇讲多个 agent 拼在一起时,钱是从哪条接缝漏出去的——一个大脑,两个前端,接缝处没有幂等键。English version: One Brain, Two Front-Ends: Refunded Twice.
大促那天,一个用户被退了两次款
大促第一天,客服后台弹出一条投诉:一个用户申请了一笔退款,收到了两笔到账。财务对账时才发现,同一个退款申请,退款中台执行了两次,钱实打实退了两遍。这不是显示 bug,是真金白银出去了两次。
那笔重复到账是财务先发现的——对账时同一个退款单号下挂了两条打款记录,金额一模一样,间隔不到一秒。客服调出会话,用户只申请过一次,界面上也只点过一次「确认退款」。所有人第一反应都是「用户手快点了两下」,可翻前端日志,用户那次点击干干净净只有一次。真正的第二次,是机器自己发起的。
第一反应换成「退款逻辑写错了」,去翻退款那段代码——逻辑是对的,一个申请对一次退款,测试也覆盖了。可生产里它就是退了两次。顺着链路往上扒,问题不在任何一个 agent 内部,在两个 agent 之间那条没人看管的接缝。
这个系统是「一个大脑,两个前端」:公域平台(电商平台自带的客服入口)和私域平台(自有小程序 / 社群的客服入口)是两个独立的前端,但它们背后共用同一个 agent 大脑,退款这种写操作都经过同一层 L2 适配层下发到退款中台。那天出事的是私域前端:它调用适配层发起退款,网络抖动导致响应超时,私域前端按默认策略重试了一次。可第一次请求其实已经到达退款中台、已经执行了——重试等于又发起了一笔。退款中台这边,看到的是两个「看起来不一样」的请求(时间戳不同、请求 ID 不同),照单全收,退了两次。
这就是这篇要讲的那条接缝,一句话概括:一个大脑接两个前端,只要共用的那层写操作没有幂等键,一次超时重试就是一次重复退款。而多 agent 落地真正的瓶颈,是组织集成,不是模型质量。
下面把这条接缝拆成四个真实的坑,每一个都发生在「两个 agent 之间」而不是某一个 agent 内部,收尾给你一张能带去架构评审的核对清单。
公域前端 + 私域前端共用一个 agent 大脑,退款写操作都经过同一层 L2 适配层下发退款中台。接缝不在任何一个前端内部,在两个前端汇入的那一层——那里没有幂等键,重试就变重复退款。双退不是模型错,是接缝处少了一个幂等键
先把结论摆桌面上:双退跟模型、跟退款逻辑都没关系。错在两个前端汇入的那层适配层没有幂等键——它把「同一笔退款的重试」当成了「两笔不同的退款」。
复盘会上把那两笔退款的原始请求拉出来并排一看就清楚了:业务语义上是同一笔——同一个用户、同一个订单、同一个退款金额——但技术上,两次请求的时间戳差了 800 毫秒,请求 ID 是前端每次调用新生成的。退款中台只认请求 ID,两个 ID 不一样,就是两笔,就退两次。
问题的根不在退款中台「太老实」,在没有一个层级为「这是不是同一笔业务」负责。前端只管「我发了没有、超时了没有」,超时就重试,这是前端的本分;退款中台只管「收到一笔就退一笔」,这是它的本分。中间那层 L2 适配层,本该是唯一知道「公域和私域的请求最终都汇到我这里」的地方,却只做了透传,没做去重——它是唯一有资格拦住重复的人,却没拦。
修复不是改退款逻辑,是在适配层加一道幂等闸:用 (request_id, member_id) 这个组合做幂等键,在一个时间窗口(这里定的是 5 分钟)内,同一个组合只允许执行一次,重试进来直接返回第一次的结果,不再触发退款。为什么是这两个字段的组合而不是单纯的请求 ID?因为请求 ID 每次重试都变,挡不住重试;而业务上「同一个人、同一笔操作」才是真正要去重的单位——member 维度 + 一个业务请求标识,才框得住「这是同一笔」。
5 分钟这个窗口也不是随手定的:太短,网络慢一点的重试落到窗口外就挡不住;太长,用户当天真有第二笔合法退款(又买又退了另一单)会被误挡。5 分钟是「覆盖住绝大多数重试间隔、又不至于误伤真实二次操作」的平衡点——这个数字得按你自己的重试策略和业务节奏调,但必须有人为它负责,而不是留空。
而这个坑在测试里几乎永远抓不到,因为它只在超时那一刻才发生——正常链路一次成功,根本不会重试。只有生产的大促流量把超时概率顶上来,重试密集触发,双退才成规模冒出来。这也是接缝 bug 的通性:它落不进任何单元测试的覆盖面,因为它需要的是两个组件之间的时序错位,而单测测的是单个组件。 绿的测试套件挡不住它。
这里有条金句值得钉在墙上:幂等不是退款系统的责任,是「两个前端汇入的那一层」的责任。 单个 agent 再健壮,也管不了它和另一个 agent 共用下游时的重复——重复发生在接缝上,只能在接缝上挡。一次大促的量级,这种双退漏个几十笔就是几十万的资损,而且每一笔都要人工去追回、去安抚,比金额本身更贵的是信任。
私域前端发起退款→超时(但请求已达退款中台并执行)→前端重试→退款中台看到新请求 ID 又执行一次=双退。修复:适配层用 (request_id, member_id) 5 分钟幂等,重试直接返回首次结果,不再触发。refundType 两个系统定义相反,翻错一次就是退错一笔
接缝上第二类漏钱,比双退更隐蔽:两个系统对同一个枚举的定义是相反的。适配层做翻译时翻错一次,退款类型就错一笔,直接资损。
场景是这样的:退款中台有自己的一套退款类型枚举 refundType,订单系统那边也有一套。两套本该一一对应,可细看会发现——它们的取值顺序是反的。退款中台里 1 代表的类型,在订单系统里对应的是另一个值;适配层在两者之间翻译时,如果照着「数字对数字」直接映射,就会把「仅退款」翻成「退货退款」、把 A 类翻成 B 类。退款类型错了意味着什么?意味着该退 30 块的按另一类退了全款,或者该走退货流程的变成了只退款不收货——每一次翻错,都是一笔算错的钱。
举一个具体的:用户申请「仅退款」(不退货、只退钱),退款中台这边 refundType 该是某个值;可适配层照着订单系统的顺序映射,把它翻成了「退货退款」——系统于是等着用户把货寄回来才放款,用户明明选的是不退货,退款就卡在那里不动,用户以为没受理,投诉又来一轮。反过来的错更贵:该走「退货退款」的被翻成「仅退款」,货没收回、钱先退了,纯亏一件货。
这个坑最狠的地方在于:它不会报错。 翻译是成功的(数字都在合法范围内),退款也是成功的(退款中台照收),只有对账时、或者用户投诉时,才发现退错了类型。它藏在两个团队各自维护的两套枚举之间,谁单独看自己那套都没问题,只有跨过接缝去对,才发现对不上。
两套枚举为什么会反?因为它们是两个团队、在两个时间点、各自独立定义的——退款中台按自己的业务顺序排,订单系统按另一套历史顺序排,谁单独看都没错,错在没有一个人负责「这两套必须对齐」。接缝上绝大多数坑都是这个形状:不是谁写错了,是没人负责两者之间那层契约。 也正因如此,这类跨团队的枚举不一致,在整个项目的阻塞清单里是最难啃的一档——修它不是改一行代码,是要两个团队坐下来对齐「到底以谁的定义为准」。
检测动作很具体:任何跨系统的枚举映射,别信「数字对数字」,逐值列一张对照表,两个团队一起 review。 退款中台的 refundType=1 到底对应订单系统的哪个值,白纸黑字写出来,两边签字确认。适配层里那段映射代码,注释里必须写清楚「此处是跨系统翻译,改任一侧的枚举都要同步改这里」——因为它是两套契约之间唯一的胶水,胶水错位,两边都察觉不到。
router 的 enum 是安全边界:内部意图不该泄漏到 C 端
第三类接缝问题:一个 agent 内部用的意图,泄漏成了另一个前端能触发的动作。router 的 enum 不只是分类表,它是一道安全边界。
这个系统的 router 有一张动作意图枚举 ActionIntent,一开始有 15 个值。但仔细看,其中好几个是内部 / 坐席专用的意图——是给客服看板、给人工坐席用的操作,比如某些后台状态流转、某些只有坐席才能发起的动作。它们和面向 C 端用户的意图,混在同一张枚举里。
问题在于:C 端用户的输入也走这个 router。当一个 enum 里既有「用户能触发的」又有「只有坐席能触发的」,LLM 按语义相似度分类时,完全可能把一句用户输入路由到一个本该只有坐席能碰的内部意图上——一个面向用户的入口,意外获得了触发内部操作的能力。这不是功能 bug,是权限边界的泄漏:本该隔离的两类操作,因为挤在同一张枚举里,隔离没了。
举个例子体感一下:坐席看板里有一个后台状态流转的动作,本来只有人工坐席在看板上点。可它和 C 端意图挤在同一张 ActionIntent 里,当用户输入一句语义上沾边的话,router 完全可能把它路由到这个坐席动作上——一个普通用户,意外触发了一个本该只有坐席能碰的状态变更。用户不需要有任何恶意,他只是说了句话,边界自己就塌了。
修复是把枚举收窄:ActionIntent 从 15 个砍到 10 个,把内部 / 坐席专用的意图从 C 端 router 的枚举里彻底移出去,让 C 端 router 只认得 C 端该有的动作。这里的原则是:router 的枚举定义了「这个入口被允许做什么」的边界,凡是这个入口不该做的,就不该出现在它的枚举里。 一张枚举同时服务两类主体(用户 + 坐席),就是把两类权限焊死在了一起。
对架构负责人,这一节的检测信号是一句话:去数一遍面向 C 端的那个 router,它的意图枚举里有没有混进「只有内部 / 坐席该用」的值。 有,就是一道正在敞开的边界——不需要等它被利用,混在一起本身就是风险。
谁有权建工单,是架构决策,不是随手一调的接口
最后一类接缝问题在数据侧:多个 agent 共享一个「事实源」系统,谁有权往里写、写的东西怎么关联,是必须一开始就定死的架构决策。
这个系统里,工单系统是跨多个 agent 的 system-of-record(事实源)——不管是哪个前端、哪个 agent 处理的会话,最终要转人工、要留档、要追溯,都落到工单系统里。它是唯一一个「所有 agent 的动作最后都汇总到这里」的地方。
正因为它是事实源,两个决策就不能含糊。第一,谁有权建工单。 如果每个 agent、每个前端都能随手往工单系统里写,那事实源很快就被污染——同一个用户问题,公域 agent 建一张、私域 agent 建一张、转人工又建一张,三张工单描述同一件事,坐席根本不知道该看哪张。第二,一次会话产生的多张工单怎么关联。 一个用户会话里,可能触发多个动作、生成多张工单,如果它们之间没有一个共同的键串起来,事后就无法还原「这一次会话到底发生了什么」。这里用的是 session_id:所有由同一次会话衍生出来的工单,都带上同一个 session_id,靠它 join 回一次完整的会话。
真实见过的乱象就是前者:一次用户会话,公域 agent 判断要转人工、建了一张工单,中途用户切到私域继续问,私域 agent 又建了一张,最后人工介入时面前摆着两张描述同一件事的工单,坐席得先花时间判断这俩是不是一回事——本该省人力的 agent,反而给坐席加了道判断题。靠 session_id 把它们 join 起来之后,坐席看到的才是「一次会话、一个用户、一串动作」,而不是一堆对不上的孤立工单。
verdict 很直接:事实源系统的写入权限和关联键,是架构决策,一旦多个 agent 开始各写各的、各不关联,事实源就退化成一堆对不上的碎片。 这不是某个 agent 的实现细节,是整个多 agent 系统的地基——地基没打好,上面每个 agent 做得再对,合起来也是一笔糊涂账。
四条接缝——共用写操作层漏钱、跨系统枚举漏钱、C 端 router 漏权限、共享事实源漏事实——没有一条在单个 agent 内部;每条的修复都不是改代码,是让相关的两个团队坐下来对齐契约。多 agent 落地的瓶颈是谁给谁点头。检测工具箱:一张接缝核对表 + 5 个架构评审该逼问的问题
把上面四个坑倒过来,就是一张接缝核对表——它的核心是承认:多 agent 系统会漏钱、漏权限、漏事实的地方,几乎都不在单个 agent 内部,而在两个 agent 之间那条没人负责的接缝上。
评审一个多 agent 架构,逐项确认这些「接缝」有没有人负责:
| 接缝 | 漏什么 | 评审必须确认 |
|---|---|---|
| 两个前端共用的写操作层 | 漏钱(重复执行) | 有没有幂等键,键是不是业务维度(member + 请求标识) |
| 跨系统的枚举映射 | 漏钱(退错类型) | 逐值对照表 + 两团队签字,不信「数字对数字」 |
| 面向 C 端的 router 枚举 | 漏权限(内部意图外泄) | 枚举里有没有混进坐席 / 内部专用的意图 |
| 共享的事实源系统 | 漏事实(工单对不上) | 谁有权写、多工单靠什么键关联 |
| 每条能力的降级路径 | 漏兜底 | 接缝断了之后,能不能干净地转人工 |
然后是 5 个能带去架构评审、当场逼问的问题:
-
两个前端共用的那层写操作,有没有幂等键? 键是不是业务维度的(
member + 请求标识),而不是每次重试都变的请求 ID?没有,一次超时重试就是一次重复扣款 / 退款。 -
所有跨系统的枚举映射,有没有一张逐值对照表?
refundType这种两套系统各自定义的枚举,别信「数字对数字」,要两个团队白纸黑字对齐、签字。 -
面向 C 端的 router,枚举里有没有内部 / 坐席专用的意图? 一张枚举同时服务用户和坐席,就是把两类权限焊死——去数一遍,混进去的就是敞开的边界。
-
事实源系统谁有权写、多条记录靠什么键关联? 每个 agent 各写各的、各不关联,事实源就退化成对不上的碎片。
-
接缝断了之后的降级是什么? 幂等挡住了重试、翻译失败了、下游超时了——每一条接缝都要有一个干净的「转人工 / 拒绝执行」出口,而不是默默重试或默默放行。
这五条的共性是同一个视角转换:别只 review 每个 agent 自己做得对不对,去 review 两个 agent 之间那条接缝上,钱、权限、事实会不会漏。 而这些接缝问题的修复,几乎都不是改代码,是让两个团队坐下来对齐契约——所以多 agent 落地真正的瓶颈,是谁给谁点头,不是模型质量。
领取这篇的核对清单
如果你想把这套接缝审查直接用到下次架构评审——不用每次都翻这篇——我整理了一份工具包给读到这里的人。回复关键词「接缝清单」,我把《多 Agent 接缝核对表 + 5 个架构评审逼问问题》发给你:一页版接缝核对表(幂等 / 枚举映射 / router 边界 / 事实源 / 降级逐项打勾)、5 个逼问问题的卡片版、以及「问题在接缝不在单个 agent」的四类症状速查表。都是这一年跟多 agent 落地项目踩出来的判断工具。
回复渠道见页脚(公众号 / X)。不方便回复的,评论区留邮箱也行。
Subscribe for updates
Get the latest AI engineering posts delivered to your inbox.



