这篇讲三块外围工作:把协作挂进可视化工作流引擎、把桌面工具授权帧从一个长 await 里递出来、以及怎么证明这套东西真的可靠。
agent 节点:协作作为画布上的一个节点
工作流画布早已支持普通工具节点、条件节点、人工节点;这个版本新增一个 agent 节点。它的配置项有:
- 目标文本:1--2000 字,支持
{{$refs}}变量引用上游节点产出; - 选择助手;工具域最多 30 个;可选知识库;三重预算可选覆盖;三个角色模型也可分别覆盖。
引擎侧的执行流程是:发起一次协作运行 → 把协作内部的帧桥接成节点子步骤事件 → 协作终态后,节点产出结构化字段供下游引用:运行标识、终态、总轮次、总结、证据数组、未竟事项。引用支持数组数字下标,下游可以写「证据第 N 条的文档名」这种路径。取消信号沿引擎的 AbortSignal 级联进协作,一路透传到模型调用。
校验分两道。编译期校验目标文本非空、$ref 路径合法、预算都落在硬区间内,桌面域只给非阻断的风险警告。策略门控则是运行期的硬门:API 或 MCP 无人值守触发且声明桌面域时,永久拒绝,协作根本不发起;只有人在画布手动试运行才放行,并内联人工授权。这条边界与对话入口完全一致。
接线层还有几个小决定。协作运行行在发起时就回填所属的工作流运行标识与节点执行标识,于是任务中心、工作流时间线、协作详情三处能互相跳转;节点执行期间,内部帧映射成节点子步骤,画布上该节点显示持续运行而不是一卡到底。节点配置面板与协作本体共用同一套表单资产:目标里可插变量引用,工具域以 chip 形式列出(桌面域带风险警告),预算与三角色模型都可以就地覆盖。试运行时间线里弹出的授权窗刻意隐藏了「一直允许」------画布试运行是一次性语境,不该顺手授予长期权限。
人工授权桥接:长 await 里的帧怎么递出来
桌面工具调用前要弹窗征求授权。麻烦在于:执行者跑在一个长 await 内部,而 SSE 帧要靠编排器这个异步生成器 yield 才能出去------await 期间生成器是挂住的,没法 yield。
直觉写法是裸 Promise.race:让「授权帧到达」与「执行结束」两个 Promise 竞速。实测会漏帧,原因是注册时序:
text
执行器启动 → 同步地立刻 post 了一个授权帧(此时还没有任何等待者)
编排器 → 这才去注册竞速等待
→ 帧丢在虚空里,弹窗永远不出现
做法是一个邮箱原语。它内部是一个 FIFO 队列加一组等待者:post 帧时若已有等待者就直接交付,否则入队;取帧时先查队列,再注册等待,然后让「取到门控帧」与「执行定型」两个事件竞速。执行结束或中止信号触发时返回一个特殊的完成标记,编排器据此跳出取帧循环。这样同步先入队的帧绝不会丢,授权帧严格先于用户决策下发;队列可中止,且中止不消费已排队的帧。
text
循环 {
const 项 = await 邮箱.取帧或完成(执行定型Promise, 中止信号);
if (项 === 完成标记) break; // 执行结束/中止:收尾
yield 授权帧; // 帧经由生成器正常下发给 SSE
}
// 取帧内部:先 shift 队列,命中则立即返回;空了才注册等待者
「先查队列再注册等待」这个顺序是整个原语的命门,和工作流引擎里人工节点「先拿挂起 Promise 再发等待事件」是同一条时序纪律:同步发生的事件永远比异步注册快,队列必须替它兜住。
前端泳道:帧流归约成轮次视图
界面上不做自由聊天记录,而是按轮次组织泳道:规划卡的验收条逐条打勾打叉,执行卡显示工具 chip、证据与未竟告警,审查卡显示裁决徽标;返工不是再冒一个新消息气泡,而是在同一组泳道里追加新一轮。
归约逻辑是一个纯 reducer:帧流进去,轮次视图出来。另有一个关键细节:终态 GET 回放与实时帧流共用同一个归并函数------事后打开历史记录看到的卡,和实时看着跑出来的卡,由同一段代码生成,不会出现「实时一个样、回放另一个样」。
纯函数归约带来的直接好处是状态合并也可测:实时帧流中途刷新页面,GET 回读的权威视图与本地已经归约出的视图走同一个合并函数对账,缺帧补帧、重复帧幂等。预算超限、失败、取消、完成四种终态各有自己的横幅样式,未竟事项以告警块单列------这些全是输入确定、输出确定的 reducer 断言,不需要起浏览器就能测。
golden 评测:mock 掉角色协议层
协作正确性最忌拿真模型测:同样的输入,十次跑出十个样。做法是在角色协议层整体注入 mock,而不是去 mock 更底层:
- 四类 mock 角色协议:规划、审查、执行-工具模式、执行-截图模式,全部用确定性 JSON 回放;
- 用例靠 marker 驱动:在目标文本里埋审查标记、工具序列标记、结论标记、慢帧标记与开关切换标记,mock 读到标记就演出对应的脚本。
| marker 类型 | mock 的行为 |
|---|---|
| 审查标记 | 按用例脚本返回通过、返工或驳回裁决 |
| 工具序列标记 | 按指定顺序调用工具并返回准备好的观察 |
| 结论标记 | 交付带指定证据角标的总结 |
| 慢帧标记 | 在指定步骤前拖延,给急停用例制造时间窗 |
| 开关切换标记 | 读到后切换审查失效等全局开关 |
评测命令产出四态报告:正向、去审查、关知识、预算 1 轮分别给通过率,不适合某开关的标注题按矩阵跳过(沿用知识评测既有的 SKIP 语义)。15 个 golden 用例分四类:知识证据 6 个、工具序列 3 个、审查拦截 3 个、预算急停 3 个。
更有意思的是三开关反向验证------不仅证明「该发生的发生」,还要证明「关掉一个机关,它该消失的行为确实消失」:
- 去审查者:经目标注入开关标记模拟审查失效,返工题不再出现任何拦截裁决;
- 关知识库:解绑知识库并移除检索工具,知识题必须全部退化为无证据结论;
- 预算 1 轮:返工题必须收敛到预算超限终态,并且带着未竟事项清单交付。
另有一道纯时间工具的护栏题,在任何开关状态下都必须通过------它负责证明开关改变的只是目标行为,整套机器没有被开关搞坏。
端到端六场景
Playwright 侧新增协作专项 6 个场景:普通对话零变化护栏、一次通过、返工一次、运行中急停、API 触发下的预算熔断、agent 节点的运行归属。普通对话零变化是故意放进去的:协作是任务模式才有的新路径,老路径的行为一个字节都不许变。全套端到端场景从上个版本的 37 个增长到 43 个。
写这些场景时还有两个测试手法上的小结论。其一,Playwright 拿到的流式响应是缓冲型的,不能指望在响应对象上取消读取;SSE 驱动的断言统一改成「发出订阅请求但不 await,再轮询详情接口直到终态」,急停和超时才稳定可复现。其二,协作知识题的夹具必须同时配齐对话模型与嵌入模型------只有对话模型时,文档永远停在向量编码排队中,知识题会以与协作无关的方式失败。
两个工程坑
一是联合类型加成员,会逼出前端所有穷尽性缺口。 节点类型枚举加上 agent 之后,画布上四处基于枚举的 Record 映射与 switch 分支被类型检查逐个逼出编译缺口,必须同步补齐;SSE 帧白名单也有两处,少补一处,新帧就在前端被静默丢弃。这不是麻烦,是穷尽性检查在替你做架构影响面分析------往封闭联合里加成员,就该付这个代价。
二是急停用例要先给取消路由热身。 开发服务器冷编译时,第一次打到取消路由的请求会触发即时编译,编译耗时把急停窗口整个吞掉,用例稳定超时。夹具的做法是:跑慢用例前先打一次必然返回 404 的取消请求,把路由编译出来;慢帧用例的时长再留 3 秒余量。这是「开发环境的慢会掩盖时序 bug」的又一个例子------编译器替你把竞态窗口填上了。
收束:多智能体的可靠性由什么组成
三篇下来,这套协作的可靠性公式可以写清楚了:
- 确定的拓扑:三个固定角色、四条合法边、返工只有一条回边;
- 结构化的契约:子任务单、产出、裁决全部 schema 校验,交接逐条落库、终态 GET 对账;
- 可注入的 mock:角色协议层整体可替换为确定性脚本,评测不依赖任何模型;
- 反向验证:关掉每个机关,证明它负责的行为确实消失,护栏题则永远通过。
模型只负责拓扑允许它负责的那一小段。框架负责的部分是可验证的承诺:返工看得见、预算烧不光一切、失败带得走真实原因、任何一轮都能回放审计。至于最终结论写得够不够精彩,那是模型的事。
建成之时,这套承诺在云端强模型下全部兑现;后来把执行者换成本机跑的小模型,确定性框架又被真机暴露出一长串善意假设------那是下一个版本的故事了。