多智能体系统中的通信协议与并发控制

Dev.to AI 2026-10-21T09:45:32.206301

[!NOTE] 执行摘要(TL;DR) Actor 模型与邮箱并发:使用 Actor 消息信封和隔离状态机来协调多智能体集群,消除死锁风险,不再需要裸互斥锁。乐观并发与背压:在共享任务板上利用 OCC 版本控制,配合令牌桶限流器,平滑缓冲 LLM API 的突发流量。生态堆叠:各个 actor 运行在《构建轻量级 Agent 框架》和《高级函数调用引擎》之上。

多智能体系统中的消息所有权至关重要

当任务可拆分、结果可验证、并行收益超过通信成本时,多个智能体就能发挥作用。本文演示进程内的 asyncio actor、有界投递、CAS 更新和令牌桶。本地队列不是持久化的分布式消息代理。

信封、邮箱与背压

每个 actor 按顺序消费自己的邮箱,同时其他 actor 可以并发等待 I/O。冻结的 dataclass 并不能冻结嵌套字典,因此投递时会为每个接收者复制 payload。有界队列在超过投递截止时间后抛出 BackpressureError。广播不是原子操作:当某个接收者超时时,其他接收者可能已经收到了事件。调用方需要按消息 ID 去重,并处理部分失败。本示例未实现持久化、投递确认或请求-响应 future。

flowchart LR
    Developer[Developer] --> Bus[Bounded message bus]
    Bus --> Reviewer[Reviewer mailbox]
    Bus --> Scanner[Scanner mailbox]
    Reviewer --> Coordinator[Collect simulated results]
    Scanner --> Coordinator
# 协调器 --> CAS(CAS 文档提交)

上面是一个可完整运行的示例。下载 Python 源码,在 Python 3.11+ 环境下执行 python3 actor_runtime.py。评审者通过字符串检查来演示消息顺序,并非真实的安全分析。协调器会等待双方完成后再提交;关闭时先清空各邮箱中的消息,再取消任务。

"""
进程内 actor 实现,带边界投递能力;不是分布式持久化消息中间件。
"""
import asyncio
import copy
import json
import time
import uuid
from dataclasses import asdict, dataclass, field
from enum import Enum

class MessageType(str, Enum):
    REQUEST = "request"
    RESPONSE = "response"
    EVENT = "event"

@dataclass(frozen=True)
class MessageEnvelope:
    source_actor: str = ""
    target_actor: str = ""
    message_type: MessageType = MessageType.REQUEST
    correlation_id: str = ""
    payload: dict = field(default_factory=dict)
    message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: float = field(default_factory=time.time)

    def to_json(self):
        return json.dumps(asdict(self))

    @classmethod
    def from_json(cls, value):
        data = json.loads(value)
        data["message_type"] = MessageType(data["message_type"])
        return cls(**data)

class BackpressureError(RuntimeError):
    pass

class MessageBus:
    def __init__(self, mailbox_size=100, delivery_timeout=1):
        if mailbox_size < 1 or delivery_timeout <= 0:
            raise ValueError("mailbox_size 与 delivery_timeout 必须为正数")
        self.mailbox_size = mailbox_size
        self.delivery_timeout = delivery_timeout
        self.actor_mailboxes = {}
        self.subscriptions = {}

    def register_actor(self, actor_id):
        if actor_id in self.actor_mailboxes:
            raise ValueError("actor 已注册")
        mailbox = asyncio.Queue(maxsize=self.mailbox_size)
        self.actor_mailboxes[actor_id] = mailbox
        return mailbox

    def subscribe(self, topic, actor_id):
        self.subscriptions.setdefault(topic, set()).add(actor_id)

    async def send(self, envelope):
        mailbox = self.actor_mailboxes.get(envelope.target_actor)
        if mailbox is None:
            raise ValueError("未知 actor")
        await self.

(本段截断,后续内容在下一分段中。)

async def _deliver(self, mailbox, envelope):
    try:
        await asyncio.wait_for(mailbox.put(copy.deepcopy(envelope)), self.delivery_timeout)
    except asyncio.TimeoutError as error:
        raise BackpressureError("mailbox delivery timed out") from error

async def publish(self, topic, envelope):
    # Each subscriber receives a separate payload; delivery is not an atomic broadcast.
    await asyncio.gather(*(
        self._deliver(self.actor_mailboxes[actor], envelope)
        for actor in self.subscriptions.get(topic, ())
        if actor in self.actor_mailboxes
    ))

class BaseActor:
    def __init__(self, actor_id, bus):
        self.actor_id = actor_id
        self.bus = bus
        self.mailbox = bus.register_actor(actor_id)
        self.task = None
        self.errors = []

    async def start(self):
        if self.task is not None:
            raise RuntimeError("actor already started")
        self.task = asyncio.create_task(self._event_loop())

    async def stop(self, timeout=5):
        if self.task is None:
            return
        try:
            await asyncio.wait_for(self.mailbox.join(), timeout)
        finally:
            self.task.cancel()
            await asyncio.gather(self.task, return_exceptions=True)
            self.task = None

    async def _event_loop(self):
        while True:
            envelope = await self.mailbox.get()
            try:
                await self.on_message(envelope)
            except asyncio.CancelledError:
                raise
            except Exception as error:
                self.errors.append(error)
            finally:
                self.mailbox.task_done()

    async def on_message(self, envelope):
        raise NotImplementedError

class ConcurrentDocumentStore:
    def __init__(self):
        self._content, self._version = "", 1
        self._lock = asyncio.Lock()

    async def get_document(self):
        async with self._lock:
            return {"content": self._content, "version": self._version}

    async def update_document(self, new_content, expected_version, author):
        async with self._lock:
            if self._version != expected_version:
                return False
            self._content = new_content
            self.
    _version += 1
    return True

class TokenBucketRateLimiter:
    def __init__(self, capacity, refill_rate_per_sec):
        if capacity <= 0 or refill_rate_per_sec <= 0:
            raise ValueError("capacity and refill rate must be positive")
        self.capacity, self.refill_rate = capacity, refill_rate_per_sec
        self.tokens, self.last_refill = float(capacity), time.monotonic()
        self._lock = asyncio.Lock()

    async def acquire(self, tokens_needed=1):
        if not 0 < tokens_needed <= self.capacity:
            raise ValueError("requested tokens must fit bucket capacity")
        while True:
            async with self._lock:
                now = time.monotonic()
                self.tokens = min(
                    self.capacity,
                    self.tokens + (now - self.last_refill) * self.refill_rate,
                )
                self.last_refill = now
                if self.tokens >= tokens_needed:
                    self.tokens -= tokens_needed
                    return
                delay = (tokens_needed - self.tokens) / self.refill_rate
            await asyncio.sleep(delay)

async def main():
    bus, store = MessageBus(), ConcurrentDocumentStore()
    limiter = TokenBucketRateLimiter(2, 2)

    class Developer(BaseActor):
        async def on_message(self, envelope):
            await limiter.acquire()
            await self.bus.publish(
                "code",
                MessageEnvelope(
                    source_actor=self.actor_id,
                    message_type=MessageType.EVENT,
                    correlation_id=envelope.correlation_id,
                    payload={"code": "def login(): return True"},
                ),
            )

    class Reviewer(BaseActor):
        def __init__(self, actor_id, bus):
            super().__init__(actor_id, bus)
            self.approved = False

        async def on_message(self, envelope):
            self.approved = envelope.payload["code"].startswith("def login")

    developer, reviewer, scanner = (
        Developer("dev", bus),
        Reviewer("review", bus),
        Reviewer("scan", bus),
    )
    actors = [developer, reviewer, scanner]
    bus.subscribe("code", "review")
    bus.subscribe("code", "scan")
    for actor in actors:
        await actor.start()

    try:
        await bus.send(
            MessageEnvelope(target_actor="dev", correlation_id="example-1")
        )
        await developer.mailbox.join()
        await asyncio.gather(
            reviewer.mailbox.join(),
            scanner.mailbox.join(),
        )
join())  # Simulated checks only; the coordinator commits after both completed.
if not all(actor.errors == [] for actor in actors) or not (reviewer.approved and scanner.approved):
    raise RuntimeError("review failed")
snapshot = await store.get_document()
merged = await store.update_document("def login(): return True", snapshot["version"], "coordinator")
print("Simulated merge: ", merged)
finally:
    for actor in actors:
        await actor.stop()

if __name__ == "__main__":
    asyncio.run(main())

扩展本地示例

CAS(比较并交换)能发现过期的写入,但不会合并冲突的文档。真实的数据库必须把「比较版本」和「更新内容」做成一个原子操作。

令牌桶(token bucket,一种限流算法)用的是单调时钟,并会拒绝超出桶容量的请求。它限制的是请求速率,而不是每分钟的 token 数,也不管并发数,更不涉及各家模型服务商自己的重试策略。

跨进程部署还需要序列化、身份认证、可靠投递、确认机制、稳定的幂等键(idempotency key,同一个请求重发也只生效一次),以及死信处理(dead-letter,投递失败的消息单独存放)。asyncio 的锁只能保护单个进程。

延伸阅读:工具执行进阶 与 审批工作流。

常见问题(FAQ)

Q1:消息加上 frozen=True 就算不可变了吗?

不算。嵌套的字典仍然可以被修改。示例里每次投递都会复制一份;分布式传输则应把每条消息独立序列化。

Q2:邮箱写满了会怎样?

投递最多等待 delivery_timeout,超时后抛出明确的错误。接下来是重试、丢弃还是落盘持久化,由调用方决定。

Q3:处理函数抛异常,会不会让 queue.join 永远等下去?

示例在 finally 里调用了 task_done(),并把错误记录下来交给协调者。但生产系统仍然需要失败消息的重试或死信策略。

查看原文