ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多Agent协同系统架构:代理中介层与消息总线设计实践

多Agent协同系统架构:代理中介层与消息总线设计实践

1. 为什么非要多一个“代理代为交互”的中间层

1.1 市面主流Agent框架的“单用户假设”

去年我们内部准备把一个多Agent协作平台从Demo推向生产时,卡了整整两周,问题不在模型效果,而在架构。任务本身听起来很简单:一个项目组五个人,每人配一个专属AI代理(Agent实例),这些代理之间能互相调用能力、交换信息,甚至代表各自主人参加项目晨会讨论技术方案。听起来很顺理成章,对不对?可真正动手设计时发现,市面上的多Agent框架几乎全都建立在一个默认前提上——单一用户。

AutoGen的GroupChat假设所有Agent在同一个对话组里围绕一个目标运转;CrewAI的流程编排、LangGraph的图式控制流,本质上都是“一个User指挥一群Worker”。你可以让多个Agent协作,但协作的指令、上下文、成果归属都天然属于同一个人。而换成“多人各带自己的Agent,Agent之间再互相协作”时,问题一下子就变了:A的代理在讨论中引用了B的私有上下文怎么办?C和D的代理同时抢同一个待办任务由谁拍板?每个人的代理都在独立思考,但它们共同改一份共享计划文档,改坏了谁来负责?

这就是我在这篇内容里反复强调的“代理代为交互”(Agent-Mediated Interaction)的架构价值:AI代理不是用户手里的一把锤子,而是用户与整个多Agent系统之间的服务中介。用户不与后端每个模型、每个Agent直接耦合,而是先把自己的意图交给自己的代理,由代理去路由、协商、汇总、汇报。这一层中间层,正是多人多AI协同系统能否落地的分水岭。

1.2 AI代理作为中介:替用户说的话、办的事

“代理代为交互”这个词听起来偏学术,拆开看其实就是三个动作:替用户保管会话,替用户路由请求,替用户参与多方协商。

替用户保管会话,意味着用户的私人上下文、偏好、历史决策都沉淀在自己的代理侧,而不是散落在每次API调用里。替用户路由请求,是指用户只需要说“帮我确认一下下周的发布窗口和资源是否有冲突”,代理会自己去问运维代理、项目代理、资源调度代理,不需要用户逐个找。替用户参与多方协商,则是代理可以在用户授权范围内,代表用户同意、拒绝、让步,比如晨会上几个代理讨论API设计方案,达成一致后再向各自用户汇报摘要,而不是把七嘴八舌的完整记录甩给用户看。

这层设计一旦成立,系统架构就随之清晰了:用户和代理群是两个平面,用户面稳定,代理面复杂;代理面的复杂度被“交互层”封装起来,用户只面对自己代理给反馈。实践中,没有这层抽象的多Agent项目多半会退化成“用户手动指挥多个线程”,这在一个人影视效果尚可,一旦多个用户介入,马上乱成一锅粥。

1.3 生产环境对多人多AI协同的硬性要求

除了逻辑上的清晰,生产环境还给了我们四个硬性要求,缺一个都不用上线:

  • 会话所有权必须明确。用户A的代理读不到用户B的私聊历史,这是底线,不能靠“提示词里说别越权”来解决。
  • 并发语义必须落地。多个代理对同一共享任务的读写不能互相覆盖,必须有锁、租约或版本控制。
  • 可干预性与安全边界。任何协同过程中,对应的人类用户要随时能介入中止或改向,系统不能变成几个AI代理之间的黑盒博弈。
  • 全链路可审计。出了问题能回溯哪条消息从哪个代理发出,哪个代理基于什么上下文做了决策。

这四点在单用户多Agent项目里很少有人较真,但在多人多AI场景里每一条都会咬人。后面章节的所有设计,基本都是在回答这四个要求怎么做。

2. 分层架构设计:接入、编排、协同控制与模型网关的四层职责

2.1 四层架构的职责边界与数据流向

在定架构时我们反复讨论过要不要把“协同控制”单独拎出来一层,后来发现不单独拎出来根本扛不住多人场景的复杂度。最终落地了四层:接入层、代理编排层、协同控制层、模型与工具层。每层只做一件事,边界清晰到连新人入职两天都能说清楚。

接入层负责所有用户触达渠道的归一化。Web端、IM工具、语音入口都汇进来,统一做身份认证、会话恢复和权限校验。代理编排层维护Agent Registry,记录每个代理属于哪个用户、具备什么角色能力、当前状态是活跃还是休眠、绑定哪套模型策略。协同控制层是整个架构的心脏:消息总线负责代理间的通信;会话语义池负责共享上下文的读写隔离;任务仲裁服务负责任务的注册、派发、租约与超时回收。模型与工具层则通过一个统一网关向下对接云端大模型、本地模型、RAG知识库和各种外部工具执行器。

数据流向是:用户请求先进接入层,凭身份映射到自己的代理实例;代理解析意图后,发现需要资源,把需求写入协同控制层的任务队列或消息总线;其他代理通过总线订阅相关主题,各自评估后再把结果返给发起方代理;最后发起方代理汇总、由用户确认或直接按授权执行。整个过程后端模型不直接暴露给用户,用户只跟自己的“代理前台”对话。

2.2 核心数据实体:代理实例、会话、消息与任务

抽象完分层,紧接着是定义数据实体。我们一开始只设计了Agent和Message两张表,上线模拟一跑就发现远远不够,会话归属和任务状态根本说不清。后来稳定下来的四个核心实体是:

  • AgentInstance:代理实例,包含agent_id、owner_id(归属用户)、role、capabilities、model_binding、status、memory_policy。这里有个容易忽略的细节,一个用户可以拥有多个代理实例,比如“写代码的”“审代码的”,所以owner_id与agent_id是多对多关系,权限模型要按agent_id粒度走。
  • Session:会话域,包含session_id、thread_id、visibility(public/project/private)、participants、created_by。visibility决定该会话的上下文能进语义池还是只能私域可见。
  • Message:代理间消息,包含msg_id、correlation_id(一次业务协同的关联ID)、from_agent_id、to_topic、to_agent_ids、payload、visibility、msg_type(proposal/accept/reject/result/query等)、ts。correlation_id特别重要,没有它排查协同问题就是灾难。
  • Task:共享任务,包含task_id、title、assignee_agent_id、status、priority、version、ownership_lease_expire。version是乐观锁版本号,ownership_lease_expire是租约过期时间,这两个字段救了系统很多次。

这四类实体的关系像一个同心圆:最外层是被授权的用户在控制台看到的“会话”,往内是代理间流转的“消息”,再往内是被反复认领和交接的“任务”,最核心是每个代理各自的“记忆策略”。后面所有协同机制的实现,都可以收拢到这四个实体上。

2.3 消息信封与路由策略:从点对点杂网到收敛总线

代理间通信最忌讳做成点对点杂网。我们一开始图省事,让代理之间直接WebSocket互发,结果三个代理还好,十个代理上线后连接矩阵爆炸、消息乱序、根本无法定位是谁先引发一连串动作。后来参考“分布式交换机系统架构”的思路,把思路整个反转过来:所有代理间通信必须经过一条收敛的消息总线,总线负责持久化、广播、点对点投递和订阅分发,代理不维护任何对方的连接地址。

总线上的消息统一走信封格式,信封字段大体上就是2.2里Message实体的内容。路由策略分两层:一层是按Topic订阅,适合广播式协同,比如“sprint-planning”主题上所有代理都能看到规划类消息并发言;另一层是按能力定向投递,适合精准协作,比如发起方需要在消息里声明to_agent_ids或目标能力标签,总线根据能力注册表把消息只送给匹配的代理。

这套设计带来一个副产品:因为所有消息都经过总线沉淀,天然就有了全量审计事件流。后面章节会讲到可观测性和debug复盘,很多人以为那是额外加的监控,其实只要总线成熟了,消息流本身就是最完整的审计日志。

3. 多人多AI协同的三个核心难题:上下文隔离、任务仲裁、协商编排

3.1 上下文隔离与共享的平衡:带可见性的语义池

多人场景第一个绕不开的问题是上下文隔离。要么把所有代理接到同一个上下文里,协同高效但隐私全无,而且上下文长度也扛不住;要么完全隔离,信息安全了,协同却变成“两个看不见对方的瞎子互传纸条”。我们的做法是设置一个“会话语义池”,作为所有代理共享上下文的统一服务,但每次读写都必须携带可见性标记。

可见性分为三级:public(全局可读)、project(项目内共享)、private(仅归属用户与指定代理)。每个代理在读取语义池内容时,服务端按代理的归属项目和用户权限过滤后再返回。这个设计在工程上等价于一个带权限过滤的分布式消息历史库,写入时打标签,读取时执行过滤逻辑。实现难度不高,但让产品层面聪明了很多:代理既能在公共工作区协作,又能在私聊里说悄悄话,两个区域的上下文在拼接进提示词之前就被服务端切干净了。

有人会问,为什么不让代理自己过滤?我们踩过坑:提示词过滤在单消息上看着有效,但长期多轮协同里,上下文会把不同可见域的消息混在一起,大模型很容易“遗忘边界”并误引用,安全是底线,不能交给随机性。语义池的服务端强制过滤才是可控方案。

3.2 任务所有权仲裁:租约机制避免多人抢活

多人多AI协同还有一个独特问题:任务所有权。单人场景里AI代理执行任务不存在“谁干”的竞争,但多人场景里,A的代理和B的代理可能同时看到同一个待办“更新项目周报”,两个代理都主动认领并执行,结果就是互相覆盖,周报越改越乱。

我们的仲裁机制分两步:认领阶段采用“先到先得+租约续期”。任务进入公共队列时带一个task ownership_lock,代理认领时通过Redis的原子操作写入agent_id和过期时间,谁先写入成功谁获得所有权。获得所有权的代理必须周期性续租,续租失败则任务自动释放回队列,让其他代理重新认领。执行阶段采用乐观锁,任务带version字段,每次更新都带上version做CAS校验,版本不一致就重读重试,杜绝两个代理同时修改同一状态字段。

这套机制借鉴了分布式系统里的租约锁思想,但应用在AI代理上要注意一个细节:代理的“执行”往往不是瞬时的,而是多轮LLM调用,所以租约时间要按任务粒度和模型响应时间设置,我们早期设在30秒,结果复杂任务还没干完就过期了,后来放宽到10-30分钟并支持代理主动续期才稳定。

3.3 代理间对话的三种协作模式与适用场景

代理之间怎么“说话”,直接影响合作效率。我们把决策问题拆开,沉淀了三种协作模式,按场景选用。

第一种是总控编排模式,适合目标清晰、步骤固定的任务,比如发布上线。一个总控代理拆解步骤,逐个向执行代理派活,执行代理只做事不决策。这个模式稳定、可控、责任明确,但灵活性差,不适合探索型任务。第二种是民主协商模式,适合方案设计类任务。所有相关代理围绕一个Topic发言,每条发言自带proposal/agree/disagree/result消息类型,由一个“主持人代理”负责收敛结论。这个模式我们用在技术评审上效果很好,几位“各有立场”的AI代理会围绕接口方案反复辩论,最后形成带决策记录的结论,比瞎开会强得多。第三种是市场竞价模式,适合多代理竞争执行的任务。任务发布到总线,各代理评估自身能力和负载后投标,由一个仲裁者按评分选择执行者。

实践下来,三人以下的小型协同用民主协商就够;跨部门、多目标的大型协同必须引入总控或市场竞价,否则收敛太慢。有人会担心总控模式退化成单Agent分化身,确实有这个倾向,所以我们规定:总控代理只负责调度和决策记录,不参与具体领域执行,每步决策都要落审计日志,避免“总控一言堂”掩盖了真正的多人多AI协同语义。

4. 原型落地的技术选型与核心机制实现骨架

4.1 技术选型:为什么是FastAPI+Redis Streams+liteLLM

描述架构太多不如跑一个原型。原型阶段我们的选型原则是快且能验证协同语义,不用一步到位上航母级基础设施。控制面用了FastAPI,理由很简单:异步IO天然适合多代理并发等待,OpenAPI文档帮助我们快速调接口,Python生态也方便对接各种Agent框架语言模型SDK。

消息总线用Redis Streams而不是Kafka或MQTT。多人多AI协同的消息量级其实不大,每秒几百条撑死了,Redis Streams的持久化、消费者组、消息确认都够用,还省一套Kafka运维。任务仲裁的锁也复用Redis原子操作,省得再引etcd。上下文语义池用PostgreSQL存消息历史,用JSONB存可见性标签,读取时在SQL里做权限过滤,简单可靠。LLM网关用litellm,它统一了上百家模型接口,这样我们既能接云上大模型,也能在中转层接本地模型,不锁死在单一供应商上。

很多团队拿到这类项目第一反应是上Kubernetes、Kafka、微服务全家桶,我的建议是原型期别这么干。协同架构最难的是协议语义,基础设施只是工具,先用Redis+PostgreSQL把交互协议跑通,后面再平滑迁到更强底座也不迟。

4.2 协商状态机与任务租约的代码骨架

消息信封设计好了,下一步是给协同流程一个状态机。我们用的协商状态机核心状态是:Proposed、Accepted、Rejected、Assigned、Executing、Done、Failed。原型里对应一个简单的Python类,核心逻辑大致如下:

class NegotiationStateMachine: ALLOWED_TRANSITIONS = { "Proposed": {"Accepted", "Rejected"}, "Accepted": {"Assigned"}, "Assigned": {"Executing"}, "Executing": {"Done", "Failed"}, } def __init__(self, task_id): self.task_id = task_id self.state = "Proposed" def transition(self, to_state, operator_agent): allowed = self.ALLOWED_TRANSITIONS.get(self.state, set()) if to_state not in allowed: raise InvalidTransition( f"task={self.task_id} state={self.state} -> {to_state} rejected" ) self.state = to_state publish_event(self.task_id, "transition", self.state, operator_agent)

每次状态跳转都会向总线发布一条transition事件,这是审计与debug的基础。注意operator_agent字段不能省,多人协同里必须知道是谁触发了状态变化,否则出了事根本不知道是哪家代理干的。

任务租约的实现也简单,直接在Redis原子操作上包一层:

def acquire_task_lease(redis, task_id, agent_id, ttl=600): key = f"task:lease:{task_id}" token = uuid.uuid4().hex ok = redis.set(key, token, nx=True, ex=ttl) if ok: return token raise TaskOwnershipConflict(task_id, agent_id) def renew_task_lease(redis, task_id, token, ttl=600): key = f"task:lease:{task_id}" if redis.get(key) != token: raise LeaseExpired(task_id) return redis.expire(key, ttl)

配合之前说的乐观锁版本号,任务表更新时带上WHERE version = old_version,返回匹配行数为0就重读。这套组合拳基本能堵住99%的并发覆盖问题。

4.3 上下文语义池的读写实现

语义池在原型里不复杂,本质是对Message实体加了可见性过滤的查询层。写入逻辑是:任何代理发出的消息都带visibility标记,public/project/private;读取时根据请求方代理所属项目、用户权限和消息的visibility做交集过滤。

def read_pool(requester_agent, thread_id, pool_conn): rows = pool_conn.execute( """ SELECT payload, from_agent_id, visibility FROM semantic_pool WHERE thread_id = %s """, (thread_id,) ).fetchall() allowed = [] for row in rows: if can_read(row["visibility"], requester_agent): allowed.append(row) return allowed

这个函数看起来简单,但有一个关键点容易被忽略:过滤不能只做到“LLM输入提示词前”,还要把来源血缘字段一起拼给模型,否则模型引用了别人的上下文却不能溯源。我们在返回的每条消息上额外带上parent_msg_id和source_agent_id,当用户问“这段结论哪来的?”时,系统能一路追溯到最初的提案消息。这节内容对应的标题是“基于AI代理代为交互的多人多AI协同系统架构研究”,如果只看了一遍,可能觉得这些机制琐碎;但它们才是协同系统架构最值钱的部分——架构不是画在PPT上的分层图,而是这些让多人多代理协作可预期、可追踪的小机制叠出来的。

4.4 本地模型与云端模型的双通道网关

模型接入我们专门花了一周设计网关层,因为多人多AI协同的负载特性是“高频短消息”:代理间协商的消息大多是短文本,走云端大模型要注意延迟和成本;而涉及方案生成、代码编写的重任务又需要强模型能力。最终思路是让每个代理在创建时绑定一个模型策略,网关按策略路由。

数据敏感的团队内部信息一律走本地模型,我们接入了几款开源模型并通过GPU服务暴露OpenAI兼容接口。云端通道则留给非敏感、高要求的生成任务,比如最终评审报告。网关统一用litellm接入同一种请求格式,模型切换对上层透明。有个开箱即用的组合值得一说:本地模型加上向量检索,成了边缘节点上跑“代理助手”的标准姿势,数据不出内网,延迟低,适合知识库问答、私有文档摘要这类协同辅助任务。这验证了一个判断:多人多AI协同系统根本不需要所有代理都用顶级大模型,分层模型策略反而能极大降低整体成本和延迟。

5. 从原型到生产:半年运行后踩过的坑和对应解法

5.1 并发争抢:目标一致的两个代理互相覆盖

原型跑通很简单,放到真实业务里立刻遇到第一个大坑:两个代理在“协作”更新同一份项目计划,但目标一致的做法互相冲突。A代理认为技术方案应该先做性能优化,B代理认为应该先加新功能,两个代理同时读取了旧计划,各自按自己的逻辑写了新版,后写的覆盖先写的,技术方案的决策直接丢了。这个场景充分暴露了前面设计的价值:如果没有乐观锁版本号和任务所有权仲裁,系统根本无法判断哪份计划才是“正确”的。

解决过程分两步:首先给Task表加version字段,更新时用CAS;其次强制所有写操作必须走仲裁器,不能由代理直连数据库。很多团队到了这一步才意识到,多Agent协同里最危险的不是模型能力不够,而是并发控制几乎为零。我们把这两个机制补上后,共享文档覆盖问题基本消失。这里也想提醒一句,版本冲突时不要简单合并字符串数据,结构化字段用字段级冲突检测,文本字段宁可返回冲突提示让使用者自己裁决,也不要让模型自动合并,否则会产生更隐蔽的错误。

5.2 上下文漂移:代理在公共任务里引用了别人的私聊

第二个坑比并发更难发现。某次项目回顾,大家发现代理A在公共报告里引用了一句结论,但团队里没有人见过这句话。追查后发现,这句话来自代理B和其用户之间的私聊,因为那次私聊里讨论的语义片段和公共任务语义相似,语义池做关键词匹配时把它捞了出来,又因为可见性过滤配置漏了private标签,被误当成公共知识喂给了代理A。

这就是上下文漂移:代理的长期记忆和上下文拼接在没有严格血缘管控时,会“越权”引用其他会话域的内容。我们后来做了两个修改:一是写入语义池的消息强制携带visibility和thread_id,读取时按请求方的可见性范围严格过滤,绝不把所有语义池内容一股脑塞给模型;二是给每条消息打上血缘标记,LLM拼接上下文时在提示词里天然带上“本条消息来源于哪个会话、由谁发出”,模型不会再无意识混淆来源。这里有个经验:过滤规则宁可写死也不能依赖模型自律,大模型不是安全边界。

5.3 黑盒协同不透明:定位问题全靠总线审计

接入真实用户后,最后一个换来教训的是可观测性。多人多AI协同是一次多代理的异步协商过程,中间还夹杂着工具调用和模型决策,任何一个环节出错,表现都像“结果不对”,但根本定位不了。刚开始我们只有应用日志,结果代理A说“我等B返回”,代理B说“我没收到消息”,DBA说“消息表里没有这条记录”,三方互相对质,一周排查不出问题。

后来我们做了一个不太优雅但极其有效的决定:把所有代理间消息、协商状态转移事件、工具调用结果都只落地到总线侧的一个事件流里。排查问题时按correlation_id检索这个事件流,等同于回放了一次完整的多代理对话。事件流结构是:消息ID、来源代理、目标主题、消息类型、状态转移、调用链ID、事件时间戳。需要可视化时,我们自己写了一个简单查询页按时间轴展示这些事件,相当于给“黑盒协同”装了行车记录仪。实践半年后,我们最常做的事情反而是新功能上线前,故意制造一个有冲突的任务去观察各代理如何协商,通过事件流改进总控编排的决策逻辑。

5.4 边缘部署、ARM节点与ROS设备接入:这套架构能走多远

最后聊聊边界。很多人在做类似系统时问:多人多AI协同非得全跑在云端数据中心吗?我们的实践是,完全不需要。架构里所有关键机制都只依赖消息总线和语义池,这两者都可以下沉。我们在几个实验现场把协同控制层跑到了本地集群上,节点有很大比例是ARM指令集的边缘设备,甚至包括一些国产化要求比较严格的硬件环境,系统照样工作。LLM侧只要有一个支持OpenAI兼容协议的本地模型服务,代理协同就完全可以在离线网络里闭环,外部网络断开影响不大。这说明架构无关芯片指令集,只要操作系统能跑容器、网络能通,编排与仲裁逻辑是平台无关的。

更有意思的是设备接入。有人问过,AI代理能不能作为真实世界设备的代理人?我们试过把协同总线桥接到ROS(机器人操作系统)节点上,让大模型代理不只是在会话里讨论,而是真正派发执行指令给物理设备,设备再通过ROS消息把状态回传给代理群。这个方向做原型时让我很有感触:架构里“代理代为交互”的含义被放大了——代理不仅替用户和别的AI交流,还能替用户和物理世界的执行器交流。多人多AI协同系统的下一步,一定不只是会议室里的文本辩论,而是代理群代表人类去协调真实世界的资源与行动。这也让系统架构研究从纯软件层面延伸到了边缘计算、设备接入和实时控制,前景比我们最初设想的要宽得多。

最后分享一条这半年下来最深的体会:做多人多AI协同,算法模型不是瓶颈,架构治理才是。数据隔离、任务仲裁、消息审计这“三件套”能让你在项目早期就规避掉绝大多数后期炸雷的问题。如果你正打算做类似系统,建议先把这三件事落在代码里,再谈智能。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进