协作团队真的能并行了,界面上也看得出来

先说一个和名字不符的事实:这个节点叫「多 Agent 协作」,但在此之前它是一条
严格的串行流水线。调度协议写死成 `next: string`——"一次只能派一个"被编进了
协议本身,模型再想并行也表达不出来。三个专家各花 10 秒,总是 30 秒。

(真并行一直在图级 fan-out:实测 3 个各 sleep 2 秒的节点连在同一个上游,
全部 +0.00s 开始、+2.37s 结束,墙钟 2.6 秒。只是协作团队走不到那条路。)

**协议改成 assignments 数组**,一轮可以派多个互不依赖的专家,asyncio.gather
并发执行,总耗时按最慢的那个算。max_parallel 可配,设成 1 即退回严格串行。

**并发只在任务真的互不依赖时才成立**,这件事的成本必须说清楚:同一轮发出去的
专家拿到的是**同一份进展快照**,谁也看不见谁这一轮干了什么。派错了不报错,
只会让两个人基于一样的旧信息重复劳动,而这在结果里很难看出来。所以判断责任
压在调度者身上,提示词里把反例写出来:

    可以同时派:「查 A 的资料」和「查 B 的资料」
    不能同时派:「查资料」和「根据资料写报告」
    不能同时派:「写初稿」和「校对初稿」

并明确告诉它拿不准就分两轮——代价只是慢一点。

**每个人单独计时**,而不是事后拿整轮墙钟去摊。界面要显示"并行省下多少",
那个数必须是量出来的:按 N×墙钟 推算等于假设每个人都跑满了最慢那条,
而实际上快的那个可能只花了三分之一。测试里专门有个快慢不对称的用例守这条。

**调度日志改成结构化字段**(agents / done / reason / parallel)。以前只有一句
中文消息串,解码层得拿正则去拆「调度 → X(理由)」——后端文案一改就散架,
而这次正是改了文案。老运行的事件没有这些字段,正则那条路留着兜底。

**界面加泳道。** 每个成员一行、每轮一格,条形宽度按耗时归一化,所以一眼看得出
谁和谁是同时干的、谁是这一轮的瓶颈。顶上一行汇总"3 名成员 · 3 轮 · 2 轮并行,
省下 11.8s";点任意一格展开那个人这一轮的任务和回复。全程串行时不装作有并行,
而是说"本次任务互相依赖,逐轮进行"。

泳道排在子步骤**前面**:先给一眼看清的形状,再往下是每一步的内容。反过来的话,
得把十几行步骤读完才知道这个团队是怎么分工的。窄栏(画布右栏 360px)下隐去
"N 人同时"的文字——并行与否看条形本来就一目了然,而缩写成"并2"没人看得懂。

验证:后端 276 passed(新增 test_supervisor.py 7 条,核心那条用**墙钟**判定
并发而不是看事件顺序——顺序只能说明"发出去了",说明不了"同时跑");
check-stream 新增 10 条泳道断言,含"同一轮里慢的那个条更长"和两种栏宽不溢出;
tsc 干净,三个 check 不回归。
This commit is contained in:
ThearchyHelios
2026-09-20 20:57:03 +08:00
parent 4d237c3b08
commit c3f7d6ffcd
7 changed files with 773 additions and 37 deletions
+41
View File
@@ -191,6 +191,47 @@ console.log('\n=== 复核说明 ===')
await page.close()
}
console.log('\n=== 协作团队的泳道 ===')
{
// 这个节点以前在界面上是一条扁平的步骤序列:看得出"谁回了什么",
// 看不出"谁和谁是同时干的"——而一轮同时派几个人正是它相对单 agent 的
// 全部优势,不画出来等于没有
for (const [w, dense, label] of [[1000, '0', '宽栏'], [380, '1', '窄栏']]) {
const page = await browser.newPage({ viewport: { width: w, height: 700 } })
const errors = []
page.on('pageerror', (e) => errors.push(e.message))
await page.goto(`${WEB}/preview.html?team=1&dense=${dense}`, { waitUntil: 'networkidle' })
await page.waitForTimeout(400)
const body = await page.locator('body').innerText()
check(`${label}:没有运行时报错`, errors.length === 0, errors.join(' | '))
check(`${label}:三个人各占一行`,
['researcher', 'analyst', 'writer'].every((n) => body.includes(n)))
check(`${label}:说清楚并行省了多少`, /2 轮并行,省下 11\.8s/.test(body),
body.slice(0, 90).replace(/\n/g, ' '))
const overflow = await page.evaluate(() =>
document.documentElement.scrollWidth - document.documentElement.clientWidth)
check(`${label}:不横向溢出`, overflow <= 0, `${overflow}px`)
if (dense === '0') {
// 条形宽度按耗时归一化,一眼看得出谁是这一轮的瓶颈。
// 都画一样长的话,"并行"就只剩一句口号
const widths = await page.evaluate(() =>
[...document.querySelectorAll('button[title*="第 1 轮"]')]
.map((b) => b.getBoundingClientRect().width))
check('同一轮里慢的那个条更长', widths.length === 2 && widths[0] > widths[1] * 1.2,
widths.map((x) => Math.round(x)).join(' vs '))
await page.locator('button[title*="第 1 轮"]').first().click()
await page.waitForTimeout(300)
const opened = await page.locator('body').innerText()
check('点开看得到这个人的任务和回复',
opened.includes('查 Milvus 与 Qdrant') && opened.includes('内置 jieba'))
}
await page.close()
}
}
console.log('\n=== 空态 ===')
{
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })