资讯动态

多Agent编排实践:用Redis持久化与状态机织成可复用协作系统

发布时间:2026/10/2 15:35:57 来源:尧图企业网站定制
整整搞了两个月我踩了很多Agent编排相关的坑最后搭起来的那套东西我自己起了个名字叫OpenRig。说起来也没什么高深的核心就一句话让一堆原本各自为战的AI Agent在同一个持久化底座上协同干活而不是靠拼手气。先交代一下背景。我手头有一个业务需要多个Agent配合完成——有的负责信息抽取有的负责写摘要有的负责路由分发还有的干复核。最开始就是一个个单独调每个Agent都有自己的一套状态跑完就丢互不相通。结果就是产出极其不稳定同一个任务换个时间跑结果能差半条街Agent之间要传个数据全靠我手工拼上下文塞进去链路一长场面非常难看。后来我把整个架构重做了思路从“调多个API”彻底转成“编排一个多智能体系统”。这篇就聊聊我是怎么把离散的AI Agent通过持久化、任务编排和并发控制织成一个可复用的协作系统。里面不会只有理论更多的是我踩过的坑、验证过的方案以及可以照着敲的代码思路。1. 做Agent的人早晚都会撞上这堵墙Agent越多系统越“健忘”先说个反直觉的现象。很多人以为Agent数量越多系统能力越强。我的实际感受是如果你没有一套持久化和编排的机制Agent数量越多系统越乱越“健忘”。这就好比你招了一堆能力很强的外包员工但每个人干完活就拍拍屁股走人不写交接文档不更新Git下一个环节的人完全不知道前面发生了什么。你指望他们高效协作不存在的。你只会得到一堆各自为战的孤岛。我的第一个项目就是这么翻车的。当时我做了三个Agent一个提取原始数据一个做数据清洗一个生成分析报告。单测的时候每个Agent表现都很好精度高、响应快。一旦串起来做端到端调用问题就来了——第二个Agent需要用到第一个Agent在处理过程中的某些中间结果但我当时的设计是等它完全结束把最终输出拼进Prompt里传给下一个。表面上逻辑通实际上中间态大量丢失很多时候第一个Agent的最终结果里根本没有后面需要的关键字段因为那是处理过程中的一个分支状态。轮询也试过回调也试过最后还是乱。真正的原因就一条整个系统没有持久化的中间状态层。每个Agent都只活在“当前这一次调用”里当次调用结束状态烟消云散。你想查它是哪个环节出的错查不到。你想让它断点续跑不可能。你想做流程回放门都没有。所以我的结论是**AI Agent这件事真正难的不是让单个Agent干活而是让一堆Agent在同一个系统里协作时所有关键状态都能被记录、被传递、被恢复。**这一步不做后面所有花哨的编排都是空中楼阁。2. 先把“持久化”这三个字说清楚你的Agent为什么总在“失忆”聊持久化之前得先弄清楚一个概念Agent和普通函数的最大区别是什么普通函数是“吃输入、吐输出”状态在函数内部走一圈就没了。Agent不一样Agent是有“记忆跨度”的执行体它需要记录自己的目标、已经完成的任务、当前执行到哪一步、上下文积累到什么程度、以及跟其他Agent的消息往来。这些状态如果只放在内存里进程一重启全没了。如果只放在上下文窗口里窗口一满最老的记忆就被挤出去了。这就是“失忆”的根源。2.1 持久化到底要存什么东西我梳理了一下一个协作型Agent最少要持久化三类数据会话状态Session State这个Agent从诞生到现在经历了哪些阶段当前处于哪个阶段。比如“信息抽取Agent”要记住自己已经从原始文本里抽了哪些字段、哪些还没抽中间产物Intermediate Artifacts每个Agent处理过程中产生的结构化中间数据。可能是JSON片段、向量索引的ID也可能是给下游Agent的指令包消息日志Message LogAgent之间互相发送的、以及Agent与外部系统交互的所有消息记录。这一步既是审计的基础也是出问题时排查链路的唯一依据。这三类数据我一个都没省全都落到Redis里。有人会问为什么不直接存MySQL不是不行但AI Agent的运转对读写速度特别敏感每个Agent每执行一步都可能要读写状态MySQL的连接数和磁盘IO在这种高频率小数据量的场景下优势不大。而Redis本身就以内存速度做读写加上它有成熟的持久化机制正好匹配这个场景。2.2 Redis持久化机制到底是个啥我用最简单的话捋一遍既然这一段是很多人会搜“redis持久化机制详解”的重点我多写几句。Redis的持久化有两条腿一条叫RDB一条叫AOF。RDBRedis Database按照你配置的时间间隔把内存里的全量数据快照到磁盘。好处是恢复快文件紧凑坏处是fork子进程做快照时如果数据量大会有短暂阻塞而且一旦在两次快照之间宕机中间的数据就丢了。默认配置是900秒内有1次写入就存一下这显然不够用。AOFAppend Only File把每一次写操作以追加日志的方式记录下来。好处是数据安全性高可以通过appendfsync配置来控制刷盘频率坏处是文件会越涨越大恢复时重放日志比较慢。Redis新版支持AOF文件里的RDB头这种混合格式体积极为紧凑恢复速度也快。两个机制实际用下来我建议这样配配置项我的建议值理由save关闭或拉长时间窗避免频繁fork阻塞主线程appendonlyyes以AOF为主保住实时状态appendfsynceverysec性能和数据安全折中最多丢1秒aof-use-rdb-preambleyes文件小恢复快maxmemory-policynoevictionAgent状态一律不能丢禁止淘汰关键键注意这里说的“持久化”不只指Redis自身的RDB/AOF。Redis持久化解决的是“Redis进程挂了不丢数据”而Agent系统的状态还需要在业务层面做一层冗余——关键任务的状态在共享存储里再存一份。两层都做才算稳。2.3 状态结构不设计好后患无穷落地的时候我一开始犯了个错误用一个大JSON把Agent的所有状态全塞进去一个键通吃。结果就是并发读改的时候互相踩一个Agent在写current_step另一个Agent在改memory整个JSON都被锁住性能差到离谱。后来改成按维度拆键每个Agent一个命名空间比如agent:extract:{session_id}:status agent:extract:{session_id}:fields agent:extract:{session_id}:messages用Redis的Hash去组织字段级操作互不影响。这算是一个很小的设计决策但对后面的并发稳定性和排查效率影响巨大。3. 从0到1搭OpenRig注册、落库、跑通第一个Agent网络下面说点实操。把分散的Agent变成协作系统我的顺序是“先单机跑通再上编排”一共四步。3.1 第一步定义Agent的统一生命周期接口所有Agent必须实现同一个生命周期模型不能各自为政。我用的是比较朴素的五段式setup初始化资源配置拉取依赖数据run执行核心逻辑可能是调用大模型也可能是调工具check自检结果是否符合预期save_state把关键状态写入持久化层handoff生成给下游Agent的交接消息。接口统一之后编排层就不用关心每个Agent内部怎么实现的它只需要按阶段去调度。这就像工厂里的工位不管工位里是什么设备只要进料口和出料口是标准化的流水线就能跑起来。这是OpenRig里最值得做的一层抽象。没有这层后面写编排代码的时候每接一个新的Agent就要写一遍特例逻辑维护成本会失控。3.2 第二步用Redis存Agent运行时状态我直接给一段能跑的代码示例展示Agent如何通过Redis保存并恢复运行状态。这里用的是Python加redis-py思路本身跨语言通用。import json import time import uuid import redis r redis.Redis( hostlocalhost, port6379, db2, decode_responsesTrue, ) class AgentSession: 把Agent的状态存储委托给Redis让Agent天然带有断点能力。 def __init__(self, agent_name: str): # 每个Agent每轮任务都生成独立session_id self.session_id f{agent_name}:{uuid.uuid4().hex[:8]} self.status_key fagent:{agent_name}:{self.session_id}:status self.data_hash fagent:{agent_name}:{self.session_id}:data def start(self, initial_state: dict): r.hset(self.status_key, mapping{ created_at: time.time(), status: running, current_step: init, }) # 初始任务数据直接落到Hash上 for k, v in initial_state.items(): r.hset(self.data_hash, k, json.dumps(v)) def update_step(self, step: str): r.hset(self.status_key, current_step, step) r.expire(self.status_key, 3600 * 24) # 只保留24小时防止垃圾状态堆积 def set_result(self, key: str, value): r.hset(self.data_hash, key, json.dumps(value)) def get_result(self, key: str): raw r.hget(self.data_hash, key) return json.loads(raw) if raw else None def finish(self, ok: bool True): r.hset(self.status_key, status, done if ok else failed)这段代码干的事很简单但解决了两个大问题第一Agent执行到一半宕机了重启后通过session_id就能拿到之前的current_step和所有中间产物第二多个Agent共享同一个Redis实例编排层随时可以窥探每个Agent的执行进度而不需要侵入式地改Agent内部代码。3.3 第三步消息队列把所有Agent串起来状态有了接下来解决“Agent之间怎么传递任务”。我选的是Redis Stream。为什么不用RabbitMQ或Kafka因为在我的场景里Agent数量还不到几十个量级消息体也不大引入重型MQ反而增加运维负担。Redis Stream天然支持消费组够用而且和状态存储共用一个基础设施。大概的流转模型是这样的上游Agent完成任务后把交接消息推入Stream - 下游Agent在消费组里监听到消息 - 下游Agent从Redis里拉取上游中间产物 - 下游Agent开始执行自己的生命周期每个Agent一个独立的StreamStream的每条消息都带一个全局唯一的task_id。这个task_id是整条协作链路的核心索引任何环节出问题我都可以拿它去追查所有Agent的落库状态。3.4 第四步手动走一遍完整链路再谈自动化四步走完之后我建议先不要急着做动态编排、动态路由那套花活。手动把三个Agent串一遍Agent A执行完手工确认状态落库手工把消息推到Agent B的StreamAgent B跑完再看状态是否正确更新。全链路走通三次以上证明状态模型和消息模型是稳的再引入编排引擎。我当时图快状态模型还没验证稳定就直接上自动编排结果出了bug根本分不清是编排的问题还是状态模型的问题排查起来极其痛苦。4. 把离散Agent编排起来任务节点、状态机与锁的配合单链路跑通之后真正的核心工作才开始怎么让十几个Agent按照业务规则协作而不是写死一堆if-else。我把这里的做法展开说说。4.1 编排的本质把运行结构画成一张有向图编排不是什么玄学它的本质就是把业务流程定义成一张有向图。图中的节点是Agent的执行单元图中的边是依赖关系和数据流。你的业务流程有多少种执行路径图就有多复杂。常见的组合方式就那么几种线性编排Agent A结束后紧接Agent B适合流水线式处理。并行编排Agent B和Agent C都依赖A的结果但彼此不依赖可以同时跑。条件编排根据A的输出内容决定下一步走B还是C。汇聚编排B和C都结束后把两份结果合并交给D做综合处理。我在OpenRig里没有引入特别重的图计算引擎而是用Redis里的一张“任务路由表”自己描述每个Agent执行完把自己的产出登记到路由表中编排器根据路由表中的结果和预设的路由规则决定下一个要激活的Agent。听起来像工作流引擎对吧对但它比传统工作流引擎更灵活的地方在于每个节点的“执行体”是带有大模型判断的Agent它可能根据上下文动态修改自己的输出甚至主动要求重跑某个上游节点。4.2 任务状态机的设计决定系统能不能“断点续跑”每一个task_id在我的系统里都对应一个状态机贯穿始终pending - running - waiting_dependencies - ready - dispatched - succeeded \- failed - retry状态机的核心作用是让“流程控制”和“Agent实际执行”解耦。编排器不直接调Agent而是把状态置为ready并丢进待分发队列某个Agent空闲时自己去队列取任务取到后把状态改成running干完再上报结果。这样哪怕某个Agent进程突然崩了编排器扫描一遍状态机发现哪个task_id卡在running超过超时时间就自动把它重置回ready交给另一个副本去跑。这就是持久化给编排带来的最直观收益——系统不再依赖任何一个单点进程的存活。4.3 用Redis锁压住并发写操作多Agent并行最怕的就是两个Agent同时写同一个任务状态。这时候Redis的分布式锁出场def acquire_agent_lock(agent_name: str, task_id: str, timeout: int 30) - bool: lock_key flock:agent:{agent_name}:{task_id} # nxTrue只有键不存在时才能设置成功天然原子 return r.set(lock_key, 1, nxTrue, extimeout) def release_agent_lock(agent_name: str, task_id: str): lock_key flock:agent:{agent_name}:{task_id} r.delete(lock_key)这个锁本身并不复杂但有几个细节必须注意。锁的过期时间要比任务实际执行的最长耗时更长否则任务没跑完锁先过期了另一个副本就会重复执行。另外不要在Agent代码里直接加各种wait、sleep来抢锁正确做法是把抢不到锁的任务重新放回队列等下一轮调度。4.4 编排器的“心跳”机制为了不让编排器本身成为单点故障我给它加了一个心跳上报机制编排器的每个调度动作都会在Redis里写一条带有时间戳的心跳记录。如果心跳超过两个周期没更新就认为编排器挂了备用编排器接管。这套逻辑很笨但极其可靠也是持久化底座带来的又一个红利——编排器状态本身就是可恢复的。5. 并发篇多个Agent抢资源时系统凭什么不乱“AI Agent怎么扛并发”这个话题在网上讨论得很多。很多人一上来就想着提高API并发上限、加机器、调参。我的经验是**对基于大模型的Agent系统来说真正的并发瓶颈通常不在模型API而在状态一致性和资源竞争上。**模型API慢一点只是性能问题状态写乱了就是正确性问题后者才是致命的。5.1 先把“坑”拆清楚你的并发卡在哪一层我总结下来Agent系统的并发压力分布在三个层面LLM API层调用大模型的频率上限慢但可控工具调用层Agent去查数据库、调外部接口的频率容易被打爆状态读写层多个Agent对同一状态键的读写冲突最容易出bug。前两层是“资源限制”用限流和队列就能解决。第三层才是“正确性风险”必须靠锁和幂等设计来解决。5.2 给LLM调用装上令牌桶我的做法是给每个Agent类型配一个独立的令牌桶计数器以Redis的原子操作限制每秒发往LLM的请求数import time def acquire_token(agent_name: str, rate: int 5) - bool: key fratelimit:{agent_name} current r.get(key) if not current: # 首次调用直接放行并初始化计数 r.set(key, rate - 1, nxTrue, ex60) return True val int(current) if val 0: r.decr(key) return True return False令牌不够就让任务在队列里等待。这个桶的存在防止了某个Agent的异常循环把API额度秒光也防止了并发尖峰把下游系统打挂。5.3 状态写入必须幂等这是我认为全网文章讲得最少、但实战最重要的一点。**Agent任务重试是常态不是异常。**网络抖动会重试、进程崩溃会重试、超时会重试。如果状态写入不幂等同一个任务被分发两次就会产生两套互相覆盖的中间状态。我在设计时强制要求状态变更必须携带task_id和step两个版本号写入前先检查版本号。只有更高版本号才能覆盖旧状态。这样一来重试的Agent发现自己的版本号比别人低就不会覆写别人的结果。一个简单的版本控制示例def set_state_with_version(task_id, step, payload): version_key ftask:state:{task_id}:version # lua脚本保证原子比较更新 lua_script local cur redis.call(get, KEYS[1]) if cur and tonumber(cur) ARGV[1] then return 0 end redis.call(set, KEYS[1], ARGV[1]) redis.call(set, KEYS[2], ARGV[2]) return 1 ok r.eval(lua_script, 2, version_key, ftask:state:{task_id}:data, step, json.dumps(payload)) return bool(ok)用Lua脚本把“比较版本号”和“写入状态”两步压成一个原子操作杜绝并发下的竞态条件。这种设计能扛住绝大多数Agent并发的正确性压力。5.4 并行度要留余量别把系统压到极限最后说个土经验Redis的CPU占用、网络带宽、Agent进程数量不要用到90%以上。Agent系统的负载曲线不是线性的它会因为LLM推理的随机性和上下文积累而突然跳变。给系统留出30%的余量你会省掉很多凌晨三点爬起来救火的痛苦。6. 生产环境里踩过的三个大坑写出来给后来人这节全是真金白银的教训。分享三个我在落地OpenRig时实际踩过、并且花了大力气才填上的坑。第一个坑只配了RDB快照结果Redis重启丢了一整轮任务状态。当时图省事以为Redis默认配置就够了。结果一次服务器重启重启前十几分钟内产生的Agent状态全丢了。下游Agent拿不到上游交接消息整条链路静默失败。排查了三个小时才意识到是持久化策略的问题。后来把appendonly打开appendfsync设成everysec同时在业务层加了状态冗余才算稳住。第二个坑所有Agent共用一个Redis逻辑库键设计没规划最后根本分不清谁是谁。前期Agent少键名随便起什么extract:123、summary:456都有。后来Agent一多排查一个问题要redis-cli keys *扫半天生产环境还差点因为keys命令阻塞了Redis主线程。后来我规范成了agent:{agent_name}:{session_id}:{field}三级命名空间并且把生产环境Redis的keys命令禁掉只允许用scan。第三个坑一个Agent的慢查询拖垮了整条任务链。有一个Agent处理数据时要扫描一个很大的中间产物Redis读多写多把其他Agent的读写延迟全带起来了。我一开始以为是Redis性能问题后来仔细分析才发现是单个Agent的执行逻辑里有一个不合理的数据遍历压根不应该把那么大的数据放到Redis里。处理方案是把它改成磁盘对象存储Redis里面只放引用地址。这也是一个很重要的经验Redis不是万能的它适合高频小对象不适合大文件。写在最后这套模型还能怎么继续扩展从自己手上这个项目走出来之后我对“多智能体编排”的认知清晰了很多。它不是一个技术框架能解决的问题本质上是把Agent当成一类有状态、可恢复、可协作的长期运行服务来设计。持久化是地基消息队列是动脉编排器是中枢并发控制是安全带少了任何一块系统都会在某个意想不到的时点崩给你看。OpenRig这个名字代表的是我自己的实践集合它不是一个标准答案也不一定适合所有人的场景。但沉淀下来的这套组合拳——统一生命周期、Redis持久化、Stream消息传递、状态机编排、分布式锁加幂等控制——放到任何需要多Agent协作的领域里都值得被复用。如果让我给正在做类似项目的朋友一句最实在的建议那就是**先花一个下午把状态模型想清楚再动代码。**状态模型定了后面的编排、并发、排查都有迹可循状态模型烂代码写得再好也白搭。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑