首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >A2A 聚合工作台:把协作协议翻译成人看得懂的界面

A2A 聚合工作台:把协作协议翻译成人看得懂的界面

原创
作者头像
OneCode
发布于 2026-09-28 17:59:54
发布于 2026-09-28 17:59:54
880
举报
文章被收录于专栏:ooderAgentooderAgent

Product · UI/UX · Agent Infrastructure

A2A 聚合工作台:把协作协议翻译成人看得懂的界面

当企业里出现第二个、第三个 Agent,"协作"就同时发生在三个层面:协议层在传消息,流程层在等回填,而人只看得见一块屏幕。本文复盘我们如何把 A2A(Agent-to-Agent)协作的协议事实,收敛成一套可读、可信、可操作的工作台界面——含真实截图与实测设计令牌。

日期 2026-09-28

载体 ooder ChatNext 工作台(8017 场景宿主 / 8018 独立智能体)

图片 真机截图,渲染期已脱敏

图片脱敏说明

本文截图均来自真实运行实例,在页面渲染阶段就地脱敏(非事后涂抹,避免遗漏与坐标错位):手机号、姓名、金额、长数字单据号、客户/机构名称(…有限公司 / …银行 / 集团 等)在截图前已被替换为掩码。产品与领域名(如「税务雷达」「财务智能体」「有成财务域」)是系统自身术语,予以保留。完整规则见文末附录。

目录

  1. 问题:协议可见 ≠ 用户可见
  2. 四条设计原则
  3. 信息架构:三栏 × 三层
  4. 视觉语言:让 Agent 一眼可辨
  5. 人工门:跨节点的人在环上
  6. 任务步骤与决策留痕
  7. 输入区:把"编排"变成可选项
  8. 双路径一致性:本地 vs 跨节点
  9. 支撑这套界面的前端架构
  10. 把 UI/UE 变成可回归的断言
  11. 结语与附录

1问题:协议可见 ≠ 用户可见

第一版工作台在功能上是"正确"的:跨节点任务能派发、能回填、人工门能推进、SSE 能推流。但用真实浏览器做了一轮展示审计后,截图证据相当难看:

  • 一个会话里出现 8 个原始 JSON 气泡(形如 {"conversationId":"conv_…","events":[{"seq":7,…}]})——那是跨节点流式中继的传输层批次,被当成"协作消息"渲染了;
  • 同一条消息同时出现在发言人气泡与协同台账行两处:10 条消息产生 20 处展示;
  • 「别的 Agent 说的话」与「我自己说的话」底色完全相同(同一个蓝紫渐变),只能靠左右位置区分;
  • 跨节点执行体的身份不可见:用户不知道对面是"本机代理"还是"远端智能体";
  • 会话列表标题被截断的原始 JSON 占据:"processInstId":"4a0d0…。

核心判断

协议正确性、协作可理解性、界面可操作性是三件独立的事。把传输层事实(消息到达)、协作层事实(谁委托了谁、结果如何)、用户层事实(我该知道什么、该做什么)混在一根时间轴上直接呈现,用户看到的就是"系统在自言自语"。工作台的第一职责是做减法:把传输层噪声挡在门外,把协作层事实翻成人话,把用户层动作放到伸手可及处。

图 1 修复后的工作台全景(跨节点会话)。同一块屏幕里并存四类对象:

人类消息「@税务雷达执行体 …」、Agent 协作「已委托 → 已回复」、人工门「上传报表」、助手消息「财务智能体」。它们共享时间轴,但视觉语言各不相同。

2四条设计原则

我们把上述问题抽象成四条可执行原则。它们不是审美偏好,而是能落到代码与自动化断言上的约束。

原则

含义

反面(审计抓到的真实形态)

P1 一个事实只呈现一次

同一条 A2A 消息在消息流里只应有一处可视呈现(气泡优先,台账行让位)

同 messageId 既有气泡又有台账行;中继批次另出 8 行

P2 发言人身份必须可辨

谁在说话(业务智能体 / 系统管道 / 我)、可信度几级,都要看得见

A2A 气泡与"我的消息"同底色;跨节点来源无任何标记

P3 过程可折叠,结果须突出

传输 / 心跳 / 思考属于过程,收进折叠区或干脆不渲染;结论、待办、产物必须在视野内

thinking 与 token 流把结论顶出屏幕;心跳刷屏

P4 失败必须显眼且可归因

投递失败 / 执行失败要有独立视觉态,并给出人话化的"为什么"

成功与失败气泡同色,失败被"看起来很正常"掩盖

由此推导的硬规则

传输层信封不得进入用户面。跨节点流式中继批次(fr_*)、人工门决策回投信封(gd_*)、心跳、无正文通知——这些对象对用户没有任何语义,唯一正确的处理是"不渲染"。这条规则同时落在前端文案源与后端落库口,形成双保险。

3信息架构:三栏 × 三层

工作台的组织方式遵循"横向按对象分栏、纵向按粒度分层"。横向三栏回答"有什么 / 在说什么 / 有谁参与";纵向三层回答"我要看多细"。

图 2 信息架构。三栏是"空间"上的分区,三层是"注意力"上的分级——两者正交:任何一栏内部都遵守 L1→L3 的粒度顺序。

图 3 L1 工作项栏。每行回答四件事:谁发起的(/✅/▶ 图标)、什么时候、什么场景(「启动流程 · 税务雷达」)、现在什么状态(运行中 / 已完成 / 已终止)。顶部 全部 192 / 协办 30 / 筛选 是注意力的第一道闸门。

图 4 协同视图入口。「Agent 10 条 / 3 体 / 3 关系」用三个数字概括协作规模:消息条数、参与执行体数、关系边数。它把"协作"从抽象概念变成可点开的具体对象。

4视觉语言:让 Agent 一眼可辨

工作台是深色主题,可用的"视觉预算"很有限:不能再引入高饱和的新色块,否则屏幕会变成调色板。因此我们把区分度花在三个更克制的通道上——左侧强调边、尾巴方向、宽度收敛,再叠加头像与发言人行的身份信息。

4.1 三类消息的实测设计令牌

下表是从运行实例的 getComputedStyle 直接采出来的(不是设计稿里的"期望值"):

通道

我发的消息

助手消息

Agent 协作消息

底色

透明(由渐变承担)

rgb(22,27,34)

rgb(22,27,34)

背景图

linear-gradient(135deg,#4dabf7,#9f7aea)

无

无

字色

#ffffff

rgb(230,237,243)

rgb(230,237,243)

圆角(含尾巴)

12 12 4 12 → 尾巴在右下

12 12 12 4 → 尾巴在左下

12 12 12 4 → 尾巴在左下

左侧强调边

无(0px)

1px 普通描边

3px 紫 rgb(159,122,234);系统管道 rgb(138,138,138)

最大宽度

铺满(实测 1117px)

铺满(实测 1314px)

收敛 min(85%,720px)

失败态

—

—

强调边转红 #e11d48 + 正文前置 ⚠

为什么用"强调边 + 宽度"而不是"换一个颜色"

深色主题里再塞一个高饱和底色,会同时破坏三层信息(底色层级、状态徽标、失败红)。而 3px 左侧竖边是一条"零面积成本"的通道:它不改变卡片尺寸、不参与文字对比度计算、可以在失败时原地变红。宽度收敛则解决另一个实际问题——协作消息通常很短(一句话),铺满 1117px 会被读成色带;收敛到 720px 后它自然形成"引文块"的视觉节奏,与人类消息的长短差异一眼可见。

图 5 Agent 协作气泡特写(跨节点会话)。上一条归属「系统」(⇄ 委托路由图标 + muted 强调边):已将任务委托给「税务雷达执行体」,等待执行…;下一条归属业务执行体(% 能力图标 + 紫色强调边 + 能力名 +

未验证

徽章):「税务雷达执行体」返回:已为你启动流程,当前状态:已暂停…。两条都带左下尾巴。

4.2 发言人身份:三个正交信息

发言人行是"身份"的承载处,我们让它同时表达三件事,且互不遮挡:

信息

表达方式

设计意图

是谁

能力名(「税务雷达执行体」)而非 fin.tax

业务人员不需要认识 agentId;技术面把原文放进 title

哪一类

紫色 ✨ + 能力图标(%//…)vs 灰色 ⇄(委托路由)

区分"业务执行体"与"系统管道",后者用 muted 强调边同族呈现

可信吗

未验证徽章(仅跨节点中继来源显示)

把"身份为声称级"这个安全事实放进用户视野,而不是埋在日志里

代码语言:javascript
复制
// 色环按「能力段」取,而非 agentId 全串 —— 否则 fin.tax(场景宿主)
// 与 llm.tax(独立智能体) 会得到两种颜色,出现"同名不同色"
var colorKey = agentKeyOf(agentId);        // 'tax' → 同一个色环索引
var trust = crossNode ? 'claimed' : '';      // 跨节点定向投递 ⇒ 声称级

图 6 本地持能路径的协作记录。同一个诉求,当"税务雷达执行体"就跑在本机时,不再产生跨节点消息;但用户仍需要知道"谁受理了",于是发一条记录级协作消息(不经网络、不触发执行):任务已由「税务雷达执行体」受理。

5人工门:跨节点的人在环上

人工门(Human Gate)是这套工作台里唯一"必须打断用户"的组件,也是最容易设计失败的地方。它的难点不在样式,而在位置的不对称:人坐在编排节点的浏览器前,而流程卡在执行体节点上。

我们最终确定的交互契约是:

  • 门归属人这一侧——门卡片是"人机交互"的产物,由工作站渲染与持有;
  • 恢复动作归流程那一侧——用户点击后,决策被打包成一条定向消息投回持有流程实例的节点,由它执行 resumeActivity;
  • 刷新必须能重建——门未决时刷新页面,卡片要回来(否则流程会静默卡死,且用户毫无察觉)。

图 7 人工门卡特写(文件上传型)。五个信息区块自上而下:① 类型标签「需要确认」+ 步骤名「上传报表(可选)」;② 一句话说明(为什么要你做什么);③ 拖拽/点选区(含支持格式提示);④ 超时进度条与剩余秒数「291 秒后超时(需人工处理)」;⑤ 主按钮「上传并分析」/ 次按钮「取消」。注意:超时不是装饰——它是"无人处理会怎样"的承诺。

门的四个设计决策

  • 选项即按钮,不做下拉:CHOICE 型门的候选分支直接铺成按钮行,一次点击即完成决策(免"展开—选择—提交"三跳)。
  • 超时可见且可控:进度条 + 秒数,超时后明确告知"需人工处理",而不是静默失效。
  • 决策即留痕:点击后立刻在时间轴上落一条「已确认 · 张三 · 时间 · 步骤」记录(见下一节)。
  • 多门并发要能同时呈现:同一会话里可能同时挂着多个执行体的门,历史回读必须支持"全量门"而不是"最新一条"——只取最新一条会让先到的门永远无人应答。

6任务步骤与决策留痕

门后面是"过程"。过程信息如果全量铺开,会把结论挤出屏幕;如果完全藏起来,用户又会在卡住时无从判断。我们的解法是折叠式步骤卡 + 常驻终态记录:

图 8 决策留痕与结果并列。上:灰色「系统」+ ✅「已确认 · 张** · 2026-09-28 15:29 · 步骤 意图确认」——不可折叠、不可删除的审计痕迹;中:「上传报表」是用户当时的答复(以人类消息形态呈现,保留原话);下:助手给出下一步需要准备的资料清单。三者构成"决策 → 答复 → 后续"的完整闭环。

另一侧的步骤卡(图 1 中部的「任务步骤 · 税务雷达 1/1 完成,等待人工处理」)承担"进度概览"职责:默认折叠成一行摘要,展开才是逐步骤明细。它的摘要行刻意包含当前动作要求("等待人工处理"),让用户在折叠态也能判断是否需要自己出手。

7输入区:把"编排"变成可选项

图 9 输入区工具行。左侧是模型与四档设计强度(快速 / 标准 / 深度 / 流程编排),中间是附件、图片等工具,右侧 1 · 税务雷达执行体 是"本次将被委派的执行体"预告。

这一行看似普通,实际解决了"多 Agent 工作台"的一个核心焦虑:用户不知道自己的话会被谁执行。因此我们把三件事做成显性:

  • 模式可预知:四档强度(快速/标准/深度/流程编排)直接暴露在工具行,而不是藏在设置里——"流程编排"意味着会走完整场景与人工门,"快速"意味着只有一次问答;
  • 执行体可预知:@ 提及后在工具行回显将被委派的执行体,用户发出去之前就能确认;
  • 过程可见:发送后,Thinking / 步骤 / 门卡按 L2→L3 顺序进入同一条时间线,而不是弹窗打断。

8双路径一致性:本地 vs 跨节点

这是最容易被忽略、也最影响信任的一条:同一个诉求,在不同部署形态下必须有一致的界面叙事。

我们的场景里,"税务雷达执行体"可能跑在本机(场景宿主节点,fin.tax),也可能跑在远端独立智能体进程(llm.tax)。前者是一次本地场景执行,后者是一次跨节点 A2A 委派。如果只在跨节点时产生协作记录,用户就会看到两种截然不同的界面:一边有"委托 → 回执",另一边结果凭空出现。

图 10 本地持能路径的完整叙事(8017 场景宿主)。顶部角标显示 Agent 1 条 / 2 体 / 1 关系(本地受理也算一条协作事实);中部依次是:助手的过程说明 →

Agent 协作

「任务已由「税务雷达执行体」受理。」→ 任务步骤卡 → 人工门卡(5 个分支选项 + 超时进度)。与跨节点路径相比,只是少了"网络投递"这一步的可见性,协作叙事的骨架完全相同。

一致性带来的工程约束(反过来说很有价值)

要做到"两种形态同一种叙事",就必须把协作事实当成一等数据来落库:本地受理也要写一条协作记录(消息类型 TASK_REQUEST + 状态 LOCAL_CONSUMED),并让刷新回读与实时推送走同一条渲染路径。否则会出现最尴尬的形态——"实时有受理提示,刷新后消失"(这正是我们在复测中抓到的一个真实回归)。

9支撑这套界面的前端架构

界面上的"干净"是有代价的。下面四条是这套 UI 能成立的技术前提——任何一条缺失,前面所有设计都会退化成"好看但会露馅"。

9.1 文案单一真源:所有用户面文字只在一个文件里生成

同一个事实在界面上有两处渲染(发言人气泡 + 协同台账行)时,只要各写一套文案,就必然漂移。我们把用户面的所有枚举、状态、发言人、错误原因收口到一个映射文件:

代码语言:javascript
复制
// core/Copy.js —— 用户面文案与枚举映射的唯一来源
function a2aLine(d, payload) {
  if (t === 'HEARTBEAT') return '';                 // 心跳不渲染
  if (isRelayInternal(d, payload)) return '';          // ★ 传输层信封不渲染
  if (st === 'FAILED' || st === 'UNDELIVERABLE')
    return '⚠️ 未能送达「' + to + '」' + (reason ? ':' + reason : '(投递失败)');
  if (t === 'TASK_RESPONSE') return '✅ 「' + from + '」返回:' + body;
  if (t === 'TASK_REQUEST') return st === 'LOCAL_CONSUMED'
      ? ' 任务已由「' + to + '」受理。'
      : ' 已将任务委托给「' + to + '」,等待执行…';
  return '';                                        // ★ 无用户面语义 ⇒ 空串(不兜底编话)
}

这条规则的反面教材(真实缺陷)

渲染层曾有一条"兜底":if (!line) line = ' 协同消息'。它的本意是"防止空白",实际效果是——所有被文案源刻意判定为"不该显示"的事件(中继批次、门决策信封、无正文通知)都被重新编出一行「协同消息已接收」。这说明兜底逻辑必须区分"我不知道"与"我知道它不该显示",否则兜底会亲手破坏上游的过滤决策。

9.2 分道渲染:一个会话里可能有多个 Agent 同时说话

流式输出必须知道"这段 token 属于谁"。渲染通道 ID 采用四段结构 agentId : processInstId : activityInstId : kind:

代码语言:javascript
复制
// StreamDomain:agentId 作为一级分道键;本地事件无 agentId ⇒ 'local'
function chId(ctx, d, kind) {
  var a = d.activityInstId || 'main';
  var p = d.processInstId || '';
  var ag = d.agentId || 'local';                 // ★ 一级分道
  return ag + ':' + (p ? p + ':' : '') + a + ':' + kind;
}
// MessageList.streamAssistant(text, agentKey):跨 agent 到达 → 先收尾当前气泡再开新气泡

这条约束直接决定了两件事:① 两个 Agent 的输出不会互相污染同一个气泡;② 前端能按 agent 渲染独立的"发言人",而不是把所有人的话合并成"助手"。

9.3 幂等与去重:同一 messageId 的多次状态推进要落在同一处

一条 A2A 消息会经历多次状态变化(发出 → 投递 → 回执),也可能因 QoS1 重投被重复送达。工作台的策略是:

  • 气泡幂等:以 messageId 为锚点,状态推进只做"原地更新正文",不新开气泡;
  • 流式去重:跨节点流事件批次带 per-conversation 单调 seq,接收端以 (conversationId|agentId) 为游标丢弃重复与乱序;
  • 一事实一处:气泡出现后,同 messageId 的台账行被移除(而不是两处同时存在)。

9.4 渲染侧护栏:结构化数据不得直接上屏

最后一道防线在渲染层。执行体返回的往往是结构化对象,一次 JSON.stringify 兜底就会把内部字段摊到用户面前:

代码语言:javascript
复制
// ✗ 曾经上过屏的形态(真实截图证据)
{"processInstId":"9431d25f-…","status":"PAUSED","statusLabel":"已暂停",…}

// ✓ 现口径:只取"人话字段",取不到就给中性句,绝不序列化兜底
keys = ['summary', 'statusLabel', 'message', 'text', 'content', …];
if (looksLikeJson(text)) return '';               // 前端闸门

10把 UI/UE 变成可回归的断言

设计原则如果只写在文档里,下一轮迭代就会退化。因此我们把这一轮的结论翻译成可执行的断言,用真实 Chrome 跑:

类别

断言

判定方式

文案合规

用户面不得出现英文枚举(TASK_RESPONSE/PUBLISHED…)与裸 ID(UUID、32hex、*-scene)

扫描消息流 + 协同行 + 门卡 + 会话列表的 innerText,正则命中即失败

结构化载荷不得直接上屏

检测 {"…":"…"} / "processInstId" 等形态

传输层信封不得渲染

按 messageId 前缀(fr_/gd_)与场景组反查 DOM,命中即失败

视觉一致性

Agent 协作气泡与"我发的消息"底色必须不同

对比 backgroundImage:协作气泡须为 none

协作气泡必须带左下尾巴与左侧强调边

校验 borderRadius 与 borderLeftWidth ≥ 3px

刷新前后气泡文案与发言人必须完全一致

实时/回读两次采样逐字比对

单一真源

前后端名称映射必须一致(能力名 / 场景名)

静态分析:解析后端常量与前端映射表交叉比对

同一 messageId 只呈现一次

统计气泡数与协同行数之和 == 去重后的消息数

这些断言在 CI 之外也直接用于本轮验收:修复前后各跑一次,用同一套脚本给出对照——

指标

修复前

修复后

中文/ID 合规扫描命中

裸字段名、UUID

0

会话内 JSON 形态气泡

8

0

同一事实的重复呈现

有(气泡 + 台账行并存)

0

刷新回读一致性

不一致(文案漂移)

一致

会话列表标题

截断的原始 JSON

中文工作项名

一条方法论

UI/UE 的验收标准应该是可采样的运行时事实,而不是"看起来还行"。getComputedStyle 能拿到颜色、圆角、宽度;DOM 能数出重复呈现;正则能扫出文案违规;刷新前后各采一次就能证明一致性。把设计决策翻译成这几类断言,UI 就获得了与后端接口同等的回归能力。

11结语与附录

回到最初的问题:为什么企业需要"A2A 聚合工作台"这样一个看起来只是"聊天框 + 列表"的界面?

因为协作的复杂度最终必须有人来承担——要么由用户承担(他在一堆协议 JSON 里自己找线索),要么由界面承担(把传输层噪声滤掉、把协作事实翻成人话、把该做的动作放到手边)。前者是"系统能跑",后者才是"系统可用"。

这一轮我们做的其实是同一件事的三个切面:

  • 减法:不该显示的一律不显示(中继信封、心跳、无正文通知);
  • 翻译:协议事实 → 人话(谁委托谁、结果如何、为什么失败、可信几级);
  • 对齐:无论执行体在本机还是远端、无论实时还是刷新后,界面叙事保持一致。

而这三点恰好都是可断言的。这可能是本文最想留下的一句话:UI/UE 不是画出来的,是约束出来的。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • A2A 聚合工作台:把协作协议翻译成人看得懂的界面
  • 目录
  • 1问题:协议可见 ≠ 用户可见
  • 2四条设计原则
  • 3信息架构:三栏 × 三层
  • 4视觉语言:让 Agent 一眼可辨
  • 4.1 三类消息的实测设计令牌
  • 4.2 发言人身份:三个正交信息
  • 5人工门:跨节点的人在环上
  • 6任务步骤与决策留痕
  • 7输入区:把"编排"变成可选项
  • 8双路径一致性:本地 vs 跨节点
  • 9支撑这套界面的前端架构
  • 9.1 文案单一真源:所有用户面文字只在一个文件里生成
  • 9.2 分道渲染:一个会话里可能有多个 Agent 同时说话
  • 9.3 幂等与去重:同一 messageId 的多次状态推进要落在同一处
  • 9.4 渲染侧护栏:结构化数据不得直接上屏
  • 10把 UI/UE 变成可回归的断言
  • 11结语与附录
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档