提到AI代理,不少人第一反应是“调API”。拿Key、拼Prompt、等返回,然后就没有然后了。但如果你真把一个AI代理当作系统里的一个活体组件——它能自己做出决策、要代表用户去跟别的代理协作、要为最终结果负责,事情就完全不是“调个接口”那么简单。
我最近在做“基于AI代理代为交互的多人多AI协同系统架构”的预研和原型验证,前后跑了两个多月,期间换过三版方案,踩了不少坑。这个方向里最核心的一个认知转变是:AI代理的本质不是模型,而是“交互面”。它替用户去感知任务状态、去跟其他代理协商、去调用工具,再把结果以用户能理解的形态交回来。当系统里同时存在多个用户、多个代理、多个模型、多个异步任务时,真正决定成败的是承载这套交互关系的系统架构,而不是某个模型有多聪明。
这篇内容我把整个架构的设计思路、核心机制、原型代码和踩坑实录完整整理出来,适合正在做AI Agent平台、多人协作工具、智能体编排系统的朋友参考,也适合想从“单代理Demo”往“多代理协同系统”迈进的团队当作路线图。
1. 从“调API”到“代理交互面”——设计要解决的根本问题
1.1 人员、代理和任务之间是“多对多”关系
传统单体业务系统里,用户和功能之间是“人找功能”,菜单就在那里,你点就行。但引入AI代理之后,关系变成了“人委托代理,代理完成任务”,而且不是一个人一个代理,是一个人可能同时挂着三四个代理,一个代理又同时服务多个项目里的多个人。
我预研阶段梳理出的第一个关键问题是:关系矩阵瞬间爆炸。假设系统里有10个用户、8个代理、每天产生200个任务,那么架构上需要管理的就不是200个“请求-响应”,而是10×8×200维度的状态关系。用户可能中途换代理接手任务,代理可能把一个任务拆成三个子任务分发给其他代理,两个代理还可能争抢同一个工具的使用权。这种复杂性,靠“每个用户直接调模型API”是撑不起来的,必须有一个中间层来统一收口。
1.2 不能简单“并发调接口”的三个理由
很多人一开始会想:多代理协同不就是开几个线程并发调模型吗?我在原型阶段试过这个思路,三个理由推翻了它。
**第一,长任务的模型不是一次调用就能结束的。**实际场景里,代理要检索资料、写代码、跑测试、修改再验证,整个生命周期可能持续几分钟甚至更久。HTTP请求的超时、断连、重试机制完全不适配这种长时交互。代理需要像一个常驻进程一样,被系统跟踪、被用户追问、被其他代理等待。
**第二,代理之间需要共享上下文,而不是各自隔离。**比如一个代理负责调研技术方案,另一个代理负责写代码,第一个代理需要知道第二个代理选择了什么框架,否则推荐方案就是空谈。如果每个代理都各自持有一份独立的Prompt上下文,信息就断层了。
**第三,权限边界必须被系统强制,而不是靠提示词约束。**人与人之间的协作有职级、有视图、有审批,代替代人交互之后,这些边界不能丢。一个实习生级别的代理不该看到财务数据,这是系统架构上要保证的,不是靠一句“请忽略敏感信息”来兜底。
1.3 架构选型的核心取舍:控制面与数据面分离
多次推倒重来之后,我最终确定了整个架构的基调,用一句话概括:控制面与数据面分离,消息驱动代替请求驱动。
控制面管“谁说了算”——代理的注册、任务的编排、权限的判定、协商的仲裁。数据面管“事情本身”——文档内容、代码仓库、数据库记录、模型生成的中间结果。两者不混在一起,好处很明显:控制面可以做到极致的轻量和高可用,哪怕数据处理集群抖动,任务调度依然在线;数据面可以独立扩缩容,某个代理占资源再多也不会挤占调度逻辑。
对比几个主流思路,我的结论是:
| 架构风格 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 中心化编排(中央调度器统管所有代理) | 流程可控、容易调试、权限收敛 | 调度器可能成为瓶颈 | 团队规模小、任务流相对固定 |
| 去中心化协商(代理之间直接通信) | 扩展性好、代理自治性强 | 调试困难、难以全局约束 | 代理数量大、协作关系动态变化 |
| 混合式(控制面中心化+执行面去中心化) | 兼顾可控性与扩展性 | 架构复杂度高 | 多人多代理协同的现实场景 |
我最终选了混合式:任务编排和权限判定集中在一个轻量核心里,但代理在拿到任务后如何执行、内部调用什么工具,完全自治,不受干预。
2. 系统架构分层与消息总线设计
2.1 五层逻辑架构
整个系统我按五层切分,每一层的职责边界很清晰:
| 层级 | 职责 | 关键组件 |
|---|---|---|
| 接入层 | 面向Web端、客户端、API的入口 | 网关、会话管理、SSE推送 |
| 编排层 | 任务分发、状态机流转、协商仲裁 | 编排引擎、任务队列、仲裁服务 |
| 代理层 | 代理运行时,负责模型调用与工具执行 | 代理运行时、模型适配器、工具沙箱 |
| 协作层 | 代理间消息交换、共享上下文、事件溯源 | 消息总线、上下文仓库、事件日志 |
| 数据层 | 文档、知识库、用户数据、审计记录 | PostgreSQL、对象存储、向量库 |
接入层和人打交道,代理层和模型打交道,中间三层的存在价值,就是让这两端的复杂度互相隔离。用户可以随时更换代理背后的模型,代理也可以同时服务不同接入渠道的用户,彼此不影响。
2.2 代理注册与能力描述
每个AI代理在接入系统前,必须先完成“自描述”。我设计的代理注册信息里,核心是一个能力描述文件,用JSON Schema表达,类似这样:
{ "agent_id": "code-reviewer-01", "model": { "provider": "local", "model_name": "qwen2.5-coder:14b", "max_tokens": 8192 }, "skills": [ { "name": "code_review", "description": "对Pull Request中的代码变更进行审查,输出问题清单", "input_schema": { "type": "object", "properties": { "repo_url": {"type": "string"}, "pr_id": {"type": "string"} } } } ], "permissions": ["read_repo", "comment_pr"], "visibility": "project_scoped" }这里有个细节我要着重提醒:**能力描述不是为了给开发者看的,而是给编排层的“代理路由”组件看的。**路由组件会把一个任务的意图描述和所有已注册代理的skills描述做语义匹配,选出最合适的候选代理。如果能力描述写得含糊、字段层级混乱,路由精度就会直线下降。我最初给代理写的skills描述是“擅长写代码”这种话,结果路由几乎成了随机分发,后来把所有能力都改成“动词+对象+输出物”的结构化表述,路由准确率才上来。
2.3 以“任务事件”为最小颗粒的消息总线
系统内部的所有协作,都通过消息总线传递,消息的粒度不是“API调用”,而是“任务事件”。一个任务事件包含足够完整的语义信息,让任何订阅方不需要回溯历史就知道发生了什么。
{ "event_id": "evt_8f2a9c", "event_type": "task_submitted", "task_id": "task_1024", "source": "user:zhang", "target": "agent:code-reviewer-01", "payload": { "repo_url": "https://git.internal/project/api", "pr_id": "187" }, "context_ref": "ctx_56", "timestamp": "2025-06-18T14:32:10Z" }我选Redis Stream作为消息总线载体,理由是它天然支持消费组、持久化、多消费者负载均衡,比在应用层自己封装消息队列省事太多。事件日志同时全量写入PostgreSQL,用于之后的问题回溯和审计,这个“事件溯源”习惯在后来的排障中帮了大忙。很多诡异的问题,不靠事件日志回放根本定位不到。
3. 多AI协同的核心机制详解
3.1 三种协作模式:路由分发、流程编排、协商式协同
多人多AI协同不是一种模式,而是三种模式混用。
**路由分发是基座。**当任务目标明确、适合单一代理完成时,编排层直接把任务路由给最匹配的代理。比如“给这份合同提取关键条款”,路由组件匹配到文档抽取专用代理,事件发出,代理处理完回传结果,任务结束。这个过程系统负荷最轻,也最容易排查问题。
**流程编排负责线性流水线。**典型场景是“调研-方案-实现-验收”链路:调研代理先产出技术选型报告,方案代理基于报告写设计方案,代码代理再按方案生成代码,验收代理最后跑测试。每个环节的产出作为下一个环节的输入,编排层通过状态机控制流转。
**协商式协同是最难、也是最有价值的部分。当多个代理对一个问题的判断不一致,或者一个复杂任务需要多个代理同时贡献时,就不能简单排队了。我实现了一个轻量协商协议:发起方广播提案,候选代理返回赞同/反对/附条件赞同,仲裁服务聚合意见后在超时时间内给出结论。“协商”不是让代理们自由聊天,而是结构化地交换“结论+依据”。**否则两个模型对话起来,上下文很快会退化成互相客套的废话。
3.2 任务上下文管理与状态机流转
每个任务从提交到结束,会经历一个明确的状态机:
PENDING -> ASSIGNED -> RUNNING -> WAITING_INPUT -> RUNNING -> COMPLETED | | +----> FAILED <--------+ +----> TIMEOUT --------+状态机由编排层统一维护,代理不直接修改任务状态,只上报事件。这样设计的理由是:在任何时间点,系统都能给出“任务现在到哪一步了”的确定性回答。用户问“我的任务为什么还没好”,不用去翻代理日志,直接查状态即可。
上下文管理我用了一个“上下文袋”的机制。任务创建时建立context_ref,所有与该任务相关的事件、中间产出、代理回复、用户补充说明,都按时间顺序追加进这个袋子里。代理每次被唤醒时,编排层把上下文袋中与当前子任务相关的内容提取出来,拼进Prompt,而不是把全量历史都倒给模型。这个“按需提取”的设计解决了一个核心矛盾:任务历史越来越长,但模型上下文窗口是有上限的。
3.3 多人协同的权限与会话隔离
多人场景下,权限控制的原则是:**代理的权限不能超过委托它的用户的权限。**用户张三是项目的管理员,他创建的代理可以读写项目仓库;用户李四只是访客,他用同一套系统调起来的代理就只能读不能写。
我在实现里把权限控制放在编排层的“代理执行令牌”里。代理每次执行敏感操作(写文件、发消息、调外部服务),都要携带这个令牌,由网关统一鉴权。代理本身不做任何权限判断——模型天然不可信,你不能在自己写的代码里信任另一个模型“会乖乖遵守规则”。
会话隔离上,我按“用户维度的私密会话”和“项目维度的共享会话”做了区分。私密会话里的上下文只属于该用户;共享会话里的内容对项目内成员可见,但写入动作仍然遵循用户权限。这个区分一度被我忽略,直到有一次一个用户的私密任务被另一个用户在项目视图中看到了,才意识到隔离必须从架构上做。
3.4 本地模型与远程模型的统一接入
做这个架构研究时,我一直在真实项目里混用本地模型和云端模型,这是当前落地时绕不开的诉求。敏感数据不出内网、又要享受大模型能力的场景实在太普遍了。
我实现的方案是“模型适配器网关”,位于代理运行时和具体模型服务之间。代理不关心背后是本地模型还是远程模型,只面向统一的接口发请求:
代理 -> ModelGateway -> local_adaptor (Ollama/vLLM HTTP) -> remote_adaptor (云模型SDK)适配器负责处理两者之间的所有差异:上下文窗口大小、服务是否长连接、超时阈值、Token计费、错误重试。本地模型我用Ollama跑过7B到14B的模型,用vLLM跑过更大的模型,两者的响应模式完全不同——Ollama加载慢但单次请求简单,vLLM吞吐高但需要预热。这些差异全部封装在适配器内部后,上面的代理层完全无感。
有一个数据我实测下来很有参考价值:本地14B模型在单张消费级显卡上处理代码审查类任务,单次响应延迟在3-8秒之间,对交互式场景略慢,但对异步协作场景完全可接受。这个结论直接影响了我后来对系统是“同步请求”还是“异步任务”的架构取舍——我最终全面倒向了异步任务模型。
4. 实操:从零搭一个最小可用的多AI协同原型
4.1 技术选型与目录结构
原型环境我用了Python 3.11,核心组件选型如下:
| 组件 | 选择 | 理由 |
|---|---|---|
| Web框架 | FastAPI | 异步原生、类型提示友好、WebSocket支持成熟 |
| 消息总线 | Redis Stream | 轻量、支持消费组、自带持久化 |
| 编排状态存储 | PostgreSQL | 事务可靠、便于审计查询 |
| 代理运行时 | 轻量Python进程 | 避免过早引入重型框架 |
| 模型服务 | Ollama + 云模型SDK | 覆盖本地/远程两类场景 |
目录结构保持极简:
multi-agent-collab/ ├── gateway/ # 接入层:Web端API、SSE推送 ├── orchestrator/ # 编排层:状态机、任务分发、仲裁 ├── agents/ # 代理层:内置两个示例代理 ├── mcp_bridge/ # 模型适配器网关 ├── shared/ # 事件定义、能力描述Schema、鉴权工具 └── docker-compose.yml4.2 代理注册的最小实现
代理启动后,向编排层发送注册请求。实现里我用一个小函数封装注册逻辑:
async def register_agent(agent_info: dict): """向编排层注册代理,返回代理令牌""" async with httpx.AsyncClient() as client: resp = await client.post( "http://orchestrator:8001/agents/register", json=agent_info ) resp.raise_for_status() data = resp.json() return data["agent_token"], data["agent_id"]注册成功后,代理进入“可用”状态,编排层会定时发心跳探活。需要强调:**代理的注册和注销都要走同样的流程,不能直接杀进程。**有一次我图省事,直接kill了一个代理进程,结果编排层的状态表里一直残留着这个代理的“在线”记录,任务被分发过去后全都超时。后来我加了“心跳超时自动置离线”的机制才解决。
4.3 编排器核心:任务分发与状态流转
编排器的核心逻辑其实不复杂,就是事件驱动。我贴一个简化版的任务分发实现:
async def dispatch_task(task: Task): # 1. 状态改为 ASSIGNED await task_store.transition(task.task_id, "ASSIGNED") # 2. 语义匹配候选代理 candidates = await route_matcher.match(task.intent, top_n=3) # 3. 按顺序尝试分发,失败则自动换下一个 for agent in candidates: try: await bus.publish( "task_assigned", payload={"task_id": task.task_id, "agent_id": agent.agent_id} ) # 记录分发日志,便于追踪 await task_store.record_assignment(task.task_id, agent.agent_id) return except PublishError: continue # 4. 全失败则进入 FAILED,并通知提交者 await task_store.transition(task.task_id, "FAILED", reason="no_available_agent")这里有个很有价值的实战细节:**分发时要“先发布事件、后写状态”,而回执时反过来。**事件发布成功不代表代理真的收到了,所以编排层必须等待代理的状态回执事件才能确认任务真正被接管。如果先写状态再发事件,代理接收失败的场景下状态就会和数据不一致。
4.4 一次真实的多人双代理协作演示
原型里我建了两个代理:writer-agent负责写方案文档,reviewer-agent负责审查并给出修改意见。演示场景是:用户A提交“为内部工具开发写一份技术方案”,用户B实时围观并随时补充要求。
流程如下:
- 用户A通过Web端提交任务,网关创建task_001,状态PENDING
- 编排器匹配到writer-agent,分发任务,状态ASSIGNED
- writer-agent调用模型生成方案初稿,完成后发布task_completed事件
- 编排器收到事件后,自动触发reviewer-agent的审查任务
- reviewer-agent返回“方案缺少回滚策略”的审查意见,状态变为WAITING_INPUT
- 用户A看到意见后补充一句“加上灰度发布和回滚步骤”
- 编排器把补充内容追加到上下文袋,重新唤醒writer-agent修改
- 修改完成后,reviewer-agent复审通过,任务COMPLETED
整个过程中,没有任何一个环节是“用户直接等模型返回”。用户B在围观时看到的是任务状态的流式变化,想介入随时可以发事件。这种体验,和“同步调API”的人机交互有本质区别。
4.5 Docker Compose一键起
开发环境我用Docker Compose串联所有组件:
services: redis: image: redis:7-alpine ports: ["6379:6379"] postgres: image: postgres:16-alpine environment: POSTGRES_DB: agent_collab POSTGRES_USER: agent POSTGRES_PASSWORD: agent_pass ports: ["5432:5432"] orchestrator: build: ./orchestrator depends_on: [redis, postgres] ports: ["8001:8001"] gateway: build: ./gateway depends_on: [orchestrator] ports: ["8000:8000"] agent-writer: build: ./agents/writer depends_on: [orchestrator] agent-reviewer: build: ./agents/reviewer depends_on: [orchestrator]启动后依次验证:Redis和PostgreSQL健康检查通过、编排器注册接口可访问、两个代理完成注册、通过网关提交测试任务、观察任务状态流转日志。这套流程走通,一个最小可用的多人多AI协同系统就立起来了。
5. 常见问题与排查技巧实录
5.1 代理“假死”:进程活着但任务不响应
原型跑起来后遇到的第一个头疼问题就是“假死”。代理进程没有崩溃,CPU占用正常,但就是不处理新到的任务。
排查过程:先看编排器日志,发现任务事件已经发布到Redis Stream,消费组也确实拉到了消息,但代理内部处理循环卡住了。进一步追查,是代理在处理一个长文本任务时同步调用了模型接口,线程被阻塞,后续的新任务全部排队等待。
解决:把所有模型调用改成异步任务,处理循环里绝不阻塞。同时给每个任务处理加上超时上限,超时后强制终止并上报失败事件。我还在代理层加了一个“当前处理数”指标,编排器分发任务时会参考该指标,代理忙不过来就自动跳过。
5.2 上下文无限膨胀与模型失控
多轮协同时,“上下文袋”越来越大,直接全量塞进Prompt的结果是:Token消耗暴增、模型开始胡言乱语、把最早的信息错误地当成了当前指令。
解决思路是“分级记忆”。短期记忆保留最近5轮完整交互;中期记忆做摘要,让模型在每轮结束时生成一段结构化摘要存入上下文袋;长期记忆只保留关键决策和结论,细节内容存向量库按需检索。
实测效果:同样是30轮交互,全量上下文方案Token消耗约4.6万,分级记忆方案降到1.2万,而且模型回答的准确率明显提升。注意这里有个经验——**摘要一定让代理自己在结束对话时生成,然后由编排层校验摘要结构,不要在做下一次请求时临时压缩历史。**临时压缩会丢失对话中的隐性信息。
5.3 协商死锁与重复执行
两个代理互相等待对方确认,协商流程卡死,是我在实现协商协议时踩的另一个坑。场景是:writer-agent把方案发给reviewer-agent审查,reviewer提出“需要补充性能测试数据”,于是writer等reviewer给更多信息,reviewer等writer修改后再审,两边都认为自己“已经回应过了”。
解决:协商里必须加“仲裁超时”。每次协商发起时,仲裁服务启动一个定时器,比如60秒,超时后自动做决策:要么强制通过当前提案,要么选择置信度更高的代理的意见,要么直接升级给人工用户。同时,任务执行要加幂等控制,每个任务事件带全局唯一的event_id,代理执行前先去重,防止同一个子任务被重复执行两遍。
5.4 权限边界泄漏的“魔幻现场”
有一次共享会话里,一个只读权限的代理居然成功给项目仓库打了Tag。检查后发现,问题出在模型适配器网关上:代理的Prompt里获取到了“拥有者身份”的上下文,模型“自以为”自己有写权限,于是调用了写接口,而网关没有校验代理令牌对应的用户权限,直接放行了。
这是我整个预研中最深刻的教训之一:**所有权限判断都必须放在系统边界层,绝不能信任提示词里封装的“角色”信息。**修复方案是给每次外部调用都附加“执行令牌”,网关针对每个外部操作重新鉴权,同时记录审计日志。从那以后,我再也没遇到过代理越权的事。
5.5 踩坑速查表
| 症状 | 根因 | 解决 |
|---|---|---|
| 代理在线但不处理任务 | 同步模型调用阻塞处理循环 | 改异步、加超时、上报处理数指标 |
| 长对话后模型效果暴跌 | 上下文全量注入、关键信息被稀释 | 分级记忆、按需提取上下文 |
| 协商双方永久等待 | 缺少仲裁超时机制 | 仲裁定时器、强制定向决策 |
| 只读代理做了写操作 | 权限判断被交给模型 | 网关统一鉴权、代理令牌机制 |
| 任务状态与实际不符 | 事件发布顺序与状态写入不一致 | 先事件后状态,回执确认严格分离 |
| 消息总线积压但无报错 | 消费组没有确认消息 | 设置合理的ACK策略,监控消费延迟 |
6. 还能往哪个方向扩展
原型验证完成之后,我还有几个明确的扩展方向,已经在陆续测试。
第一个是事件回放与复盘工具。既然所有交互都以事件形式持久化了,理论上完全可以做“回放任意时间段、任意项目、任意代理的所有交互”。这对多人团队复盘AI代理的决策过程很有价值——出了问题不再是黑盒,可以精确到某个时间点某个代理看到了什么上下文、做出了什么决策。
第二个是多代理记忆的分层持久化。目前原型里上下文袋是跟任务绑定的,任务结束就归档。但长期看,代理自己应该有跨任务的“项目记忆”。比如reviewer-agent改进后,应该能记住用户偏好的审查风格,而不是每次任务都从零学习。这部分我计划用向量库来实现。
第三个是人机混合编排。现在的编排器只能调用AI代理,但实际工作中“需要真人审批”的环节一直存在。我想把人工审批节点也做成一种“特殊代理”,接入同样的消息协议。这样一来,整个流程的编排语言就统一了——对上层来说,AI代理和人工审批节点都是一等公民,都有状态、有超时、有回执。
最后再分享一点体会
两个多月的预研走下来,我最大的感想是:**做多人多AI协同系统,难点根本不在模型能力,而在于把“交互”本身做成可靠的基础设施。**模型会迭代、会换供应商、会升级参数,这些都不重要;重要的是你的架构能不能让新代理像插USB设备一样即插即用,能不能让两个代理在完全不共享提示词的前提下高效协作,能不能在某个代理发疯时把影响范围控制在最小。
这套架构目前虽然还是原型,但设计思路已经迁移到了我的实际项目里——团队里最近新接入的代码审查代理、文档生成代理,都是按这个模式挂载的。有朋友听说我在做这个方向,问我“要不要上K8s”“要不要用服务网格”,我的回答是:先把消息协议、代理注册、状态流转这几个基础机制做扎实,比什么花哨的中间件都管用。架构从来不是越大越好,而是每一层都能回答“为什么需要它”。