跳转至

Swarm Blackboard 模式:无监督 P2P(研究用)

解决什么问题

传统 Manager-Worker 有中心节点瓶颈。如果我们让一群 Agent 自主协作呢? Swarm Blackboard 是一种去中心化模式:没有 Manager,所有 Agent 通过一块共享黑板 (Blackboard) 读写信息、自主认领任务。

这是一种研究性质的模式——适合探索性任务(如旅行灵感收集),但生产可控性差。

场景 为什么选它
旅行灵感收集 多个 Agent 各自搜索,把发现写到黑板
学术调研 无中心调度,各自探索再汇总
创意头脑风暴 不需要严格分工,自由碰撞

复杂度

⭐⭐⭐⭐⭐ 高。去中心化 = 难调试 + 难控制收敛。

和其他模式的关系

模式 谁决定下一步 什么时候用
Swarm Blackboard 每个 Agent 自主决定(读黑板→写黑板) 发散性探索任务,不需要严格控制
Manager-Worker Manager 集中分配 确定性任务、需要可控产出
Group Chat 主持人控制轮次 多角色需要有序讨论

Swarm 比 Manager-Worker 更自由(无中心调度),比 Group Chat 更松散(无主持人、无轮次控制)。代价是可控性差。

角色关系

   ┌───────────────────────────────┐
   │       Blackboard (共享黑板)     │
   │  "东京樱花季3月下旬"            │
   │  "京都岚山竹林必去"             │
   │  "大阪道顿堀美食街"             │
   └──┬──────┬──────┬──────┬───────┘
      │      │      │      │     读/写
      ▼      ▼      ▼      ▼
    ┌───┐ ┌───┐ ┌───┐ ┌───┐
    │ A1 │ │ A2 │ │ A3 │ │ A4 │  ← P2P,无 Manager
    └───┘ └───┘ └───┘ └───┘
  • 所有 Agent 地位对等
  • 通过 Blackboard 间接通信
  • 每个 Agent 自主决定"读什么、写什么、何时停止"

完整 Python 实现

"""
Swarm Blackboard: 日本旅行灵感收集
4个 Agent 自主搜索,把灵感写到共享黑板,互相参考补充。
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any


# ── MockLLM ──────────────────────────────────────────────
class MockLLM:
    def __init__(self):
        self.call_log: list[dict] = []

    def chat(self, role: str, prompt: str) -> str:
        self.call_log.append({"role": role, "prompt": prompt[:80]})

        if role == "culture_scout":
            if "黑板" in prompt and "樱花" in prompt:
                return json.dumps({"action": "write", "content": "补充: 上野公园也是赏樱名所", "done": True})
            return json.dumps({"action": "write", "content": "东京樱花季3月下旬至4月初,推荐目黑川、千鸟渊", "done": False})

        if role == "nature_scout":
            if "黑板" in prompt and "岚山" in prompt:
                return json.dumps({"action": "write", "content": "补充: 岚山小火车需提前1周订票", "done": True})
            return json.dumps({"action": "write", "content": "京都岚山竹林必去,嵯峨野观光小火车绝美", "done": False})

        if role == "food_scout":
            return json.dumps({"action": "write", "content": "大阪道顿堀: 章鱼烧、拉面、蟹道乐", "done": True})

        if role == "budget_scout":
            if "黑板" in prompt:
                return json.dumps({"action": "write", "content": "预算参考: 日本7日人均¥8000-12000,JR Pass可省30%交通费", "done": True})
            return json.dumps({"action": "write", "content": "交通建议: 买JR Pass全国版¥1500,覆盖新干线", "done": False})

        return json.dumps({"action": "noop", "content": "", "done": True})


# ── Blackboard ───────────────────────────────────────────
@dataclass
class Blackboard:
    """共享黑板:所有 Agent 可读可写。"""
    entries: list[dict] = field(default_factory=list)

    def write(self, author: str, content: str):
        entry = {"author": author, "content": content, "id": len(self.entries)}
        self.entries.append(entry)
        print(f"  [黑板 ← {author}] {content[:50]}")

    def read_recent(self, n: int = 5) -> list[dict]:
        return self.entries[-n:]

    def read_all(self) -> list[dict]:
        return list(self.entries)

    def summary(self) -> str:
        return "\n".join(f"  [{e['author']}] {e['content']}" for e in self.entries)


# ── Swarm Agent ──────────────────────────────────────────
@dataclass
class SwarmAgent:
    name: str
    role: str
    llm: MockLLM = field(repr=False)
    is_done: bool = False

    def act(self, blackboard: Blackboard) -> bool:
        """读黑板 → 思考 → 写黑板。返回 True 表示自己认为完成了。"""
        if self.is_done:
            return True

        # 读取黑板内容
        recent = blackboard.read_recent(5)
        bb_text = "\n".join(f"[{e['author']}] {e['content']}" for e in recent)
        prompt = f"你是{self.name}。黑板内容:\n{bb_text}\n\n请决定下一步行动(write/noop),用JSON回复:"

        raw = self.llm.chat(self.role, prompt)
        try:
            action = json.loads(raw)
        except json.JSONDecodeError:
            action = {"action": "noop", "content": "", "done": True}

        if action.get("action") == "write" and action.get("content"):
            blackboard.write(self.name, action["content"])

        if action.get("done"):
            self.is_done = True
            print(f"  [{self.name}] 宣布完成")

        return self.is_done


# ── Swarm Runner ─────────────────────────────────────────
@dataclass
class SwarmRunner:
    agents: list[SwarmAgent]
    blackboard: Blackboard = field(default_factory=Blackboard)
    max_ticks: int = 5

    def run(self, topic: str) -> Blackboard:
        print(f"[Swarm] 话题: {topic}")
        self.blackboard.write("系统", f"探索话题: {topic}")

        for tick in range(self.max_ticks):
            print(f"\n--- Tick {tick+1} ---")
            all_done = True
            for agent in self.agents:
                if not agent.is_done:
                    done = agent.act(self.blackboard)
                    if not done:
                        all_done = False

            if all_done:
                print(f"\n[Swarm] 所有 Agent 完成,共 {tick+1} 轮")
                break

        print(f"\n[Swarm] 黑板最终内容:")
        print(self.blackboard.summary())
        return self.blackboard


# ── 运行入口 ─────────────────────────────────────────────
def main():
    llm = MockLLM()

    agents = [
        SwarmAgent("文化探子", "culture_scout", llm),
        SwarmAgent("自然探子", "nature_scout", llm),
        SwarmAgent("美食探子", "food_scout", llm),
        SwarmAgent("预算探子", "budget_scout", llm),
    ]

    runner = SwarmRunner(agents=agents, max_ticks=5)
    bb = runner.run("日本旅行灵感收集")

    # 验证
    assert len(bb.entries) >= 5  # 至少系统+4个 Agent 各写一条
    contents = " ".join(e["content"] for e in bb.entries)
    assert "樱花" in contents
    assert "道顿堀" in contents or "章鱼烧" in contents
    assert "JR Pass" in contents or "交通" in contents
    print(f"\n✓ 共 {len(llm.call_log)} 次 LLM 调用,黑板收集 {len(bb.entries)} 条灵感")


if __name__ == "__main__":
    main()

运行记录

[Swarm] 话题: 日本旅行灵感收集
  [黑板 ← 系统] 探索话题: 日本旅行灵感收集

--- Tick 1 ---
  [黑板 ← 文化探子] 东京樱花季3月下旬至4月初,推荐目黑川、千鸟渊
  [黑板 ← 自然探子] 京都岚山竹林必去,嵯峨野观光小火车绝美
  [黑板 ← 美食探子] 大阪道顿堀: 章鱼烧、拉面、蟹道乐
  [美食探子] 宣布完成
  [黑板 ← 预算探子] 交通建议: 买JR Pass全国版¥1500,覆盖新干线

--- Tick 2 ---
  [黑板 ← 文化探子] 补充: 上野公园也是赏樱名所
  [文化探子] 宣布完成
  [黑板 ← 自然探子] 补充: 岚山小火车需提前1周订票
  [自然探子] 宣布完成
  [黑板 ← 预算探子] 预算参考: 日本7日人均¥8000-12000,JR Pass可省30%交通费
  [预算探子] 宣布完成

[Swarm] 所有 Agent 完成,共 2 轮

[Swarm] 黑板最终内容:
  [系统] 探索话题: 日本旅行灵感收集
  [文化探子] 东京樱花季3月下旬至4月初,推荐目黑川、千鸟渊
  [自然探子] 京都岚山竹林必去,嵯峨野观光小火车绝美
  [美食探子] 大阪道顿堀: 章鱼烧、拉面、蟹道乐
  [预算探子] 交通建议: 买JR Pass全国版¥1500,覆盖新干线
  [文化探子] 补充: 上野公园也是赏樱名所
  [自然探子] 补充: 岚山小火车需提前1周订票
  [预算探子] 预算参考: 日本7日人均¥8000-12000,JR Pass可省30%交通费

✓ 共 8 次 LLM 调用,黑板收集 8 条灵感

踩坑记录

现象 trace 信号 修法
黑板爆炸 几百条垃圾信息 blackboard entries 数量远超 Agent 数 × 预期每人贡献数 限制每个 Agent 最多写 N 条
永不收敛 Agent 一直觉得没写完 tick 数触达 max_ticks 且仍有 Agent 未 done 设 max_ticks + "黑板超过 M 条就停"
重复内容 3个 Agent 写了一样的 黑板中多条 entry 的文本相似度 > 0.9 写入前查重,或 prompt 里加"不要重复黑板已有内容"
搭便车 有的 Agent 只读不写 某 Agent 的 write 事件数为 0 但 read 事件数正常 要求每个 Agent 至少贡献 1 条才能宣布完成

工程备忘

  1. 仅限研究/探索场景:生产环境不推荐纯 Swarm,可控性太差。
  2. 黑板 = 共享内存:生产中可以用 Redis 或数据库实现。
  3. 读写控制:可以加读写锁防止并发冲突,但 Swarm 本身就是松散的。
  4. 收敛策略:(a) 所有 Agent 宣布 done (b) 达到 max_ticks (c) 黑板条数达上限。
  5. 质量控制:可以加一个"评审 Agent"定期清理黑板上的低质量条目。
  6. 与 Manager-Worker 的对比:Manager-Worker 有中心调度,适合确定性任务;Swarm 无中心,适合发散性任务。
  7. 扩展方向:加入投票机制(Agent 给黑板条目点赞),高票内容优先保留。

读完以后

如果需要中心化调度、确定性产出,回到 Manager-Worker。 如果 Agent 之间需要有序讨论而非自由写黑板,看 Group Chat。 如果任务有明确的依赖关系和分支逻辑,看 Graph Orchestration

参考资料