[控场AI]
· 9 分钟阅读· 4,822 字

AI生成UI的安全防线:用TypeScript构建组件校验门

AI生成UI的安全防线:用TypeScript构建组件校验门

模型生成可交互UI已成现实,开发者必须在渲染前建立组件白名单校验门,而非直接信任模型的"发明"。

随着OpenAI Intelligent UI、Claude Haiku 5.5等产品让模型具备直接生成可交互界面的能力,一个新的安全命题浮现:模型吐出的按钮、表单和图表,可能指向从未被授权的地址,甚至携带恶意脚本。本文通过一个纯TypeScript的最小示例,阐述了应对这一风险的核心架构:在渲染前设置一道"组件校验门",以封闭的白名单目录(七个组件类型、受限属性集、action白名单)对模型生成的UI树进行解析、校验,至多进行一次修复,然后接受或拒绝——绝不发明新组件类型。整套流程用pass@1、pass@repair、reject rate三个指标量化,并与发布阈值挂钩形成可重复的决策机制。作者强调,厂商提供的智能UI能力无法替代开发者自己的渲染门,涉及资金或数据的action仍须引入人工环节。

当模型开始自己"发明"界面

你的模型返回了一个表单。它带着提交按钮,而按钮指向的 action,是你从未批准过的地址。这不是科幻场景,而是"智能UI"(Intelligent UI)走向主流后,开发者必须面对的现实。

据原文梳理,多家厂商在同一时间窗口推进了相同方向的能力。OpenAI 发布了面向全体用户的 GPT-6 与 Intelligent UI,响应内容可以包含图形、可点击按钮、表单、图表以及交互式体验——官方构建了一套原生可流式组件库,外加一个在模型生成界面时就同步处理的编译器,由模型自主决定布局。Anthropic 同期推出 Claude Haiku 5.5,主打高频、成本敏感的分类与路由任务。Google Cloud 则谈到 agent 直接把成品交付到文档和收件箱流程中。

不同产品,同一方向:厂商正在教模型吐出可交互的 UI。这很强大,但如果你原封不动地渲染模型发明的任何东西,它同样危险。

原文给出的核心命题值得记住:模型发明的组件只是一个"提案",而一个把允许挂载内容列入白名单的"门"(gate),才是产品。

Allowlist, validate, repair once, then render or reject

Intelligent UI 的技术背景

Intelligent UI 并非单一产品,而是大语言模型能力向「生成可交互界面」延伸的统称。传统上,模型输出纯文本或结构化 JSON,由前端代码将其转译成界面元素;而 Intelligent UI 的核心转变在于:模型直接输出一棵描述界面结构的组件树(通常是类 JSX 或自定义 DSL),渲染器拿到后可以直接挂载成按钮、表单、图表等可交互元素。这带来了表达力的跃升——模型可以根据上下文动态决定用下拉菜单还是按钮组——但也把安全边界从「文本内容是否合规」推进到了「界面行为是否被授权」。一个精心构造的提示词,完全可能诱导模型生成一个看起来合法、却把表单数据提交到攻击者服务器的 UI 树,而用户在视觉上无从分辨。这正是本文所关注的核心安全隐患。

这个"组件校验门"要解决什么

原文作者用纯 TypeScript 搭了一个最小可运行示例,一条命令即可跑通:

npx tsx gate.ts

没有 API key,没有真实模型,只有 TypeScript。所谓"模型"其实是一张固定的 fixture 表,通过率和发布阈值都是示例输入。真正的价值在于那道"门"的逻辑——它在渲染前,对一棵 UI 树按组件类型、属性和 action 策略进行校验。

需要强调的是,这与"路由决策的 schema gate"是两回事:后者校验 JSON 结构,而这里校验的是一棵 UI 树,在挂载前检查它的组件类型、props 与行为策略。

整个门做四件事:解析树 → 对照目录与 action 策略校验 → 若无效则修复一次(本地剥离未知 props,或把问题反馈给模型)→ 决定接受或拒绝,绝不发明新的组件类型。

目录即契约:七个组件,封闭属性

示例定义了七个白名单组件:Text、Button、Input、Form、Chart、Card 和 Stack。每个组件都有一份封闭的属性清单。按钮和表单只能指向 action 白名单内的目标;action 中的原始 JavaScript 被禁止;超过四层深度的树直接拒绝;图表必须携带数值序列。

const ACTION_ALLOWLIST = new Set([
  "https://app.example.com/confirm",
  "https://app.example.com/cancel",
  "https://app.example.com/submit-feedback",
  "app://navigate/home",
  "app://navigate/settings",
]);

const MAX_DEPTH = 4;

作者点出关键:OpenAI 的 Intelligent UI 固然提供了原生组件库和编译器,但你的产品仍然需要那份"你愿意挂载什么"的清单。目录模糊,渲染器就会跟着模糊。

校验逻辑本身不依赖 React 或 iframe 沙箱,只做这份目录所需的检查:未知组件直接报错并停止下探,必填属性缺失、属性不在白名单、原始 JS、action 越界、series 非数值等逐项记录为 issue。作者的一句话很到位:没有拒绝路径的校验,不过是一次更漂亮的崩溃。

白名单(Allowlist)与黑名单(Blocklist)的设计哲学差异

「目录即契约」采用的是白名单思路,而非常见的黑名单过滤。黑名单方式试图列举所有危险情形并加以屏蔽,但模型的输出空间几乎无限,攻击者总能找到未被列入黑名单的变体(如用 onclick 替代 javascript:)。白名单方式则相反:只声明允许存在的组件类型和属性,任何未出现在清单中的内容一律拒绝,把「证明合规」的举证责任转移给模型输出本身。这种封闭假设(closed-world assumption)在安全工程中有着悠久传统,也是 Content Security Policy(CSP)、防火墙 ACL 等机制的共同基础。代价是灵活性:每次需要新增组件类型,都必须显式地扩展目录并完成审查,而不能依赖「只要不明显有害就放行」的宽松策略。文中反复强调「绝不发明新组件类型」,本质上就是在贯彻这一封闭世界假设。

模型自由发明时会出现的失败形态

示例用八个 fixture 覆盖了模型放飞时的典型 UI 畸形:目录外的组件、表单提交到未列出的地址、想运行脚本的按钮、缺标题的图表、把树撑爆的嵌套 Stack、把字符串伪装成序列的图表,以及看起来像事件处理器的 props。

其中最关键的是"未知组件"这一例。一个把 MagicWidget 修复成 MagicWidgetV2 的循环,依然是拒绝对象。规则很硬:类型不在目录里,就不要挂载。

一次修复,然后决断

修复流程被刻意设计得很短:解析 → 校验 → 若有问题,做一次受约束的修复 → 再校验 → 停止。

function gate(fixture: Fixture): Outcome {
  const firstParsed = parseJson(mockComplete(fixture, "first"));
  if (!firstParsed.ok) {
    return { status: "reject", pass: "first", issues: [...] };
  }
  const firstIssues = validateTree(firstParsed.value);
  if (firstIssues.length === 0) {
    return { status: "accept", pass: "first", value: firstParsed.value };
  }
  return repairOnce(fixture, firstParsed.value);
}

对于未知属性,harness 直接在本地剥离,而不是让模型去发明 CSS;对于越界 action 和缺标题,fixture 模拟一次模型重试;对于 MagicWidget,重试仍然无效,门直接拒绝。

这里也呼应了多厂商协同的现实:在真实技术栈中,mockComplete 会替换成你的 provider 调用,一个 Haiku 级别的廉价快速模型可以守在"这棵树是否被允许?"的路径上,而更大的模型负责起草 UI。无论如何,目录要留在你自己的 harness 里。作者的收束句同样精炼:一次修复是策略,无限修复是死循环。

指标与发布阈值

门最后输出三个数字:pass@1(首次就通过校验的比例)、pass@repair(一次修复后可挂载的比例)、reject rate(应当回退到文本而非发明 UI 的比例)。

pass@1       0.13  (1/8)
pass@repair  0.88  (7/8)
reject rate  0.13  (1/8)

gate: SHIP  (pass@repair >= 0.75 required)

示例中的发布阈值是 0.75,作者明确提醒:阈值要从真实流量里选,而不是从博客里抄。 八个 fixture 里只有一个首次通过,六个经一次修复后变为有效,剩下一个始终未知——门选择拒绝它,而不是去发明一个渲染器。这正是整件事的要义:接受目录内有效的 UI,拒绝其余,并把两者都计数。

pass@k 指标的来源与含义

pass@1 和 pass@repair 的命名来自代码生成领域的标准评估指标 pass@k,最早由 OpenAI 在 HumanEval 基准测试中系统化使用:给模型 k 次生成机会,只要其中有一次通过单元测试,就算「通过」。在本文的语境中,这一思路被迁移到 UI 校验场景:pass@1 衡量模型一次生成就产出合规 UI 的能力,pass@repair 则衡量在给予一次修复机会后的最终合规率。这种分层指标的好处在于,它能把「模型原始质量」和「harness 修复能力」拆开度量,便于判断到底是模型需要提示词优化,还是修复逻辑需要加强。将指标与硬性发布阈值(如 pass@repair >= 0.75)挂钩,使发布决策从主观判断变为可重复验证的流程,是 MLOps 实践向 UI 生成场景的自然延伸。

边界与现实局限

原文难得地花了大段篇幅讲"哪里会失效",这比方案本身更有参考价值。

厂商的 Intelligent UI 不是你的门。 OpenAI 的组件库和编译器服务于 ChatGPT 的聊天体验,但你的产品依然掌握着"要挂载哪些组件、这些组件能触发哪些 action、何时回退到文本"的决定权。

七个组件不是设计系统。 生产环境需要带版本的注册表、可访问性和视觉 QA,但模式不变:封闭类型、封闭属性、封闭 action、一次修复、拒绝。

修复可能制造"看似有效的谎言"。 把错误反馈给模型,它可能发明一个工单里从未出现过的 confirm URL。目录有效 ≠ 语义正确。如果 action 涉及资金或数据删除,就必须给门配上人工环节。

深度上限和序列检查都是启发式。 MAX_DEPTH = 4 是示例,"series 必须是数值"也是。真正的检查矩阵应该从生产故障里长出来。

更大的命题

原文最后把智能 UI 定义为一份契约——介于概率性采样器(模型)与确定性渲染器(你的产品)之间的契约。目录提供类型,校验器提供证据,修复提供一次廉价的二次机会,拒绝路径提供停止点,指标提供发布决策。

当模型越来越擅长"发明"界面,开发者的护城河不再是能不能生成 UI,而是能否清楚地展示:什么被允许、什么被拦截、以及为什么。你的模型刚刚发明了一个表单——让它靠本事去赢得渲染权。

#智能UI#TypeScript#组件校验#AI生成UI#GPT-6#Intelligent UI#action白名单#AI安全
分享:

相关推荐