Agent_Swarm_分布式协作的通信编排与节点发现机制分析

JiuwenSwarm 基于 openJiuwen 框架实现了多 Agent 分布式协作机制:A2X 注册中心提供动态节点发现与预占,共享工作空间定义协作边界,PyZMQ 传输层支撑跨节点通信,远程启动编排让 Teammate 在独立进程中运行。本文深入解析这套分布式协作体系的设计思路与核心实现。

一、为什么需要分布式协作

前文我们分析了 Swarm Agent 的 Team Agent 多智能体架构。在默认的 inprocess 模式下,Leader 和 Teammate 跑在同一个进程里,共享内存和计算资源。对于大多数场景,这已经足够了------通信延迟低、部署简单、状态管理直观。

但当任务规模进一步上升,单机模式的瓶颈开始显现。

算力天花板是最直接的制约。一个进程能调用的 GPU 显存、CPU 核心数都有物理上限。假设一个调研任务需要同时启动 8 个 Teammate,每个都在做 DeepSearch,单进程的模型推理会成为严重的吞吐瓶颈。

资源隔离是另一个现实问题。一个 Teammate 执行复杂代码时可能消耗大量内存,如果和 Leader 挤在同一进程里,Leader 的任务调度和消息路由也会被拖慢。更不用说某些 Teammate 可能需要专门的运行环境(特定的 Python 版本、系统依赖等)。

JiuwenSwarm 的答案是分布式模式(distributed)。通过 PyZMQ 传输层,Leader 和 Teammate 可以跑在不同的机器上,各自拥有独立的计算资源。A2X 注册中心则让节点发现变得自动化------Leader 不需要知道 Teammate 的 IP 地址,只需要从注册中心预占一个"空白 Agent",远程启动编排会自动完成连接。

运行环境

项目 配置值
操作系统 Windows 10 / macOS / Linux
Python 3.11 / 3.12 / 3.13
传输协议 PyZMQ (ZeroMQ)
存储后端 SQLite(单机)/ PostgreSQL(分布式)
注册中心 A2X Registry
Agent 框架 openJiuwen(内置)

分布式模式与单机模式的关系

单机模式(inprocess)是分布式模式的子集。两者的核心抽象------TeamAgentTeamManagerTeamMonitorHandler、事件类型体系------完全共享。分布式模式在此基础上增加了三个关键能力:

  • 节点发现:通过 A2X 注册中心,Leader 动态发现可用的远程 Teammate
  • 跨节点通信:PyZMQ 传输层替代进程内调用,支持 Direct 和 PubSub 两种拓扑
  • 远程启动编排:Leader 通过控制面消息远程激活 Teammate,自动完成身份切换和路由注册

架构总览

分布式模式下的核心组件分工:

组件 职责 源码位置
distributed_runtime 分布式配置解析、传输参数标准化、依赖检查 agents/harness/team/distributed_runtime.py
a2x_registry_runtime A2X 注册中心集成(空白 Agent 注册/预占/卡片替换) agents/harness/team/a2x/a2x_registry_runtime.py
remote_member_bootstrap 远程成员启动编排(Bootstrap 握手、ACK 确认、路由注册) agents/harness/team/remote_member_bootstrap.py
A2AChannel A2A 协议入口(Ingress),外部 Agent 系统接入网关 gateway/channel_manager/protocol/a2a/a2a_connect.py
A2AServer / A2AClient A2A 协议服务端/客户端(openJiuwen 扩展层) openjiuwen/extensions/a2a/
A2ATransformer openJiuwen 内部数据模型与 A2A 协议数据的双向转换 openjiuwen/extensions/a2a/a2a_transformer.py
ExtensionRegistry 扩展注册中心(回调框架、配置管理) extensions/registry.py

消息从 Leader 到远程 Teammate 的数据流:

plain 复制代码
Leader LLM → spawn_member → Remote Bootstrap Wrapper
    ↓
A2X Registry: reserve_blank_teammate_agent()
    ↓
PyZMQ Direct/DB: 发送 Bootstrap Envelope
    ↓
Teammate Bootstrap Daemon: 接收 Envelope
    ↓
adopt member_name → 注册 Leader 路由 → 发送 ACK
    ↓
Leader ACK Listener: 确认就绪 → 成员状态 UNSTARTED→READY

二、A2X 注册中心:动态节点发现

A2X 注册中心是分布式协作的"电话簿"。它的核心职责是让 Leader 能动态发现可用的远程 Teammate,而不需要硬编码对方的地址。

2.1 注册中心的角色模型

A2X 注册中心定义了两种角色:teamleaderteammate。Leader 从注册中心预占(reserve)空闲的 Teammate,Teammate 启动时将自身注册为"空白 Agent"等待被预占。

配置路径在 config.yamlreact.a2x_registry 段:

yaml 复制代码
react:
  a2x_registry:
    base_url: "http://127.0.0.1:8000"
    dataset: "my_team_dataset"
    role: "teammate"          # 或 "teamleader"
    endpoint: "tcp://127.0.0.1:18600"
    reservation_ttl_seconds: 30

resolve_a2x_config() 负责解析这些配置并填充默认值。几个关键参数:

参数 说明 默认值
base_url 注册中心 API 地址 http://127.0.0.1:8000
dataset 数据集标识,用于隔离不同团队 无(必填)
endpoint 当前节点的通信地址 自动从 transport 配置推导
reservation_ttl_seconds 预占超时时间 30 秒
role 角色类型 teammate

2.2 空白 Agent 的注册流程

Teammate 进程启动时,如果检测到自身是分布式模式下的 teammate 角色,会自动向注册中心注册一个"空白 Agent":

python 复制代码
async def register_blank_agent_if_teammate(client, config, *, source):
    if not config.get("distributed_mode"):
        return False
    if config.get("role") != "teammate":
        return False

    dataset = config.get("dataset")
    endpoint = config.get("endpoint")
    if not dataset or not endpoint:
        return False

    result = await client.register_blank_agent(dataset=dataset, endpoint=endpoint)
    # ...
    return True

空白 Agent 的卡片是一个标准化模板:

python 复制代码
_TEAMMATE_CARD_DESCRIPTION = "Task Planner(team-1)"
_TEAMMATE_CARD_STATUS = "busy"
_TEAMMATE_CARD_SKILLS = [{"name": "plan", "description": "子任务拆解"}]

这个卡片告诉注册中心:"这里有一个空闲的 Teammate 节点,可以被分配任务"。

2.3 Leader 预占机制

Leader 端的流程通过 reserve_blank_teammate_agent() 实现。当 Leader 的 LLM 调用 spawn_member 创建新成员时,如果检测到配置了远程成员名或全局远程模式,会触发以下流程:

python 复制代码
async def reserve_blank_teammate_agent(config_base, *, source="leader-bootstrap"):
    client, config = await init_a2x_client(config_base)
    if not config.get("distributed_mode") or config.get("role") != "teamleader":
        return None

    reservation = await client.reserve_blank_agents(
        dataset=dataset,
        n=1,
        ttl_seconds=int(config.get("reservation_ttl_seconds") or 30),
    )
    # 从返回结果中提取 service_id 和 endpoint
    for agent in reservation.agents:
        service_id = _agent_service_id(agent)
        endpoint = _agent_endpoint(agent)
        if service_id and endpoint:
            return ReservedBlankAgent(
                client=client,
                reservation=reservation,
                dataset=dataset,
                service_id=service_id,
                endpoint=endpoint,
            )

预占机制有一个 TTL(默认 30 秒)。如果在 TTL 内 Leader 没有完成 Bootstrap 握手,预占会自动释放,该 Teammate 回到可用池。

2.4 空白到就绪的卡片替换

Teammate 收到 Bootstrap 消息后,会将自己的注册卡片从"空白"替换为实际的任务规划者卡片:

python 复制代码
async def replace_teammate_agent_card_after_bootstrap(
    client, *, dataset, service_id, member_name, source, ...
):
    agent_card = build_teammate_agent_card(member_name)
    await client.replace_agent_card(dataset, service_id, agent_card)
    return True

当团队销毁时,Teammate 的卡片会被还原回空白状态(restore_teammate_blank_agent_on_destroy),这样该节点可以被下一个团队复用。

2.5 发现逻辑的整体流程

把注册中心的发现逻辑串起来:

plain 复制代码
1. Teammate 进程启动
   → register_blank_agent_if_teammate() 注册空白卡片
   → 启动 Bootstrap Daemon 监听控制面消息

2. Leader LLM 调用 spawn_member
   → reserve_blank_teammate_agent() 从注册中心预占一个空白 Agent
   → 获取 service_id + endpoint

3. Leader 发送 Bootstrap Envelope
   → 通过 PyZMQ Direct 或 DB 消息通道发送到 Teammate

4. Teammate 收到 Bootstrap
   → 替换注册卡片(空白 → 实际角色)
   → 采用分配的 member_name
   → 注册 Leader 路由
   → 发送 ACK 确认

5. Leader 收到 ACK
   → 更新成员状态 UNSTARTED → READY
   → 成员正式加入团队

三、共享工作空间的协作边界

工作空间(Workspace)是 Agent 协作中共享状态和产出物的载体。在分布式模式下,工作空间的边界管理尤为关键------不同节点的 Teammate 需要知道哪些文件是共享的、哪些是自己独占的。

3.1 工作空间的结构

JiuwenSwarm 的工作空间根目录由 configure_agent_teams_home() 设定,指向用户工作区目录(~/.jiuwenswarm/)。在这个根目录下,每个团队有独立的子目录:

plain 复制代码
workspace/
├── agent/                         # 全局工作区
│   ├── skills/                    # 技能目录(全局共享)
│   └── reports/                   # 报告输出目录
├── session/                       # 会话工作区
│   └── sess_xxx/                  # 按会话隔离
└── team_home/                     # 团队工作区
    └── {team_name}/
        └── workspaces/
            ├── team_leader_workspace/
            └── {member_name}_workspace/

3.2 技能文件的继承与共享

技能是工作空间中最核心的共享资源。在分布式模式下,Leader 和远程 Teammate 各自维护独立的文件系统,技能通过 copy_member_skills() 机制复制到 Teammate 的本地目录:

python 复制代码
def copy_member_skills(member_skills_dir, *, skills_configured, selected_skills):
    selected_skill_set = set(selected_skills)
    for skill_dir in global_skills_dir.iterdir():
        if not skill_dir.is_dir() or not (skill_dir / "SKILL.md").is_file():
            continue
        if skills_configured and skill_dir.name not in selected_skill_set:
            continue
        dest = member_skills_dir / skill_dir.name
        if not dest.exists():
            shutil.copytree(skill_dir, dest)

这意味着每个 Teammate 拿到的是技能的副本,而非引用。这种设计是有意为之:远程节点可能无法访问 Leader 的文件系统,副本确保了独立性。

3.3 工作空间的协作边界

JiuwenSwarm 通过 TeamWorkspaceInfo 明确定义了工作空间的协作边界:

  • 全局共享区 :技能定义、用户偏好文件(MEMORY.mdUSER.md
  • 团队共享区:任务数据库、消息历史(通过 PostgreSQL 在分布式模式下共享)
  • 成员私有区:每个成员独立的技能工作空间目录、浏览器会话

在分布式模式下,任务数据库从 SQLite 切换到 PostgreSQL,这是保证跨节点数据一致性的关键。is_postgresql_storage()ensure_postgresql_for_leader() 负责检测和自动启动 PostgreSQL:

python 复制代码
async def ensure_postgresql_for_leader(config_base, *, ...):
    if runtime_role(config_base) != "leader":
        return
    team_cfg = config_base.get("team", {})
    if not is_postgresql_storage(team_cfg):
        return
    host, port = extract_pg_endpoint(team_cfg)
    if await is_pg_available(host, port):
        return
    # 尝试自动启动 PostgreSQL
    started = await try_start_pg_cluster()
    # ...

3.4 远程成员的辅助工作空间清理

当 Leader 在 Teammate 进程中创建了辅助性的工作空间(用于 Bootstrap 阶段的临时 Leader 上下文),团队建立后会自动清理:

python 复制代码
def _cleanup_auxiliary_leader_workspace(team_name, leader_member_name):
    helper_workspace = team_home(team) / "workspaces" / f"{leader}_workspace"
    if helper_workspace.exists():
        shutil.rmtree(helper_workspace)

这避免了临时文件在远程节点上累积。

四、跨节点通信与远程启动编排

跨节点通信是分布式协作的"神经系统"。JiuwenSwarm 使用 PyZMQ 作为传输层,提供 Direct(点对点)和 PubSub(发布订阅)两种通信拓扑。

4.1 传输层配置

分布式模式的传输配置在 config.yamlteam.transport 段:

yaml 复制代码
team:
  runtime:
    mode: "distributed"
    role: "leader"              # 或 "teammate"
    member_name: "team_leader"  # 或具体的成员名
  transport:
    type: "pyzmq"
    params:
      leader:
        host: "192.168.1.100"
        direct_port: 18555
        pub_port: 18556
        sub_port: 18557
      teammate:
        host: "192.168.1.101"
        direct_port: 18600

normalize_distributed_transport_fields() 负责将这些配置标准化为 PyZMQ 可用的连接字符串:

python 复制代码
def normalize_distributed_transport_fields(config_base, team_cfg):
    if role == "leader":
        local_direct_addr = f"tcp://0.0.0.0:{leader_direct_port}"
        known_peers = [
            {"agent_id": local_member_name,
             "addrs": [f"tcp://{teammate_host}:{teammate_direct_port}"]}
        ]
        pubsub_bind = True     # Leader 绑定 PubSub 端口
    else:
        local_direct_addr = f"tcp://0.0.0.0:{teammate_direct_port}"
        known_peers = [
            {"agent_id": leader_member_name,
             "addrs": [f"tcp://{leader_host}:{leader_direct_port}"]}
        ]
        pubsub_bind = False    # Teammate 连接到 Leader 的 PubSub

注意一个设计细节:Leader 的 PubSub 地址是 bind(绑定),Teammate 的 PubSub 地址是 connect(连接)。这是典型的 ZeroMQ PubSub 模式------发布者绑定端口,订阅者连接过来。

4.2 通信拓扑

分布式模式支持两层通信拓扑:

Direct(点对点) :Leader 和 Teammate 之间的一对一消息通道。用于发送任务指令、接收执行结果。每个节点在 direct_addr 上监听。

PubSub(发布订阅) :Leader 广播消息给所有 Teammate。用于团队级别的通知(如任务取消、团队销毁)。Leader 绑定 pub_port(发布)和 sub_port(接收订阅确认),Teammate 连接这两个端口。

4.3 远程启动编排

远程启动编排是分布式模式最复杂的部分。它解决的核心问题是:Leader 如何在远程节点上"激活"一个正在等待的 Teammate。

这个过程由 remote_member_bootstrap.py 中的多个组件协同完成:

4.3.1 Leader 端:spawn_member 拦截

当 Leader 的 LLM 调用 spawn_member 工具时,attach_spawn_member_remote_bootstrap_wrapper() 会拦截这个调用,在正常的成员创建之后追加远程 Bootstrap 逻辑:

python 复制代码
async def wrapped_invoke(self, inputs, **kwargs):
    result = await orig_invoke(inputs, **kwargs)     # 1. 正常创建成员记录
    ok = bool(getattr(result, "success", False))
    if not ok:
        return result

    mname = inputs.get("member_name")
    # 2. 判断是否需要远程启动
    if (not active_remote_all) and key not in active_remote_names:
        return result

    # 3. 确保远程成员记录存在
    await _ensure_remote_member_record(active_team_agent, key, inputs)
    # 4. 强制状态为 UNSTARTED(等待 ACK 确认)
    await db.update_member_status(key, team_name, MemberStatus.UNSTARTED.value)
    # 5. 发送 Bootstrap 消息
    delivered = await _send_bootstrap_message(active_team_agent, ...)
    return result

判断哪些成员需要远程启动的规则由配置决定:

  • jiuwen_remote_member_names:指定的成员名列表,这些成员走远程路径
  • jiuwen_remote_all_spawn_members:设为 true 时,所有 spawn_member 创建的成员都走远程路径
4.3.2 Bootstrap Envelope 的构建与发送

Bootstrap 消息是一个结构化的 JSON 信封(Envelope),包含了 Teammate 启动所需的所有信息:

python 复制代码
def build_bootstrap_envelope(team_agent, *, session_id, member_name, prompt):
    return {
        "type": "jiuwen.remote_teammate_bootstrap",
        "version": 1,
        "bootstrap_id": str(uuid.uuid4()),
        "team_name": team_name,
        "session_id": session_id,
        "member_name": member_name,
        "leader_member_name": leader_member_name,
        "leader_agent_id": leader_agent_id,
        "leader_direct_addr": leader_direct_addr,
        "messager": messager_config,    # 消息传输配置
        "prompt": prompt or "",         # 初始任务指令
    }

信封的发送有三层保障:

  1. 控制面直发:通过 TeamAgent 的 messager 直接发送到已注册的 Peer
  2. Raw ZMQ 发送:如果 messager 不可用,直接使用 ZeroMQ DEALER socket 发送
  3. DB 消息回退:前两种都失败时,写入数据库消息队列(当前版本已禁用此路径)
python 复制代码
async def _send_bootstrap_message(team_agent, session_id, member_name, prompt):
    # 优先:通过 messager 控制面直发
    if messager is not None and peer_agent_id and peer_addr:
        register(MessagerPeerConfig(agent_id=peer_agent_id, addrs=[peer_addr]))
        await send(peer_agent_id, control_event)

    # 回退:通过 Raw ZMQ 发送
    if not direct_sent and peer_agent_id and peer_addr:
        direct_sent = await _send_bootstrap_via_raw_zmq(
            peer_addr=peer_addr, envelope=envelope, ...
        )
4.3.3 Teammate 端:Bootstrap Daemon

Teammate 进程启动后会运行一个常驻的 Bootstrap Daemon(run_teammate_bootstrap_daemon),持续监听来自 Leader 的控制面消息:

python 复制代码
async def run_teammate_bootstrap_daemon(*, stop_event, poll_interval=1.0):
    # 1. 绑定 ROUTER socket 到 bootstrap_direct_addr
    bootstrap_router = ctx.socket(zmq.ROUTER)
    bootstrap_router.bind(direct_bootstrap_addr)

    # 2. 注册空白 Agent 到 A2X
    await register_teammate_blank_agent_at_startup(config)

    # 3. 持续监听
    while not stop_event.is_set():
        frames = await bootstrap_router.recv_multipart(flags=zmq.NOBLOCK)
        identity, payload = frames[0], frames[-1]
        raw = json.loads(payload.decode("utf-8"))
        event_type = raw.get("event_type")

        if event_type == REMOTE_BOOTSTRAP_DIRECT_EVENT_TYPE:
            # 处理 Bootstrap 请求
            adopted_member = await _apply_bootstrap_envelope_from_control_plane(...)
        elif event_type == REMOTE_TEAM_DESTROY_DIRECT_EVENT_TYPE:
            # 处理团队销毁通知
            adopted_member = await _apply_team_destroy_envelope_from_control_plane(...)

        await bootstrap_router.send_multipart([identity, b"ok"])

Daemon 使用 ZeroMQ ROUTER socket,这是一个支持异步接收并自动回复的模式。收到消息后回复 b"ok" 给 Leader 作为传输层确认。

4.3.4 身份切换与路由注册

Teammate 收到 Bootstrap Envelope 后,会执行几个关键操作:

身份切换 :Teammate 从默认的 teammate_1 切换为 Leader 分配的 member_name

python 复制代码
def _adopt_teammate_member_name(team_agent, member_name):
    # 更新运行时上下文中的 member_name
    ctx.member_name = target
    # 更新 messager 的 node_id
    mc.node_id = target

路由注册:注册 Leader 的地址到本地路由表,这样 Teammate 才能向 Leader 发送消息:

python 复制代码
def _apply_leader_route_from_envelope(team_agent, envelope):
    register(MessagerPeerConfig(
        agent_id=leader_agent_id,
        addrs=[leader_direct_addr]
    ))

ACK 确认:通过团队消息通道向 Leader 发送 ACK,通知 Bootstrap 已完成:

python 复制代码
ack = build_bootstrap_ack_envelope(
    member_name=target_member,
    team_name=team_name,
    handshake_applied=bool(route_applied and card_replaced),
)
ack_id = await mm.send_message(
    content=json.dumps(ack),
    to_member_name=leader_member,
)
4.3.5 Leader 端:ACK 确认与状态更新

Leader 端通过 attach_remote_bootstrap_ack_listener() 注册事件监听器,当检测到来自 Teammate 的 ACK 消息时,将成员状态从 UNSTARTED 更新为 READY

python 复制代码
async def on_event(event):
    ack = parse_remote_bootstrap_ack_json(row.content)
    if ack is None:
        return
    # 校验 member_name、team_name
    # 更新成员状态
    ok = await tb.db.update_member_status(ack_member, team_name, MemberStatus.READY.value)
    # 标记消息已读(不让 LLM 看到控制面消息)
    await mm.mark_message_read(message_id, leader_name)

一个值得注意的设计:ACK 消息会被标记为已读,不会出现在 Leader LLM 的上下文中。这是有意为之------Bootstrap 的控制面消息对 LLM 来说是透明的,LLM 只关心最终的任务结果。

4.4 本地启动守护

分布式模式下有一个容易忽略但很关键的设计:Leader 端的本地启动必须被禁用。

默认情况下,send_message 工具在检测到新成员时会自动触发 spawn_teammate。但在分布式模式下,Teammate 由远程节点管理,Leader 不应该尝试在本地创建。attach_distributed_local_spawn_guard() 负责这个守护:

python 复制代码
def attach_distributed_local_spawn_guard(team_agent, *, session_id, channel_id):
    # 禁用 send_message 的自动启动
    if hasattr(tool, "_on_teammate_created"):
        setattr(tool, "_on_teammate_created", None)

    # 替换 spawn_teammate 为空操作
    async def _skip_local_spawn_teammate(self, ctx, *args, **kwargs):
        logger.info("distributed local spawn guard skipped local spawn_teammate")
        return None
    team_agent.spawn_teammate = types.MethodType(_skip_local_spawn_teammate, team_agent)

4.5 团队销毁时的远程通知

当团队销毁时,Leader 需要通知所有远程 Teammate 释放资源并恢复到空白状态。这通过 attach_clean_team_remote_destroy_wrapper()_notify_reserved_teammate_team_destroy() 实现:

python 复制代码
async def _notify_reserved_teammate_team_destroy(team_agent, session_id, member_name, reservation):
    envelope = build_team_destroy_envelope(team_agent, session_id=session_id, ...)
    # 通过控制面发送销毁通知
    await send(peer_agent_id, control_event)

Teammate 收到销毁通知后,会:

  1. 停止正在执行的任务
  2. 将注册中心的卡片恢复为空白状态
  3. 清理本地工作空间
  4. 回到等待下一个 Bootstrap 的状态

4.6 依赖检查与降级

分布式模式有额外的依赖要求:pyzmq(ZeroMQ 的 Python 绑定)和可选的 asyncpg(PostgreSQL 异步驱动)。missing_distributed_dependencies() 在启动时检查这些依赖:

python 复制代码
def missing_distributed_dependencies(config_base):
    if not is_distributed_mode(config_base):
        return []
    missing = []
    if importlib.util.find_spec("zmq") is None:
        missing.append("pyzmq")
    if is_postgresql_storage(team_cfg) and importlib.util.find_spec("asyncpg") is None:
        missing.append("asyncpg")
    return missing

如果依赖缺失,系统会通过 fallback_distributed_to_local() 自动降级到本地模式:

python 复制代码
def fallback_distributed_to_local(config_base):
    normalized = copy.deepcopy(config_base)
    runtime_cfg["mode"] = "local"
    runtime_cfg["role"] = "leader"
    transport_cfg["type"] = "inprocess"
    transport_cfg.pop("params", None)
    # 如果配置了 PostgreSQL,降级为 SQLite
    if is_postgresql_storage(team_cfg):
        team_cfg["storage"] = {"type": "sqlite", "params": {"connection_string": "team.db"}}
    return normalized

这保证了即使分布式环境未就绪,系统也能以单机模式正常运行。

五、A2A 协议:外部系统接入

除了内部的多 Agent 协作,JiuwenSwarm 还通过 A2A(Agent-to-Agent)协议支持与外部 Agent 系统的互联互通。A2A 是一个开放标准协议,Google 主导,用于不同 Agent 框架之间的通信。

5.1 A2A Channel:Ingress 网关

A2AChannel 是 JiuwenSwarm 对外暴露的 A2A 入口。它启动一个独立的 FastAPI 服务,监听 A2A 协议的 JSON-RPC 请求:

python 复制代码
class A2AChannel(BaseChannel):
    name = "a2a"

    async def start(self):
        agent_card = AgentCard(
            name=self.config.app_name,
            capabilities=AgentCapabilities(streaming=True),
            supported_interfaces=[
                AgentInterface(url=f"http://{host}:{port}/a2a", protocol_binding="jsonrpc")
            ],
            skills=[AgentSkill(id="chat", name="chat", description="Send user prompt to JiuwenSwarm via Gateway")]
        )
        app_builder = A2AFastAPIApplication(agent_card=agent_card, http_handler=request_handler)
        fastapi_app = app_builder.build()

A2A Channel 的配置同样在 config.yaml 中:

yaml 复制代码
channels:
  a2a:
    enabled: true
    host: "127.0.0.1"
    port: 19100
    rpc_path: "/a2a"
    protocol_version: "1.0.0"

5.2 请求转发的核心机制

外部 A2A 客户端发送的请求会经过 _A2AAgentExecutor 转发到 JiuwenSwarm 的内部消息通道:

python 复制代码
class _A2AAgentExecutor:
    async def execute(self, context, event_queue):
        # 1. 从 A2A context 中提取 query 和 files
        query, files = self._channel.map_a2a_parts_to_params(context.message)

        # 2. 分发到内部消息通道
        pending = await self._channel.dispatch_a2a_request(
            request_id=request_id, session_id=context_id, query=query, files=files
        )

        # 3. 将内部响应转换为 A2A Artifact 事件
        while True:
            response_msg = await pending.queue.get()
            response_parts = self._channel.message_to_a2a_parts(response_msg)
            await event_queue.enqueue_event(TaskArtifactUpdateEvent(...))
            if is_terminal:
                break

dispatch_a2a_request() 将 A2A 请求转换为内部 Message 对象,通过 on_message 回调注入消息路由。响应通过 asyncio.Queue 异步返回给执行器。

5.3 数据模型转换

A2A 协议有自己的数据模型(AgentCardPartTaskArtifact 等),和 openJiuwen 内部的数据模型(AgentResultArtifactPart)不同。A2ATransformer 负责两者之间的双向转换:

python 复制代码
class A2ATransformer:
    @classmethod
    def to_a2a_request(cls, request):
        # 内部 dict → A2A SendMessageRequest

    @classmethod
    def from_a2a_response(cls, response):
        # A2A StreamResponse → 内部 AgentResult

A2AAgentCardAdapter 则负责 Agent 卡片的转换,将 openJiuwen 的 AgentCard 转为 A2A 协议的 AgentCard

python 复制代码
class A2AAgentCardAdapter:
    @classmethod
    def to_a2a_agent_card(cls, agent_card, *, interface_url, protocol_binding):
        return A2AAgentCard(
            name=agent_card.name,
            description=description,
            capabilities=AgentCapabilities(streaming=True),
            supported_interfaces=[AgentInterface(url=interface_url, protocol_binding=protocol_binding)]
        )

5.4 A2A Client:主动调用外部 Agent

除了作为服务端接收请求,JiuwenSwarm 还通过 A2AClientA2ARemoteClient 主动调用外部 A2A Agent:

python 复制代码
class A2ARemoteClient(RemoteClient):
    async def invoke(self, inputs, timeout=None):
        invoke_coro = self.client.invoke(inputs=inputs)
        if timeout:
            return await asyncio.wait_for(invoke_coro, timeout=timeout)
        return await invoke_coro

    async def stream(self, inputs, timeout=None):
        async for chunk in self.client.stream(inputs=inputs):
            yield chunk

A2ARemoteClient 实现了 RemoteClient 接口,可以像调用本地 Agent 一样调用远程 A2A Agent,支持同步调用(invoke)和流式调用(stream),并带有超时保护。

六、配置与部署

6.1 分布式团队配置

完整的分布式模式配置示例:

yaml 复制代码
team:
  team_name: "distributed_team"
  lifecycle: "persistent"
  teammate_mode: "build_mode"

  runtime:
    mode: "distributed"
    role: "leader"             # Leader 节点配置此项
    member_name: "team_leader"

  transport:
    type: "pyzmq"
    params:
      leader:
        host: "192.168.1.100"
        direct_port: 18555
        pub_port: 18556
        sub_port: 18557
      teammate:
        host: "192.168.1.101"
        direct_port: 18600

  storage:
    type: "postgresql"
    params:
      connection_string: "postgresql+asyncpg://user:pass@192.168.1.100:5432/team_db"

  metadata:
    jiuwen_remote_member_names: ["researcher", "writer"]
    jiuwen_remote_all_spawn_members: false

react:
  a2x_registry:
    base_url: "http://192.168.1.100:8000"
    dataset: "team_dataset"
    role: "teamleader"         # Leader 节点
    reservation_ttl_seconds: 30

Teammate 节点的配置基本相同,只需要修改 runtime.rolereact.a2x_registry.role 为对应的值。

6.2 A2A Channel 配置

yaml 复制代码
channels:
  a2a:
    enabled: true
    host: "0.0.0.0"
    port: 19100
    rpc_path: "/a2a"
    card_path: "/.well-known/agent-card.json"
    protocol_version: "1.0.0"

6.3 依赖安装

分布式模式需要额外的依赖包:

bash 复制代码
pip install pyzmq asyncpg    # 分布式模式
pip install ".[a2a]"         # A2A 协议支持

6.4 启动流程

Leader 节点:

bash 复制代码
pip install jiuwenswarm pyzmq asyncpg
jiuwenswarm-init
jiuwenswarm-start

Teammate 节点:

bash 复制代码
pip install jiuwenswarm pyzmq
jiuwenswarm-init
# 修改 config.yaml 中的 runtime.role = "teammate"
jiuwenswarm-start

服务启动后,Teammate 进程会自动向 A2X 注册中心注册空白 Agent,启动 Bootstrap Daemon 等待 Leader 的连接请求。

七、实操演示:同机跨进程分布式调研任务

用一个完整的同机跨进程场景走一遍分布式协作流程:注册中心、Leader、Teammate 三个进程都在同一台 Windows 机器上,通过 A2X 注册中心做节点发现,PyZMQ 做跨进程通信。

7.1 场景说明

用户在 Leader 节点的 Web 前端发送:

"帮我调研 AI Agent 的两个方向:1)主流 Agent 框架对比;2)Agent 通信协议现状。每个方向生成独立的 Markdown 报告。"

这个场景在分布式模式下的处理方式与单机模式有本质区别:Leader 分析任务后,不是在本地创建 Teammate,而是从 A2X 注册中心预占一个远程空白 Agent,通过 PyZMQ 发送 Bootstrap 消息激活它。远程 Teammate 收到任务后独立执行,结果通过共享 PostgreSQL 数据库同步回 Leader。

7.2 步骤一:准备环境

确认代码库已在本地,分布式依赖已安装。在仓库根目录执行:

plain 复制代码
uv sync --extra distribute

Windows 同机联调需要额外安装并启动 PostgreSQL(用于共享 TeamDB):

plain 复制代码
# 用 winget 安装 PostgreSQL 16
winget install -e --id PostgreSQL.PostgreSQL.16

安装完成后确认 PostgreSQL 服务在本机 127.0.0.1:5432 可用,并创建配置中使用的数据库 jiuwen_team

plain 复制代码
# 使用 PostgreSQL 安装目录下的 psql.exe
psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE jiuwen_team;"

7.3 步骤二:拉取并启动 A2X 注册中心

分布式 Team 依赖 A2X 注册中心做节点发现,注册中心不是 jiuwenswarm 内置组件,需要从上游单独拉取:

plain 复制代码
git clone -b feature/Agentregistry https://gitcode.com/openJiuwen/agent-protocol.git
cd agent-protocol
pip install -e .

启动注册中心(监听本机 127.0.0.1:8000):

plain 复制代码
a2x-registry

注册中心启动后不要关闭,后续 leader 和 teammate 进程都会连接它。

7.4 步骤三:创建双配置目录

Leader 和 Teammate 需要各自的配置目录和数据目录,避免相互覆盖。在仓库根目录执行:

plain 复制代码
$leaderHome = "$PWD\.local-distributed\leader"
$teammateHome = "$PWD\.local-distributed\teammate"

New-Item -ItemType Directory -Force "$leaderHome\config" | Out-Null
New-Item -ItemType Directory -Force "$teammateHome\config" | Out-Null

Copy-Item ".\jiuwenswarm\resources\config.team.distributed.leader.yaml" "$leaderHome\config\config.yaml"
Copy-Item ".\jiuwenswarm\resources\config.team.distributed.teammate.yaml" "$teammateHome\config\config.yaml"

7.5 步骤四:配置 Leader 节点

Leader 的 config.yaml 中需要确认以下关键配置(已由模板填充,按需调整):

plain 复制代码
team:
  team_name: "jiuwen_team"
  lifecycle: "persistent"
  teammate_mode: "build_mode"

  runtime:
    mode: "distributed"
    role: "leader"
    member_name: "team_leader"

  transport:
    type: "pyzmq"
    params:
      direct_addr: "tcp://0.0.0.0:28555"
      pubsub_publish_addr: "tcp://127.0.0.1:28556"
      pubsub_subscribe_addr: "tcp://127.0.0.1:28557"
      metadata:
        pubsub_bind: true

  storage:
    type: "postgresql"
    params:
      connection_string: "postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/jiuwen_team"

react:
  a2x_registry:
    base_url: "http://127.0.0.1:8000"
    dataset: "team_pool"
    role: "teamleader"

关键配置说明:

配置项 说明
runtime.mode: distributed 启用分布式模式
runtime.role: leader 当前节点为 Leader
transport.type: pyzmq 使用 ZeroMQ 传输
react.a2x_registry.base_url 指向本机 A2X 注册中心
team.storage PostgreSQL 连接,数据库需提前创建

7.6 步骤五:配置 Teammate 节点

Teammate 的 config.yaml 中需要确认以下关键配置:

plain 复制代码
team:
  team_name: "jiuwen_team"
  lifecycle: "persistent"
  teammate_mode: "build_mode"

  runtime:
    mode: "distributed"
    role: "teammate"
    member_name: "teammate_1"

  transport:
    type: "pyzmq"
    params:
      direct_addr: "tcp://0.0.0.0:28611"
      bootstrap_direct_addr: "tcp://127.0.0.1:28610"
      pubsub_publish_addr: "tcp://127.0.0.1:28556"
      pubsub_subscribe_addr: "tcp://127.0.0.1:28557"

  storage:
    type: "postgresql"
    params:
      connection_string: "postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/jiuwen_team"

react:
  a2x_registry:
    base_url: "http://127.0.0.1:8000"
    dataset: "team_pool"
    role: "teammate"
    endpoint: "tcp://127.0.0.1:28610"

注意 Leader 与 Teammate 配置的关键差异:

配置项 Leader Teammate
runtime.role leader teammate
runtime.member_name team_leader teammate_1
react.a2x_registry.role teamleader teammate
react.a2x_registry.endpoint 无(不需要) Teammate 的 ZMQ 监听地址
transport.params.bootstrap_direct_addr 控制面 ROUTER 绑定地址

7.7 步骤六:启动 Teammate 节点

先启动 Teammate 节点(确保在 Leader 之前启动,这样 Teammate 能先注册空白 Agent 到注册中心):

plain 复制代码
cd D:\Download\jiuwenswarm

$env:JIUWENSWARM_DATA_DIR = "$PWD\.local-distributed\teammate"
$env:JIUWENSWARM_CONFIG_DIR = "$PWD\.local-distributed\teammate\config"
$env:GIT_AUTHOR_NAME = "teambot"
$env:GIT_AUTHOR_EMAIL = "teambot@example.com"
$env:GIT_COMMITTER_NAME = "teambot"
$env:GIT_COMMITTER_EMAIL = "teambot@example.com"
$env:AGENT_SERVER_PORT = "28193"

.\.venv\Scripts\python.exe -m jiuwenswarm.server.app_agentserver

启动后观察日志,应该看到以下关键输出:

plain 复制代码
[A2XRegistryRuntime] blank agent registered source=startup dataset=team_pool service_id=xxx endpoint=tcp://127.0.0.1:28610
[RemoteMemberBootstrap] teammate direct bootstrap listener started addr=tcp://127.0.0.1:28610 local_member=teammate_1
[RemoteMemberBootstrap] teammate bootstrap daemon started local_member=teammate_1

这三行日志分别表示:

  1. 空白 Agent 已注册到 A2X 注册中心
  2. ZMQ ROUTER socket 已绑定到控制面地址
  3. Bootstrap Daemon 已启动,开始监听

7.8 步骤七:启动 Leader 节点

再启动 Leader 节点(另开一个终端):

plain 复制代码
cd D:\Download\jiuwenswarm

$env:JIUWENSWARM_DATA_DIR = "$PWD\.local-distributed\leader"
$env:JIUWENSWARM_CONFIG_DIR = "$PWD\.local-distributed\leader\config"
$env:GIT_AUTHOR_NAME = "teambot"
$env:GIT_AUTHOR_EMAIL = "teambot@example.com"
$env:GIT_COMMITTER_NAME = "teambot"
$env:GIT_COMMITTER_EMAIL = "teambot@example.com"
$env:AGENT_SERVER_PORT = "28192"
$env:GATEWAY_PORT = "29101"
$env:WEB_PORT = "29100"

.\.venv\Scripts\python.exe -m jiuwenswarm.app

启动后确认以下几项:

  1. PostgreSQL** 连接正常**:日志中出现 PostgreSQL 连接成功信息
  2. Gateway** 已就绪**:日志中显示 Gateway 启动端口
  3. Web Channel 已就绪:日志中显示 Web 服务端口

7.9 步骤八:启动 Web 前端

再开一个终端启动前端(也可跳过,直接用 API 测试):

plain 复制代码
cd D:\Download\jiuwenswarm\jiuwenswarm\channels\web\frontend

$env:VITE_API_BASE = "http://127.0.0.1:29100"
$env:VITE_WS_BASE = "ws://127.0.0.1:29100"

npm run dev -- --host 0.0.0.0 --port 5173

7.10 步骤九:切换到集群模式并发送任务

浏览器打开 http://127.0.0.1:5173 后:

  1. 在 Web 前端底部的模式切换按钮组中,点击最右侧的集群模式按钮(👥 图标)
  2. 在输入框中发送调研任务:
plain 复制代码
帮我调研 AI Agent 的两个方向:
1)主流 Agent 框架对比(LangGraph vs CrewAI vs AutoGen)
2)Agent 通信协议现状(MCP、A2A、ACP)
每个方向生成一份独立的 Markdown 报告,保存到工作目录。

7.11 步骤十:观察分布式协作过程

此时在两个节点上可以同时观察到协作过程。

Leader** 节点:**

消息发送后,Leader 分析任务并决定创建两个 Teammate。此时关键流程如下:

  1. Leader LLM 调用 spawn_member,创建 framework_researcher 成员
  2. remote_member_bootstrap_wrapper 拦截调用,从 A2X 注册中心预占空白 Agent
  3. 通过 PyZMQ Direct 发送 Bootstrap Envelope 到 Teammate 节点
  4. 重复以上步骤创建 protocol_researcher 成员

团队面板实时更新:

在 Leader 端的团队面板中,可以看到两个成员的状态变化:

  • framework_researcher:🟢 就绪 → 🟡 忙碌(执行中)
  • protocol_researcher:🟢 就绪 → 🟡 忙碌(执行中)

成员名称旁边会显示"远程"标识,表示这些成员运行在 Teammate 节点上。

7.12 步骤十一:追加新任务

调研进行过程中,可以在 Leader 端直接追加新方向:

plain 复制代码
再帮我加一个方向:Agent 的安全与对齐问题。

Leader 收到后,会再次从 A2X 注册中心预占空白 Agent。但由于只有一个 Teammate 节点,此时会有几种处理方式:

  • 如果注册中心还有空闲 Agent,会预占并创建新的远程成员
  • 如果没有空闲 Agent,Leader 会在本地创建一个进程内 Teammate 处理(降级行为)

7.13 步骤十二:查看调研结果

调研完成后:

  1. Leader** 端聊天区域**:Leader 汇总所有调研结果
  2. 团队成员面板:所有远程成员状态变为 🟢 就绪,随后变为 ⚪ 已关闭
  3. 工作目录:报告文件已保存

7.14 效果对比

与单机模式相比,分布式模式的核心差异:

维度 单机模式(inprocess) 分布式模式(distributed)
Teammate 运行位置 与 Leader 同进程 独立进程
通信方式 进程内调用 PyZMQ 跨进程通信
共享存储 SQLite(本地文件) PostgreSQL(网络共享)
节点发现 无需 A2X 注册中心
算力扩展 受限于单机 可扩展到多进程
故障隔离 进程内相互影响 进程独立,互不影响
启动复杂度 一条命令即可 需配置传输层和注册中心

分布式模式适合任务量大、需要资源隔离、或有多个独立进程协作需求的场景。对于大多数日常任务,单机模式仍然是更实用的选择。但同机跨进程分布式可以让开发者在单机环境下验证完整的分布式协作逻辑,无需多台机器。

7.15 快速启动脚本

为方便重复启动,可以将上述命令保存为 PowerShell 脚本 start-distributed-team.ps1

plain 复制代码
# start-distributed-team.ps1
$repoRoot = "D:\Download\jiuwenswarm"
$leaderHome = "$repoRoot\.local-distributed\leader"
$teammateHome = "$repoRoot\.local-distributed\teammate"

# 创建目录(首次需要)
New-Item -ItemType Directory -Force "$leaderHome\config" | Out-Null
New-Item -ItemType Directory -Force "$teammateHome\config" | Out-Null

# 复制配置模板(首次需要,或每次用最新模板)
# Copy-Item "$repoRoot\jiuwenswarm\resources\config.team.distributed.leader.yaml" "$leaderHome\config\config.yaml" -Force
# Copy-Item "$repoRoot\jiuwenswarm\resources\config.team.distributed.teammate.yaml" "$teammateHome\config\config.yaml" -Force

# 启动 Teammate(后台)
Start-Process powershell -ArgumentList "-NoExit", "-Command", @"
cd $repoRoot
`$env:JIUWENSWARM_DATA_DIR = `"$teammateHome`"
`$env:JIUWENSWARM_CONFIG_DIR = `"$teammateHome\config`"
`$env:GIT_AUTHOR_NAME = `"teambot`"
`$env:GIT_AUTHOR_EMAIL = `"teambot@example.com`"
`$env:GIT_COMMITTER_NAME = `"teambot`"
`$env:GIT_COMMITTER_EMAIL = `"teambot@example.com`"
`$env:AGENT_SERVER_PORT = `"28193`"
.\.venv\Scripts\python.exe -m jiuwenswarm.server.app_agentserver
"@

# 启动 Leader(后台)
Start-Process powershell -ArgumentList "-NoExit", "-Command", @"
cd $repoRoot
`$env:JIUWENSWARM_DATA_DIR = `"$leaderHome`"
`$env:JIUWENSWARM_CONFIG_DIR = `"$leaderHome\config`"
`$env:GIT_AUTHOR_NAME = `"teambot`"
`$env:GIT_AUTHOR_EMAIL = `"teambot@example.com`"
`$env:GIT_COMMITTER_NAME = `"teambot`"
`$env:GIT_COMMITTER_EMAIL = `"teambot@example.com`"
`$env:AGENT_SERVER_PORT = `"28192`"
`$env:GATEWAY_PORT = `"29101`"
`$env:WEB_PORT = `"29100`"
.\.venv\Scripts\python.exe -m jiuwenswarm.app
"@

Write-Host "分布式 Team 已启动"
Write-Host "- Teammate: AGENT_SERVER_PORT=28193"
Write-Host "- Leader: AGENT_SERVER_PORT=28192, GATEWAY_PORT=29101, WEB_PORT=29100"
Write-Host "- Web 前端: http://127.0.0.1:5173"
Write-Host ""
Write-Host "启动前请确保:"
Write-Host "1. A2X 注册中心已启动 (a2x-registry)"
Write-Host "2. PostgreSQL 数据库 jiuwen_team 已创建"

八、写在最后

回顾 JiuwenSwarm 的分布式协作设计,几个关键决策值得一提。

注册中心驱动的节点发现,让 Leader 不需要硬编码 Teammate 的地址。空白 Agent 的预占-替换-归还机制,确保了节点的生命周期管理是完整的。TTL 超时自动释放,避免了"僵尸预占"问题。

多层保障的消息投递,从控制面直发到 Raw ZMQ 再到 DB 回退,确保了 Bootstrap 消息的可靠性。ZeroMQ ROUTER-DEALER 模式的使用,让控制面通信和数据面通信可以解耦。

透明的本地启动守护 ,通过 Monkey-patch 的方式禁用了 Leader 端的本地 spawn_teammate,这看起来不够优雅,但有效地解决了 agent-core 层面与分布式模式的兼容问题。

当然,当前的分布式模式还有一些可以改进的方向。部署层面,PostgreSQL 是一个较重的依赖,如果未来能支持轻量级的分布式存储(如基于 Raft 的嵌入式 KV),会降低部署门槛。安全层面,当前的 ZeroMQ 通信没有加密,跨公网部署需要额外的 VPN 或 SSH 隧道。可观测性层面,分布式模式下的调试和问题定位比单机模式困难得多,一个分布式的链路追踪系统会很有帮助。

总的来说,JiuwenSwarm 的分布式协作方案没有发明全新的通信协议或调度算法,而是将成熟的组件(ZeroMQ、PostgreSQL、A2A)通过合理的编排组合起来,解决实际问题。注册中心做发现,传输层做通信,Bootstrap 做启动------每一层职责清晰,组合起来就是一个可工作的分布式多 Agent 系统。


参考资料:

相关推荐
莱歌数字1 小时前
双歧管拓扑优化针翅冷板:汽车功率逆变器高热通量热管理的破局之道
人工智能·科技·制造·散热·液冷散热
P-ShineBeam1 小时前
智能体-LangChain框架-Tools工具的使用指南
数据库·人工智能·语言模型·自然语言处理·langchain
八月瓜科技1 小时前
擎策·知海知识产权数据库迭代更新,专利检索&管理效率再提一倍!
数据库·人工智能·科技·深度学习·机器人
Promise微笑1 小时前
洞察无形:红外热像仪行业标准解析与深度选型指南
网络·人工智能·算法
Artech1 小时前
[MAF预定义ChatClient中间件-05]动态修改ChatOptions和请求消息
ai·agent·maf·agent管道
searchforAI1 小时前
利用AI翻译视频做双语笔记,一套视频翻译到知识库沉淀的完整方案
人工智能·笔记·gpt·音视频·语音识别·知识图谱·机器翻译
Z-D-K1 小时前
考验AI的“自我和意识“-AI对《红楼梦》后40回的改写(19)
人工智能·ai·aigc·交互·agi
w_t_y_y1 小时前
Agent设计模式(二)语义压缩(Semantic Compaction)
人工智能