多智能体系统里,Agent 之间的通信是个容易被低估的难题。本文以 AgentScope-Java 的 MsgHub 为例,聊聊我们在实践中踩过的坑,以及订阅-广播机制的设计思路。
一、为什么要专门做个通信层
1.1 常见但别扭的做法
三个狼人 Agent 讨论今晚刀谁,你会怎么写?
手动透传消息
// 没有 MsgHub 的情况
Msg aResponse = wolfA.call().block();
wolfB.observe(aResponse).block();
wolfC.observe(aResponse).block();
Msg bResponse = wolfB.call().block();
wolfA.observe(bResponse).block();
wolfC.observe(bResponse).block();
// 代码重复,维护噩梦
并发调用
// 同时调用所有 Agent
var responses = Flux.just(wolfA, wolfB, wolfC)
.flatMap(agent -> agent.call())
.collectList()
.block();
// 问题:每个 Agent 基于相同的初始上下文,无法互相影响
同步阻塞(死锁风险)
// 如果每个 Agent 都在等其他人先发言
Msg aResponse = wolfA.call().block(); // A 等 B
Msg bResponse = wolfB.call().block(); // B 等 C
Msg cResponse = wolfC.call().block(); // C 等 A
// 死锁
这些方案要么啰嗦,要么有隐患。我们需要一种让 Agent 既能"听到"彼此,又不会互相卡住的机制。
二、MsgHub 的设计思路
2.1 核心机制
MsgHub 用发布-订阅模式解耦消息传递:
// AgentBase.java
public class AgentBase {
// 每个 Agent 维护自己的订阅者列表
private final Map<String, List<AgentBase>> hubSubscribers;
// observe() 只收消息,不触发响应
protected Mono<Void> doObserve(Msg msg) {
if (msg != null) {
memory.addMessage(msg);
}
return Mono.empty(); // 不调用 LLM
}
// call() 完成后自动广播
private Mono<Msg> notifyPostCall(Msg finalMsg) {
return broadcastToSubscribers(finalMsg).thenReturn(finalMsg);
}
}
2.2 订阅关系的建立
// MsgHub.java
private void resetSubscribers() {
if (enableAutoBroadcast) {
for (AgentBase agent : participants) {
List<AgentBase> others = participants.stream()
.filter(a -> !a.equals(agent))
.collect(Collectors.toList());
agent.resetSubscribers(name, others);
}
}
}
订阅关系示意:
狼人 A → 订阅者:[狼人 B, 狼人 C]
狼人 B → 订阅者:[狼人 A, 狼人 C]
狼人 C → 订阅者:[狼人 A, 狼人 B]
三、完整工作流程
3.1 时序示意
sequenceDiagram
participant Hub as MsgHub
participant A as 狼人 A
participant B as 狼人 B
participant C as 狼人 C
Note over Hub: hub.enter()
Hub->>A: resetSubscribers([B, C])
Hub->>B: resetSubscribers([A, C])
Hub->>C: resetSubscribers([A, B])
Note over Hub: 第 1 轮讨论
A->>A: call() - 调用 LLM
Note right of A: Memory: [系统提示]
A-->>B: broadcast("淘汰甲")
A-->>C: broadcast("淘汰甲")
B->>B: observe() - 存入 Memory
C->>C: observe() - 存入 Memory
B->>B: call() - 调用 LLM
Note right of B: Memory: [系统提示,A 的发言]
B-->>A: broadcast("同意")
B-->>C: broadcast("同意")
A->>A: observe() - 存入 Memory
C->>C: observe() - 存入 Memory
C->>C: call() - 调用 LLM
Note right of C: Memory: [系统提示,A,B 的发言]
C-->>A: broadcast("没问题")
C-->>B: broadcast("没问题")
Note over Hub: 第 2 轮讨论(信息更充分)
A->>A: call() - 调用 LLM
Note right of A: Memory: [系统提示,B,C 第 1 轮发言]
3.2 实际使用示例
try (MsgHub werewolfHub = MsgHub.builder()
.name("WerewolfDiscussion")
.participants(werewolves.toArray(ReActAgent[]::new))
.announcement(prompts.createWerewolfDiscussionPrompt(gameState))
.enableAutoBroadcast(true)
.build()) {
werewolfHub.enter().block(); // 建立订阅关系
// 2 轮讨论,让信息充分流通
for (int i = 0; i < 2; i++) {
for (Player werewolf : werewolves) {
Msg response = werewolf.getAgent().call().block();
String content = utils.extractTextContent(response);
emitter.emitPlayerSpeak(werewolf.getName(), content, "werewolf_discussion");
}
}
// 投票阶段:关闭广播,确保独立决策
werewolfHub.setAutoBroadcast(false);
FanoutPipeline votingPipeline = FanoutPipeline.builder()
.addAgents(werewolves.stream()
.map(p -> (AgentBase) p.getAgent())
.toList())
.concurrent()
.build();
List<Msg> votes = votingPipeline.execute(votingPrompt, VoteModel.class).block();
}
四、关键技术细节
4.1 避免死锁的关键:observe() 与 call() 分离
方法 触发时机 调用 LLM 作用
observe(msg) 被动接收广播 否 仅将消息存入内存
call() 主动调用 是 执行 ReAct 循环,生成回复
这个分离确保了:
- 接收消息不会触发新的响应
- 只有显式调用
call()才会产生输出 - 避免"收到消息 → 自动回复 → 触发更多回复"的无限循环
4.2 异步广播实现
private Mono<Void> broadcastToSubscribers(Msg msg) {
if (hubSubscribers.isEmpty()) {
return Mono.empty();
}
return Flux.fromIterable(hubSubscribers.values())
.flatMap(Flux::fromIterable)
.flatMap(subscriber -> subscriber.observe(msg))
.then();
}
基于 Project Reactor 实现非阻塞广播,支持背压处理。
4.3 信息不对称的处理
问题:顺序发言导致信息差异
第 1 轮:
├─ A 发言 → Memory: [系统提示] ← 不知道 B、C 的想法
├─ B 发言 → Memory: [系统提示,A 的话] ← 知道 A,不知道 C
└─ C 发言 → Memory: [系统提示,A、B 的话] ← 知道 A 和 B
缓解方案:多轮讨论 + 轮换顺序
for (int i = 0; i < 2; i++) {
List<Player> roundOrder = new ArrayList<>(werewolves);
if (i % 2 == 1) {
Collections.reverse(roundOrder); // 奇数轮倒序
}
for (Player werewolf : roundOrder) {
werewolf.getAgent().call().block();
}
}
第 2 轮时,每个 Agent 都能基于更完整的上下文做决策。
五、应用场景
5.1 狼人杀游戏
// 狼人夜间讨论(私密频道)
try (MsgHub werewolfHub = MsgHub.builder()
.participants(werewolves.map(Player::getAgent))
.announcement(discussionPrompt)
.enableAutoBroadcast(true)
.build()) {
// 自动广播确保信息共享
}
// 白天全体讨论(公共频道)
try (MsgHub dayHub = MsgHub.builder()
.participants(alivePlayers.map(Player::getAgent))
.announcement(dayDiscussionPrompt)
.enableAutoBroadcast(true)
.build()) {
// 所有存活玩家共享信息
}
5.2 团队头脑风暴
// 创意收集阶段:启用广播,激发灵感
MsgHub brainstormingHub = MsgHub.builder()
.participants(teamMembers)
.enableAutoBroadcast(true)
.build();
// 投票决策阶段:关闭广播,避免从众
brainstormingHub.setAutoBroadcast(false);
FanoutPipeline votingPipeline = FanoutPipeline.builder()
.addAgents(teamMembers)
.concurrent()
.build();
六、工程实现要点
6.1 线程安全
private final List<AgentBase> participants =
new CopyOnWriteArrayList<>(builder.participants);
6.2 动态参与者管理
hub.add(newAgent).block();
hub.delete(existingAgent).block();
// 自动更新订阅关系
6.3 资源清理
try (MsgHub hub = createHub()) {
hub.enter().block();
// 使用 hub
} // 自动调用 exit(),释放订阅关系
七、使用建议
推荐做法
- 区分讨论与决策阶段
// 讨论阶段:启用广播
hub.enableAutoBroadcast(true);
// 决策阶段:关闭广播,并发执行
hub.setAutoBroadcast(false);
pipeline.concurrent();
- 多轮讨论促进信息流通
for (int i = 0; i < 2; i++) {
// 轮流发言
}
- 使用 try-with-resources 管理资源
try (MsgHub hub = ...) {
// 使用
}
避免的做法
- 不要在 observe() 中调用 call()
// 错误示例
protected Mono<Void> doObserve(Msg msg) {
memory.addMessage(msg);
return call().then(); // 会导致无限循环
}
原因:doObserve 处理消息时调用 call(),而 call() 又可能产生新消息触发 doObserve,形成递归。
- 谨慎并发调用 call()
// 除非你明确需要并发且处理了状态隔离
Flux.just(agent1, agent2)
.flatMap(agent -> agent.call())
.block();
并发调用可能破坏 Agent 的状态一致性,特别是当 Agent 维护对话历史或会话上下文时。
八、源码
- AgentScope-Java: github.com/agentscope-…
- MsgHub:
agentscope-core/src/main/java/io/agentscope/core/pipeline/MsgHub.java - AgentBase:
agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java
总结
MsgHub 通过订阅-广播机制解决了多 Agent 通信的几个痛点:
- 避免死锁:observe/call 分离设计
- 性能:基于 Reactor 的异步广播
- 灵活性:支持动态增删参与者
- 真实感:保留信息不对称,更贴近实际场景
这个设计不仅适用于游戏场景,也可用于团队协作、角色扮演等需要多 Agent 交互的场合。