ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从单机到集群:多Agent高可用架构设计与踩坑实录

从单机到集群:多Agent高可用架构设计与踩坑实录 深夜写代码的人里十有八九都幻想过自己的程序能永不停机。我这里说的不只是服务器不宕机而是你那套 AI 应用能在你睡觉的时候自己派活儿、自己干活、自己汇报。我最近打造了一个 24 小时不停工作的多 Agent 集群从最开始只有一个 Agent 来回跑到后来拆成一组带调度、带故障转移的集群前前后后踩了不少坑。这篇文章就是我的实践记录和复盘总结如果你是搞 Agent 开发、想让自己的 AI 应用从“单机玩具”走向“生产可用”的开发者这篇内容应该能帮你少走不少弯路。先说结论把 Agent 从单个进程挪到集群里真正的难点不在“连起来”而在“怎么让一堆 Agent 协同工作还不打架、不丢任务、不崩溃”。我用了主控节点加 Worker 节点的结构用 Docker Compose 拉起基础组件中间接消息队列做任务分发再配合心跳检测和自动拉起机制来实现故障转移。整套系统跑起来之后最大的变化就是我再也不用半夜起来重启脚本了任务挂了它会自己重试节点死掉它会自动剔除日志和状态也都能集中看到。1. 为什么非要把 Agent 做成集群1.1 单个 Agent 的瓶颈在哪里我们刚开始做 Agent 应用的时候大多数人都是从单进程开始的就是一个 Python 脚本或者一个 FastAPI 服务里面挂着 LangChain 或者自研的 Agent 循环。单 Agent 跑起来很简单处理一些简单的“给我总结一下这篇文章”、“帮我写个周报”这种任务完全够用。但你一旦把任务量提上来问题就全暴露了。第一个瓶颈是上下文窗口。一个大模型 Agent 的上下文是有限的如果你让它连续处理几十个任务上下文会被各种中间结果塞满后面的任务质量会肉眼可见地下降。第二个问题是单点故障一个进程崩了所有的任务就全停了没有任何容错。第三个问题更隐蔽——效率。单个 Agent 只能顺序干活前一个任务在等 LLM 响应的时候后面的任务就只能排队而这时候你的 CPU、磁盘、甚至网络带宽其实都在闲着。我记得当时我跑了一个批量处理任务大概要处理 2000 多条数据每个任务要调用好几轮 LLM估算下来要跑十几个小时。中间只要有一次 API 超时没处理好整个流程就断在那儿了。后来我实在受不了了才下决心把 Agent 改成集群架构。说白了单个 Agent 就像一个人开了一家小店生意一多你就发现你既当收银员又当厨师又当保洁忙不过来的时候只能干着急。1.2 集群带来的三项核心价值多 Agent 集群解决的核心问题用大白话讲就三个词并行、冗余、分工。并行最好理解。多个 Worker 节点同时拉任务原来十几个小时才能跑完的批量任务在三个节点上可能四五个小时就跑完了。而且由于每个 Agent 有独立的上下文它们互不干扰每个任务都能拿到“干净的”上下文窗口。冗余是集群存在的真正理由。24 小时不停工作的系统最怕的就是夜里三点某个节点因为内存泄漏或者网络波动挂掉。有了集群架构主控节点会实时检测 Worker 的心跳一旦发现某个节点不响应就自动把它的任务重新分发到其他健康的节点上。这个就是我们常说的故障转移也是“24 小时不停工”这句话背后的核心保障。分工则是让集群效率最大化的关键。不同 Agent 可以承担不同角色——有的负责数据抓取有的负责内容生成有的负责质检。还可以针对不同任务类型调配不同的模型比如简单分类用轻量模型跑复杂推理才调用大参数模型。这样整个系统的资源利用率会高很多成本也能压下来不少。2. 系统架构与关键组件的选型实战2.1 主控与 Worker 的职责划分我先讲清楚我这套集群的基本结构。整个系统分成两类节点主控节点和 Worker 节点。主控节点不干活它只负责三件事接收任务、调度任务、监控各 Worker 的状态。Worker 节点才是真正跑 Agent 的地方一个 Worker 可以并行跑多个 Agent 实例每个实例从队列里拉取任务执行。为什么要把“派活”和“干活”分开原因很简单Agent 执行任务的时候会调用 LLM、处理工具、读写数据库非常消耗内存和 CPU。如果让同一个进程既做调度又执行任务一旦任务量上来调度的响应速度就会变慢整个系统会变成“调度器先卡死然后所有 Worker 一起卡死”的连锁故障。分开之后主控节点非常轻量哪怕 Worker 全部挂掉主控也还活着新任务还能继续进队列不会丢。我自己用的是 Python 写的调度器本质上就是一个长运行的异步服务。它订阅任务请求接口把任务塞进消息队列然后维护一张 Worker 注册表记录每个节点的存活状态和当前负载。还有个细节值得注意主控节点本身要做成无状态的这样万一主控也挂了可以快速起一个新的从队列里继续干活不会出现“调度器丢了”这种灾难。2.2 消息队列选型Kafka 还是 Redis任务队列是集群的通信中枢。我这里聊一个很多初学者都会纠结的问题到底用 Kafka 还是 Redis。两种我都实际用过直接说结论。Redis Stream 适合中小规模集群。它部署极其简单一个容器就搞定了而且性能足够好单机就可以支撑每秒几千条任务的吞吐。如果你只是想在多个 Worker 之间做一个简单的公平队列Redis Stream 或者 Redis List 完全够用。我在项目早期就是用 Redis Stream 实现任务分发的代码量很少调试也方便。Kafka 则适合任务量大、需要持久化和回放能力的场景。它的优势是日志机制非常强所有消息都有持久化消费者挂了可以从上一次的位置继续消费不会漏消息。缺点就是重——部署 Kafka 至少要搭 zookeeper或者 KRaft 模式下的 controller运维成本明显高一个量级。这里有个非常实际的经验如果你的 Worker 在执行任务时是“边消费边标记完成”的模式那 Redis 就够了但如果你需要“任务发出去之后还能追溯整个生命周期”那 Kafka 的日志结构会省心很多。两种方案的取舍本质上是“重可靠”还是“轻运维”的问题。目前我这套集群用的是 Kafka因为我比较需要消费位移和重放能力后期排查问题的时候能少很多麻烦。不过说实话如果你的场景没到每天百万级任务直接用 Redis 上别折腾 Kafka 了。2.3 框架选择LangChain、CrewAI、Dify 谁更合适Agent 框架的选择直接影响开发和运维体验。很多人一上来就问 LangChain、CrewAI、Dify 应该选哪个我的回答是先搞清楚你到底想要什么。这三者定位完全不同。LangChain 是底层框架它给你的是积木而不是成品。你可以用它的 Tool、AgentExecutor、Memory 等组件自由拼装你自己的 Agent 系统。灵活性最高但学习曲线也最陡。如果你要做的集群 Agent 有大量自定义逻辑——比如复杂的工具调用、动态 Prompt、自定义记忆策略——LangChain 是合理的底子。CrewAI 是偏上层的多 Agent 编排框架它的设计哲学就是让多个 Agent 像团队一样协作有“角色”和“任务”的概念写起来非常直观。我当时在原型阶段用过一个星期体验是很爽的但问题在于它对底层运行时的屏蔽比较多一旦要接入自己的集群调度逻辑反而感觉它的抽象在碍事。CrewAI 的优势在于快速验证多 Agent 协作的流程但它更像“帮你把流程搭好”而不是“帮你构建一套稳定运行的系统”。Dify 则是更完整的平台型产品提供可视化编排、知识库、工作流管理这些能力。如果你不想写代码只想通过拖拽构建一套 Agent 应用Dify 确实很香。但是它是个平台不是库你很难把它拆开来嵌进自己的集群架构里。我最终的方案是自己写 Agent 核心逻辑用 LangChain 只做工具调用部分调度层完全自研这样既能保持灵活性又能完全掌控集群行为。注意框架只是脚手架集群的核心价值在于调度、容错和可观测性这些框架帮不了你太多。不要把框架当成全部。2.4 Agent 记忆与状态共享的设计集群环境里最容易被忽略的就是“记忆”。单个 Agent 跑的时候你可以把历史消息存在内存里方便得很。但集群里有多个 Worker每个 Worker 都可能处理同一个用户的不同任务那这个用户的上下文在哪个节点上我是不是每次都要把历史聊天记录从头带一遍这里需要区分两种记忆会话级记忆和任务级记忆。会话级记忆用 Redis 存就行按 session_id 做 keyAgent 每次处理任务之前先从 Redis 拉历史摘要。摘要不用太长能把关键信息带上就行。任务级记忆则要看任务生命周期如果一个任务需要多轮 Agent 协作才能完成那中间状态最好也放到共享存储里避免某个 Worker 执行到一半挂掉之后进度全丢。另外一个经验是别把完整的对话历史都塞给 Agent。用一个“记忆压缩”策略——把历史总结成要点比如“用户是做跨境电商的上次聊过物流时效问题”这样既省 token 又避免上下文被无关信息稀释。我在集群里跑过一阵子之后发现很多时候 Agent 回答质量下降不是模型问题而是上下文里塞了太多不相干的历史记录记忆设计的优先级非常高。3. 实操从零搭建一个可运行的 Agent 集群3.1 基础环境准备用 Docker Compose 快速拉起依赖我不喜欢把时间浪费在装环境上所以整套集群的基础组件全用 Docker Compose 编排。你需要拉起来的东西一般包含消息队列我用 Kafka也可以用 Redis、分布式缓存Redis、以及可选的向量数据库用于长期记忆。先给出一个最简的 Compose 编排。注意我这里省略了具体镜像版本你自己按需替换即可。version: 3.8 services: redis: image: redis:7-alpine ports: - 6379:6379 command: redis-server --appendonly yes zookeeper: image: bitnami/zookeeper:3.9 environment: - ALLOW_ANONYMOUS_LOGINyes ports: - 2181:2181 kafka: image: bitnami/kafka:3.6 ports: - 9092:9092 environment: - KAFKA_BROKER_ID1 - KAFKA_LISTENERSPLAINTEXT://:9092 - KAFKA_ADVERTISED_LISTENERSPLAINTEXT://localhost:9092 - KAFKA_ZOOKEEPER_CONNECTzookeeper:2181 depends_on: - zookeeper这个 Compose 文件启动之后你就有了一套可以随时重启、清空重来的基础环境。测试阶段一定要保证这些组件是“可丢弃的”别把生产数据跟测试环境混在一起否则后面你连排查问题的入口都找不到。配合容器化的好处是任何机器上都可以快速复现同一套环境。我有一次在另一台新服务器上部署只需要把 Compose 文件复制过去一条docker compose up -d就搞定了所有依赖。这就是容器对于集群项目的意义——它把“环境准备”这个最无聊但最容易出错的环节变成了一句命令。3.2 Worker 注册与心跳探活集群里所有 Worker 都需要向主控节点注册。这个机制很简单每个 Worker 启动的时候先调用主控的注册接口告诉主控“我上线了我的 ID 是什么我能跑什么类型的任务”。主控会把这个信息记在内存里同时用一个键值对结构存到 Redis保证主控重启后还能恢复节点列表。注册之后就是心跳探活。Worker 每隔几秒发一个心跳包主控收到心跳就更新对应节点的最后活跃时间。如果某个节点超过一定时间没有心跳主控就把它标记为“失联”同时把它正在执行的、尚未提交结果的任务重新放回待处理队列。我用的心跳方式是向 Redis 写入一个带过期时间的 keyheartbeat:{worker_id}每次写入时设置 TTL 为 10 秒。主控检查的时候只要 key 还存在就代表节点活着key 过期了就认为节点挂了。这个方案比你自己维护时间戳要简单得多也不依赖复杂的第三方组件实测非常稳定。Worker 注册表里还应该记录每个节点的当前并发能力。比如某台机器内存大可以同时跑 5 个 Agent另一台比较弱只允许并发 2 个。调度器在派发任务的时候根据这些负载信息决定往哪些节点投递避免一台机器忙死、其他机器闲死。3.3 任务分发、重试与结果回收任务分发的流程是这样的用户提交任务后主控节点把任务封装成一个标准消息发到对应的 Kafka topic 上。每个 Worker 进程作为一个消费者组的一员从 topic 拉取消息。这里有个关键点同一个任务只让一个 Worker 消费不能让多个 Worker 同时处理否则会导致重复执行和资源浪费。Kafka 的消费者组机制天然解决了这个问题这也是我说 Kafka 在该场景下比 Redis 省心的原因之一。拿执行日志举例。每个 Worker 在执行任务时会周期性地把中间状态写入 Redistask:{task_id}:progress和task:{task_id}:status。这样主控或者前端面板随时能查到一个任务的进度比如“正在调用 LLM”、“工具执行中”、“已完成”。结果回收的逻辑也很直白。Worker 执行完任务后把最终结果写回 Redis并在 Kafka 的结果 topic 里发一条完成通知。主控收到通知后把任务状态更新为“完成”然后将结果回传给调用方。这里我专门设计了一个结果 TTL比如默认存 7 天超过时间就删掉避免 Redis 被历史任务结果填满。重试机制是我的血泪经验。任务重试绝不能简单地把消息重新丢回原 topic否则一旦任务本身有问题它会无限循环把队列“堵死”。我的做法是每个消息带上retry_count字段如果任务失败判断重试次数是否超过阈值。没有超过就发到重试 topic带上递增的计数超过阈值就把任务标记为“失败终态”扔进死信队列等待人工处理。3.4 故障转移让集群真正 7x24故障转移这块是我踩坑最多的区域主要问题集中在“半死节点”上。什么叫半死就是进程还活着心跳还在发但它的 Agent 因为某些原因卡死了——比如调用外部 API 超时或者死锁在某个工具调用上。这种情况心跳完全正常任务却一直不出结果。解决方案是给每次任务执行加上超时控制。主控在派发任务时会在消息里带上一个timeout字段Worker 端如果超过这个时间还没结束当前 Agent 执行就会被强制终止并把任务标记为“执行超时”。主控再把超时任务分配给另一个 Worker 从头执行。这样就保证了没有一个任务可以永久占用资源。另外还要针对“节点彻底挂掉”做防护。我上面提到的心跳机制只能解决节点不可达的情况真正的问题在于挂掉的节点上那些还没完成的任务怎么办我的做法是把每个正在执行的执行记录都放 Redis主控通过扫描executing:{ task_id}的锁信息来判断一个执行是否正常结束。如果节点失联对应锁过期之后主控会把这些任务重新入队。这里需要注意设置合理的锁过期时间太短会导致任务重复执行太长会导致故障恢复变慢。我一般把锁时间设成任务预设超时时间的两倍。这些机制都完善之后我发现我基本上可以“开摆”了。以前半夜系统崩溃我要爬起来看日志、重启进程现在集群会自动把任务转移到其他节点顶多有个别任务重跑一遍。这里有一个必须接受的现实容错的代价是“至少一次执行”不是“正好一次执行”。你的业务要对重复执行有容忍度否则就得在业务层面额外做幂等。4. 常见问题与排查技巧实录4.1 任务为什么全部卡在排队中我遇到过最吓人的问题某天早上一看监控所有任务的状态都是“排队中”一个都没有被执行。查了半天发现Kafka 消费者组的 rebalance 出了问题——某个 Worker 节点异常退出后消费者组一直在重新分配分区但协调器那边迟迟没有拉起来新的消费者整个组就进入了一种“假死”状态。排查思路是先看任务是不是真的没有被消费然后看消费者状态。我最后的解决方法是给消费者组设置了固定的 session.timeout 和 heartbeat.interval并且保证每个 Worker 退出时会主动调用consumer.close()让消费者组快速重新分配。还有一个细节消费端的并发度不能一味地调高Kafka 消费者组里一个分区只能被一个消费者线程消费如果你的 Worker 数量大于分区数部分 Worker 会空转任务还是卡在队列里。这类问题的本质是集群里最容易被忽略的“时间同步问题”——不光是时钟同步还有超时参数之间的互相纠缠。配置超时参数时别只用默认值最好在预发环境模拟节点挂掉观察 rebalance 到底要花多久再反过来调整参数。4.2 多 Agent 上下文互相污染集群化之后我遇到过一个非常隐蔽的问题Agent A 生成的中间结果不知怎么跑到了 Agent B 的上下文里。最终展示出来的回答经常“串味”比如上一单客户的需求出现在下一单客户的回复里。这个问题直接把我的信任度打没了。后来追踪发现是共享记忆模块的 key 设计有问题。我在写 Redis 时只用了 session_id 做 key但同一个 session 的任务可能同时被多个 Worker 并发处理不同的任务阶段会把中间状态覆盖进同一个 key。解决方案有两个一是给记忆 key 加上 task_id 维度确保不同任务读写互不干扰二是在 Worker 拉取上下文的时候再做一次数据隔离校验确保自己读到的历史记录的时间线和当前任务是匹配的。这个问题的教训是不要相信“内存里那点状态没问题”只要系统涉及并发就一定要显式地设计数据隔离边界。Agent 的记忆在集群环境下不是“共享变量”而是“有主键的数据”。4.3 内存泄漏与长尾任务集群跑久了之后最容易出现的就是单个 Worker 内存持续走高。我一开始以为是模型调用的问题后来用memory_profiler跑了一遍才发现是 LangChain 工具调用里某个 HTTP 客户端的连接没有及时关闭导致连接池里的连接越积越多最终把内存撑爆。排查内存泄漏的方法就三板斧第一给每个 Worker 加内存监控设定的阈值超过就自动重启先保住整体稳定性第二用 objgraph 或者 tracemalloc 定位具体是哪些对象没有被回收第三针对常驻对象做代码审查重点关注全局变量、静态缓存、网络连接池这类“易积累”资源。长尾任务则是另一个坑。所谓长尾任务就是个别任务因为依赖的服务特别慢执行时间远超平均水平。这些任务虽然数量少却会长期占用 Worker 的并发额度拖慢后面所有任务。我现在会在调度层做任务时长分级把预计耗时短的任务优先派发长时间任务走独立的低优先级队列并且限制同时执行的数量避免被长尾任务堵塞。4.4 没有监控就是瞎跑24 小时系统没有监控等于在雾里开车。我这里说的监控不一定要上特别重的 Prometheus Grafana 全家桶你可以用更轻量的方式起步日志集中化 关键指标记录。我最初给每个 Worker 打日志但日志分散在不同容器里排查问题的时候要挨个进容器翻效率极低。后来我把所有日志统一通过logging输出到标准输出让 Docker 的日志驱动收集起来再用一个轻量的日志采集服务把这些日志集中到一个地方。这样我可以用一条命令查看所有 Worker 的日志再配合关键词过滤排查效率翻了好几倍。关键指标我建议至少记录这几项每个 Worker 的实时并发数、任务排队长度、任务执行耗时分布、失败率、重试次数。这些指标不需要依赖复杂工具可以每 10 秒写一条 json 到日志中。当你怀疑系统出问题时第一件事不是看代码而是先看这些指标曲线定位是调度问题、执行问题还是外部依赖问题。5. 这个项目带给我的几个经验最后说点不太好量化但特别重要的体会。第一集群化不是银弹。如果你的 Agent 应用本身逻辑混乱、任务定义模糊搬到集群上只会把问题放大十倍。先在一个 Worker 上跑通完整流程再谈扩展。第二Agent 集群的运维成本远比想象中高。它不是一个“写完就完事”的系统你要持续处理队列堆积、节点失联、内存增长这些乱七八糟的事。我个人觉得如果任务量没有达到一定规模单 Agent 加合理重试完全够用不必为了“技术炫酷”而上集群。我当时上集群的契机是被批量任务折磨到不行了才做的而不是因为“别人都这么搞”。第三也是最重要的多 Agent 集群的系统设计核心从来不是“AI 有多聪明”而是“任务能不能被可靠地分发、执行和回收”。模型能力是变量基础设施才是常量。你把调度、容错、监控这些基础打牢了后面换再强的模型系统都能无缝跑起来。反过来说模型再强调度一团乱麻系统还是会两天一小崩、三天一大崩。这套集群我到现在还在跑也不断往里加新的技能和工具。如果让我给还在犹豫要不要上集群的人一个建议我会说先用单 Agent 把你的业务逻辑验证清楚再上集群解决规模问题。顺序反了等着你的就是无穷无尽的返工。
返回列表