ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

把 Agent 当作分布式状态机:可靠性工程的落地实践

把 Agent 当作分布式状态机:可靠性工程的落地实践 凌晨一点四十七分运维群里弹出一条消息“生产环境的 Agent 任务在 37 步卡住了重试三次全挂整个调研任务挂起用户问了几次什么时候能好。”这个画面我太熟悉了。做 Agentic AI 基础设施这段时间我见过太多“跑着跑着就没了”的任务有的 Agent 死循环在某个工具调用上有的重启后完全不记得自己干到哪一步有的同一个操作重复执行了三遍。这些问题的根源其实高度一致——大家把 Agent 当成“一次性脚本”来写可 Agent 本质上是一台长期运行的分布式状态机。这个认知一旦建立起来许多坑都可以提前避开。这篇内容我会从状态机的角度拆解 Agent 的可靠性问题适合正在搭建 Agent 应用的开发者、维护 Agent 基础设施的工程师以及对 Agentic AI 架构原理感兴趣的人。1. 先建立心智模型为什么说 Agent 是一台分布式状态机1.1 状态机不是高深概念一个例子讲透很多人一听“状态机”三个字就头皮发麻觉得这是计算机专业课里那种画满圆圈和箭头的图。其实用一个生活场景就能说清楚你点了一份外卖订单状态只有几种可能——已下单、商家已接单、配送中、已送达、已取消。状态和状态之间不是随便跳的商家接单之前不可能跳到“配送中”订单取消之后也不可能回到“配送中”。系统在任意一个时刻都处于这些有限状态中的某一个并且下一个状态由当前状态和外部事件共同决定。这就是一个标准的有限状态机。状态机模型真正厉害的地方不在于它能描述多少种状态而在于它强制你承认一件事任何时刻系统都有一个确定的“现在”。有了这个确定的“现在”你才能回答三个致命问题——系统现在卡在哪系统过去经历了什么系统未来还能不能继续走对于外卖系统来说答案都写在订单表里。对于 Agent 来说答案应该写在哪里很多团队答不上来因为他们压根没把 Agent 的运行过程当成状态流转来看。1.2 Agent 与状态机的对应关系逐项拆解把一次完整的 Agent 执行过程摊开看你会发现它和状态机的结构一一对应。Agent 的“状态”至少包含以下几类信息当前执行到哪个阶段、已经收集了哪些信息、已经调用过哪些工具、推理产生的中间结论、尚未处理的待办子任务。Agent 的“迁移函数”则是 LLM 本身——模型接收当前状态通常以 prompt 形式呈现决定下一步动作动作导致状态发生变化。传统状态机的迁移函数是一段确定性代码输入同样的状态永远得到同样的输出Agent 的迁移函数是概率性的同一个状态给同一个模型两次可能走出不同的路径。这个不确定性正是所有可靠性问题的起点。传统状态机还有“终止状态”的概念要么成功终态要么失败终态。Agent 任务也应该有明确的终态目标达成、目标判定为不可达成、或者用户主动终止。但在实际系统里我见过太多 Agent 任务压根没有定义终态模型在一个目标已经完成之后继续“发挥”或者在一个无法逾越的障碍前反复重试本质上就是状态机缺少了合法的终止转移条件。这不是模型的问题是设计问题。1.3 这套心智模型能带来什么把 Agent 看作状态机最大的价值不是“听起来很专业”而是能立刻借用分布式系统领域几十年积累的治理工具。状态机要求状态可查询、可持久化、可恢复于是自然引出状态存储选型、事件日志、检查点机制。状态机要求迁移过程有记录于是自然引出 trace 日志、步骤审计、操作回放。状态机要求状态转移可靠于是自然引出幂等、重试、超时、补偿事务。这些工具都不是 AI 领域的新东西但套在 Agent 上异常好用。反过来说不把 Agent 当状态机而当成一个“放大的函数调用”就会出现我最常看到的那类事故进程一重启Agent 忘掉自己做过什么任务一失败没有中间现场可以查同一个操作被重复执行了两次下游系统收到两笔订单。函数出错了你重跑一遍就行状态机出错了你得先搞清楚“现在到底在哪个状态”。2. 长期运行Agent 的可靠性分水岭2.1 短期任务和长期任务的本质区别不是所有 Agent 都需要复杂的可靠性设计。你写一个“总结这条新闻”的 Agent从输入到输出不超过三十秒中途崩了大不了让用户重新点一次按钮这不叫长期运行。真正需要认真对待的是那种生命周期长达几小时甚至几天的任务自动生成市场调研报告、批量处理几百个文件、持续监控系统日志做根因分析、多 Agent 协同完成一个大型研发任务。这些任务的共同点是执行路径长、外部依赖多、中间结果价值高、失败代价大。长期运行任务的第一个残酷现实是任务还没跑完运行环境早就变了。进程可能被重启、容器可能被调度走、网络可能抖动、模型 API 可能限流、外部服务可能升级接口。如果 Agent 的所有状态都放在内存里环境一变一切归零。就好比你爬一座需要三天的山爬了一天半宿营休息第二天醒来发现帐篷、补给、地图全被吹走了还得从山脚重新开始。这不是可靠性问题这是设计缺陷。2.2 中间结果膨胀与上下文管理长期运行会带来一个短期任务完全遇不到的问题Agent 的“记忆”越来越重。每执行一步Agent 都会产生新的观察结果、中间推理、工具返回值。如果不加控制几十步之后上下文就塞满了模型开始“忘记”最开始的任务目标表现就是答非所问、重复劳动、行为漂移。我把这个现象叫做“长跑失忆症”。解决思路不是无限扩大上下文窗口而是引入真正的记忆分层。短期记忆保存本轮执行的关键上下文中期记忆沉淀阶段性结论长期记忆只留下跨任务复用的模式和经验。每一步都往长期记忆里塞原始日志相当于把整个仓库都装进背包里爬山必死无疑。我的经验是原始过程日志进事件存储模型真正读取的只有压缩后的结构化摘要。2.3 部分完成也是结果断点续跑的意义长期任务还有一个容易忽略的语义用户可能不需要“100%完成”而是需要“目前已知的全部”。我做过一个竞品信息收集任务三个数据源里有两个成功、一个持续超时。如果系统设计成“全部成功才算成功”这个任务永远无法结束。但如果把任务定义为“尽可能收集记录失败数据源最终输出时标注数据缺口”这个任务就是一个带警告的正常交付。这要求 Agent 的执行框架支持中间结果的落盘、支持任务的暂停与恢复、支持对失败子任务的降级处理。我把这个能力称为“优雅的部分完成”。它不是降低标准而是把可靠性从“能不能跑完”提升到“跑到哪算哪、没跑的部分有交代”。这个理念是我推荐所有做长时间 Agent 任务的人先想清楚的第一件事。3. 分布式状态管理让状态有唯一的权威来源3.1 状态天然是分散的如果你只跑一个单 Agent 的简单任务其实谈不上“分布式”。但稍微复杂一点的系统状态就会悄悄散落得到处都是。主控 Agent 有一套编排状态知道现在该执行哪个子任务子 Agent 各自有自己的执行状态知道自己进度如何记忆存储里有向量化的上下文外部系统里还有 Agent 触发的实际结果比如已经发出的邮件、已经创建的任务单、已经扣费的订单。这些状态分散在不同的进程、不同的存储、甚至不同的外部服务里。问题的核心在于这些状态之间没有天然的一致性保证。主控以为子 Agent 完成了子 Agent 实际崩了Agent 以为自己还没发邮件实际上邮件已经在第一次超时前发出去了只是响应没回来。分布式系统领域把这类问题叫“状态的不一致”而在 Agent 系统里这种不一致几乎是常态。3.2 用事件溯源记录 Agent 的执行轨迹我处理分布式 Agent 状态时最依赖的工具是事件溯源Event Sourcing。思想很简单不保存“当前状态”本身而是保存“状态变更的每一个事件”。当前状态可以随时通过重放事件计算出来。对应到 Agent就是把 Agent 的每一步执行轨迹——思考thought、动作action、观察observation——都持久化为追加式的事件日志。任何时候想知道“Agent 现在为什么处于这个状态”重放一遍日志就清楚了。事件溯源最大的好处是天然适合审计和恢复。某一步之后系统崩溃了恢复时从最后一个快照开始重放后续事件就能还原崩溃前的执行现场。我在生产系统里用一张 event 表记录 Agent 的每一步字段包括 trace_id、step_id、事件类型、事件内容、时间戳。配合上检查点机制恢复时间往往可以控制在秒级。很多团队舍不得这一步的存储成本但以我实测的经验看这钱花得值——排查问题时的效率提升远超那点存储开销。3.3 幂等设计分布式状态管理的救命稻草状态分散带来的最危险问题就是副作用被重复执行。Agent 重试一个工具调用时如果工具本身不具备幂等性就可能出现重复扣费、重复发消息、重复建单。解决办法是在 Agent 框架层面引入统一的幂等机制每一步动作生成一个全局唯一的操作 ID所有副作用调用都携带这个 ID接收方记录“这个 ID 我已经处理过了”。幂等的实现有两种常见策略。第一种是依赖外部系统的去重能力比如支付接口的商户订单号唯一约束第二种是在自己的状态存储里做“已执行动作表”执行副作用前先写入一条“预执行”记录成功后再更新为“已执行”。我用后一种方案兜底因为它不依赖外部的配合自己就能控制。预执行记录要带有唯一约束这样即使两个重试请求同时到达也只有一个能插入成功。这一步是 Agent 防重复执行最扎实的防线。4. 可靠性工程落地存储、恢复与幂等的实战方案4.1 状态存储选型不同状态用不同的仓库状态存储没有银弹我的做法是按状态类型分而治之。Agent 的步骤级事件日志放在关系型数据库里方便查询和审计高频读取的当前状态放在 Redis 里保证访问延迟大体积的中间产物例如抓取下来的网页、生成的图片、临时的数据快照放在对象存储里成本低、容量大。这里有一个关键原则Redis 里的状态永远只是缓存数据库/对象存储里的才是权威状态。很多人会问我为什么不直接把所有状态都放 Redis因为 Redis 的持久化机制在高写入场景下做不到强一致而且内存容量有限。关系型数据库虽然慢一点但事务特性、唯一约束、数据可靠性都是现成的。我做选型时看一张简单的对照表核心就是问自己一个问题这个状态丢了我能承受吗状态类型推荐存储理由步骤事件日志关系型数据库需要事务、查询、审计当前执行状态Redis 数据库双写兼顾延迟与可靠性大体积中间产物对象存储容量大、成本低向量化记忆向量数据库需要语义检索能力4.2 持久化策略事件流加定时快照状态存储定下来之后下一个问题是写入频率。每个步骤都同步写数据库性能和成本都受不了完全不写崩溃恢复就无从谈起。我采用的折中方案是“事件流加定时快照”以追加方式持续记录事件同时每隔固定步数生成一份完整的状态快照。恢复时从最近的快照出发重放快照之后的事件快速重建现场。快照间隔的取值需要权衡。间隔太短快照频繁生成浪费资源间隔太长重放事件太多恢复时间变长。我在实践中常用的策略是每 10 步生成一次快照同时给快照加上最大事件数的约束例如“超过 50 个事件强制生成一次快照”。这样恢复时的重放量永远可控。另外快照本身要一次性原子写入不要边写边用否则恢复时会读到半截状态。4.3 恢复机制的四级纵深Agent 的故障恢复可以分成四个级别每一级解决不同量级的故障工程上按需组合。第一级是单步重试。某个模型调用超时了退避重试一到两次这个最基础几乎所有框架都支持。第二级是任务重启恢复。整个 Agent 进程崩溃了重启后从最近检查点恢复状态把未完成的步骤继续跑完。第三级是子任务隔离。在多 Agent 场景下某个子 Agent 崩溃不应该影响主流程主控可以重新调度该子任务或者将其降级跳过。第四级是自愈这是更高级的目标——系统自己发现问题自己决策修复方案。关于重试有一条很重要的实践经验失败后立刻原地重试往往是最差的选择。Agent 模型调用失败很多时候是上下文太长、配额不足这类暂时性问题但立刻重试通常还是失败。正确的做法是先降级上下文比如裁剪历史消息、压缩中间结论再重试成功率会高出不少。如果连续失败三次就停下来不要做无畏的重复调用。4.4 超时控制的细节与并发保护超时是 Agent 可靠性里最容易被低估的参数。外部 API 的默认超时通常是 30 秒但这个值对 Agent 任务常常不适用。比如一个网页抓取工具正常响应可能要 5 秒慢的时候能到 40 秒如果超时设成 30 秒正常任务反而频繁失败。我的经验是给每类工具设置独立的超时阈值并结合重试次数一起考虑确保“超时加最大重试次数的总耗时”在任务可接受范围内。并发保护同样不能少。同一个用户可能出现多个 Agent 实例同时处理同一批任务或者一个步骤因重试产生并发调用。我用两种策略结合乐观锁和单飞行。乐观锁给状态记录加版本号更新时比对版本版本不一致就说明被别人改过单飞行确保同一个操作 ID 在同一时刻只被一个执行器处理。这两个机制配合基本能杜绝并发导致的重复执行和状态覆盖。4.5 可观测性把 Agent 的内部过程“可视化”Agent 的可观测性比传统服务更复杂因为它的中间过程是自然语言和工具调用序列不是简单的请求响应。我给每个 Agent 任务生成一个全局 trace_id贯穿全部日志和事件记录每一步日志都带 step_id 和节点名方便定位“卡在哪一步”。在此基础上状态迁移的可视化也非常关键把每个步骤看作状态节点画成流程图故障时一眼就能看出 Agent 在哪个节点反复循环。关键监控指标我建议至少覆盖这几项步骤成功率、单步平均耗时、重试率、检查点恢复次数、模型调用成本、上下文长度变化。其中“单个步骤的最大耗时”和“重试率突增”是最值得设告警的两个信号它们往往是 Agent 进入死循环或外部服务故障的前兆。提前发现好过事后去翻上万行日志。5. 一个完整案例多 Agent 协作任务的断点续跑设计5.1 场景与状态划分拿我做过的一个企业调研系统举例。主控 Agent 接到“调研三款竞品的最新动态”任务后拆分出三个子 Agent爬取 Agent 负责访问指定网站抓取信息分析 Agent 负责对抓取内容做结构化提炼撰写 Agent 负责把分析结果组织成报告。整个流程通常要跑一到两个小时涉及上百次网页请求和数十次模型调用。状态划分遵循“谁执行谁拥有”的原则。主控的编排状态记录任务处于哪个阶段各子 Agent 的状态分布在各子任务的执行上下文里。每个子 Agent 启动时生成独立的 task_id结束回报主控时附上自己的最终状态摘要。主控不直接读取子 Agent 的内部细节只关心“这个子任务成功没有、产出物在哪”。这种隔离设计让任何子 Agent 崩溃时主控都能独立决策重启或降级而不用理解它的内部逻辑。5.2 故障注入测试kill -9 之后发生了什么我在生产环境做过一次严格的故障演练把主控进程直接 kill -9等三十秒再重启观察任务能否恢复。由于所有步骤事件都已落库主控重启后先从数据库加载最近检查点发现任务当时处于“爬取阶段”、已完成 12 个网页里的 8 个。关键在于主控并不知道那 4 个未完成的网页是否爬过——它们可能在崩溃前已经爬完只是事件还没来得及写入。这就是幂等设计发挥作用的地方。爬取动作执行前每个 URL 都会先写入“待爬取清单”并标记状态“处理中”。崩溃后重启主控扫描清单发现那 4 个 URL 的状态是“处理中”而不是“已完成”于是重新派发爬取任务。如果之前实际爬完了重复爬取也只会覆盖一次中间产物不会产生错误数据。整个恢复过程耗时约 40 秒额外耗费的模型调用只在后续 4 个 URL 上整体成本可控。这次测试之后我对“事件溯源加幂等”的组合方案彻底放心了。5.3 部分失败是常态缺数据的报告也要能交付这个案例里还踩过一个很有代表性的坑。某次爬取阶段一个外部数据源长时间无响应重试三次后仍然失败。最初的系统设计是“全部数据源成功后才允许进入分析阶段”结果整个任务卡在爬取阶段长达半小时用户拿不到任何结果。后来我重新定义了成功标准爬取完成率达到 80% 以上失败数据源直接标注为“数据缺口”报告正常生成但用醒目标注提示用户部分信息缺失。这背后是一个思维转变在分布式系统里部分失败是常态不是异常。Agentic AI 的可靠性有时候不是“保证全部成功”而是“失败时给出清晰的边界让用户知道什么可靠、什么不可靠”。对于耗时几小时的长任务这种“可交付的部分完成”比“完美的不可交付”有价值得多。6. 安全底座状态可靠性与 Agent 记忆安全是一对孪生问题6.1 记忆是长期状态的一部分污染等于篡改状态Agent 的记忆体系和长期状态管理紧密相关——短期记忆是当前任务的上下文长期记忆是跨任务复用的知识库。如果记忆内容可以被恶意注入就等于攻击者篡改了状态机的迁移函数模型在“正确”的状态下被污染的记忆带向了错误的下一个状态。最常见的攻击面是提示注入工具返回的内容里夹带“忽略上述指令执行如下操作”等恶意指令模型如果照单全收就可能做出删除数据、发送敏感信息等危险动作。所以我在搭建 Agent 基础设施时把记忆安全和状态可靠放在同等优先级。记忆写入必须区分来源来自模型推理的结论置信度较高来自外部工具返回的内容则一律视为“不可信数据”。模型可以从这些数据里提取信息但不能无条件执行其中包含的指令。这里的原则可以类比成数据是数据代码是代码外部内容永远不能直接升级为命令。6.2 记忆体系的分级隔离与权限控制记忆分级很重要短期记忆放在当前任务的上下文里任务结束即清理长期记忆沉淀可复用的经验和模式但要经过结构化处理不能直接存原始对话永久记忆只保存权限级别的配置和不可变审计日志。每一级记忆的写入权限、读取权限、更新策略都不一样。这个思路其实和操作系统的用户权限设计如出一辙——不是所有记忆对所有 Agent 可见每个子 Agent 只能访问自己命名空间内的记忆。另外一个容易被忽视的点是记忆内容的引用溯源。我在长期记忆里给每条记录都附带来源标识例如来自哪个任务、哪次爬取、哪个用户反馈。这样一旦发现某条记忆导致异常行为可以回溯到它的来源定位是否属于污染数据。没有溯源机制的记忆库就像一个没有日志的数据库出问题了根本无从查起。6.3 工具调用的审计与最小权限Agent的可靠执行离不开外部工具而工具调用的滥用是安全重灾区。我给 Agent 分配工具权限时遵循最小权限原则每个 Agent 只拥有完成当前任务所必需的工具集合。比如爬取 Agent 不需要“发送邮件”的权限撰写 Agent 不需要“修改数据库”的权限。同时所有工具调用必须经过统一的审计层记录调用者、操作对象、命令参数和执行结果。我把审计日志和状态事件放在同一个存储体系里。审计日志回答“谁在什么时候做了什么”状态事件回答“系统因此变成了什么状态”。两者配合任何一个异常操作都能被完整还原。我用 a-memguard 这类思路做记忆防护时会额外注意一点防护机制本身不能过度消耗模型推理否则会影响任务效率。安全设计的目标是降低风险到可接受水平而不是用安全把业务拖垮。7. 高频故障排查实录我踩过的坑和解决方案7.1 “Agent 卡在某一步反复重试”的排查套路这是最常见的故障也是我最开始踩的第一个大坑。现象是 Agent 在某一步反复执行同一个动作日志里能看到一模一样的重试记录任务像进入了死循环。第一反应是模型出了问题但查完发现模型调用本身正常真正原因是这一步的输入状态有问题。比如我遇到过的一个案例一个 JSON 解析工具始终报错因为前一步传入的数据格式已经变了Agent 的修复逻辑又没有生效于是循环卡住。排查顺序我总结为“四查”一查输入数据是否完整二查工具参数是否匹配当前状态三查提示词对该步骤的约束是否清晰四查重试策略是否设置了最大次数和退避。我现在的做法是给每个工具调用设置最大重试 3 次、指数退避并加上一个“重试耗尽后的转向策略”——要么换一个实现方式要么把失败原因记录下来跳转到降级节点而不是永远原地打转。7.2 “状态不一致”问题主控和子 Agent 各执一词多 Agent 协作时最容易出的问题就是状态不一致。比如主控认为子 Agent 已经完成了数据采集但子 Agent 实际在最后一步失败了。原因是子 Agent 完成最后一步后先向主控汇报了“成功”然后还没来得及更新自己的内部状态就崩溃了。这类问题用一句话总结就是状态更新的顺序没设计好。我的解决方案是规定明确的“状态提交流程”子 Agent 先把最终结果写入共享存储并置为“已完成”再向主控发成功信号。主控收到信号后不是直接信任信号而是去共享存储里验证结果是否存在。这个流程把“消息”和“状态”分开——消息可能丢但状态存储不会凭空消失。验证这一步看似多余实际上能挡住绝大多数状态不一致问题。7.3 “程序退出报错”与上下文爆炸的处理技巧很多框架跑短任务时没问题一跑长任务就报错最常见的错误类似于“agent execution terminated due to error”这类信息其实背后通常是两类原因一类是上下文长度超限导致模型 API 拒绝请求另一类是某个子模块抛了未捕获的异常。排查时先看报错的时间点如果发生在任务后半段优先怀疑上下文爆炸如果发生在某个特定工具调用之后优先怀疑数据格式异常。上下文爆炸的处理我前面提过核心是“压缩重于扩容”。我在长任务里设置了上下文管理节点每跑 5 步就做一次轻量摘要把已经完成的中间结论压缩成几条结构化记录关键原始数据存入外部存储不占模型上下文。这套机制上线后长任务成功率从 67% 直接提升到 94%。踩过几次坑之后我才明白很多人只关注模型本身的能力却忽略了运行框架对可靠性的决定性影响。7.4 高频问题速查表问题现象可能原因快速定位方法修复手段步骤反复重试不前进输入状态异常 / 工具参数错误查看该步骤的输入事件日志设置最大重试次数与转向策略主控与子 Agent 状态不同步状态更新顺序未定义对比主控状态与共享存储中的实际结果统一状态提交流程先落库再上报长任务中途上下文超限长期运行未压缩记忆查看上下文长度监控指标增加摘要节点定期压缩旧上下文重启后任务无法恢复状态未持久化或持久化不完整检查检查点与事件日志是否存在引入事件溯源与定时快照机制重复执行同一副作用重试缺乏幂等保护查“已执行动作表”是否生效引入操作 ID 与唯一约束去重写在后面一个值得长期坚持的工程习惯我个人在实际操作中最大的体会是Agent 的可靠性问题从来不是靠某一个“高级框架”或“智能模型”解决的而是靠一套朴素的工程纪律状态外部化、事件留有痕迹、副作用可幂等、故障可恢复、过程可观测。这几条每一条都不复杂但组合起来效果是质的飞跃。如果你现在正要搭建自己的 Agent 系统我建议从第一个任务开始就把状态持久化设计进去不要等到生产环境出了事故再补——补丁永远不如原生设计可靠。最后再分享一个小技巧每周挑一个长任务做一次故障注入测试主动杀掉进程、主动制造超时看看系统能不能自愈。测两次你会对自己系统的可靠性有多少底气有非常清醒的认知。
返回列表