过去两年身边不少团队都在搭AI聊天助手但绝大多数人的做法还停留在“我给模型发一条消息模型返回一条回复”的阶段。单对单聊天完全够用可一旦进入企业内部协同、团队项目管理、自动化流程编排这类场景你会发现需要的是另一个层面的系统让AI代理代表用户去发言、去调用工具、去协调信息让不同的人和不同的模型在同一个工作空间里互相配合。我参与“基于AI代理代为交互的多人多AI协同系统架构”这个课题的落地时踩过不少坑也沉淀了一些能直接抄走的设计思路。这篇文章把整套系统从顶层架构到代码实现拆开来讲适合正在做AI Agent平台、多模型RAG系统或协作型智能应用的开发者和架构师也适合想弄清这类系统原理的产品经理。1. 为什么要“替代交互”而不是做聊天框1.1 代交互并不是套壳很多人把AI代理理解为“带了系统提示词的ChatGPT API”这是一个很常见的误区。“代为交互”和“聊天机器人”的本质区别在于聊天机器人的主体是人类模型永远在等人类发起话题而AI代理的主体是任务模型需要自己决定下一步做什么、调用哪个工具、向谁请求信息甚至主动去跟其他代理协商。举个例子一个项目群里可能有产品经理、前端、后端、测试四类角色大家共享同一份需求文档。传统方式是人去逐条问AI“帮我总结这个PR的影响面”“帮我检查这几天有没有pending的bug”。而在代交互模式下后端AI代理收到代码变更事件后会主动调用静态分析工具把风险摘要发给测试AI代理测试代理拿到摘要后生成回归用例再抄送给产品代理生成周报。整个过程中人类只在关键决策点出现日常的重复沟通全部由代理链完成。这套机制落到系统架构上首先要解决的是“身份”问题。每一个代理都必须有独立的身份、状态、能力边界和记忆空间它不能是全局共享的一个大模型实例。项目里我们把每个代理设计成独立实体注册到统一的代理注册中心由编排服务按事件驱动的方式调度。这跟“一个API网关转发到不同模型”是两种完全不同的架构。1.2 多人多AI组合带来的挑战当系统规模从“一人一模型”变成“多人多模型”问题会成指数级上升。我自己在早期设计时以为只要在最上层加一个路由器就行结果被现实反复教育。第一个问题是消息的语义归属。同一个用户在同一个工作区可能同时跟代码代理、文档代理、数据分析代理沟通每一条消息发给谁、谁应该收到谁的回执、哪些消息需要抄送给其他代理这与单房间聊天完全是两个逻辑。第二个问题是上下文隔离。不同的任务、不同的参与者、不同的代理组合必须拥有独立的会话上下文否则A项目的代码讨论会被B项目的需求文档污染模型输出质量肉眼可见地下降。第三个问题是并发与状态一致性。多个代理可能同时更新同一个任务状态如果不控制并发数据覆盖、重复执行、任务丢失都遇到过。这也是我坚持把系统拆成接入层、编排层、代理层、模型层和工具层五层的原因。每一层只解决一个问题层与层之间通过消息总线解耦这样不管是接新的本地模型还是替换供应商都不会牵一发动全身。2. 系统架构的总体设计思路2.1 五层逻辑视图整个系统我定义为五层结构。接入层负责接收用户输入包括Web端、IM机器人、API网关统一把用户请求转成标准事件。网关层处理连接管理、鉴权和会话路由保持长连接的同时把消息转发到正确的编排器。编排层是核心大脑负责解析用户意图、拆分任务、挑选合适的代理、监控任务状态并把中间结果汇总回传给用户。代理层是真正跟大模型打交道的部分每个代理封装了模型调用、工具调用和记忆管理。模型层向下对接多种推理后端包括云端API和本地模型。工具层提供搜索、数据库、代码执行、文件读写等能力。五层之间用事件流连接而不是用传统的同步REST调用。为什么一定要用事件驱动因为多AI协同天然是异步的。用户发出一个复杂任务后代理A可能要去调外部工具代理B要等A的结果才能继续这个过程中如果全用同步HTTP请求超时和重试会逼疯所有人。事件驱动让每个节点只关心自己订阅的事件生产者和消费者彻底解耦系统本身的扩展性也随之提升。2.2 代理注册中心与能力路由代理注册中心是整个系统里我建议第一个做的模块。它不只存代理的ID和名称还要存每个代理的能力描述、运行状态、负载情况、依赖的模型类型以及可用的工具列表。架构图里它看上去只是一个数据库表但实际运行时它就是代理的“通信录状态面板”。路由策略上我们使用了基于能力的匹配。用户或上游代理发出一个任务带上能力标签比如“代码审查”“SQL生成”“文档翻译”编排器在注册中心里检索所有支持该能力的代理再结合当前空闲状态、历史成功率、模型性价比做一次打分排序。这个设计最大的好处是新增代理不需要改编排逻辑注册进来就能被路由命中。我在项目里遇到过一个很典型的坑早期把路由逻辑写在编排器的if-else里每接入一个新代理就要改一次代码改到第五个代理时嵌套条件已经没法看了。后来统一改成注册中心能力标签新增代理只写一个注册请求编排器完全不用动。2.3 消息总线是多AI协同的“神经系统”多人多AI系统里消息总线不是一个可选项而是一个必选项。它的作用可以用一句话概括让所有节点只跟总线说话不直接互相寻址。用户消息进来后发布到“human_task”主题编排器订阅这个主题处理后把代理间协作消息发布到“agent_task”主题代理层订阅并响应响应结果再发回“agent_reply”主题。具体选型上我实测过Redis Pub/Sub、NATS JetStream和RabbitMQ最终在项目里用了NATS。原因是它同时支持主题订阅和请求响应模式消息体积小吞吐高还自带持久化能力适合既要实时又要可靠的多代理场景。如果团队已经重度使用Redis用Redis Stream也完全能跑只是基于Stream做消息确认和重新投递时要多写一些代码。不推荐把消息总线直接落在MySQL表上轮询带来的延迟和锁竞争在代理数量超过十个后就很致命。消息总线还承担了可观测性的职责。每一条事件都带有trace_id从用户请求开始贯穿所有代理调用排查问题的时候直接按trace_id拉链路日志比在多个服务里瞎翻日志高效得多。3. 核心模块设计与关键细节3.1 会话与上下文隔离会话管理是很多初版设计最容易翻车的地方。我见过不少实现把“会话ID到模型消息列表”直接存在全局内存Map里用户一多就内存暴涨而且不同会话之间还有串数据的高危风险。正确做法是把会话拆成两层会话元数据层和上下文内容层。会话元数据包括会话ID、参与者列表、关联代理列表、创建时间、任务目标、当前阶段状态。这些人读的数据放在关系型数据库或键值库里即可。上下文内容层存的是真正发给模型的Message序列我建议把每个完整事务追加到消息队列里异步落盘同时在上游维护一份热缓存。这样既保证读取速度快又确保重启后上下文不丢。隔离的粒度和维度需要提前定义清楚。以我们的实践为例同一会话内用户与不同代理的交互必须按“用户-代理”二元组隔离代理被不同会话复用时会话上下文必须严格隔离否则这个代理会被前一个项目的语气和内容带偏模型工具调用产生的知识检索结果也要按照会话ID隔离防止把私有数据跨会话泄漏。上下文隔离做不好后面调模型效果调什么参数都不好使。3.2 代理生命周期管理代理不是常驻内存的幽灵它有完整的生命周期注册、就绪、忙碌、空闲、异常、注销。系统里我实现了一个统一的代理状态机每当代理被选中执行任务时状态从就绪切到忙碌任务完成或者超时后切回空闲心跳超时标记为异常主动下线则进入注销状态。生命周期管理的关键是心跳机制。每个代理启动后定期上报心跳携带当前状态、正在处理的任务ID、资源占用情况。编排器根据心跳判断代理是否可用避免把任务派发给一个已经卡死的代理。这个机制在我们接入本地模型时尤其重要本地小模型偶发卡顿很正常有心跳至少能自动摘除故障节点而不是把整个编排器拖垮。异常恢复策略也要提前设定。代理在处理任务中途崩溃时任务应该能重新入队或者由编排器重新选择备用代理执行。我踩过的坑是早期没做任务原子性管理代理崩溃后任务既没完成也没删除残留在“处理中”状态用户一直干等。后来加了任务重投递机制每条任务带一个投递次数上限超过上限就转人工处理这才稳住。3.3 工具调用与权限边界多人多AI系统里权限问题的复杂度远高于单用户系统。工具调用必须挂在代理身份上而不是挂在模型会话上。简化说明就是代码代理调用代码执行工具时它代表的是当前用户因此执行权限必须按当前用户的角色校验不能因为代理“很智能”就默认拥有全量权限。我在项目里维护了一张工具权限映射表记录了“工具名、允许角色、可操作资源范围、调用频控限制”。每次代理发起工具调用请求编排器先拿到发起会话对应的用户身份再查映射表权限不足直接拒绝并把拒绝原因写进调用记录里。这个设计在前几次内部试用时就被证明确实必要有人通过诱导代理调用文件删除接口试探过权限边界如果代理权限直接等于服务账号权限那次事故就发生了。另外工具调用必须有审计日志。谁在什么时间让哪个代理调了哪个工具、入参是什么、结果是什么、耗时多少全部落库。这不只是为了追溯安全事件也是后面做成本分析和模型质量评估的重要数据源。4. 从零搭建一套可运行的原型4.1 技术栈和基础拓扑我建议团队在验证架构时不要一步到位上K8s先用一台开发机或者三台云主机把逻辑跑通。我这里给出一个经过验证的原型拓扑一台服务器跑编排服务、注册中心、NATS消息总线一台机器跑本地推理服务建议用Ollama加载Qwen2.5或Llama3系列模型所有AI代理进程以Python服务方式运行每个代理一个进程。技术栈方面编排服务用FastAPI WebSocket原因很简单异步性能好、类型提示完善、自动生成OpenAPI文档方便前端联调。消息总线用NATS本地没有就直接用官方二进制包启动配置非常简单。代理进程内部用LangChain或直接调用OpenAI SDK都行但建议不要在代理层绑定特定框架保持核心逻辑是纯HTTP/WebSocket调用。搭建环境时有个小细节需要先确认前端设备的系统架构在服务器上跑uname -m看输出是x86_64还是aarch64这决定了模型镜像和基础镜像的选择。如果团队里有ARM架构的开发机拉镜像时要显式指定platform参数否则会出现“镜像拉下来了但启动不了”的尴尬。这个细节我们项目里真的折腾过一晚上。4.2 核心代码实现思路因为系统的核心是注册中心和编排器我先从这两块代码讲起。注册中心我使用Python的dataclass定义代理信息同时用asyncio.Lock保证并发安全。import asyncio from dataclasses import dataclass, field class AgentStatus(str, Enum): IDLE idle BUSY busy OFFLINE offline dataclass class AgentInfo: agent_id: str name: str capabilities: list[str] backend: str # 例如 ollama:qwen2.5 或 openai:gpt-4o status: AgentStatus AgentStatus.IDLE current_task: str 有了这个数据结构后面所有调度和路由逻辑都围绕它展开。代理启动时向注册中心发送注册请求注册中心把AgentInfo缓存到内存里同时向NATS发布一条agent_registered事件让编排器感知到新节点可用。编排器里最关键的是处理用户消息的入口函数。它负责拆解任务、找到合适的代理、发送任务给代理执行。这里给出一个简化版的路由逻辑async def dispatch_task(conversation_id, task_desc, capability): agents registry.find_by_capability(capability) for agent in agents: if agent.status ! AgentStatus.IDLE: continue await nats_client.publish( agent_task, { conversation_id: conversation_id, task_id: uuid4().hex, agent_id: agent.agent_id, task_desc: task_desc, }, ) agent.status AgentStatus.BUSY return {status: accepted, agent_id: agent.agent_id} return {status: no_agent_available}这段代码看着简单但就是这套系统中的主循环骨架。真实项目里还需要补三样东西任务的超时控制、失败重试次数、以及把任务状态写进数据库以便恢复。4.3 同时接入本地模型和云端模型多人多AI系统里往往既有云端大模型又有本地小模型。云端模型负责复杂推理和生成本地模型负责私域数据处理和高频低延迟任务。这个混跑模式对架构提出的要求是代理不感知底层模型代理只感知一个统一的模型网关接口。我在系统里封装了一个模型网关所有代理调用模型都走同一套接口。网关根据后端标识把请求转发到Ollama或OpenAI API。接入本地模型时Ollama的调用非常简单部署好Ollama服务后用它的HTTP API即可。在本地加载模型时要注意显存和内存占用我建议按任务类型拆分模型比如小参数模型专门做意图分类大参数模型专做内容生成避免一个模型把资源吃光。混跑时最大的坑是超时和限流策略不一致。云端API有天然限流本地模型没有本地模型偶尔会因为显存不足变得很慢而云端则稳定得多。所以网关里必须给不同后端配置不同的超时时间和最大并发数。我们给本地模型设置的是45秒单次调用超时和2个并发云端模型设置的是20秒超时和10个并发整体稳定性明显提升。4.4 用Docker Compose管理整套服务原型阶段不建议把所有服务塞进同一个容器我建议至少拆成五个服务编排器、注册中心、NATS、模型网关、示例代理。Docker Compose定义好依赖顺序NATS要最先启动注册中心其次编排器和代理后续跟上。services: nats: image: nats:2.10-alpine command: [-js] registry: build: ./registry depends_on: [nats] orchestrator: build: ./orchestrator depends_on: [nats, registry] agent-code: build: ./agents/code environment: - OLLAMA_URLhttp://host.docker.internal:11434 depends_on: [nats]这里特别提醒一下容器内访问宿主机上本地模型的方式。Linux环境直接用宿主机IP就行macOS或Docker Desktop环境用host.docker.internal。本地模型Ollama默认监听11434端口容器内连接时注意防火墙放行。整体启动后可以用NATS命令行工具确认主题订阅情况或者通过编排器的日志查看代理注册是否成功。5. 多AI协同的真实问题与排查方式5.1 上下文膨胀无底洞多代理协同系统里最常见的问题就是上下文无节制膨胀。一个代理执行多轮任务后消息历史会越攒越多即使模型本身有128K上下文也会被代理间的中间结果、工具调用记录、失败重试日志塞满。解决思路有两个层级。第一层是上下文压缩每次对话达到一定轮数后把早期对话交给一个小模型生成摘要替换掉原始消息。第二层是上下文结构化把长期记忆、短期工作内容、单次任务消息分开存储每次真正发给模型的只是一个汇总包。我建议在项目一开始就做结构化设计不然等到上线后再补是很痛苦的迁移工程。我在项目里用了一个比较笨但有效的策略代理与代理之间的中间消息不直接进入上下文核心而是先存到Redis只有最终汇总到用户的消息才写入模型上下文。这个策略让Token消耗降了大概三成效果很明显。5.2 代理互相调用形成死循环多AI协同系统会引入一个传统系统里很少见的问题代理A给代理B发任务B执行完又给A发消息两个代理互相触发形成一个没有出口的事件循环。最离谱的一次我调试时看到两个代理在NATS里交换了上百条消息直到我手动停掉进程才结束。解决这个问题的方案是给每条任务加上深度限制和环检测。深度限制就是每条任务最多编排几次代理转发超过默认5层就强制终止。环检测则是在任务流转路径里记录所有经过的代理ID如果某个代理ID再次出现且任务参数相同系统判定为循环并终止任务同时在日志里标记环路信息。这个机制在原型阶段就建议加上不要等跑出问题再补因为多代理链路的扩放速度真的很快人工根本盯不过来。5.3 提示注入与权限劫持多人多AI系统里提示注入的风险会被放大因为代理不仅接收人类输入还会接收其他代理的输出、外部工具返回的内容、网页抓取内容。攻击者可以把恶意指令藏在工具返回的文本里诱导代理执行非预期操作。防御思路不能只靠模型自己是清醒的必须在系统层面做隔离。我在项目里做了三层防御外部工具返回的内容一律标记为“不可信数据”不直接混入系统提示词代理发出的工具调用都要经过权限校验网关权限校验不依赖于模型判断第三高危工具比如删除、写文件、发送消息一律要求二次确认或者经过编排器审批流。这三件事做完之后系统安全性会明显上一个台阶。5.4 典型问题速查表症状可能原因处理方式用户消息发出去没有响应编排器未订阅事件主题或代理心跳超时检查NATS主题订阅关系查看代理心跳记录多个用户互相看到对方的聊天内容会话上下文隔离失败检查会话上下文存储key是否包含会话ID同一任务重复执行消息消费未做幂等控制给每条任务添加唯一task_id消费端做去重本地模型访问超时显存不足或模型仍在预热降低并发数确认模型加载日志代理进入忙等但任务丢失任务状态未落库增加任务表持久化启动时扫描未完成任务这套速查表是我们自己碰到过的最高频问题按表格顺序排查能省下不少时间。如果用户反馈的问题不在表里我建议先从消息链路trace_id入手把整条链路的日志打出来看通常都能找到断点。6. 架构设计与后续扩展的一些想法研究做到现在我最深的感受是多人多AI协同系统的瓶颈并不在模型能力而在架构对关系的表达能力。单纯把N个模型API并联起来只是一张简单的扇形结构真正的协同需要处理的是“谁代表谁、谁依赖谁、谁有权限做什么”这一连串关系问题。架构设计不能只画出漂亮的盒子连线还要在代码里把消息路由、权限边界、状态恢复这些基础能力做扎实。如果你想在现有基础上继续扩展我建议按三步走。第一步是把纯文本代理扩展为多模态代理让代理能处理图片和语音消息第二步是引入更细粒度的任务编排图把代理间的依赖关系显式建模为DAG而不是靠消息内容隐式传递第三步是加入效果评估系统记录每个代理在每类任务上的成功率、耗时和用户满意度用数据反向优化调度策略。每一步都可以独立落地不需要推翻原有架构。最后分享一个实施层面的小建议不要一开始就追求全功能可以从一个很窄的场景切入比如“两个代理协同处理周报”跑通以后再逐步加代理、加工具、加并发。架构不是一夜之间长成的是跟着真实问题一步步演化出来的。希望这篇实践记录能帮你省掉那些我已经踩过的坑。
阅读完成 · 觉得有帮助?