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

12 阅读4分钟

多智能体系统里,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: [系统提示]          ← 不知道 BC 的想法
├─ B 发言 → Memory: [系统提示,A 的话]   ← 知道 A,不知道 C
└─ C 发言 → Memory: [系统提示,A、B 的话] ← 知道 AB

缓解方案:多轮讨论 + 轮换顺序

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(),释放订阅关系

七、使用建议

推荐做法

  1. 区分讨论与决策阶段
   // 讨论阶段:启用广播
   hub.enableAutoBroadcast(true);
   
   // 决策阶段:关闭广播,并发执行
   hub.setAutoBroadcast(false);
   pipeline.concurrent();
  1. 多轮讨论促进信息流通
   for (int i = 0; i < 2; i++) {
       // 轮流发言
   }
  1. 使用 try-with-resources 管理资源
   try (MsgHub hub = ...) {
       // 使用
   }

避免的做法

  1. 不要在 observe() 中调用 call()
   // 错误示例
   protected Mono<Void> doObserve(Msg msg) {
       memory.addMessage(msg);
       return call().then();  // 会导致无限循环
   }

原因:doObserve 处理消息时调用 call(),而 call() 又可能产生新消息触发 doObserve,形成递归。

  1. 谨慎并发调用 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 交互的场合。