共享黑板模式(Blackboard)实战:多 Agent 如何并发协作而不冲突?

共享黑板模式(Blackboard)实战:多 Agent 如何并发协作而不冲突?

作 者:吴佳浩(Alben)

公众号:全栈架构师笔记

系列专栏:《企业级 Agent 实战指南------------Multi-Agent 架构设计:从单体 ReAct 到群智协同》· 第 03 篇


导读

点对点通信是网状的,Agent 一多就会演变成 O(N2)O(N^2) O(N2) 的通信风暴;共享黑板(Blackboard)是星型的,把复杂度重新降回 O(N)O(N) O(N)。

很多团队以为共享内存就是挂一个 Redis 让所有 Agent 随便读写;但在高并发分布式协作中,脏读、脏写与脑裂会让整个系统产生不可逆的幻觉扩散。

没有版本控制的共享黑板是并发灾难,基于 CAS 乐观锁与分区隔离的黑板架构才是 Multi-Agent 协同的基石。


在多智能体系统从玩具走向工业级落地的过程中,当协同的 Agent 数量从 2 个增加到 5 个、10 个时,点对点消息传递(P2P Messaging)会立刻引发三大系统灾难:

绝境现象 具体翻车表现 架构根因
1. 通信网络风暴 5 个 Agent 互相同步状态,产生 通信拓扑呈网状 O(N2)O(N^2) O(N2) 扩散,
(Message Storm) 海量重复冗余的上下文转发 缺乏集中状态收敛中枢
2. 状态脏写覆写 Coder A 和 Coder B 同时修改全局 缺乏并发控制与版本比对机制,
(Dirty Overwrites) 架构配置,后提交的无情覆盖前者 发生典型的分布式写冲突
3. 认知撕裂与脑裂 各 Agent 手头的状态版本不一致, 缺乏唯一的"单一信任事实源"
(State Split-Brain) 架构师看到的版本落后于一线开发 (Single Source of Truth)

要解决多 Agent 并发协作中的混乱问题,经典人工智能与分布式系统中的最高范式就是:分级共享黑板模式(Partitioned Blackboard Pattern)。


一、共享黑板模式的核心架构:单一事实源与订阅分发

黑板模式的核心思想非常纯粹:所有 Agent 不直接互相通信,而是共同围绕一块"带版本控制与事件通知的共享黑板"进行读写。

(全局状态与任务契约中枢)
Architect Agent
(写入系统规范)
  • 🔸 分区隔离原则 :黑板分为 Global(全局规范)、Working(协同工作区)与 Private(私有暂存区)。私有探索不污染全局;
  • 🔸 单一信任源(Single Source of Truth):所有 Agent 的推理基准完全对齐于黑板的最新版本;
  • 🔸 事件驱动感知:当黑板特定分区发生状态变更时,通过 Redis Pub/Sub 或 WebSocket 广播通知下游关注该事件的 Agent。

一句话总结这一章的核心观点:

黑板模式把复杂网状沟通降维为星型结构,确保所有 Agent 拥有完全一致的全局心智。


二、并发控制深水区:基于 CAS 的乐观锁机制

当多个子代理(如 3 个并发 Coder Agent)同时尝试修改黑板上的同一份配置文件时,系统必须引入乐观并发控制(OCC / Compare-And-Swap):

乐观并发控制三原则

  • 🔸 提交必须携带预期版本号 :所有写操作必须声明 expected_version;
  • 🔸 冲突立即拒绝,杜绝脏写:一旦版本不匹配,原子化拒绝写入并返回最新快照;
  • 🔸 语义合并重试:被拒绝的 Agent 必须重新拉取最新版本,结合自身修改意图进行语义合并后再次提交。

一句话总结这一章的核心观点:

乐观并发控制保证了数据一致性,语义合并保证了业务连续性。


三、生产级代码实战:带 CAS 乐观锁与事件广播的共享黑板

以下为基于 Python 3.11+ 构建的企业级共享黑板核心实现,完整包含分区存储、CAS 乐观并发控制与状态变更广播:

python 复制代码
"""
shared_blackboard_engine.py

生产级带 CAS 并发控制的共享黑板引擎

包含:
- 分区隔离
- CAS 乐观锁
- 并发冲突检测
- 黑板事件通知
"""

import asyncio
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional

from pydantic import BaseModel, Field


# ============================================================
# Blackboard Entry
# ============================================================


class BlackboardEntry(BaseModel):
    """黑板中的数据项"""

    key: str
    value: Any

    # CAS 版本号
    version: int = 1

    # 最近修改者
    last_modified_by: str

    # 更新时间
    updated_at: datetime = Field(
        default_factory=datetime.utcnow
    )


# ============================================================
# CAS 返回结果
# ============================================================


class CASMutationResult(BaseModel):
    """CAS 写入结果"""

    success: bool
    current_version: int
    message: str

    entry: Optional[BlackboardEntry] = None


# ============================================================
# Shared Blackboard
# ============================================================


class SharedBlackboardHub:
    """
    企业级共享黑板

    包含三个逻辑分区:

    1. Global Partition
       全局知识区

    2. Working Partition
       Agent 协作区(CAS)

    3. Scratchpad
       Agent 私有临时空间
    """

    def __init__(self):
        # 全局知识
        self._global_partition: Dict[
            str,
            BlackboardEntry,
        ] = {}

        # 协作区(CAS)
        self._working_partition: Dict[
            str,
            BlackboardEntry,
        ] = {}

        # Agent 私有工作区
        self._agent_scratchpads: Dict[
            str,
            Dict[str, Any],
        ] = {}

        # 黑板事件监听器
        self._subscribers: List[
            Callable[[str, BlackboardEntry], None]
        ] = []

        # CAS 原子锁
        self._lock = asyncio.Lock()

    # --------------------------------------------------------
    # Event Subscription
    # --------------------------------------------------------

    def subscribe_events(
        self,
        callback: Callable[[str, BlackboardEntry], None],
    ) -> None:
        """注册黑板事件监听器"""

        self._subscribers.append(callback)

    # --------------------------------------------------------
    # Read
    # --------------------------------------------------------

    def read_working_entry(
        self,
        key: str,
    ) -> Optional[BlackboardEntry]:
        """读取协作区数据"""

        return self._working_partition.get(key)

    # --------------------------------------------------------
    # CAS Write
    # --------------------------------------------------------

    async def cas_write_working_entry(
        self,
        key: str,
        new_value: Any,
        expected_version: int,
        agent_role: str,
    ) -> CASMutationResult:
        """
        CAS 原子写入

        流程:

            Read
              │
              ▼
        Version Check
              │
        ┌─────┴─────┐
        │           │
      Pass       Conflict
        │           │
        ▼           ▼
      Update      Reject
        """

        async with self._lock:

            existing = self._working_partition.get(key)

            # ------------------------------------------------
            # 新建数据
            # ------------------------------------------------

            if existing is None:

                if expected_version != 0:
                    return CASMutationResult(
                        success=False,
                        current_version=0,
                        message=(
                            f"Key '{key}' "
                            "does not exist. "
                            "expected_version must be 0."
                        ),
                    )

                new_entry = BlackboardEntry(
                    key=key,
                    value=new_value,
                    version=1,
                    last_modified_by=agent_role,
                )

                self._working_partition[key] = new_entry

                self._notify_subscribers(
                    key,
                    new_entry,
                )

                return CASMutationResult(
                    success=True,
                    current_version=1,
                    message="Key created successfully.",
                    entry=new_entry,
                )

            # ------------------------------------------------
            # CAS Version Check
            # ------------------------------------------------

            if existing.version != expected_version:
                return CASMutationResult(
                    success=False,
                    current_version=existing.version,
                    message=(
                        "CAS Conflict: "
                        f"Current={existing.version}, "
                        f"Expected={expected_version}"
                    ),
                    entry=existing,
                )

            # ------------------------------------------------
            # 更新数据
            # ------------------------------------------------

            existing.value = new_value
            existing.version += 1
            existing.last_modified_by = agent_role
            existing.updated_at = datetime.utcnow()

            self._notify_subscribers(
                key,
                existing,
            )

            return CASMutationResult(
                success=True,
                current_version=existing.version,
                message="Version updated successfully.",
                entry=existing,
            )

    # --------------------------------------------------------
    # Event Notify
    # --------------------------------------------------------

    def _notify_subscribers(
        self,
        key: str,
        entry: BlackboardEntry,
    ) -> None:
        """广播黑板事件"""

        for subscriber in self._subscribers:
            try:
                subscriber(key, entry)

            except Exception as e:
                print(
                    "Blackboard subscriber error:",
                    e,
                )

    # --------------------------------------------------------
    # Scratchpad
    # --------------------------------------------------------

    def write_scratchpad(
        self,
        agent_role: str,
        key: str,
        value: Any,
    ) -> None:
        """
        写入 Agent 私有 Scratchpad。

        Scratchpad 为 Agent 独占,
        无需加锁。
        """

        if agent_role not in self._agent_scratchpads:
            self._agent_scratchpads[agent_role] = {}

        self._agent_scratchpads[agent_role][key] = value

    def read_scratchpad(
        self,
        agent_role: str,
        key: str,
    ) -> Optional[Any]:
        """读取 Agent 私有 Scratchpad"""

        return (
            self._agent_scratchpads
            .get(agent_role, {})
            .get(key)
        )

本篇总结

  • 🔸 点对点通信是网状灾难,共享黑板是星型解耦;
  • 🔸 分区设计是核心:全局规范只读、协作工作区 CAS 保护、私有暂存区自由探索;
  • 🔸 CAS 乐观并发控制是保证多 Agent 并发写入不脏写的唯一数学保障;
  • 🔸 事件驱动广播让下游 Agent 在状态更新时实现毫秒级自动感知与响应。

在多 Agent 规模化运行中,当 Agent 之间产生死锁、互相反驳或消耗海量 Token 时,如何进行治理?

筒子们本篇为《企业级 Agent 实战指南》· 第三章的第 3 篇,后续续会更新完整的agent的开发的全部过程,如果你对Agent开发感兴趣不妨关注一下本合集。

在下一篇中,我们将深入解构:《多智能体系统的通信风暴与死锁治理:生产级降级与容灾方案》!

相关推荐
颜进强43 分钟前
装了个 AI Skill 却查不了数据?一篇讲透 Skill 调用接口的三种方式(scripts / CLI / MCP)
前端·后端·ai编程
武子康43 分钟前
Ray 的 remote 调用之后:任务怎样衔接,状态留在哪里?
人工智能·llm·agent
程序员cxuan44 分钟前
WorkBuddy + ima 搭建本地知识库
人工智能·后端·程序员
AI深栈44 分钟前
第 17 章 · 编排 AI 流程:StateGraph 节点、条件边与意图路由
java·人工智能
知几蜗牛44 分钟前
vLLM为了新GPU拆掉旧抽象,为什么还要再造一套可移植层
人工智能
知几蜗牛44 分钟前
百万行PR也能顺滑滚动,GitHub靠的不是再加一层虚拟列表
人工智能
IT_陈寒44 分钟前
Redis误删数据后的血泪教训:我竟然这样找回来了
前端·人工智能·后端
火山引擎开发者社区44 分钟前
AgentKit MCP 网关上手指南|让企业存量服务快速接入 AI
人工智能
百万蹄蹄向前冲1 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js