比较边界,而不是品牌名称

关于 Agent 界面的讨论,常常把 A2UI、AG-UI 和某个 AI 框架运行时塞进同一张功能表里,然后选出一个赢家。这种框定掩盖了真正的工程决策。声明式的 UI 契约、事件协议和框架自有的渲染器,其实位于不同的边界上:

边界 它回答的问题 它不能提供
UI 契约 什么样的受限界面描述可以被渲染? 身份、授权、业务事实
事件协议 事件、状态与生命周期更新如何流动? 安全渲染或副作用权限
框架运行时 这个产品如何渲染并协调界面代码? 与每一个客户端或服务的互操作
动作服务 这个主体此刻能否对这个对象执行这个副作用? 一个由模型选定的策略

与这些生态相关的名称、消息类型、传输方式、SDK 和支持矩阵都会变化。先把某个规范或软件包版本固定下来,并在你自己的部署中测试它,然后再把某项能力当作「已经可用」。

json
{
  "ui_contract": "pinned-spec-or-schema-commit",
  "event_profile": "pinned-transport-profile",
  "framework_runtime": "package-and-revision",
  "renderer_catalog": "product-catalog-revision",
  "supported_clients": ["tested-client-and-version"],
  "action_api": "server-contract-revision",
  "checked_at": "recorded-time"
}

这份记录比一个市场排名更有用:它告诉运维者,当某个依赖发生变化时,必须重新测试哪些东西。

先从用户任务出发

只有当一个 Agent 界面能让某个用户任务更容易理解或完成时,它才是合理的。它不会仅仅因为模型「能描述组件」就变得合理。

用户任务 有用的界面 不安全的捷径
比较返回的选项 带来源和刷新时间的只读卡片 把生成的排序或价格当作权威事实
优化一次搜索 取值受限的固定筛选项 让模型选择任意查询字段
审阅一份草稿 产品自有的编辑器 + 明确的保存 从一条聊天回复直接写入记录
请求一个外部副作用 摘要、信息披露和确认 让一个按钮去执行模型选定的动作

解释性内容优先用文本;稳定、高影响的工作流优先用固定产品流程;只有当受限的自由度确实能改善体验时,才使用生成式契约。

三种实现风格

声明式 UI 契约

A2UI 式的做法传递的是一份关于组件和数据的描述,而不是可执行的浏览器或原生代码。它的价值来自一个产品自有的渲染器,把一个小而经过审阅的目录映射到本地组件上。

它可以在独立运营的多个 Agent 服务与客户端之间支撑起一条交换边界,但它不会让一个远程 Agent 变得可信:未知组件、富文本、URL、数据引用和动作,仍然需要严格的准入控制。

事件与状态协议

AG-UI 式的做法关注投递:进度事件、文本增量、生命周期迁移、用户输入、状态更新、重连和取消。它可以承载一份 UI 契约、一段文本,或应用自定义的载荷。它并不规定哪种界面可以安全渲染,也不授予接收方执行某个工具调用的权限。

应当把顺序、重复、重连行为、背压、重放和事件留存,都当作部署契约中显式的一部分。一条流式传输通道并不等于持久化的工作流状态。

框架自有的运行时

React 或其他框架运行时,可以把渲染代码、服务端数据访问和客户端交互都保持在同一个产品代码库里。当客户端和部署都处在同一个所有权边界之下时,这往往是最简单的选择。

但它也带来了框架和运行时上的耦合。不要仅仅因为模型能选择一个工具,就暴露任意的组件生成能力或服务端函数。把模型的输出约束在产品定义的意图之内,并把凭据、对象查找和执行都留在应用服务之后。

使用一张决策矩阵

基于关于你自己系统的证据来选择,而不是一个永久性的「最佳」标签:

问题 更适合 UI 契约的场景 更适合事件协议的场景 更适合产品运行时的场景
独立的客户端/服务 你需要一份共享、版本化的载荷 你需要生命周期上的互操作 一个产品拥有全部客户端
投递行为 载荷可以被简单地获取或更新 流式、续传和中断很重要 框架传输能满足实测需求
渲染 多个渲染器必须消费同一份目录 渲染仍由应用自行定义 一套经过审阅的组件系统就够了
升级所有权 结构兼容性被显式治理 事件兼容性被显式治理 一个团队能同步升级运行时与客户端
安全边界 载荷不可信且被严格校验 事件不可信且绑定到会话 服务端动作由产品掌控并授权

把一个产品运行时与一个小型的声明式目录组合起来,是合理的;只用一条事件流而不用声明式 UI,同样是合理的。除非某一层确实解决了一个真实的所有权或兼容性问题,否则不要把每一层都堆上去。

共同的安全模型

没有任何一种方案会把模型输出变成权威。一棵生成出来的组件树、一个事件名、一次 JSON Schema 校验通过的结果、一条提示词指令,或一段工具标注,都不能授予权限。

接收方必须:

  1. 认证调用方,并把界面绑定到当前会话;
  2. 校验契约版本、字节大小、组件数量、嵌套深度、更新频率和允许出现的属性;
  3. 转义文本,并约束图片、富内容、URL、回调和数据引用;
  4. 在服务端用租户与对象校验来解析数据;
  5. 只把一个动作映射到一个经过审阅的应用意图;
  6. 在执行时,重新授权这个精确的动作和当前的对象状态;
  7. 把确认和幂等键绑定到主体、租户、动作和规范化后的参数上;
  8. 记录脱敏后的策略与生命周期证据。
python
from dataclasses import dataclass

@dataclass(frozen=True)
class UiIntent:
    name: str
    action_token: str
    confirmation_id: str | None

def accept_intent(session, intent, tokens, inventory):
    if intent.name not in {"select_offer", "request_confirmation"}:
        return {"status": "rejected", "reason": "unknown_intent"}

    offer = tokens.resolve(intent.action_token, subject_id=session.subject_id)
    if offer is None or not inventory.is_available(offer.id):
        return {"status": "rejected", "reason": "stale_or_unavailable"}

    return {"status": "accepted", "offer_id": offer.public_id}

这是一个应用层策略片段,而不是任何协议或 SDK 的实现。任何有实质后果的写入,都需要一个独立、幂等的接口,在副作用发生之前立即重新校验用户、对象、参数、确认步骤和业务不变量。

传输与渲染都需要失败语义

一个吸引人的演示,往往会遗漏用户最容易注意到的那些情况:一次重复点击、一块过期的屏幕、一次在副作用之后发生的重连,或者在会话进行到一半时发生的渲染器升级。

在上线之前,先定义清楚下面这些:

关注点 需要记录的契约
事件投递 顺序、去重标识、重连游标、超时、背压
界面生命周期 创建、替换、过期、清理、不受支持客户端的兜底
数据新鲜度 来源、观测时间、刷新行为、过期数据的展示策略
动作 确认、取消、幂等、结果未知时的响应
兼容性 结构/事件版本协商、迁移夹具、回滚
隐私 脱敏、留存、删除传播、租户隔离

一次完成的渲染,并不能证明一次副作用已经完成。一条完整走完的事件流,也不能证明所有客户端都观察到了同一个状态。

无障碍与可信的呈现

生成式界面仍然是产品界面。组件目录必须规定语义化角色、标签、键盘导航、增量更新后的焦点、对比度、缩放、减少动效、本地化、复数处理,以及从右到左(RTL)布局。

同时保护用户免受界面层的提示注入和钓鱼攻击:

  • 把 Agent 生成的内容与产品自有的导航和安全控件区分开;
  • 对来自外部的声明,展示其来源和新鲜度;
  • 把凭据、付款和权限控件保留给固定组件;
  • 只通过一套经过审阅的策略去打开外部目标;
  • 对破坏性或对外可见的动作,要求用户做出清晰、可理解的确认。

无论载荷是经由声明式契约、事件协议还是框架运行时到达的,这些控制都同样重要。

测试你的选择

一次采用,应当以一份测试计划收尾,而不是一张架构图:

测试 要暴露出来的失败
契约模糊测试 未知属性、循环树、超大载荷、不安全 URL
事件重放 重复事件、重连后的缺口、乱序更新
授权 跨租户令牌、过期对象、模型提供的标识符
副作用恢复 执行后超时、重复确认、取消竞态
无障碍 仅用键盘的路径、屏幕阅读器更新、超长的本地化文本
升级 旧版客户端、不兼容的目录、渲染器回滚
隐私 遥测中出现敏感载荷、从已缓存界面中删除

在有代表性的任务上,度量用户完成率、澄清请求次数、拒绝率、恢复时长和无障碍失败。不要把一份通用的厂商对比,当作某个架构适合你的用户的证据。

A2UI、AG-UI 与框架运行时可以共存

一份 UI 契约可以通过一个事件协议来传递;一个产品运行时可以渲染一个严格的目录;一个远程 Agent 可以提议一个界面,而由宿主应用来掌控渲染器和动作服务。这些都是组合层面的选择,而不是「某一个生态会取代其他生态」的预测。

关于底层的 UI 契约边界,参阅 A2UI:构建安全的 Agent 驱动界面契约。关于服务间的委派,参阅 A2A 协议:Agent 间任务、信任与生产边界。关于工具与资源的边界,参阅 MCP 协议:生产环境的工具与资源边界

常见问题

事件流可以承载一份声明式 UI 载荷吗?

可以,前提是部署定义清楚了消息信封、结构版本、大小限制、会话绑定、校验行为和客户端兼容性。接收方仍然必须把载荷当作不可信输入,并且只渲染它已批准的目录。

声明式契约比框架运行时更安全吗?

它们应对的是不同的风险。一个严格的声明式目录,可以减少载荷边界上的任意代码执行;一个产品自有的运行时,可以安全地渲染经过审阅的组件。两者都需要服务端授权、数据过滤、URL 管控、可观测性和恢复行为。

团队现在应该标准化到某一种方案上吗?

先标准化你自己产品能够治理的那些契约:身份传播、动作授权、审计事件、无障碍、载荷限制和升级测试。只有在固定了某个外部协议或运行时的版本、并证明它与你所支持的客户端兼容之后,才去采用它。

应该记录哪些日志?

记录脱敏后的契约版本、界面与动作标识符、策略决策、生命周期类别、延迟和关联 ID。不要默认去存储原始提示词、UI 载荷、私密数据或隐藏的模型推理过程。

延伸阅读