关于 ag2-network-discussion
介绍如何使用 AG2 Network 的 discussion 适配器,让两个或更多参与者按固定轮次发言。内容涵盖轮次校验、Agent 跳过非本轮调用、人工参与和通道关闭。
主要功能
- 01固定的轮转发言顺序
- 02轮次校验与发送资格检查
- 03支持人工与 Agent 共同参与
- 04手动及基于 TTL 的通道关闭
适用场景
- 01组织多 Agent 小组讨论
- 02按轮次收集对草稿的评审
- 03在固定讨论轮转中加入人工参与者
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ag2ai/ ag2-skills --skill ag2-network-discussion安装后在 Claude Code 中输入/ag2-network-discussion确认已加载。
详细介绍
AG2 Network — 讨论适配器
discussion 是一个 N 方轮流发言频道。参与者按固定顺序发言,并无限循环,直到你将其关闭。适配器通过 validate_send 强制执行“轮到你再发言”;Hub 的 can_send 探测可让每个 Agent 的默认处理程序在不轮到自己时跳过无效的大模型调用。
前置要求:请先阅读
ag2-network-quickstart。本技能假设你已了解Hub.open、HubClient.register、频道生命周期以及Envelope基础知识。
适用场景
- “三个 Agent 轮流辩论”
- “由固定成员组成的圆桌讨论/头脑风暴”
- “轮流评审并评论草稿”
- 任何固定顺序的 N 方轮流发言场景,其中每位参与者在每轮都有一个发言时段
以下情况不要使用 discussion:
- 有条件的交接(“如果 alice 提到安全问题,就交给安全专家”)→ 使用
ag2-network-workflow。 - 每个步骤只执行一次的流水线 → 使用
ag2-network-workflow和TransitionGraph.sequence([...])。 - 两位参与者且没有固定顺序 → 使用
conversation(ag2-network-quickstart中有介绍)。 - 严格的 1Q1R → 使用
consulting(ag2-network-quickstart中有介绍)。
结构
| 参与者 | 2+ |
| 轮次顺序 | 轮流(创建者先发言,然后按注册顺序轮到其他参与者) |
| 自动关闭 | 否 |
| 终止方式 | 显式调用 channel.close() 或 TTL(见下文“关闭”) |
| 默认视图 | NamedWindowedSummary(recent_n=N*2),其中 N 为参与者数量 |
| 默认预期 | turn_within(120s, warn)、turn_within(600s, hide) |
| 可调参数 | {"ordering": ORDERING_ROUND_ROBIN}(目前仅支持此排序方式) |
视图的最近消息窗口设为 N*2,因此每个 Agent 的视图大致涵盖最近两个完整轮次——足以提供连贯回复所需的上下文,同时避免讨论内容增加时提示词不断膨胀。
讨论 Agent、NetworkPlugin 与轮流发言
HubClient.register(...) 默认会附加 NetworkPlugin,添加 peers / channels / tasks / context / delegate 这些身份级动词。DiscussionAdapter.tools_for(...) 返回 []:讨论适配器不提供任何特定于频道的工具(没有 say)。参与者只需返回回复即可完成这一轮;默认处理程序会将轮末的 EV_TEXT(reply.body) 作为该轮的发言内容发出。
由于没有 say,讨论场景不存在特有的“双重发送”风险。轮流发言完全由 validate_send 强制执行:如果发送者不是 state.expected_next_speaker,则会以 ProtocolError 拒绝发送(提示 expects <other> to speak)。can_send 探测(如下所示)可让每个处理程序在不轮到自己时跳过大模型调用,因此正常运行时不应出现越序发送。
attach_plugin=False 只会移除 NetworkPlugin 的五个动词(delegate / peers / channels / tasks / context);不会影响适配器工具(讨论适配器没有此类工具)。如需使用不带插件的参与者,请设置此选项。
# Bare discussion participant (no plugin verbs):
alice = await alice_hc.register(alice_agent, Passport(name="alice"), Resume(), attach_plugin=False)
以下示例为简洁起见设置了 attach_plugin=False;如果参与者需要 delegate / peers 等功能,请移除该设置。完整说明:ag2-network-tools-and-views → “适配器自有工具(tools_for)”。
最简示例
import asyncio
from ag2 import Agent
from ag2.config import AnthropicConfig
from ag2.knowledge import MemoryKnowledgeStore
from ag2.network import (
EV_CHANNEL_CLOSED,
EV_TEXT,
ORDERING_ROUND_ROBIN,
Hub,
HubClient,
LocalLink,
Passport,
Resume,
)
async def main() -> None:
config = AnthropicConfig(model="claude-sonnet-4-6")
hub = await Hub.open(MemoryKnowledgeStore(), ttl_sweep_interval=0)
link = LocalLink(hub)
alice_hc, bob_hc, carol_hc = (HubClient(link, hub=hub) for _ in range(3))
alice = await alice_hc.register(
Agent("alice", prompt="The optimist. One short sentence.", config=config),
Passport(name="alice"), Resume(), attach_plugin=False,
)
bob = await bob_hc.register(
Agent("bob", prompt="The realist. One short sentence.", config=config),
Passport(name="bob"), Resume(), attach_plugin=False,
)
carol = await carol_hc.register(
Agent("carol", prompt="The skeptic. One short sentence.", config=config),
Passport(name="carol"), Resume(), attach_plugin=False,
)
channel = await alice.open(
type="discussion",
target=[bob.agent_id, carol.agent_id],
knobs={"ordering": ORDERING_ROUND_ROBIN},
)
await channel.send("Topic: should every developer learn Rust?")
# Halt after 6 EV_TEXT envelopes (= 2 full alice→bob→carol cycles).
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")
for hc in (alice_hc, bob_hc, carol_hc):
await hc.close()
await hub.close()
async def wait_for_text_count(hub, channel_id, expected, *, timeout=180.0):
deadline = asyncio.get_event_loop().time() + timeout
while asyncio.get_event_loop().time() < deadline:
wal = await hub.read_wal(channel_id)
if sum(1 for e in wal if e.event_type == EV_TEXT) >= expected:
return
await asyncio.sleep(0.05)
raise asyncio.TimeoutError("did not reach expected count")
asyncio.run(main())
target=[bob.agent_id, carol.agent_id] 中的顺序很重要——它决定了创建者之后的轮换顺序。因此,轮次会按 alice → bob → carol → alice → bob → carol → … 依次循环,直到达到上限。
跳过轮次的工作方式(can_send 探测)
alice 发送第一个信封时,中心会将 EV_TEXT 扇出给 bob 和 carol。两个默认处理器会并行触发:
- bob 的处理器 —
await hc.can_send(channel_id, bob.agent_id)。适配器返回“是,bob 是expected_next_speaker。”处理器运行Agent.ask,并发送 bob 的回复。 - carol 的处理器 —
await hc.can_send(channel_id, carol.agent_id)。适配器返回“否,expected_next_speaker是 bob。”处理器直接返回,不会调用大模型。
bob 的回复到达后,会再次进行相同的扇出。此时 expected_next_speaker = carol,因此 carol 的处理器会参与,bob 的处理器则跳过。整个过程中不会浪费大模型调用。
如果你为讨论频道编写自定义处理器,请遵循此模式——并将非文本信封委托给 default_handler,以保留对 EV_CHANNEL_INVITE 的自动确认(否则频道会一直处于 PENDING 状态,直到 invite_ack_timeout 超时,随后中心会关闭频道):
from ag2.network import Envelope, EV_TEXT, default_handler
async def my_handler(envelope: Envelope) -> None:
if envelope.event_type != EV_TEXT:
await default_handler(envelope, client) # invite-ack + lifecycle bookkeeping
return
if not await hc.can_send(envelope.channel_id, my_agent_id): # async — must await
return # not our turn — skip the LLM call
# ... read WAL, project view, run Agent.ask, send reply ...
轮询顺序中的 HumanClient
讨论参与者不一定非得是 Agent。注册一个 HumanClient(hc.register_human(Passport(name="user", kind="human")) — 参见 ag2-network-quickstart → "HumanClient"),并像添加其他参与者一样将其加入参与者顺序。它没有会自动回复的通知处理器,因此由应用程序驱动其轮次:使用 on_envelope(...) 订阅(或使用 next_envelope(...) 拉取),信封到达时检查 await hc.can_send(channel_id, user.agent_id)(这是异步操作,必须等待)——如果轮到真人发言,就提示对方并执行 await user.send(channel_id, text);否则继续等待。(框架自身的讨论测试正是这样做的——真人根据适配器状态,等待轮到自己。)
状态对象
@dataclass(slots=True)
class DiscussionState:
participant_order: list[str]
expected_next_speaker: str
turn_count: int = 0
创建时会根据 Participant.order 对参与者排序,从而固定 participant_order。每次接受 EV_TEXT 后,轮询顺序都会按 (current_index + 1) % len(participant_order) 前进。可通过 hub._adapter_states[channel_id] 读取状态,以便检查或测试(下划线表示“运维 API”,不供生产环境中的 Agent 逻辑使用)。
验证规则
DiscussionAdapter.validate_send 会拒绝:
- 来自非
state.expected_next_speaker的EV_TEXT— 抛出ProtocolError。 - 非参与者发送的消息。
- 发送到已关闭频道的消息。
协议信封(EV_CHANNEL_*、ag2.task.*)不会进行轮次检查——它们用于记录状态,而非控制轮流发言。
自定义排序方式
目前仅提供 ORDERING_ROUND_ROBIN:
from ag2.network import ORDERING_ROUND_ROBIN
channel = await alice.open(
type="discussion",
target=[bob.agent_id, carol.agent_id],
knobs={"ordering": ORDERING_ROUND_ROBIN}, # equivalent to "round_robin"
)
创建时传入任何其他排序方式都会报错。未来的排序方式(动态、加权、优先级)可在此接入,而不影响轮询模式的约定。
如果你现在需要有条件地控制轮次,可将适配器切换为 workflow — TransitionGraph.round_robin(participants, max_turns=N) 能提供相同的模式,并支持日后添加条件。参见 ag2-network-workflow。
关闭
discussion 不会自动关闭。有四种可用模式,可根据由谁决定来选择:
| 模式 | 决定方 | 适用情况 |
|---|---|---|
应用侧上限 → channel.close() | 你的编排代码 | 轮次数、时间或任何外部条件 |
Agent 侧工具(通过 ChannelInject) | 大模型 | 参与者决定“就到这里” |
| 自定义适配器子类 | 框架 | 基于内容停止(讨论场景中较少见) |
TTL / turn_within 预期 | 中心的清理器 | 仅作安全兜底 |
应用侧上限(规范做法)
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")
关闭原因会记录在 EV_CHANNEL_CLOSED.event_data["reason"] 中 — 请选择描述性内容,便于观察者区分正常达到上限关闭与 TTL 超时或预期条件不满足导致的关闭。
Agent 侧工具
与其他适配器采用相同模式 — 注入当前的 Channel,调用 close():
from ag2.network.client.inject import ChannelInject
async def end_discussion(reason: str, channel: ChannelInject) -> str:
if channel is None:
return "no active channel"
await channel.close(reason=f"agent_close:{reason}")
return f"closed: {reason}"
for agent in (alice_agent, bob_agent, carol_agent):
agent.tool(end_discussion)
任何参与者都可以结束讨论。完整说明请参见 ag2-network-quickstart 中的“五种关闭方式”。
当 discussion 不够用时
一旦你需要有条件地轮流发言(“如果 bob 提到截止日期,就转到 deadlines_expert”),discussion 就不是合适的适配器了。请迁移到 workflow ——参与方数量相同,但轮流发言由 TransitionGraph 管理,并支持 FromSpeaker、ToolCalled 和 ContextEquals 条件。迁移方法请参阅 ag2-network-workflow。
默认预期
| 预期 | 阈值 | 处理方式 | 作用 |
|---|---|---|---|
turn_within | 120s | warn | 记录 AUDIT_KIND_EXPECTATION_VIOLATED。通道保持打开。 |
turn_within | 600s | hide | 在视图投影中将响应较慢的发言者标记为“跳过”,持续一个周期。通道保持打开。 |
根据预期,discussion 会刻意避免自动关闭——不应因为某个发言者响应较慢,就让整个讨论组结束。如果希望通道在某个周期停滞时终止,请通过 ag2-network-governance 将预期覆盖为 auto_close。
快速参考 — 导入
from ag2.network import (
DISCUSSION_TYPE, # = "discussion"
ORDERING_ROUND_ROBIN, # = "round_robin"
DiscussionAdapter, # the adapter class itself (rarely instantiated directly)
DiscussionState, # state dataclass
HumanClient, # a non-LLM participant in the rotation — HubClient.register_human(...)
)
字符串形式 type="discussion" 和 ordering="round_robin" 也能正常使用——这些常量与字符串等效,并可在较大的代码库中提供类型安全。