1. 从单兵作战到集群协作:我为什么要折腾多 Agent 系统
最开始接触 Agent 这个概念的时候,我和大多数人一样,停留在"写个提示词,让模型帮我干一件事"的阶段。比如让它帮我总结一篇文章、生成一段代码、翻译一份文档。这种单 Agent 模式用起来确实爽,但用久了就会发现一个很尴尬的问题:它本质上还是一个"你问我答"的被动工具,你不发指令,它就永远停在那里。真正让我下决心去做多 Agent 集群的,是一个很具体的场景——我需要一个能在我睡觉的时候依然持续运转、自己给自己派活、自己检查自己产出质量的东西。
单 Agent 的天花板其实很明显。第一,它的上下文窗口是有限的,你不可能把一整套复杂流程的所有信息都塞进一个对话里;第二,它没有"分工"的概念,你让它同时做调研、写稿、校对,它会在不同角色之间反复横跳,最后每件事都做得半吊子;第三,它没有自我纠错机制,一旦某一步跑偏,后面就一路错到底。这三个问题叠加起来,就导致单 Agent 只能处理"一次性、短链路、低容错"的任务。
多 Agent 集群解决的恰恰是这三个问题。把一个大任务拆成若干子任务,每个子任务交给一个专门的 Agent,每个 Agent 只关心自己那一亩三分地,上下文压力瞬间就下来了。Agent 之间通过消息传递来协作,就像一个小团队一样,有人负责调研、有人负责执行、有人负责审核。更关键的是,你可以设计一个"调度 Agent"或者"监督 Agent",让它不断地给其他 Agent 派活、检查产出、决定下一步该干什么,这样整个系统就能自己转起来,不需要你一直盯着。
我做的这个集群,核心目标就是"24 小时不停工作"。注意,这里的"不停工作"不是指让模型 7×24 小时满负荷烧算力,那既不现实也没必要。它真正的含义是:系统具备自主的任务循环能力——当没有新任务时,它能自己去任务队列里找活干;当任务失败时,它能自己重试或者换策略;当所有任务都完成时,它能进入低功耗的"待命+巡检"状态,而不是直接死掉。这套东西适合谁呢?适合那些手头有大量重复性、流程化工作,又希望用自动化手段把它们串起来的人,比如内容运营、数据整理、代码维护、监控告警这些场景。
2. 集群的骨架:调度、执行、监督三层结构怎么搭
2.1 为什么是三层而不是两层
很多人一开始设计多 Agent 系统,会本能地做成"一个主 Agent + 一堆干活 Agent"的两层结构。我一开始也是这么干的,结果跑了不到两天就发现问题了:主 Agent 既要负责拆解任务、又要负责派发、还要负责验收,它的上下文很快就被各种任务状态撑爆了,而且它一旦卡住,整个系统就全停了。这就是典型的单点瓶颈。
后来我改成了三层结构:调度层、执行层、监督层。调度层只做一件事——决定"现在该干什么",它维护一个全局的任务队列和状态机,根据当前状态决定下一个动作是派发新任务、重试失败任务、还是触发审核。执行层是一组各司其职的 Worker Agent,每个只负责一类具体工作,比如抓取、生成、转换、写入。监督层则独立于前两者,专门负责质量把关和异常检测,它不参与任务派发,只对执行层的产出做评估,把不合格的结果打回或者标记出来。
这么拆的好处是,每一层的上下文都变得非常干净。调度层只需要知道"任务 ID、状态、优先级"这些元信息,不需要知道任务内容本身;执行层只需要知道自己那一个子任务的输入输出;监督层只需要拿到待审核的产出和对应的验收标准。三者通过一个共享的任务存储(我用的是轻量的键值存储加一个消息队列)来通信,彼此解耦。
2.2 调度层的状态机设计
调度层是整个集群的大脑,它的核心是一个状态机。我给每个任务定义了这么几个状态:pending(待处理)、dispatched(已派发)、running(执行中)、reviewing(审核中)、done(完成)、failed(失败)、retrying(重试中)。状态之间的流转规则必须写死,不能靠模型自由发挥,否则系统会变得不可预测。
具体来说,调度层每隔一个固定周期(我设的是 30 秒)跑一次巡检循环,做这么几件事:扫描所有pending的任务,按优先级排序后派发给对应的执行 Agent;检查所有running的任务,如果超过预设的超时时间还没回结果,就标记为failed并进入重试队列;检查reviewing的任务,如果监督层给了通过信号就转done,给了拒绝信号就转retrying。这个循环是集群"永动"的关键,只要这个循环在跑,系统就不会停。
这里有个经验:状态机的流转一定要幂等。什么意思呢?就是同一个任务被重复处理多次,结果应该是一样的。比如一个任务已经是done了,调度层又扫到它,不能把它重新派发一遍。我一开始没注意这点,结果因为消息队列的重复投递,同一个任务被执行了三次,产生了三份重复产出。后来我在每次状态变更前都加了一个"当前状态校验",只有状态匹配才允许流转,问题就解决了。
2.3 执行层的 Agent 隔离
执行层的每个 Agent 我都是独立进程跑的,而不是塞在同一个进程里用多线程。为什么?因为 Agent 执行过程中会调用外部接口、读写文件、占用内存,一旦某个 Agent 崩了,如果是同进程多线程,很可能把整个进程带崩。独立进程虽然资源开销大一点,但隔离性好,一个挂了不影响其他,调度层检测到它没心跳,重新拉起一个就行。
每个执行 Agent 启动时都会向调度层注册自己,声明"我能处理哪类任务"。调度层派发任务时,只派给声明了对应能力的 Agent。这就避免了"让写代码的 Agent 去干翻译"这种错配。Agent 的能力声明我建议用简单的字符串标签,比如fetch、generate、transform、persist,不要搞太复杂的本体论,够用就行。
2.4 监督层的验收标准从哪来
监督层最难的地方不是技术实现,而是"验收标准怎么定义"。你让一个模型去判断另一个模型的产出好不好,如果没有明确的评判维度,它就会和稀泥,什么都说"看起来不错"。我的做法是,在任务创建的时候就附带一份验收清单,里面是若干条可判定的检查项。比如一个内容生成任务,验收清单可能是:字数是否在范围内、是否包含指定关键词、是否通过了敏感词过滤、格式是否符合模板。监督 Agent 逐条检查,全部通过才算合格。
对于无法用规则判定的主观质量,我会让监督 Agent 给出一个 1 到 5 的评分,并附上理由,低于阈值的打回重做。这里要注意,监督 Agent 的提示词里必须明确"你的职责是挑毛病,不是夸奖",否则模型天生倾向于给好评。我甚至在提示词里加了"如果你找不到任何问题,说明你检查得不够仔细"这种话,实测下来确实能让它更严格一些。
3. 让集群真正"永动"起来的几个关键机制
3.1 心跳与自愈:Agent 挂了怎么办
一个 24 小时运行的系统,最怕的就是某个环节悄无声息地死掉。我的做法是给每个执行 Agent 加心跳机制:Agent 每完成一个任务或者每隔 60 秒,就向调度层发一个心跳信号,带上自己的 ID 和当前状态。调度层维护一张心跳表,如果某个 Agent 超过 3 个心跳周期没上报,就判定它失联,把它正在处理的任务重新放回pending队列,同时尝试重新拉起这个 Agent 进程。
这里有个坑我踩过:重新拉起进程时不能无脑重启,否则如果是因为配置错误导致 Agent 一启动就崩,就会陷入"崩溃-重启-崩溃"的死循环,把系统资源耗光。我的解决办法是加一个重启计数器,同一个 Agent 在 10 分钟内重启超过 5 次,就把它标记为"故障",停止自动重启,改为告警通知人工介入。这个阈值可以根据实际情况调,但一定要有,不能让它无限重启。
3.2 任务队列的持久化
集群要 24 小时跑,任务队列就必须持久化,不能放在内存里。我一开始图省事用了一个内存队列,结果有一次进程意外退出,队列里几十个任务全没了,只能从头再来。后来换成了基于磁盘的持久化队列,每次入队、出队、状态变更都落盘。虽然性能上慢了一点点,但换来的是"断电也不丢任务"的可靠性,这笔账怎么算都划算。
持久化队列的选型上,我建议用成熟的轻量级方案,不要自己造轮子。核心要求就三条:支持持久化、支持多消费者、支持消息确认(ack)。消息确认很重要,它保证了一个任务在被真正处理完之前不会从队列里消失。如果 Agent 拿到任务后崩了,没有发 ack,队列会把任务重新投递给其他 Agent。
3.3 限流与退避:别把外部接口打爆
集群跑起来之后,执行 Agent 会频繁调用各种外部接口。如果不加限制,很容易触发对方的频率限制,轻则被限流,重则被封禁。我在每个执行 Agent 里都加了令牌桶限流器,控制单位时间内的请求数。同时,对于失败的重试,采用指数退避策略:第一次失败等 1 秒重试,第二次等 2 秒,第三次等 4 秒,以此类推,直到达到最大重试次数。
指数退避这个策略看起来简单,但效果非常明显。我实测下来,加了退避之后,因为瞬时并发过高导致的失败率下降了大概七成。而且退避还能给外部服务一个喘息的机会,避免雪崩。这里要注意,退避的上限要设一个合理的值,不能无限增长,我一般设到 5 分钟封顶,再长就没意义了,不如直接标记失败走人工。
3.4 空闲时的巡检与预热
系统不可能一直有任务,总有空闲的时候。空闲时如果什么都不做,那"24 小时工作"就名不副实了。我的做法是让调度层在空闲时执行巡检任务:检查各个 Agent 的健康状态、清理过期的临时文件、统计过去一段时间的任务成功率、预热一些常用的缓存。这些活儿不重,但能让系统保持"活着"的状态,也能提前发现潜在问题。
预热这块我特别想提一句。有些 Agent 启动后第一次执行任务会特别慢,因为要加载模型、建立连接、初始化缓存。如果能在空闲时提前把这些做好,等真正来任务时就能秒响应。我一般会在系统启动后和每次空闲巡检时,让 Agent 跑一个"空任务"来触发初始化,这样正式任务就不会被冷启动拖慢。
4. 实测中暴露的问题与我的处理方式
4.1 上下文污染:Agent 越跑越"糊涂"
这是我在实测中遇到的最头疼的问题。执行 Agent 如果长时间运行,它的对话历史会越来越长,里面混杂着各种任务的残留信息,导致它在处理新任务时经常"串台",把上一个任务的内容带到下一个任务里。我一开始以为是模型能力问题,后来才发现是上下文管理没做好。
解决办法有两个层面。第一,每个任务用独立的会话上下文,任务开始前清空历史,任务结束后销毁上下文,绝不复用。第二,对于确实需要跨任务记忆的信息(比如用户的偏好设置),单独存到一个结构化的"长期记忆"里,由 Agent 主动查询,而不是靠对话历史携带。这么改完之后,串台问题基本消失了。
4.2 死循环:两个 Agent 互相踢皮球
有一次我设计了一个"生成-审核"的流程,结果生成 Agent 和监督 Agent 陷入了死循环:生成 Agent 产出一版,监督 Agent 打回;生成 Agent 改一版,监督 Agent 又打回,来回几十次,任务永远完不成。排查后发现,是因为监督 Agent 的验收标准太模糊,生成 Agent 根本不知道该怎么改。
后来我加了一个最大迭代次数的限制,同一个任务被打回超过 3 次,就强制升级为"人工介入"状态,不再自动重试。同时,监督 Agent 打回时必须给出具体的、可操作的修改意见,不能只说"不合格"。这两条加上之后,死循环就再也没出现过。这个经验我觉得很通用:任何自动化的循环都必须有终止条件,否则迟早会出问题。
4.3 资源泄漏:跑久了内存越来越高
集群连续跑了大概一周之后,我发现内存占用在缓慢但持续地上升。查了半天,发现是几个地方在泄漏:一是 Agent 处理完任务后没有释放临时对象;二是日志文件没有轮转,越写越大;三是某些连接没有正确关闭。这些问题在短时间运行时不明显,但 24 小时不停地跑,累积效应就出来了。
处理方式就是一条条排查:给所有临时资源加显式的释放逻辑,日志配置按大小和时间双维度轮转,连接使用上下文管理器确保关闭。另外我还加了一个定时重启策略,每个执行 Agent 每运行 24 小时就优雅重启一次,把可能积累的碎片清理掉。这个策略有点"笨",但非常有效,属于兜底手段。
4.4 任务优先级反转:重要任务被饿死
任务队列如果只是简单的先进先出,会出现一个问题:大量低优先级的任务把队列占满,高优先级的任务排在后面迟迟得不到处理。我一开始没设优先级,结果有一次一个紧急任务等了快一个小时才被执行。
后来我给任务加了优先级字段,调度层派发时按优先级排序,高优先级先出队。但这里又有个新问题:如果高优先级任务源源不断地来,低优先级任务就会被"饿死",永远排不上。所以我用了加权轮询的策略,高优先级任务占大头,但每处理若干个高优先级任务,就穿插一个低优先级任务,保证公平性。这个平衡点需要根据实际业务调,没有标准答案。
5. 从能跑到好用:几个提升集群稳定性的细节
5.1 结构化日志是排查问题的命根子
集群跑起来之后,出问题是必然的,关键是出问题后你能不能快速定位。我强烈建议从第一天就用结构化日志,也就是每条日志都是 JSON 格式,包含时间戳、Agent ID、任务 ID、日志级别、消息内容这些字段。这样你可以很方便地按任务 ID 把所有相关日志串起来,一眼看出这个任务从派发到完成经历了什么。
我一开始用的是纯文本日志,排查一个问题要在几千行里肉眼搜索,效率极低。换成结构化日志之后,配合简单的查询工具,定位问题的速度提升了不止一个档次。这个投入绝对值得,别省。
5.2 可观测性:让集群的状态一目了然
除了日志,我还加了一些指标采集:任务总数、成功率、平均处理时长、各 Agent 的负载、队列积压量。这些指标汇总到一个简单的看板上,我随时能知道集群现在健不健康。有一次我发现成功率突然从 95% 掉到了 70%,一看指标发现是某个外部接口的响应时间变长了,及时做了降级处理,避免了更大范围的影响。
可观测性这东西,平时感觉不到它的价值,但真出事的时候,它就是你的眼睛。没有它,你只能靠猜;有了它,你能直接看到问题在哪。
5.3 灰度与回滚:新 Agent 别一上来就全量
每次我给集群加新 Agent 或者改现有 Agent 的逻辑,都不会直接全量上线。而是先让新版本处理一小部分任务(比如 10%),观察一段时间,确认没问题再逐步放大比例。如果发现异常,立刻回滚到旧版本。这个习惯帮我避免了好几次因为新逻辑有 bug 导致的大面积故障。
灰度发布的关键是能快速回滚。所以我在设计时就保证了 Agent 的版本是可切换的,调度层可以根据配置决定把任务派给哪个版本。回滚只需要改一个配置项,几秒钟就能生效。
5.4 成本控制:别让集群变成烧钱机器
24 小时运行意味着持续的成本支出,如果不加控制,账单会很吓人。我做了几件事来控制成本:一是给每个 Agent 设了每日的调用次数上限,超了就暂停;二是对于简单的任务,用更便宜的模型,只有复杂任务才用贵的;三是空闲时降低巡检频率,减少不必要的调用。这几招下来,成本大概降了四成,而系统的实际产出并没有明显减少。
成本控制的核心思路是分级处理:不是所有任务都值得用最好的资源。把任务按重要性分级,重要的用好的,不重要的用凑合的,整体性价比就上来了。
6. 我踩过的那些坑,以及给后来者的建议
6.1 别一上来就追求"全自动"
我见过不少人,一开始就想做一个完全不需要人干预的集群,结果做出来的东西要么动不动就卡死,要么产出质量惨不忍睹。我的建议是,先做半自动:让集群处理大部分流程,但在关键节点保留人工确认。等系统稳定了、你对它的行为有把握了,再逐步减少人工干预。全自动是目标,但不是起点。
6.2 提示词要当代码一样管理
Agent 的行为很大程度上由提示词决定,所以提示词不能随手写在代码里,而应该单独管理、版本化、可回滚。我现在的做法是把每个 Agent 的提示词存在独立的文件里,改动走版本控制,每次改动都记录原因。这样当某个 Agent 行为异常时,我能快速对比是提示词改坏了还是其他原因。
6.3 测试环境要能模拟真实负载
在本地跑几个任务没问题,不代表在生产环境跑几千个任务也没问题。我专门搭了一个测试环境,用脚本模拟高并发、任务失败、Agent 崩溃这些场景,在上线前把能想到的极端情况都跑一遍。很多问题都是在这个阶段暴露出来的,比在生产环境出事强太多了。
6.4 文档和注释别偷懒
集群这种东西,隔一个月再看,你自己都可能忘了某个模块是干嘛的。所以我在每个关键模块都写了注释,说明它的职责、输入输出、依赖关系。另外还维护了一份架构文档,画清楚各层之间怎么通信、状态怎么流转。这份文档在我后来加新功能、排查问题时帮了大忙。
6.5 接受"不完美",持续迭代
最后想说的一点是,多 Agent 集群这种东西,不存在"一次做对"的可能。它一定是在不断出问题、不断修的过程中慢慢成熟的。我现在的这个集群,和最初版本相比,几乎每个模块都重写过。所以别追求一步到位,先让它跑起来,然后在跑的过程中不断优化。能跑起来,就已经赢过大多数停留在设计阶段的人了。
这套集群到现在已经稳定运行了几个月,期间处理了上万条任务,成功率维持在九成以上。它当然不完美,还有很多可以改进的地方,但它确实做到了我最初想要的那件事:在我睡觉的时候,它还在干活。这种"有个不知疲倦的帮手在替你工作"的感觉,是单 Agent 时代完全体会不到的。如果你也在做类似的东西,希望上面这些经验能帮你少走点弯路。