用 multiagent 协调者模式组织专家子 Agent 团队协作产出销售提案,用 Session Threads API 与线程级事件流精确观察每个子 Agent 的运行。
概述
本篇介绍如何用 Managed Agents 的 multiagent 协调者模式,组织一支专家子 Agent 团队协作产出一份销售提案,并用 Session Threads API 与 Session 级事件流精确观察、实时渲染每个子 Agent 的运行。示例统一用 curl(REST)。
场景:虚构公司 Northstar(向中端市场运营团队卖工作流自动化平台)要自动写销售提案。现状:销售代表为每个潜客手工做定制提案——调研该细分领域公司通常关注什么、从案例库里挑两个相关案例、按内部规则表算定价、拼成两页文档。每步用不同数据源、不同判断。
用一个协调者 Agent 跑三个专家:研究员用联网搜索找该细分市场的典型关注点;案例挑选器读案例库挑两个最佳匹配;定价建模员只看规则文件和席位数。再加一个校验员,在协调者动笔前做一次对齐自检。协调者负责排序它们并写出提案。
环境变量与模型
Authorization: Bearer $DASHSCOPE_API_KEY。多智能体的 multiagent 配置与 Session 事件流属于 Managed Agents 能力,当前仅 cn-beijing。
模型 ID 用 qwen3.8-max / qwen3.7-max / qwen3.7-plus。roster 里每个成员都是完整 Agent,可按角色混用不同模型——高价值的写作/协调用旗舰模型,机械的读文件、套规则可换更廉价的模型档位,用不同模型 ID 区分成本即可。本例统一用 qwen3.7-plus 演示。
多智能体场景的两个高频问题,先放在最前面:
定义三个专家子 Agent
每个子 Agent 有自己的 system prompt、输出形态,且只给它需要的工具。研究员只有搜索;案例挑选器只能读本地库;定价建模员只看 pricing_rules.md。按角色限定工具,能防止定价员从网上拉竞品数字,也把整个案例库挡在协调者上下文之外。
阿里云百炼的编排工具由平台在 multiagent 模式下自动注入,无需在 tools 里声明(协调者拿到 create_agent / list_agents / wait_for_agents,worker 拿到 submit_result)。
下面每个
POST /agents 返回的 id(形如 agent_<ULID>)请记下来,后面拼进协调者的 roster。研究员 prospect_researcher(只开搜索 MCP)
mcp_servers.name 是市场服务的 code(声明时不校验存在性,写错会静默失败,生产化篇讲过排查方法)。市场服务由平台代理调用,不需要提供凭证。想确认某服务提供哪些工具名:bl mcp tools --server WebSearch。
案例挑选器 case_study_picker(只读本地库)
定价建模员 pricing_modeler(只看规则文件)
再加一个校验员:写作前的方案自检
三个专家各自只交一份局部结论,谁都不负责「三份结论合起来是否讲得通」。所以 roster 里再额外放一个 proposal_checker 子 Agent:它在协调者动笔之前,把关案例挑选与定价框架是否对齐潜客优先级。
它由协调者在写提案前委派一次,读取研究结论、案例挑选、定价框架,判断三者是否互相自洽、是否命中潜客最关心的痛点,然后回传一句判断和修改建议。它是一个和其他专家一样的普通 worker:可被委派、可收发消息、跑在自己的线程和上下文里。
校验员是纯判断,不挂任何工具(
tools 留空数组即可,进阶:提示词版本管理与回滚已验证无工具的纯 LLM agent 可以正常跑)。它承担判断类工作,可以把 model.id 设成比其他 worker 更强的模型来提高判断质量——能力与成本分档通过选用不同模型 ID 实现,model 字段没有 effort 之类的推理档位参数。multiagent.agents[ ] 的成员类型:{"type":"agent","id":...} 引用一个已创建的子 Agent,可选带 version 锁定版本(见下节);{"type":"self"} 把协调者自己也作为可委派对象,最多 1 个——写两个会报 AGENT_010 multiagent.agents 中 self 类型最多只能有一个。给团队素材并接线协调者
素材清单
7 个短案例(覆盖医疗/制造/物流/零售/金融/公共部门),能看到挑选器挑出适配潜客的两个。每个案例含 title/industry/employees/summary:
- St. Clair Health:区域医院网 6200 人,凭证与预授权流程分散在 11 个系统 → 合并成 3 个自动化流程,预授权周转降 58%,年省 190 万。
- BlueRidge Health Plan:区域支付方 2800 人,理赔异常排队邮件 19% 需返工 → 异常路由端到端自动化,返工率降到 6%,理赔平均快 11 天。
- Calder Manufacturing:工业 3100 人,采购单审批平均 9 天 → PO 周期降到 2.1 天,冒进支出降 14%。
- Northwind Logistics:3PL 4400 人,承运商入驻每家 3 周 → 降到 4 天,Q1 多激活 22% 承运商。
- Harborview Retail Group:专业零售 5600 人,门店库存异常靠 Slack+表格 → 140 店异常分诊自动化,缺货降 31%。
- Aperture Payments:金融 1900 人,KYC 与商户入驻平均 6 个工作日 → SLA 降到 36 小时,同人力吞吐 2.4 倍。
- Summit County Government:公共部门 3700 人,建筑许可纸质包过五部门 → 单一数字入口并行评审,中位许可时长 41 → 17 天。
- PRODUCT one-pager:Northstar 是给中端运营团队的工作流自动化平台,核心能力——可视化流程构建、200+ SaaS 连接器、基于角色审批、SOC 2 Type II;典型效果 40–60% 人工工单下降、3 周首个工作流上线。
- PRICING rules:每席位 65/月,或年付 52/月;用量档 light 1.0x / standard 1.15x / heavy 1.30x 乘在单价上;企业档(>500 席位)加 48000/年平台费、年付单价降到 44/月;所有选项含 onboarding,企业档含专属 CSM。
上传 9 个文件
把 7 个案例 md、product_one_pager.md、pricing_rules.md 逐个上传。上传用 multipart,上传后须轮询 GET /files/{id} 到 status=available 才能挂载。
file_id。
建 Environment(沙箱)
研究员的 MCP 搜索由平台代理调用、不走沙箱网络,但挑选器/定价员要读挂载文件(沙箱工具),所以 Environment 仍然需要。
networking.type 当前取值为 unrestricted(全部出网放行)。响应 id 形如 env_<...>。建协调者 Agent(挂 roster)
协调者引用上面四个子 Agent(三专家 + 校验员):
id(agent_<ULID>)记为 $AGENT_ID。
roster 成员的版本语义:「不带 version」既不是在协调者创建时快照定版,也不是在 Session 创建时定版——协调者不动、把 pricing_modeler 从 v1 升到 v2 后起 Session,委派出来的 pricing 线程直接用 v2;Session 创建后再升 v3、随后才发消息,线程用的还是 v3。也就是说改子 Agent 不需要更新协调者;反过来,想要「团队配置冻结、可复现」,就在成员里显式写
version,平台会接受并回显(GET 协调者时成员对象带 version: N)。这个语义跟进阶:提示词版本管理与回滚讲的 Session 版本锁定是对偶的:Session 的 agent 字段锁协调者自身的版本快照,roster 的 version 锁成员引用;都不写就都是「拿最新」。起 Session(挂 9 个文件资源)
mount_path 必须以 /uploads/ 开头,沙箱内实际路径为 /mnt/session + mount_path。7 个案例挂到 /uploads/case_studies/<slug>.md,另两个挂到 /uploads/ 根。指令文本里写的都是实际路径 /mnt/session/uploads/...。
字段就叫
agent(对象只传 id 即锁定该 Agent 当前最新版本快照;要锁旧版本传 {"id": ..., "version": N})。响应里 agent 字段回显的是完整配置快照(含 system 与 tools,但不含 multiagent——roster 不进 Session 快照,成员版本按上表规则在委派时解析)。另外注意:resources[ ] 回显的 file_id 与提交的不同——挂载时平台为每个文件生成了 session 作用域的新 file id,原 upload id 仍然有效。响应 id 形如 sesn_<ULID>,记为 $SESSION_ID。启动提案并观察
Session 创建时不携带初始输入,所以流程是先建 Session、再发一条 message 事件驱动它开始工作。
mark_artifacts 登记产物。
流式观察(Session 级单流,按 thread_id 分流)
SSE 是 Session 级单流——协调者与全部子 Agent 的输出都汇聚到同一条流里。没有 thread 级的 SSE 端点:GET /sessions/{sid}/threads/{tid}/events/stream 返回 404 {"type":"api_error","message":"not support"}。想流式看,只有 Session 这一条流;想只看某个 thread,用下节的 Thread Events 轮询。
:connected,再收 event: message 帧(夹带 :keepalive)。每条事件顶层都带 thread_id,这是分流键:协调者主线程是 thrd_ 前缀(primary thread),每个被委派的子 Agent 是一个 sthr_ 前缀的 Session Thread。
多智能体专属的四种事件类型:
thread_created:某子线程出现。thread_id是新的sthr_...,content[0].data为{"agent_name": "prospect_researcher", "session_thread_id": "sthr_..."}。thread_message_sent:一条消息从某线程发出。发送方的thread_id(协调者是thrd_、worker 是sthr_),content[ ].text是消息本体,metadata.to_session_thread_id/to_agent_name指向接收方。thread_message_received:对端视角的同一条消息。thread_id是接收方线程,metadata.from_session_thread_id/from_agent_name指向发送方(协调者发来的消息from_agent_name是"coordinator")。thread_status:线程状态流转。content[0].data为{"agent_name": ..., "thread_status": "running"|"idle", ...},结束时带stop_reason: {"type": "end_turn"}。
tool_call / tool_call_output / mcp_call / mcp_call_output / model_request_start|end / reasoning / message / session_status)与入门篇相同,只是全部带上了 thread_id。协调者被自动注入的编排工具在事件流中的形态如下:
worker 的回传通道:worker 的最终 assistant 文本会自动作为
thread_message_sent 回传协调者;submit_result 是另一个显式提交工具,二者可能先后各出现一次。子 Agent 的 system prompt 里不需要写「通过 send_to_parent 返回」这类话——没有叫这个名字的工具,写了反而可能让模型犹豫;直接说「返回严格 JSON」即可。若不想用长连接,也可轮询历史事件:
GET /sessions/$SESSION_ID/events?order=asc。注意单页上限 100 条(limit 传更大也只回 100),翻页用响应里的 next_page 游标:...&page=<next_page>。一个完整的多智能体回合有 168 条事件,要翻两页。Session Threads API:每个子 Agent 一行
多智能体的结构化观察不走事件流也行——Session Threads 四个端点直接给线程级的清单、详情、事件与归档(这组 API 尚未进入正式公开文档):
List: GET /sessions/{session_id}/threads
parent_thread_id:委派者线程。挂在协调者主线程下是thrd_...;若某个 worker 又委派了自己的子 Agent,那条线程的 parent 会是前者的sthr_...(多级委派)。agent.version:这条线程实际运行的成员版本——上一节表格里说的「不带 version 就在委派时拿最新」,在这里直接可查。roster 没锁版本、子 Agent 中途升了 v2,新 Session 里这一栏就是 v2。status:running/idle/terminated。跑批时轮询这个端点,就是一张实时的「子 Agent 工作台」。
limit / page 与其他列表一致;已归档 Thread 默认不返回。
Get: GET /sessions/{session_id}/threads/{thread_id}——单个 Thread 详情,字段与 List 一致(多一个 request_id)。两个边界:协调者主线程(thrd_ 前缀)不在 threads 集合里,用它的 id 去 GET 返回 400 invalid_parameter "Invalid request"——threads 资源只寻址被委派的子线程,主线程只是事件流上的一个 thread_id 值;已归档的 Thread 也返回同样的 400(List 里同样没有它)——归档即从可寻址空间移除,但 Thread 的事件仍然可查(见下)。
Events: GET /sessions/{session_id}/threads/{thread_id}/events——单线程的事件序列,就是上一节那条流按 thread_id 切好的一片:从 thread_created(收到任务)到 thread_status(idle + stop_reason),中间是它自己的模型调用与工具调用。不带 limit 时返回该线程全部事件;带 limit 时响应带 next_page 游标。这是「只看某个子 Agent 干了什么」的最短路径——不用再从 168 条 Session 事件里按 thread_id 过滤。
Archive: POST /sessions/{session_id}/threads/{thread_id}/archive
archived_at 填充、updated_at 刷新),之后 List 不再返回它;重复归档幂等。用途:长会话里清掉历史 worker 线程,让 threads 列表只留活跃的——事件数据不受影响。
各专家回传了什么
用 Thread Events 遍历四个子线程的 thread_message_sent(或 submit_result 的 arguments.content),能拿到每个专家的原始 JSON:
- 研究员:
priorities/pain_points/sources——外部情报(6 次bailian_web_search,事件流里是mcp_call/mcp_call_output,server_label为WebSearch)。 - 挑选器:
picks——先glob列目录、再read全部案例、打分挑两个(典型结果:St. Clair Health + Aperture Payments,医疗行业优先,完全正确)。 - 定价员:
options——read规则文件后按 600 席位 × heavy 1.30x 计算(保守档 $486,720 = 52 × 1.3 × 600 × 12,与规则文件严丝合缝)。 - 校验员:
aligned/issues/suggestions——一句对齐判断加修改建议。
客户端:把一条流渲染成团队工作台
前三节讲的是服务端给的观察面。客户端要消费这条流,核心只有一个数据结构——以 thread_id 为键的分桶字典,再配合四个行为,渲染逻辑比想象的简单:
- 去重按事件
id。SSE 不提供游标 / 续传 token,断线重连后新连接可能重放旧帧,按id去重即可。 - assistant 文本整条下发,无需拼接增量。流式文本以整条
completed消息一帧到位,sequence_number常为null、没有同 id 多帧增量;解析时按「有sequence_number就按序累积、没有就当整条」处理。多智能体的「实时感」其实主要来自结构性事件:thread_created(谁开工)、thread_status(谁完工)、mcp_call/tool_call(谁在干什么)。 session_status是唯一的会话级事件。本回合仅有的 2 条不带thread_id的事件就是它(running 与 idle+stop_reason),分流时当会话状态处理,不属于任何线程。- 主线程没有
thread_created。thrd_在流开始之前就存在了——首次见到某个thrd_事件时把它注册成 coordinator 即可。
GET /sessions/{id}/events 为准(order=asc + next_page 翻页,单页上限 100,本回合 168 条要翻两页);只关心个别线程时,用 GET .../threads/{tid}/events 做单线程对齐更省。
最后是观察面的选型:
| 要什么 | 用什么 |
|---|---|
| 全团队实时进展(一条连接) | Session 级 SSE 流 + 分桶字典 |
| 一张「谁在跑 / 谁完了」的工作台 | 轮询 GET /sessions/{id}/threads(status + agent.version) |
| 深挖某个子 Agent 干了什么 | GET .../threads/{tid}/events(单线程全量) |
| 权威留档 / 事后归因 | GET /sessions/{id}/events 翻页拉全量 |
读提案,以及一次失败归因
产物文件取回:从协调者主线程的事件里找写 proposal.md 的 tool_call(工具 write,路径 /mnt/session/outputs/proposal.md,arguments.content 就是全文——本例还 mark_artifacts 登记过,能即时拿到 file_id),或从 Files 列表按文件名找。
Files 列表按 scope 过滤的正确参数是
GET /files?scope_type=session&scope_id=<sesn_...>——传 session_id 会被静默忽略,返回整个工作空间的文件列表(靠每项的 scope 字段区分归属)。- Run 1:三个 worker 全部正确,但协调者写出的 proposal.md 里,案例名变成了案例库里不存在的「Community Health Network」「Bayshore Medical Group」,定价表变成 $39,000/$58,500/$78,000——协调者在汇总长上下文时幻觉了。
- Run 2:同配置同输入,proposal.md 里是正确的 St. Clair Health + Aperture Payments。
- worker 对 ≠ 团队对。多智能体架构把调研/挑选/定价拆给专家后,最后一公里的「汇总写作」本身仍是幻觉高风险区——协调者值得配最强的模型,prompt 里要求「引用专家回传的原文数据、禁止改写数字」也有帮助。
- 归因靠 Threads API。出问题时,
GET /threads/{id}/events逐线程检查:挑选器回传的 picks 是对的、定价员的 options 是对的、错在协调者的 write 参数——问题精确定位到协调者。单 Agent 出错时只有一整条日志;多智能体为每个环节保留了独立记录,便于归因。 - 评测要进流程。进阶:提示词版本管理与回滚的方法论在这里同样适用:上线前用固定的潜客画像跑一轮,对 proposal.md 做断言(案例名必须在案例库白名单里、定价必须命中规则表的计算结果),再决定放行。
为什么用三个(+ 一个校验)子 Agent,而非一个
单个 Agent 带上全部工具也能写出提案,但按角色限定工具带来实际收益:
- 定价员只有规则文件、没有任何联网通道,它无法拉竞品价,定价始终来自内部规则。
- 案例挑选器这里读 7 个文件、生产里读几百个——这个体量留在子 Agent 的上下文里,而不是灌进协调者上下文。
- 协调者只决定顺序和交接,自己不做专家工作,上下文保持干净。
proposal_checker 也是同一思路的延伸:它在动笔前把案例与定价拉回潜客优先级上对齐,让不一致在写作前就被拦住——本例里它驱动协调者改了 4 稿。校验过程跑在自己的线程里,判断依据与建议都留在事件日志中可回溯。代价是多起一个 worker 线程——多一轮委派与回传,也多一份运行开销。若某些潜客场景不需要这道自检,可以把它从 roster 里去掉,协调者会直接进入写作。
收尾(归档)
统一用 archive(保留审计、停计量)。注意顺序:running 状态的 Session 不能 DELETE(返回 400 invalid_parameter),先等它 idle 或用归档;Agent 只能 archive: