跳到主要内容

数据截至 (上游 commit 3a4e2ae3eec0)

第 5 章 · 多智能体与服务层

这一章讲:AgentScope 2.0 怎么表达「多个 agent 协作」,以及 app 层的会话、总线、沙箱是怎么搭的。


5.1 先说一件重要的事:编排原语没了

AgentScope 1.x 的招牌是 msghubsequential_pipeline 这类编排原语。在本 commit 的 src/ 下,这些符号是 0 命中。

这是有意的路线选择。README 里写着:

Our approach leverages the models' reasoning and tool use abilities rather than constraining them with strict prompts and opinionated orchestrations.

所以 2.0 的多智能体长这样:

维度1.x 的做法2.0 的做法
谁决定下一个谁说话框架的 pipeline模型调 TeamSay 工具
消息怎么传进程内的 msghub消息总线的每会话 inbox
协作在哪一层SDK 层app 服务层

代价是:纯 SDK 用法下没有开箱的多 agent 协作,你得起 app 服务或者自己拼。这是个真实的门槛,第 6 章会作为边界再提一次。


5.2 团队:leader 和 worker

五个工具

协作能力被做成了五个普通工具(src/agentscope/app/_tool/):

工具谁用干什么
TeamCreateleader 侧建队并成为 leader(一个会话只能带一个队)
AgentCreateleader 侧生一个 worker,带角色描述和初始任务
TeamSay双方发消息给某个成员或广播(leader 版与 worker 版描述不同)
TeamDeleteleader 侧解散
AgentInviteleader 侧,有条件把用户名下另一个「可受邀」的 agent 借进本队(src/agentscope/app/_tool/_agent_invite.py:140

工具可见性按「会话的队内角色」定,不按 agent 自己的身份

这条规则写在包的模块 docstring 里(src/agentscope/app/_tool/__init__.py:13-23),装配逻辑在 get_toolkitsrc/agentscope/app/_service/_toolkit.py:181-220):

会话在某个队里当 worker ─────────► 只给 TeamSay(worker 版描述)
会话不在任何队里 / 是本队 leader ─► 给 TeamCreate + AgentCreate
+ TeamSay(leader 版) + TeamDelete
└─ 用户名下至少有一个 invitable agent
时,再加 AgentInvite

两个细节值得单说:

  • 判据是会话,不是 AgentRecord.source 被借来的 agent 其记录里 source 仍然是 'user',但它在本队的那个会话是 worker,所以照样只看得到 TeamSay
  • AgentInvite 是有条件挂载的:没有可受邀 agent 时干脆不构造这个工具。注释给的理由是 input_schema 的 enum 不能为空——空 enum 会顶坏工具 schema 校验器,也会诱导模型去调一个没有合法目标的工具。

因为工具集在装配时就定死了,而每个工具在 __call__ 时都重新从存储读当前的会话与团队状态,所以「同一次运行里先 TeamCreateAgentCreate」是成立的:toolkit 从头到尾没变,变的是存储里的状态。

协作时序

用户 ──► Leader
│ TeamCreate("重构小队", "把 parser 拆成三个模块")
│ AgentCreate(name="alice", prompt="负责 lexer")
│ AgentCreate(name="bob", prompt="负责 parser")

│ (leader 这一轮结束,等着)

alice / bob 各自被唤醒,独立跑自己的 reply 循环

│ 干完活,调 TeamSay(to=leader) 汇报

Leader 被唤醒,汇总

提示词工程占了大头

TeamSay 的工具描述里,leader 版和 worker 版是两份不同的文本(src/agentscope/app/_tool/_team_say.py:36-76)。反复强调的是别轮询

DO NOT repeatedly call this to check on a member's progress — members will automatically notify you via TeamSay when they finish their task. Wait for their message instead of polling.

以及 worker 版:

When you finish your assigned task, you MUST call this tool to report your results back to the leader.

这些「什么时候用 / 什么时候别用」的段落就是编排逻辑——只不过写在自然语言里,交给模型执行,而不是写成框架代码。这是 2.0 路线的直接体现。

worker 的权限从哪来

_merge_leader_permissionssrc/agentscope/app/_tool/_agent_create.py:60)按模板的三个开关,把 leader 的权限状态叠进 worker:

开关打开的效果
override_leader_mode用模板的权限模式,否则继承 leader 的
extend_leader_permission_rulesleader 的规则追加在模板规则之后
extend_leader_working_directories合并工作目录,冲突时模板优先

规则的顺序很讲究:模板规则在前,因为引擎每个阶段是首个匹配即返回。这样模板的意图优先于 leader 的历史授权,同时用户已经批过的东西不用再批一遍。

默认模板 DEFAULT_SUB_AGENT_TEMPLATE:38-45)三个开关都是「完全跟随 leader」,注释解释了直觉:一个通用 worker 就该和 leader 表现一致,除非开发者注册了更有主见的模板。

AgentInvite 不走这条路。 被借来的 agent 起一个全新的 PermissionContext,leader 的权限状态一点都不带过去(is_state_injected: bool = False_agent_invite.py:154-156)。理由也直白:那是别人的 agent,不该继承你这边批过的授权。


5.3 消息总线:三种消费语义

MessageBussrc/agentscope/app/message_bus/_base.py:53)刻意不按业务分类,而按「一条数据的生命怎么结束」分类

模式原语语义用在哪
A · 排空队列queue_push / queue_drain单消费者,读走即删,TTL 兜底agent 收件箱、唤醒队列
C · 回放日志log_append / log_read / log_trim多消费者各持游标,可回放会话事件流(SSE 断线重连)
D · 瞬时广播publish / subscribe只有当前订阅者收到,无历史唤醒信号

模块 docstring 里还解释了为什么不提供「一条消息被 N 个消费者各消费一次」这种计数广播:那需要消费者组协调(Redis Streams 的 XREADGROUP/XACK),状态成本高。建议生产者在写入时就扇出——给每个收件人各推一条。

The bus stays simple; deduplication is the producer's responsibility.

业务用的 key 全部集中在 MessageBusKeyssrc/agentscope/app/message_bus/_keys.py:20),总线本身不认识任何业务词汇。


5.4 inbox 交接:一个漂亮的并发协议

问题

A 给 B 发了条消息,推进 B 的 inbox。要不要顺便唤醒 B?

朴素做法是「B 看起来不忙就唤醒」。这会丢消息——B 可能刚刚做完最后一次 inbox 排空,此刻正在流式输出、写库、释放锁,看起来很忙,但它再也不会回头看 inbox 了。

解法

_bus_ops.py:141-161 的注释把思路写得极清楚:让两个极小的临界区互斥

┌──── inbox_lock ────┐
生产者: │ 推入条目 │
│ 读消费者标志 │
└────────────────────┘

┌──── inbox_lock ────┐
消费者(某次运行): │ 排空剩余条目 │
│ 清除消费者标志 │
└────────────────────┘

两种交错顺序都安全:

谁先拿到锁结果
生产者先消费者接着排空时能看到这条,继续处理
消费者先标志已清除,生产者读到 None,于是入队一个唤醒

而且因为唤醒只在「没有消费者」时产生,不会唤醒一个无事可做的会话

三个函数各司其职:

函数谁调干什么
deliver_to_inbox:163生产者推入 + 按需唤醒
register_inbox_consumer:206每次运行开头打上「我在消费」的标志,带租约 TTL
has_pending_inbox_or_release:230每次运行结尾还有活就继续,没有就清标志

消费者标志带 TTL(SESSION_RUN_TTL_SECS),所以进程崩了也不会永久抑制唤醒

inbox 什么时候被读

InboxMiddlewaresrc/agentscope/app/middleware/_inbox_middleware.py:24)挂在 on_reasoning 上:每次推理之前排空 inbox,把里面的 HintBlock 注入上下文,同时发 HintBlockEvent 给前端。

所以「团队成员发来的消息」在 agent 眼里就是一段追加的提示——它甚至能在自己一轮推理和下一轮推理之间收到新消息。


5.5 唤醒队列:三种触发

enqueue_run_triggersrc/agentscope/app/_bus_ops.py:72)把触发分成三类,调度器对它们的处理不同:

kind何时用会话正在跑时怎么办
wake唤醒空闲会话排空 inbox直接丢弃——正在跑的那次自己会排空
resume用人工确认结果恢复挂起的会话重新入队,等挂起的那次释放锁
message真实的用户消息(如飞书消息)resume,重新入队

差别的根据是「这条触发带不带输入」:不带输入的可以丢,带输入的丢了就丢数据。


5.6 ChatService:把 agent 装进服务

ChatService.runsrc/agentscope/app/_service/_chat.py:227)是 HTTP 端点和唤醒调度器共用的入口,保证两条路径的校验、组装、持久化完全一致。

它承担四件 SDK 层不管的事:

职责靠什么
会话串行bus.session_run() 拿分布式锁——跨进程保证一个会话同时只有一次运行
事件双写每个事件同时写回放日志(给断线重连)和实时频道(给当前订阅者)
状态持久化跑完把重建的回复 MsgAgentState 落库
异常兜底run() 吞掉所有异常并记日志,防止一次失败拖垮触发它的 HTTP 任务或调度器

第四点值得注意:异常run 里被吞,但报错给客户端是 _run_impl 的活。这个分层让「触发源不受影响」和「用户能看到错误」两件事各自成立。


5.7 工作区:工具在哪执行

八种后端,一套接口

后端隔离方式
LocalWorkspace无隔离,直接跑在本机
DockerWorkspace容器
AppleContainerWorkspacemacOS 原生容器
BubblewrapWorkspaceLinux 命名空间沙箱
E2BWorkspace / DaytonaWorkspace / OpenSandboxWorkspace第三方云沙箱
K8sWorkspaceKubernetes Pod

工作区不只是「跑命令的地方」

WorkspaceBasesrc/agentscope/workspace/_base.py:223)同时是四种东西:

Workspace
├── 执行环境 ──► get_backend() 给工具用
├── 卸载存储 ──► 实现 Offloader,压缩上下文/超长结果落这里
├── 技能仓库 ──► skills/ 目录,按 agent 分区
└── MCP 注册表 ► .mcp 文件,持久化声明 + 懒加载实例

所以 examples/console/main.py 里那句 offloader=workspace 一点也不奇怪:能跑命令的地方,天然就是能放文件的地方。

MCP 的两层:声明与实例

源码里区分得很清楚(:264-274):

字段是否持久化
声明_mcp_specs是,写进 .mcp 文件
实例_mcp_instances否,懒加载

有状态的 MCP 连接是稀缺资源,_enforce_mcp_capacity:804)按 LRU 淘汰其他 agent/会话的活实例,粒度是「轮」而不是「每次工具调用」——注释说明了原因:工具调用根本不经过工作区。


5.8 代码地图

主题文件路径符号名
建队src/agentscope/app/_tool/_team_create.pyTeamCreate
生成员与权限继承src/agentscope/app/_tool/_agent_create.pyAgentCreate_merge_leader_permissionsDEFAULT_SUB_AGENT_TEMPLATE
借用他人 agentsrc/agentscope/app/_tool/_agent_invite.pyAgentInvite
成员通信src/agentscope/app/_tool/_team_say.pyTeamSay(看两版工具描述)
团队工具可见性规则src/agentscope/app/_tool/__init__.pysrc/agentscope/app/_service/_toolkit.py模块 docstring、get_toolkit
总线抽象src/agentscope/app/message_bus/_base.pyMessageBusqueue_pushlog_appendpublish
业务 key 约定src/agentscope/app/message_bus/_keys.pyMessageBusKeys
inbox 交接协议src/agentscope/app/_bus_ops.pydeliver_to_inboxregister_inbox_consumerhas_pending_inbox_or_release
唤醒触发src/agentscope/app/_bus_ops.pyenqueue_run_trigger
inbox 注入src/agentscope/app/middleware/_inbox_middleware.pyInboxMiddleware
会话运行src/agentscope/app/_service/_chat.pyChatServicerun_run_implinterrupt
工作区基类src/agentscope/workspace/_base.pyWorkspaceBaselist_toolslist_skills_enforce_mcp_capacity
卸载协议src/agentscope/workspace/_offload_protocol.pyOffloader
服务示例examples/agent_service/main.py
相关测试tests/in_memory_message_bus_test.pyservice_message_bus_test.pychannel_routing_test.pyservice_team_tools_test.py