claude 最近更新了交互是 UI,可以在聊天框流式输出可交互的页面 claude:

同时看到一个叫 Generative-UI-MCP 的项目,作者的想法很直接:用 MCP 协议把 Claude 能生成可交互 UI 这套东西复刻出来。
这个项目本身不复杂,但它把一件平时很难说清楚的事拆开了:Claude 这种交互式 UI,真正新在哪里;它为什么不像“AI 帮你写了一段前端代码”那么简单;以及它背后最先要解决的,到底是渲染问题,还是协议问题。
我后来又对着一段真实的 SSE 流式输出日志,把整个过程重新拆了一遍。两边对起来之后,会更清楚:Claude 风格的流式 UI,本质上不是“模型输出 HTML”,而是“模型持续输出一个宿主能够可靠消费的 UI 协议”。
这篇文章想讲的,就是这件事。
它是一个可持续交互UI 页面
Claude 这波交互式 UI / Artifacts,真正新的地方不是“AI 生成了一个页面”,那个早就有人做过了。新的地方在于:生成出来的东西还能继续用,还能继续跟模型交互,还能挂工具、更新状态。
这跟“AI 写了段前端代码,你复制出去跑”是完全不同的东西。
以前那种方式,模型是起点,生成完就结束了。现在这种方式,模型是这个 UI 会话里的持续参与者。用户在界面上的操作可以再回到模型,模型返回的新内容可以局部更新界面,来回转。
这个闭环大概长这样:
这个循环跑得起来,才是真正的“交互式”。带几个按钮的 HTML 不叫交互式,事件能回流才叫。
一段真实的流式输出,能把这件事看得很清楚
如果看一段真实的 SSE 流式输出,会发现模型并不是一次性吐出一个完整页面,而是在流里持续输出不同类型的内容块,前端边收边拼,拼完再交给对应工具渲染。
拆开之后,大概是五步。
第一步:先加载 UI 生成规范
模型一开始并没有直接生成 widget,而是先调用了一个类似 visualize:read_me 的工具,输入参数非常短:
这一步很关键。它说明模型在真正开始“画界面”之前,先去拿一份运行时 UI 规范。也就是说,生成不是裸奔的,模型先要知道这次该遵守什么规则。
第二步:工具返回一整套设计系统和流式输出约束
这份返回内容里,有几段特别关键。
先是模块说明:
然后是角色定义:
接着是它最关键的几条约束:
还有专门面向流式渲染的顺序规则:
如果只看表面,这像一份设计规范;但从运行角度看,它其实更像一份“让模型输出稳定 UI 消息”的生成协议。
它在做几件事:
规定什么该出现在工具里,什么该出现在自然语言里
规定代码要按什么顺序流式吐出来
规定哪些视觉效果会破坏流式体验,所以禁止使用
规定组件必须适配宿主环境,比如 CSS 变量、深色模式、受控脚本能力
也就是说,这不是“给模型一些审美建议”,而是在给模型划一条窄轨道。
真正的关键,不是 HTML,而是模型输出的结构
随后模型开始调用另一个工具,比如 visualize:show_widget。这一段流最容易让人误解,因为它看起来像一堆零碎碎片:
单看这种片段,几乎没有可读性。但它们其实不是乱码,而是工具调用参数的一部分。宿主会把同一个 block 下不断到来的 partial_json 拼起来,最后还原成一个完整 JSON。
像这次,重组之后大概是这样:
这里面每个字段都很有意思。
title 是这次 widget 的标识。loading_messages 不是装饰,而是在把“等待”转成可感知的生成过程。i_have_seen_read_me 则像一个状态确认,表明模型是在已经读过规范的前提下生成的。
而真正的界面,都放进了 widget_code。
这一步揭示了流式 UI 的核心事实:模型不是直接输出最终页面,而是在输出一个宿主可以消费的 UI 消息。
为什么 Generative-UI-MCP 这个项目看起来很小,却很有价值
我原本以为要复刻 Claude 交互式 UI,需要很多东西:自定义渲染器、状态管理、完整前端运行时、组件库、DSL。
结果 Generative-UI-MCP 的核心极简得有点反直觉:
一个
load_ui_guidelines工具,按需给模型加载 UI 生成规范一个 system prompt 资源,把最基础的输出约束提前注入
没有大而全的组件系统,也没有复杂 DSL。
这个取舍反而很能说明问题:复刻 Claude 交互式 UI,最先要解决的不是“怎么渲染”,而是“怎么让模型输出一个宿主能稳定消费的结构”。渲染反而是后面的事。
所以这件事首先是协议问题,不是渲染问题
Claude 的交互式 UI 有一个很强的体验特征:widget 的出现是稳定、可预期的。不会这次变成代码块,下次又变成自然语言夹 HTML。
要做到这件事,不能靠模型“自觉”,只能靠协议。
Generative-UI-MCP 暴露出来的,正是这层东西:
widget 要用专用 fence 包裹
fence 里必须是结构化 JSON
widget_code字段里装 HTML 或 SVG解释文本必须写在 widget block 外面
多个 widget 要拆成多个 block
输出顺序得适合流式渲染
这些约束堆起来,本质上已经非常接近一个轻量的 UI 消息协议。
宿主做的事情,本质上就是在这些消息之间路由。
为什么规范一定要按需加载,而不是一次性全塞进 prompt
复刻项目把 UI 规范拆成了几个模块:interactive、chart、mockup、diagram、art,需要什么再加载什么。
这不只是为了省 token,更关键的是:不同 UI 类型的约束本来就不一样。
图表有图表的规则
表单有表单的规则
mockup 有 mockup 的规则
diagram 有 diagram 的规则
art 又是另一套生成方式
如果全塞进一个大 prompt,模型会被很多无关约束污染。按需加载的价值在于:只有在真正要生成某类 UI 时,模型才拿到那一类规则,输出才更稳定。
这和前面真实流里先传:
其实是同一个思路。
流式的难点,从来不是“更快”,而是“边流边分帧”
很多人对流式的理解停留在“更快显示”。但从真实输出和复刻项目都能看出来,宿主真正要解决的是 parser。
宿主不能只是把 token 一个个打印出来。它必须知道:
当前是普通文本,还是 widget block
当前是在输出 block 起始,还是中间片段
JSON 有没有完整闭合
什么时候可以直接显示文本
什么时候该进入收集模式
什么时候该把完整
widget_code交给渲染器
整个过程大概是这样:
Claude 那种“widget 自然浮现”的感觉,技术上并不是魔法,而是 parser 在做边流边分帧。
为什么连 <defs>、style 顺序、阴影这些细节都要管
第一次看到这类规范,很容易觉得它管得太细:
SVG 里
<defs>要先于图形HTML 里
style在前、script在后尽量避免渐变、阴影、模糊
但这些规则不是在管审美,而是在管用户看到的每一帧是否合法。
原因很简单:
<defs>还没到,图形先出来,marker 和 clipPath 会先错后正style太晚到,用户会先看到裸 UI,再看到样式突然补齐渐变、模糊、阴影在流式 patch 过程中更容易出现跨帧不一致
Claude 输出 widget 很少出现明显抖动,不只是因为模型更强,也因为这套约束把中间态的不稳定性压下去了。
从一个真实 widget 看,这套方法到底偏向什么样的前端
在那段真实输出里,模型最终生成的是一个“25 个常用 UI 线条图标”的交互面板。它按类别展示图标,点击可以高亮,并在底部给出反馈。
从生成出来的 widget_code 可以看出几个很鲜明的取舍。
第一,布局非常简单,核心是稳定的 grid,而不是复杂的响应式技巧。
第二,样式极轻,全部基于宿主给的 CSS 变量,不写死颜色,天然适配深色模式。
第三,图标直接内联 SVG,不依赖图片资源,这样既容易流式输出,也容易在 hover 和 active 态切换颜色。
第四,JS 很短,只做本地交互,不做复杂状态管理,不请求网络,不引入框架。
这说明这类流式 UI 更像“会话中的即时交互壳”,不是完整前端应用。复杂逻辑交给模型,局部互动留在前端。
它很适合:
图标面板
对比卡片
轻量筛选器
小型图表
交互式解释器
内嵌 mockup
但不太适合:
超复杂业务表单
大型多页应用
重状态后台系统
强实时协作编辑器
因为它的优势是即时生成、即时嵌入、即时互动,不是长期运行的大型应用壳。
真实流里最后还有一个很重要的信号:文本和 UI 必须分工
在工具把 widget 渲染完之后,系统又返回了一句提示:
这句提示的价值很大。它明确告诉模型:已经渲染出来的东西,不要再重复讲一遍。
随后模型补上的自然语言也很克制,只做三件事:
概括这个 widget 是什么
告诉用户如何操作
提示用户下一步还能让模型做什么
这和前面 readme 里的那句:
正好闭环。
也就是说,Claude 风格的流式 UI,不只是“会渲染 widget”,它还在管理文本和视觉之间的职责边界。
Generative-UI-MCP 看不到的部分,反而是产品化最难的部分
老实说,看完这个复刻项目,会更清楚原版系统有哪些东西不是只靠协议就能补齐的。
1. 沙箱
模型生成的 HTML/JS 不能直接裸跑。必须有 iframe 隔离、白名单 CDN、脚本能力限制、资源权限边界。
否则模型只要生成一段恶意脚本,宿主就会出问题。
2. action 协议
用户点击之后发生什么,不能靠模型随便写 onclick 并自由决定逻辑。成熟设计更像是宿主先定义一套统一 action schema,比如:
filter_changedsubmit_formrequest_refreshselect_item
widget 只发动作,宿主决定本地处理、调工具,还是再问模型。
3. 增量 patch
Claude 在多轮对话里更新 widget,很多时候不是整块重生成,而是局部更新。这件事要求宿主维护状态,也要求模型知道什么时候该返回 patch,什么时候该返回完整替换。
demo 和产品级体验之间差得最多的地方,大概就在这里。
真正值得记住的一句话
看 Generative-UI-MCP 最大的收获,不是学到某个新技巧,而是更清楚地意识到:
做交互式 UI,这件事从来不是先解决渲染,而是先解决协议。
协议稳定了,才有后面的这些东西:
流式 parser
widget 渲染
沙箱执行
事件回流
工具挂载
增量更新
Claude 把这条链路基本跑通了,所以它用起来不像 demo。Generative-UI-MCP 把这条链路最前面的那段逻辑开源出来了,所以这件事第一次变得足够可理解、可讨论、可拆解。
回头再看那些看似零碎的流式片段,尤其是不断出现的 input_json_delta、widget_code、tool_use,就不会再觉得它们只是噪音。它们其实正是这整套生成式 UI 协议在运行时留下的痕迹。
附录:这次流里真实出现的原始提示词
下面这部分不是整理后的模板,而是从真实流式输出里还原出的原始提示词和规范文本。