简介:围绕多智能体系统协作研究,这份 PDF 文献聚焦 Internet of Agents(IoA)框架的设计与实验验证,面向关注异构智能体集成、分布式计算与自动化团队形成的研究者和开发者。它针对现有框架生态隔离、单设备仿真、通信协调僵化等问题,提出灵活的多智能体集成协议、类即时消息架构以及动态团队组建与对话流控制机制,并在通用助理、具身智能和检索增强生成等任务上验证协作效果。压缩包仅含 1 个 PDF 文件,大小约 4.21MB,便于集中阅读与归档。已有 249 人学习下载。读者可获取 IoA 的体系结构、关键机制与实验对比结论,理解动态任务分配和异构智能体协作的实现思路,也可结合论文公开的代码线索继续复现与拓展研究;其中对第三方智能体集成和分布式协作场景的讨论,适合作为方案设计与论文研读参考。
1. 从单机编排到跨设备组网:IoA 想解决的三个真问题
同一个开放域任务,AutoGPT 和 Open Interpreter 各自单跑,与把它们接进同一个群聊协作后再跑,后者的胜率是 66% 到 76%。这里没换更强的模型,变的只是协作结构。IoA(Internet of Agents)由清华、北大、北邮和腾讯合作提出,抓的就是这件反直觉的事:多智能体系统的瓶颈常常不在单个 agent 的智力,而在它们之间那根线怎么接。
现有 agent 框架的毛病集中在三处。生态隔离,agent 只能在自家生态里定义,第三方能力接不进来;单设备仿真,几乎所有框架都在一台机器上假装分布式,跟真实的多机多端差得远;通信管道硬编码,谁跟谁说话、何时从讨论切到执行,全写在代码里,任务一变就得改代码。IoA 的应对是三条:一套 agent 集成协议、一套类即时消息的架构、一套动态的组队与对话流控制机制。
适合人群也明确:手里已经有 ReAct、AutoGPT 这类跑得起来的 agent、想试着把它们组网协作的开发者,以及需要在多台机器上做分布式智能体实验的研究者。
2. IoA 的服务端与客户端分层,以及 Agent 注册发现协议
理解 IoA 的第一步不是读它的实验数据,而是看清 server 和 client 各自被切成了三层。这个切法决定了后面所有"加一个 agent""改一次路由"的成本。
2.1 交互层、数据层、基础层各自负责什么
服务端是中心枢纽,承担 agent 注册、发现和消息路由;客户端是个体 agent 的 wrapper,负责把第三方 agent 接进这套协议。两侧结构对称,都是三层。
| 层 | 服务端区块 | 客户端区块 | 职责 |
|---|---|---|---|
| Interaction Layer | Agent Query / Group Setup / Message Routing | Team Formation / Communication | 找 agent、建群、消息扇出;组队决策与消息处理 |
| Data Layer | Agent Registry / Session Management | Agent Contact / Group Info / Task Management | 注册表、活跃连接;联系人、群信息、任务状态 |
| Foundation Layer | Data Infra / Network Infra / Security | Agent Integration / Data Infra / Network Infra | 持久化、网络通信、鉴权;第三方 agent 适配接口 |
选中心化 hub 而不是全互联 P2P,是为了让发现成本可控:任何 agent 只要问一次服务端,就能拿到候选名单,不用维护一张全网路由表。代价是 server 成为单点,所以 Foundation Layer 里的 Session Management 和 Security 必须做实,否则重启一次服务端就等于全网掉线。分层带来的直接好处是扩展性——新增一种 agent 类型只需要改客户端的 Agent Integration Block,消息路由逻辑一行都不用动。
2.2 注册报文该写什么:能力描述字段设计
注册是发现的前提。一个 agent 加入 IoA 时,它的 wrapper 要向服务端提交一份能力自述,这份自述后面会被别的 agent 用自然语言检索到。
{ "agent_id": "react_retriever_01", "name": "RetrieverAgent", "endpoint": "ws://10.0.12.7:8765/agent", "capabilities": ["web_search", "vector_retrieval", "citation"], "description": "擅长从本地向量库与公开网页检索事实,输出带出处的片段", "model": "gpt-3.5-turbo", "max_concurrency": 2, "status": "idle" }capabilities是硬过滤条件,查询里指定了就必须命中;description是软匹配文本,供语义检索用;endpoint决定服务端把群消息投到哪个连接;max_concurrency是自我保护,防止一个 agent 被同时塞进十个群聊。description写得太泛(比如"我能做很多事情")是召回噪声的主要来源,经验做法是把领域、输入形态、输出形态三件事写清楚,上面这条就属于合格写法:领域是检索,输入是查询串,输出是带出处的片段。
2.3 一次完整的注册、发现、建群链路
客户端 wrapper 的核心逻辑不复杂,但顺序不能乱。
import asyncio, json, websockets class IoAClient: def __init__(self, server_url, profile): self.server_url = server_url # 例如 ws://127.0.0.1:8765/ws self.profile = profile # 2.2 里的注册报文 self.ws = None async def connect(self): self.ws = await websockets.connect(self.server_url) await self.ws.send(json.dumps({"type": "register", **self.profile})) ack = json.loads(await self.ws.recv()) # 必须等 ack,此时服务端才把 agent_id 与这条连接绑定 assert ack["type"] == "register_ack", ack print("registered as", ack["agent_id"]) async def query_agents(self, text, top_k=3): await self.ws.send(json.dumps({ "type": "agent_query", "query": text, # 自然语言描述需要什么能力 "top_k": top_k # 返回候选数量 })) return json.loads(await self.ws.recv())["agents"] async def heartbeat(self, interval=20): while True: await asyncio.sleep(interval) await self.ws.send(json.dumps({ "type": "ping", "agent_id": self.profile["agent_id"] })) async def listen(self): async for raw in self.ws: msg = json.loads(raw) if msg["type"] == "group_message": print(msg["group_id"], msg["sender"], msg["content"])register之后如果不先等register_ack就发别的消息,服务端的 Session Management 还没完成绑定,消息会被直接丢弃——这是接入时最容易撞的一堵墙。心跳必须放在独立协程里,interval要小于服务端的session_timeout,一般取它的三分之一。agent_query返回的是候选列表而不是直接建群,建群这一步交给客户端的 Team Formation 决定,这是 IoA 和硬编码 pipeline 的分水岭。top_k调大能让决策上下文更全,但每个候选的 description 都要进 prompt,token 成本线性上涨,实验阶段 3 到 5 比较划算。
2.4 注册与发现的四个常见坑
agent_id重名。多台机器用同一份默认配置启动,注册表里后注册的把先注册的覆盖掉,表现为"队友时有时无"。常见做法是把主机名或容器序号拼进 id。
能力声明与实现不符。声明了code_execution却没有沙箱,任务派过去直接失败,而失败信息只回到群里,排查要翻半天。注册报文里的capabilities应当由代码里的能力注册表自动生成,而不是手写。
心跳与长任务冲突。一个 agent 跑五分钟的检索任务时阻塞了事件循环,心跳发不出去被判离线,群里的任务随之超时。把执行体丢进线程池或子进程是标准解法。
重连后没有重新注册。断线重连只是换了一条 WebSocket 连接,Agent Registry 里记的还是旧 endpoint,消息投过去石沉大海。客户端的重连回调里必须重发一次register。
3. 自动化团队形成与对话流控制的有限状态机
组队和对话流是 IoA 里最不像"框架"、最像"机制"的部分。它借的是即时消息的心智模型:先查通讯录,再拉群,群内再分工。
3.1 组队为什么不能写死
硬编码的 pipeline 隐含一个假设——任务形态固定,参与者固定,轮次固定。真实任务里,主 agent 得先看有谁能干活,再决定拉谁进群,进了群还得判断此刻是在讨论还是在执行。把这三件事写进代码,任务一变就得改代码;交给 agent 自己决定,则需要一个受约束的决策空间,否则群聊会发散。IoA 的做法是给决策空间加上状态边界。
3.2 从 Agent Query 到 Group Setup 的执行链
一次典型的组队过程分五步:
- 主 agent 把任务拆成能力需求描述,例如"需要一个能跑 Python 并读文件的执行者"。
- 发
agent_query,拿到候选列表。 - 按
capabilities做硬过滤,再按description做一轮语义筛选,通常留下 2 到 4 个。 - 发
group_setup,服务端分配group_id,Message Routing 开始按群扇出。 - 群内进入会话状态机,由状态决定当前该说什么话。
第 3 步值得单独说。候选太多会拖长决策上下文,太少又容易漏掉合适的人。我一般会把硬过滤放在前面,因为capabilities是确定性的,而description的语义匹配有噪声,先砍规模再精排的顺序更稳。
3.3 会话状态的抽象与转移条件
这一层借鉴了言语行为理论:说话本身就是行动。群里的每条消息都带一个意图标签,状态根据意图和上下文推进。
| 状态 | 允许的发言意图 | 进入下一状态的条件 | 建议轮次上限 |
|---|---|---|---|
| DISCUSSION | question / info | 有成员提出可执行方案 | 6 |
| PROPOSAL | proposal / agree / object | 同意比例达标或主 agent 拍板 | 3 |
| ASSIGN | assign / accept | 所有子任务都有 owner | 2 |
| EXECUTION | progress / result | 全部子任务回报完成 | 视任务 |
| REVIEW | verdict / reject | 校验通过或打回次数用尽 | 2 |
| CLOSED | — | 终态 | — |
from enum import Enum class State(str, Enum): DISCUSSION = "discussion" PROPOSAL = "proposal" ASSIGN = "assign" EXECUTION = "execution" REVIEW = "review" CLOSED = "closed" # 每个状态允许的意图,越界的消息不参与转移判断,只记录 ALLOWED = { State.DISCUSSION: {"question", "info"}, State.PROPOSAL: {"proposal", "agree", "object"}, State.ASSIGN: {"assign", "accept"}, State.EXECUTION: {"progress", "result"}, State.REVIEW: {"verdict", "reject"}, } def next_state(state, intent, ctx, cfg): if intent not in ALLOWED[state]: return state, "intent_not_allowed" if state is State.DISCUSSION and intent == "proposal": return State.PROPOSAL, "proposal_raised" if state is State.PROPOSAL and ctx["agree_ratio"] >= cfg["agree_ratio"]: return State.ASSIGN, "consensus" if state is State.ASSIGN and ctx["unassigned"] == 0: return State.EXECUTION, "all_assigned" if state is State.EXECUTION and ctx["pending"] == 0: return State.REVIEW, "all_reported" if state is State.REVIEW and ctx["rejected"] <= cfg["max_reject"]: return State.CLOSED, "accepted" if ctx["turns_in_state"] >= cfg["turn_limit"][state]: return State.CLOSED, "timeout" # 兜底,别让群聊无限循环 return state, "continue"intent由成员 agent 发消息时标注,或用一个轻量分类器打标,成本很低。turns_in_state是防死循环的关键闸门:灵活不等于无界,没有这个计数器,两个 agent 互相 object 能把 token 烧光。
3.4 状态机参数怎么调
agree_ratio调太低,一个 agent 提方案就全员执行,质量塌得很快;调太高,PROPOSAL 阶段频繁卡死。经验值是成员数不超过 3 时取 0.5,5 人以上取 0.6 到 0.7,因为人多了意见本来就更难统一。max_reject建议设 2,第三次仍不通过就带着警示标注返回,比无限重试省得多。turn_limit的 DISCUSSION 项最需要调,它直接决定组队阶段花掉多少 token,一般按单次任务预算的 20% 到 30% 倒推。
4. 异构 Agent 接入与多设备分布式部署
IoA 相对其它 agent 框架在集成上的关键差异是:不要求第三方 agent 用同一套工具抽象,只要能被包装成"字符串进、字符串出"就接得进来。
4.1 Wrapper 适配层:把第三方 agent 包成 IoA 客户端
class ReActWrapper: """把任意 callable 形式的 agent 适配成 IoA 可调用的执行体。""" def __init__(self, agent_id, fn, capabilities): self.agent_id = agent_id self.fn = fn # 签名固定为 fn(task: str) -> str self.capabilities = capabilities async def handle(self, msg, loop): task = msg["content"] # 阻塞型 agent 必须丢线程池,否则心跳协程被卡住会被判离线 result = await loop.run_in_executor(None, self.fn, task) return { "type": "group_message", "group_id": msg["group_id"], "sender": self.agent_id, "intent": "result", # 供状态机判断 "content": result, "reply_to": msg["msg_id"], # 便于做幂等与串线排查 }fn的签名约束是整套适配的核心:AutoGPT、Open Interpreter、ReAct 循环、甚至一段纯 Python 函数,只要满足这个签名就能接进来。run_in_executor不是为了性能,是为了让事件循环空出来发心跳。reply_to带上原消息 id,重连重发时按msg_id去重就能避免群里出现重复结论。
4.2 分布式部署的配置项与端口规划
# server.yaml server: host: 0.0.0.0 ws_port: 8765 session_timeout: 60 # 秒,超时未心跳则踢掉会话 registry: backend: sqlite # 小规模实验够用,多机压测换 postgres persist_path: ./data/registry.db routing: fanout_mode: group # group 按群扇出,direct 点对点 security: token_ttl: 3600| 参数 | 含义 | 单机实验 | 多机部署建议 |
|---|---|---|---|
| host | 监听地址 | 127.0.0.1 | 0.0.0.0,否则跨机连不上 |
| ws_port | WebSocket 端口 | 8765 | 固定,防火墙放行 |
| session_timeout | 心跳超时 | 60 | 不小于心跳间隔的 3 倍 |
| backend | 注册表存储 | sqlite | postgres,避免多进程写冲突 |
| fanout_mode | 路由模式 | group | 群成员跨网段时仍用 group |
| token_ttl | 鉴权有效期 | 3600 | 与实验时长匹配,避免中途过期 |
4.3 启动顺序与联通性自检
# 1) 起服务端,先确认端口在听 python -m ioa.server --config server.yaml & ss -lntp | grep 8765 # 2) 起两个能力不同的 agent(可分别跑在不同机器上) python -m ioa.client --agent-id retriever_01 --endpoint ws://10.0.12.1:8765/ws & python -m ioa.client --agent-id coder_01 --endpoint ws://10.0.12.1:8765/ws & # 3) 查注册表,确认两个 agent 都可被发现 python -m ioa.cli query --server ws://10.0.12.1:8765/ws \ --text "能写并执行 python 代码的 agent"第 3 步最容易被跳过,但注册成功不等于能被发现。query返回空通常意味着description与查询文本的语义距离太远,或者该 agent 的status不是idle。定位这类问题时,先用一段几乎照抄description的文本查一次,如果查得到,就说明是描述写法的问题而不是链路问题。
4.4 故障对照表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 连上但注册失败 | token 过期或 ack 超时 | 看服务端 Security 日志 |
| 队友列表为空 | capabilities 过滤过严 | 换纯文本 query 再试一次 |
| 群消息重复 | 重连后没做去重 | 客户端按 msg_id 幂等 |
| agent 频繁被判离线 | 长任务阻塞心跳 | 执行移到线程池或子进程 |
| 跨机器不通 | endpoint 填了 127.0.0.1 | 改 bind 地址与注册 endpoint |
| 群建了但没人说话 | group_setup 成员列表为空 | 检查建群前的候选筛选逻辑 |
5. 验证实验设计与 RAG 场景下的两个调优技巧
5.1 胜率评估怎么设计才站得住
开放域任务上的对比实验,结论强度取决于控制变量的干净程度。几个必须做的动作:同一批任务分别跑单 agent 和 IoA 组队,judge 用固定的模型且对两边匿名,任务顺序随机化以抵消上下文缓存带来的偏差,每一组至少重复 3 次取均值。IoA 在 GAIA 上只用几个基础 ReAct agent 就超过了此前的工作,在 RAG 问答上 GPT-3.5 版本的实现接近甚至超过 GPT-4,这两个结果的说服力恰恰来自任务与 judge 的一致性,而不是任务数量堆得多。评估脚本里建议把每次运行的group_id、状态转移序列和最终答案一起落盘,出问题时才能回放整场对话。
5.2 让 GPT-3.5 逼近 GPT-4 的两个协作技巧
第一个技巧是拆职责而不是拆步骤。把检索和回答分给两个 agent:retriever 只负责返回带出处的片段,answer agent 只负责在给定片段上组织答案。同一个模型同时干这两件事时,上下文里既有噪声片段又有写作指令,注意力会被分散;拆开之后两个 agent 的 prompt 都更短更聚焦,这是收益最大的改动。
第二个技巧是把自检挪出原上下文。在 REVIEW 状态放一个独立的校验 agent,只做一件事:逐句判断答案是否被片段支撑。
def review(answer, passages, judge): prompt = ( "只依据给定片段逐句检查答案。" "输出 JSON: {\"unsupported\": [句子], \"verdict\": \"pass\"|\"reject\"}" ) payload = f"片段:\n{passages}\n\n答案:\n{answer}" out = judge(prompt, payload) # judge 用与答题不同的系统提示 return out["verdict"] == "pass"关键在judge用的是另一套系统提示、另一次调用,而不是让答题的 agent 自己回看。同一上下文里的自检几乎必然通过,因为模型倾向于认可自己刚才的输出。max_reject建议设 2,第三次仍不通过就直接返回带警示标注的答案,比无限重试省 token,也更接近真实系统的行为。
本文还有配套的精品资源,点击获取