AgentScope MsgHub 多智能体通信机制详解

引言

在多智能体系统中,如何让多个 AI 智能体高效、安全地进行通信和协作?本文将以 AgentScope-Java 的 MsgHub 为核心,深入解析其订阅 - 广播机制的设计哲学与实现细节。


一、核心问题:多智能体通信的挑战

1.1 传统方案的问题

假设我们有三个狼人智能体 A、B、C,需要讨论今晚要攻击的目标:

❌ 手动传递消息(繁琐且易错)

java 复制代码
// 没有 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();

// ... 代码重复,难以维护

❌ 并发调用(信息不同步)

java 复制代码
// 同时调用所有 Agent
var responses = Flux.just(wolfA, wolfB, wolfC)
    .flatMap(agent -> agent.call())
    .collectList()
    .block();
// 问题:每个 Agent 都基于相同初始信息,无法互相影响

❌ 同步阻塞(可能死锁)

java 复制代码
// 如果每个 Agent 都在等其他人先发言
Msg aResponse = wolfA.call().block();  // A 等 B
Msg bResponse = wolfB.call().block();  // B 等 C
Msg cResponse = wolfC.call().block();  // C 等 A
// 💀 死锁!

二、MsgHub 的核心设计

2.1 订阅 - 广播模式

MsgHub 采用 发布 - 订阅模式,通过以下关键机制避免上述问题:

核心组件

java 复制代码
// 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);
    }
}

订阅关系建立

java 复制代码
// 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 → [others]
            agent.resetSubscribers(name, others);
        }
    }
}

订阅关系图示:

css 复制代码
狼人 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 关键代码示例

java 复制代码
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) {
            // 顺序调用,每个 Agent 都能听到前面的发言
            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 响应式异步广播

java 复制代码
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 信息不对称的设计哲学

问题:顺序发言导致信息差异

less 复制代码
第 1 轮:
├─ A 发言 → Memory: [系统提示]          ← 不知道 B、C 的想法
├─ B 发言 → Memory: [系统提示,A 的话]   ← 知道 A,不知道 C
└─ C 发言 → Memory: [系统提示,A、B 的话] ← 知道 A 和 B

解决方案:多轮讨论

java 复制代码
// 2 轮讨论弥补信息差
for (int i = 0; i < 2; i++) {
    for (Player werewolf : werewolves) {
        werewolf.getAgent().call().block();
    }
}

第 2 轮时:

  • A 已经听过 B、C 在第 1 轮的发言
  • B 已经听过 A、C 在第 1 轮的发言 + A 在第 2 轮的发言
  • C 已经听过所有人第 1 轮 + A、B 第 2 轮的发言

优化建议:轮换发言顺序

java 复制代码
// 每轮换顺序,减少偏差
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();
    }
}

五、实际应用场景

5.1 狼人杀游戏

java 复制代码
// 狼人夜间讨论(私密频道)
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 团队头脑风暴

java 复制代码
// 创意收集阶段:启用广播,激发灵感
MsgHub brainstormingHub = MsgHub.builder()
    .participants(teamMembers)
    .enableAutoBroadcast(true)
    .build();

// 投票决策阶段:关闭广播,避免从众
brainstormingHub.setAutoBroadcast(false);
FanoutPipeline votingPipeline = FanoutPipeline.builder()
    .addAgents(teamMembers)
    .concurrent()
    .build();

5.3 多人角色扮演

java 复制代码
// 剧本杀、互动小说等场景
MsgHub storyHub = MsgHub.builder()
    .participants(characters)
    .announcement(storyContext)
    .enableAutoBroadcast(true)
    .build();
// 每个角色的发言都会成为其他人的上下文

六、性能与扩展性

6.1 线程安全

java 复制代码
// 使用 CopyOnWriteArrayList 保证线程安全
private final List<AgentBase> participants = 
    new CopyOnWriteArrayList<>(builder.participants);

6.2 动态参与者

java 复制代码
// 运行时添加/删除参与者
hub.add(newAgent).block();
hub.delete(existingAgent).block();
// 自动更新订阅关系

6.3 资源管理

java 复制代码
// try-with-resources 自动清理
try (MsgHub hub = createHub()) {
    hub.enter().block();
    // 使用 hub
} // 自动调用 exit(),释放订阅关系

七、最佳实践总结

✅ Do's

  1. 明确讨论 vs 决策的边界

    java 复制代码
    // 讨论阶段:启用广播
    hub.enableAutoBroadcast(true);
    
    // 决策阶段:关闭广播,并发执行
    hub.setAutoBroadcast(false);
    pipeline.concurrent();
  2. 使用多轮讨论促进信息流通

    java 复制代码
    for (int i = 0; i < 2; i++) {
        // 轮流发言
    }
  3. 合理管理资源

    java 复制代码
    try (MsgHub hub = ...) {
        // 使用
    } // 自动清理

❌ Don'ts

  1. 不要在 observe() 中调用 call()

    java 复制代码
    // ❌ 错误示例
    protected Mono<Void> doObserve(Msg msg) {
        memory.addMessage(msg);
        return call().then();  // 会导致无限循环!
    }
  2. 不要同时调用多个 Agent 的 call()

    java 复制代码
    // ❌ 除非你确实需要并发
    Flux.just(agent1, agent2)
        .flatMap(agent -> agent.call())  // 并发调用
        .block();

八、源码地址

  • 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 通过巧妙的订阅 - 广播机制,实现了高效、安全的多智能体通信:

  • 🎯 避免死锁:observe/call分离
  • 🚀 高性能:响应式异步广播
  • 🤝 灵活协作:支持动态参与者
  • 🎭 真实模拟:信息不对称反而更贴近现实

这种设计不仅适用于狼人杀游戏,还可以广泛应用于团队协作、角色扮演、头脑风暴等多智能体场景。


相关推荐
谭光志12 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..12 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects12 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰12 小时前
一些基础和问题解决
后端
陈随易13 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN13 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana13 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端
学渣超13 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构
北冥you鱼13 小时前
Go 语言读取链上数据:从基础到实战
开发语言·后端·golang
程序员cxuan13 小时前
Claude Code 为了封禁中国用户,竟然在代码里下毒
人工智能·后端·程序员