
Product · UI/UX · Agent Infrastructure
当企业里出现第二个、第三个 Agent,"协作"就同时发生在三个层面:协议层在传消息,流程层在等回填,而人只看得见一块屏幕。本文复盘我们如何把 A2A(Agent-to-Agent)协作的协议事实,收敛成一套可读、可信、可操作的工作台界面——含真实截图与实测设计令牌。
日期 2026-09-28
载体 ooder ChatNext 工作台(8017 场景宿主 / 8018 独立智能体)
图片 真机截图,渲染期已脱敏
图片脱敏说明
本文截图均来自真实运行实例,在页面渲染阶段就地脱敏(非事后涂抹,避免遗漏与坐标错位):手机号、姓名、金额、长数字单据号、客户/机构名称(…有限公司 / …银行 / 集团 等)在截图前已被替换为掩码。产品与领域名(如「税务雷达」「财务智能体」「有成财务域」)是系统自身术语,予以保留。完整规则见文末附录。
第一版工作台在功能上是"正确"的:跨节点任务能派发、能回填、人工门能推进、SSE 能推流。但用真实浏览器做了一轮展示审计后,截图证据相当难看:
核心判断
协议正确性、协作可理解性、界面可操作性是三件独立的事。把传输层事实(消息到达)、协作层事实(谁委托了谁、结果如何)、用户层事实(我该知道什么、该做什么)混在一根时间轴上直接呈现,用户看到的就是"系统在自言自语"。工作台的第一职责是做减法:把传输层噪声挡在门外,把协作层事实翻成人话,把用户层动作放到伸手可及处。

图 1 修复后的工作台全景(跨节点会话)。同一块屏幕里并存四类对象:
人类消息「@税务雷达执行体 …」、Agent 协作「已委托 → 已回复」、人工门「上传报表」、助手消息「财务智能体」。它们共享时间轴,但视觉语言各不相同。
我们把上述问题抽象成四条可执行原则。它们不是审美偏好,而是能落到代码与自动化断言上的约束。
原则 | 含义 | 反面(审计抓到的真实形态) |
|---|---|---|
P1 一个事实只呈现一次 | 同一条 A2A 消息在消息流里只应有一处可视呈现(气泡优先,台账行让位) | 同 messageId 既有气泡又有台账行;中继批次另出 8 行 |
P2 发言人身份必须可辨 | 谁在说话(业务智能体 / 系统管道 / 我)、可信度几级,都要看得见 | A2A 气泡与"我的消息"同底色;跨节点来源无任何标记 |
P3 过程可折叠,结果须突出 | 传输 / 心跳 / 思考属于过程,收进折叠区或干脆不渲染;结论、待办、产物必须在视野内 | thinking 与 token 流把结论顶出屏幕;心跳刷屏 |
P4 失败必须显眼且可归因 | 投递失败 / 执行失败要有独立视觉态,并给出人话化的"为什么" | 成功与失败气泡同色,失败被"看起来很正常"掩盖 |
由此推导的硬规则
传输层信封不得进入用户面。跨节点流式中继批次(fr_*)、人工门决策回投信封(gd_*)、心跳、无正文通知——这些对象对用户没有任何语义,唯一正确的处理是"不渲染"。这条规则同时落在前端文案源与后端落库口,形成双保险。
工作台的组织方式遵循"横向按对象分栏、纵向按粒度分层"。横向三栏回答"有什么 / 在说什么 / 有谁参与";纵向三层回答"我要看多细"。

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

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

图 4 协同视图入口。「Agent 10 条 / 3 体 / 3 关系」用三个数字概括协作规模:消息条数、参与执行体数、关系边数。它把"协作"从抽象概念变成可点开的具体对象。
工作台是深色主题,可用的"视觉预算"很有限:不能再引入高饱和的新色块,否则屏幕会变成调色板。因此我们把区分度花在三个更克制的通道上——左侧强调边、尾巴方向、宽度收敛,再叠加头像与发言人行的身份信息。
下表是从运行实例的 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 强调边):已将任务委托给「税务雷达执行体」,等待执行…;下一条归属业务执行体(% 能力图标 + 紫色强调边 + 能力名 +
未验证
徽章):「税务雷达执行体」返回:已为你启动流程,当前状态:已暂停…。两条都带左下尾巴。
发言人行是"身份"的承载处,我们让它同时表达三件事,且互不遮挡:
信息 | 表达方式 | 设计意图 |
|---|---|---|
是谁 | 能力名(「税务雷达执行体」)而非 fin.tax | 业务人员不需要认识 agentId;技术面把原文放进 title |
哪一类 | 紫色 ✨ + 能力图标(%//…)vs 灰色 ⇄(委托路由) | 区分"业务执行体"与"系统管道",后者用 muted 强调边同族呈现 |
可信吗 | 未验证徽章(仅跨节点中继来源显示) | 把"身份为声称级"这个安全事实放进用户视野,而不是埋在日志里 |
// 色环按「能力段」取,而非 agentId 全串 —— 否则 fin.tax(场景宿主)
// 与 llm.tax(独立智能体) 会得到两种颜色,出现"同名不同色"
var colorKey = agentKeyOf(agentId); // 'tax' → 同一个色环索引
var trust = crossNode ? 'claimed' : ''; // 跨节点定向投递 ⇒ 声称级
图 6 本地持能路径的协作记录。同一个诉求,当"税务雷达执行体"就跑在本机时,不再产生跨节点消息;但用户仍需要知道"谁受理了",于是发一条记录级协作消息(不经网络、不触发执行):任务已由「税务雷达执行体」受理。
人工门(Human Gate)是这套工作台里唯一"必须打断用户"的组件,也是最容易设计失败的地方。它的难点不在样式,而在位置的不对称:人坐在编排节点的浏览器前,而流程卡在执行体节点上。
我们最终确定的交互契约是:

图 7 人工门卡特写(文件上传型)。五个信息区块自上而下:① 类型标签「需要确认」+ 步骤名「上传报表(可选)」;② 一句话说明(为什么要你做什么);③ 拖拽/点选区(含支持格式提示);④ 超时进度条与剩余秒数「291 秒后超时(需人工处理)」;⑤ 主按钮「上传并分析」/ 次按钮「取消」。注意:超时不是装饰——它是"无人处理会怎样"的承诺。
门的四个设计决策
门后面是"过程"。过程信息如果全量铺开,会把结论挤出屏幕;如果完全藏起来,用户又会在卡住时无从判断。我们的解法是折叠式步骤卡 + 常驻终态记录:

图 8 决策留痕与结果并列。上:灰色「系统」+ ✅「已确认 · 张** · 2026-09-28 15:29 · 步骤 意图确认」——不可折叠、不可删除的审计痕迹;中:「上传报表」是用户当时的答复(以人类消息形态呈现,保留原话);下:助手给出下一步需要准备的资料清单。三者构成"决策 → 答复 → 后续"的完整闭环。
另一侧的步骤卡(图 1 中部的「任务步骤 · 税务雷达 1/1 完成,等待人工处理」)承担"进度概览"职责:默认折叠成一行摘要,展开才是逐步骤明细。它的摘要行刻意包含当前动作要求("等待人工处理"),让用户在折叠态也能判断是否需要自己出手。

图 9 输入区工具行。左侧是模型与四档设计强度(快速 / 标准 / 深度 / 流程编排),中间是附件、图片等工具,右侧 1 · 税务雷达执行体 是"本次将被委派的执行体"预告。
这一行看似普通,实际解决了"多 Agent 工作台"的一个核心焦虑:用户不知道自己的话会被谁执行。因此我们把三件事做成显性:
这是最容易被忽略、也最影响信任的一条:同一个诉求,在不同部署形态下必须有一致的界面叙事。
我们的场景里,"税务雷达执行体"可能跑在本机(场景宿主节点,fin.tax),也可能跑在远端独立智能体进程(llm.tax)。前者是一次本地场景执行,后者是一次跨节点 A2A 委派。如果只在跨节点时产生协作记录,用户就会看到两种截然不同的界面:一边有"委托 → 回执",另一边结果凭空出现。

图 10 本地持能路径的完整叙事(8017 场景宿主)。顶部角标显示 Agent 1 条 / 2 体 / 1 关系(本地受理也算一条协作事实);中部依次是:助手的过程说明 →
Agent 协作
「任务已由「税务雷达执行体」受理。」→ 任务步骤卡 → 人工门卡(5 个分支选项 + 超时进度)。与跨节点路径相比,只是少了"网络投递"这一步的可见性,协作叙事的骨架完全相同。
一致性带来的工程约束(反过来说很有价值)
要做到"两种形态同一种叙事",就必须把协作事实当成一等数据来落库:本地受理也要写一条协作记录(消息类型 TASK_REQUEST + 状态 LOCAL_CONSUMED),并让刷新回读与实时推送走同一条渲染路径。否则会出现最尴尬的形态——"实时有受理提示,刷新后消失"(这正是我们在复测中抓到的一个真实回归)。
界面上的"干净"是有代价的。下面四条是这套 UI 能成立的技术前提——任何一条缺失,前面所有设计都会退化成"好看但会露馅"。
同一个事实在界面上有两处渲染(发言人气泡 + 协同台账行)时,只要各写一套文案,就必然漂移。我们把用户面的所有枚举、状态、发言人、错误原因收口到一个映射文件:
// 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 = ' 协同消息'。它的本意是"防止空白",实际效果是——所有被文案源刻意判定为"不该显示"的事件(中继批次、门决策信封、无正文通知)都被重新编出一行「协同消息已接收」。这说明兜底逻辑必须区分"我不知道"与"我知道它不该显示",否则兜底会亲手破坏上游的过滤决策。
流式输出必须知道"这段 token 属于谁"。渲染通道 ID 采用四段结构 agentId : processInstId : activityInstId : kind:
// 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 渲染独立的"发言人",而不是把所有人的话合并成"助手"。
一条 A2A 消息会经历多次状态变化(发出 → 投递 → 回执),也可能因 QoS1 重投被重复送达。工作台的策略是:
最后一道防线在渲染层。执行体返回的往往是结构化对象,一次 JSON.stringify 兜底就会把内部字段摊到用户面前:
// ✗ 曾经上过屏的形态(真实截图证据)
{"processInstId":"9431d25f-…","status":"PAUSED","statusLabel":"已暂停",…}
// ✓ 现口径:只取"人话字段",取不到就给中性句,绝不序列化兜底
keys = ['summary', 'statusLabel', 'message', 'text', 'content', …];
if (looksLikeJson(text)) return ''; // 前端闸门设计原则如果只写在文档里,下一轮迭代就会退化。因此我们把这一轮的结论翻译成可执行的断言,用真实 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 就获得了与后端接口同等的回归能力。
回到最初的问题:为什么企业需要"A2A 聚合工作台"这样一个看起来只是"聊天框 + 列表"的界面?
因为协作的复杂度最终必须有人来承担——要么由用户承担(他在一堆协议 JSON 里自己找线索),要么由界面承担(把传输层噪声滤掉、把协作事实翻成人话、把该做的动作放到手边)。前者是"系统能跑",后者才是"系统可用"。
这一轮我们做的其实是同一件事的三个切面:
而这三点恰好都是可断言的。这可能是本文最想留下的一句话:UI/UE 不是画出来的,是约束出来的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。