这次我们来看一个很值得 AI 应用开发者反复琢磨的工程观点TikTok 工程师在 AI Engineer 主题分享里提到AI Agent 本质上是分布式系统。很多人听到 Agent 第一反应是大模型、提示词、工具调用但真正让 Agent 在上线后翻车的往往是分布式系统才有的那一堆问题重复请求、状态丢失、并发竞争、超时重试。最典型的案例就是重复退款用户在前端多点了一次或者 Agent 在调用支付工具时超时重试结果同一个订单被退了两次钱。这篇文章会把这条观点拆开讲。先说明为什么 Agent 是分布式系统再围绕「重复退款」这个高频故障讲清楚根因、幂等设计、Saga 补偿、状态持久化和可观测性。最后给出一套可以在自己项目里直接落地的测试流程和排查清单。如果你想做 AI Agent 产品或者在做支付、订单、库存这类强一致性的 Agent 工作流这篇文章可以直接收藏。1. 核心结论AI Agent 就是一套分布式系统先看结论。单次 LLM 调用本质上是一个无状态请求输入提示词返回文本链路短失败重试的成本也低。但 Agent 不一样Agent 要自己规划步骤、调用多个工具、拼接上下文、记忆中间结果甚至还要在多个服务之间来回传递数据。从工程视角看Agent 的工作流已经具备分布式系统的全部特征对比维度单体 LLM 调用Agent 工作流分布式系统特征执行链路请求 - 模型 - 响应规划 - 工具调用 - 状态更新 - 下一步规划多节点、多跳协作状态无状态或仅会话级任务级状态跨多个步骤持久化状态分布式存储失败模式超时、限流部分成功、部分失败、重复执行部分失败与一致性并发单请求隔离多任务并发、共享资源竞争并发控制、锁、幂等重试语义可安全重试重试可能导致重复扣款、重复退款幂等语义缺失可观测性单次日志跨服务、跨步骤链路追踪全链路追踪与监控所以与其把 Agent 当成一个「会自己思考的程序」不如把它当成一个「由模型做决策的分布式任务系统」。模型负责规划但执行层必须按分布式系统的标准来建设幂等、事务、补偿、重试、状态恢复、可观测性一个都不能少。很多团队第一次做 Agent精力全花在提示词和模型选型上结果一上线就发现重复退款、重复发消息、重复创建订单。这不是模型太笨是底层架构没有按分布式系统的规则来做防护。2. 重复退款一个典型的分布式系统故障重复退款问题的表象是「钱退多了」本质是「同一个退款请求被系统执行了多次且没有被拦截」。先看一个典型的 Agent 退款流程用户提交退款申请。Agent 判断订单状态和退款原因。Agent 调用支付网关执行退款。支付网关返回超时Agent 侧等待失败。Agent 重试或用户再次提交。支付网关实际已经退款成功第二次调用又退了一次。这个流程听起来很简单但每一步都可能产生重复重复场景触发原因对应的分布式问题用户重复提交前端按钮未置灰、用户连点重复请求客户端自动重试网络抖动导致请求超时客户端自动重发超时重试导致重复投递Agent 内部重试LLM 判断退款工具调用失败再次触发同一工具无状态重放网关回调重复支付网关回调多次回调幂等缺失状态丢失后恢复Agent 进程重启内存中的任务状态丢失任务被重新拉起状态未持久化多实例并发同一个任务被多个工作节点同时消费分布式锁缺失值得注意的是这里面大多数场景都跟「模型能力」无关。即使 LLM 每一步都判断正确只要底层没有做幂等重复退款依然会发生。TikTok 工程师强调的正是这一点不要试图用模型的「聪明」来兜底要在架构层把重复执行的后果消除掉。对于 Agent 这类由 LLM 自动决策的系统来说重复问题更隐蔽。普通 API 服务可以在入口层统一做幂等但 Agent 的每一次工具调用都可能是一个独立的分布式动作必须把幂等能力下沉到工具调用层和任务链路层。3. Agent 工作流的分布式要素拆解要像设计分布式系统一样设计 Agent先要把 Agent 工作流拆成几个核心组件。3.1 编排者Orchestrator编排者负责整个 Agent 任务的生命周期接收任务、调用 LLM 做规划、调度工具、维护状态、处理重试和失败。编排者相当于分布式系统中的「流程引擎」或「调度中心」。它必须是无状态的任务状态要放到外部存储中这样即使编排者崩溃其他节点也能接手继续执行。3.2 执行器Executor执行器负责真正调用外部工具支付网关、订单系统、消息推送、数据库查询等。执行器是故障高发区因为外部服务不可控超时、限流、返回异常、响应丢失。执行器必须把每一次调用都记录清楚并且支持「查询上一次执行结果」的能力。3.3 记忆与状态存储Memory / State StoreAgent 在多个步骤之间需要共享信息用户意图、订单号、工具返回结果、当前执行到哪一步。这些状态不能只放在模型上下文里因为模型上下文会滚动丢失进程崩溃也会丢。必须把关键状态持久化到 Redis、MySQL 或专门的流程状态库中。3.4 工具网关Tool Gateway工具网关是对外调用的统一入口也是最容易加防护的位置。所有 Agent 工具调用都走网关网关负责幂等校验、限流、超时控制、重试策略和审计日志。可以这样理解如果没有工具网关LLM 可以直接调用任意服务重复执行、并发冲突、权限混乱都会失控加上网关后Agent 的调度能力和底层系统的防护能力就解耦了。3.5 可观测性组件Agent 是多步决策系统必须能够回答这几个问题当前任务在哪个步骤每个步骤的输入输出是什么哪些工具被调用过调用结果是什么失败原因是什么分布式系统的日志、追踪、指标三件套在 Agent 场景里一个不能少而且要记录到「工具调用级」。4. 幂等设计防重复退款的第一道防线幂等是指同一个操作执行一次和执行多次产生的效果相同。在退款场景里最直接的做法是每个退款任务只允许成功一次重复请求直接返回第一次的结果。4.1 幂等键设计幂等键是每次业务操作的唯一标识。在 Agent 退款链路里幂等键需要满足设计要求说明全局唯一每次退款请求必须携带唯一 ID稳定不变同一个业务操作的重试必须复用同一个 ID可追溯幂等键由任务 ID 工具调用 ID 业务参数指纹拼接生成跨服务传递从 Agent 编排层生成后透传到工具网关和支付系统错误做法是每次重试都生成一个新的 UUID这样幂等系统无法识别重复请求。正确做法是任务创建时就生成一个task_id任务内部每次工具调用生成tool_call_id二者组合作为幂等键。4.2 基于 Redis 的幂等锁最简单的实现用 Redis 的SETNX做幂等标记。import redis import uuid r redis.Redis(host127.0.0.1, port6379, db0) def try_acquire_idempotency_key(idempotency_key: str, ttl: int 3600) - bool: 尝试获取幂等锁。 返回 True 表示第一次请求可以继续执行。 返回 False 表示重复请求应直接返回已有结果。 result r.set( fidempotency:{idempotency_key}, processing, nxTrue, exttl, ) return result is True def complete_idempotency_key(idempotency_key: str, result: str) - None: 任务成功后保存最终结果后续重复请求直接返回该结果。 r.set(fidempotency:{idempotency_key}, result, ex86400) def get_idempotency_result(idempotency_key: str) - str | None: 查询已完成的幂等结果。 value r.get(fidempotency:{idempotency_key}) if value and value ! processing: return value return None使用逻辑idempotency_key frefund:{task_id}:{tool_call_id} if not try_acquire_idempotency_key(idempotency_key): # 重复请求取已有结果 result get_idempotency_result(idempotency_key) return {status: duplicated, result: result} # 执行业务逻辑 refund_result call_refund_api(order_idorder_id, amountamount) # 保存结果 complete_idempotency_key(idempotency_key, refund_result)这个方案的要点是第一个请求执行期间后续请求会看到processing状态此时不应该直接放行而是返回「任务处理中」或者等待。否则在并发请求下两个请求都可能因为拿不到最终结果而继续重试。4.3 基于数据库的幂等表Redis 的方案适合高并发拦截但如果要强一致和审计数据库幂等表更可靠。CREATE TABLE idempotency_records ( id BIGINT AUTO_INCREMENT PRIMARY KEY, idempotency_key VARCHAR(128) NOT NULL COMMENT 幂等键, request_body TEXT COMMENT 请求体, response_body TEXT COMMENT 响应体, status TINYINT NOT NULL DEFAULT 0 COMMENT 0处理中 1成功 2失败, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_idempotency_key (idempotency_key) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 幂等记录表;数据库方案的优点是唯一索引能从根本上拦截重复插入缺点是每次请求多一次数据库写入。在实践中可以组合使用Redis 做前置拦截数据库做最终一致性保证。4.4 在 Agent 工具调用层强制幂等对于 Agent 项目不能只依赖业务系统做幂等。建议在工具网关层自动注入幂等键Agent 每发起一次工具调用网关读取当前任务的task_id和调用序号。网关自动生成tool_call_id附加到请求头。后端业务系统根据请求头中的幂等键做去重。这样做的收益是即使 LLM 因为幻觉或状态混乱重复调用同一个工具网关也会拦截不会产生真实业务影响。5. Saga 事务与补偿多步骤 Agent 任务的一致性保障退款不是单步操作。真实场景下退款可能涉及支付网关退款、积分回滚、优惠券恢复、消息通知等多个步骤。如果第 3 步失败前面已经执行成功的步骤必须恢复原状。这就是分布式事务里的 Saga 模式把一个长事务拆成多个本地事务每个本地事务都有对应的补偿动作。正向执行失败或业务判定失败时按逆序执行补偿。5.1 退款 Saga 流程定义{ saga_id: saga_refund_20250101, task_id: task_xxx_001, steps: [ { step_id: 1, name: lock_refund, action: refund_service.lock_refund, compensation: refund_service.unlock_refund, status: pending }, { step_id: 2, name: deduct_points, action: points_service.recover_points, compensation: points_service.cancel_recover, status: pending }, { step_id: 3, name: restore_coupon, action: coupon_service.restore_coupon, compensation: coupon_service.cancel_restore, status: pending }, { step_id: 4, name: notify_user, action: notification_service.send_refund_notify, compensation: none, status: pending } ] }5.2 Saga 状态流转Saga 执行器按顺序执行每个步骤步骤成功 - 标记succeeded继续下一步。步骤失败 - 标记failed从当前步骤开始向前执行补偿。补偿成功 - 标记compensated。补偿失败 - 触发告警进入人工处理队列。需要特别注意的是补偿动作本身也必须幂等。如果补偿操作执行了一半又失败重试补偿时不能产生新的副作用。所以补偿动作要使用和正向操作一样的幂等键体系并在补偿逻辑里记录补偿状态。5.3 Agent 场景下 Saga 的特殊处理传统 Saga 的每个步骤都是确定性服务输入相同则输出相同。但 Agent 场景里某些步骤可能由 LLM 决策输出不确定。这会带来一个隐患补偿时无法准确知道「当时做了什么」。解决方案是给每个步骤增加「确定性快照」记录 LLM 决策时的原始输入。记录工具调用的入参和出参。补偿逻辑不依赖 LLM 重新决策而是根据快照执行固定反向操作。也就是说在涉及资金、权益、库存等强一致场景时不要让 LLM 直接决定补偿动作。LLM 可以做任务拆解和解释但真正执行正向和反向操作的必须是确定性的代码。如果业务允许建议在关键节点加入人工审批退款金额超过阈值、用户投诉、多轮退款失败时挂起任务等人工确认。6. 状态持久化与可观测性让 Agent 行为可回放重复退款问题出现后最怕的是查不出原因。Agent 是多步决策一次任务可能调用 5 个、10 个工具任何一步出错都可能导致最终结果异常。没有状态持久化和可观测性排查问题只能靠猜。6.1 工作流状态持久化每个 Agent 任务需要存储以下状态状态字段说明task_id任务唯一标识status任务状态pending / running / succeeded / failed / compensatedcurrent_step当前执行到第几步steps_snapshot步骤快照包含每个步骤的执行状态和补偿状态input_data任务初始输入output_data任务最终输出error_message失败原因created_at / updated_at时间戳状态更新建议用「追加 版本号」的方式而不是直接覆盖。每次状态变更都写入一条状态变更记录这样即使后续出现问题也能完整还原任务的执行轨迹。6.2 全链路 Trace IDAgent 的一次任务会跨多个服务和多次工具调用必须用 trace_id 串起来。import uuid import logging task_id str(uuid.uuid4()) # 每次工具调用都透传 trace_id headers { X-Trace-Id: trace_id, X-Task-Id: task_id, X-Tool-Call-Id: tool_call_id, } logger logging.getLogger(agent) logger.info( tool_call_start, extra{ trace_id: trace_id, task_id: task_id, tool: tool_name, args: tool_args, }, )日志里至少包含任务 ID、trace ID、工具名、入参、出参、耗时、状态码。这样回放一条完整链路时才能准确看到「哪一步调了什么、结果是什么、为什么走到重复执行」。6.3 关键指标与告警指标定义告警建议工具调用成功率成功调用数 / 总调用数低于 95% 触发告警幂等拦截命中率被幂等拦截的请求数 / 总请求数突然升高说明存在重复风暴任务失败率失败任务数 / 总任务数高于阈值触发告警Saga 补偿触发次数补偿动作执行次数补偿频繁说明正向链路不稳平均完成时长任务从开始到结束的耗时超时增加说明存在阻塞7. 落地验证在测试环境里模拟重复退款架构设计得再完整没有验证等于没做。下面给出一套测试流程可以在自己的 Agent 项目里跑通重点验证重复退款是否会被拦截。7.1 测试环境准备准备一个模拟支付网关支持以下行为正常退款成功。延迟 5 秒返回。直接返回超时。再准备一个最小 Agent 工作流用户提交订单号 - Agent 调用退款工具 - 返回退款结果。7.2 测试用例测试编号测试场景操作预期结果T01单次正常退款提交退款请求退款成功状态为 succeededT02用户重复点击同一个任务 ID 连续提交 3 次仅第一次真正执行后两次返回已处理结果T03网关超时重试模拟网关超时触发 Agent 重试重试不产生第二次退款T04进程重启恢复任务执行中杀掉进程重启后恢复任务任务从断点恢复不重复执行已完成步骤T05多实例并发消费同一任务被两个 worker 同时消费只有一个 worker 执行成功另一个被幂等拦截T06补偿链路积分回滚失败触发 Saga 补偿已执行的退款被正确补偿最终状态为 failed/compensatedT07回调重复支付网关回调退款结果 2 次回调处理为幂等不产生副作用7.3 验证脚本示例import requests # 模拟用户重复提交退款 url http://127.0.0.1:8000/api/agent/refund payload { task_id: task_test_001, order_id: order_001, amount: 99.00, reason: user_request, } for i in range(3): response requests.post(url, jsonpayload, timeout30) print(f第 {i 1} 次请求 status{response.status_code}, body{response.text})执行后判断标准第一次请求正常进入退款流程第二、三次请求返回幂等命中的结果且模拟支付网关只收到一次退款指令。8. 常见问题与排查清单问题现象可能原因排查方式解决方案用户重复提交后出现两笔退款入口层未做幂等查看支付网关调用日志确认同一 power键是否被传给网关在 API 层生成幂等键透传到工具网关和支付系统Agent 超时重试导致重复调用重试时重新生成了幂等键对比两次调用的请求参数和幂等键重试时复用同一个 task_id 和 tool_call_id进程重启后任务从头执行任务状态未持久化检查任务状态表是否有更新记录将任务状态持久化到数据库或 Redis补偿执行后数据不一致补偿动作没有做幂等查看补偿日志确认是否重复执行补偿动作使用独立幂等键并记录补偿状态多实例并发导致重复退款缺少分布式锁或唯一索引查看两个实例的日志时间线用 Redis SETNX 或数据库唯一索引做并发拦截支付网关回调重复回调处理逻辑未幂等检查回调处理是否依赖外部状态回调处理先查幂等记录存在则直接返回成功日志查不到完整链路没有透传 trace_id搜索任务 ID 无法串联全部日志每次工具调用都记录 task_id、trace_id、tool_call_idSaga 补偿失败卡死没有补偿重试机制查看失败任务是否处于 stuck 状态增加补偿重试队列和人工处理入口排查时建议按以下顺序进行先确认是「重复请求」还是「重复执行」。重复请求未拦截属于幂等缺失重复执行属于状态恢复和锁的问题。再对比两次请求的幂等键。幂等键相同但都执行了说明幂等校验没生效幂等键不同说明生成规则有问题。最后看状态流转记录。通过任务状态表和工具调用日志还原当时的执行顺序。9. 工程建议与总结回到 TikTok 工程师那条分享AI Agent 本质是分布式系统。这句话的价值在于它给出了一个清晰的工程判断标准——你在构建 Agent 时是否会像对待分布式系统一样对待它如果答案是否你的 Agent 迟早会遇到重复退款这类问题。几条工程建议第一次做 Agent 工作流时优先把幂等、状态存储、重试、日志追踪这四项基础能力搭好再优化提示词。涉及资金、库存、权益等场景正向操作和补偿操作都要确定性执行LLM 只负责规划和解释不直接操作业务结果。所有工具调用统一走网关网关层自动注入幂等键和链路 ID。任务状态必须持久化不能依赖模型上下文或进程内存。上线前至少跑一遍「重复请求」「超时重试」「进程重启」「并发消费」四类测试。重复退款本质上是分布式系统一致性问题不能用「再加一句提示词」来解决。把 Agent 当分布式系统工程化才能让它在生产环境里真正稳定。这套思路同样适用于消息发送、订单创建、权限变更等所有容易被 Agent 重复执行的高风险操作。如果正在做 AI Agent 应用尤其是涉及支付、订单、客服自动化这类方向建议先把这篇文章里的幂等和 Saga 方案落到自己的项目里试一遍再上线也不迟。