游乐游手机版
首页/前端开发/文章详情

如何让大语言模型输出可交互的用户界面

时间:2026-06-13 06:49
通过Markdown+Spec协议,LLM输出扁平JSON组件规格,经流式提取与渲染生成可交互界面。组件库vtu-cmpts支持状态驱动与事件回传,无需前端编码即可实现从文本到界面的高效转换,显著提升信息获取效率并简化开发流程。

从文本到界面:Generative UI 如何重塑人机交互体验

让我们设想一个常见的交互场景——用户向 LLM 提问“2026年有哪些值得关注的北欧风室内设计”,模型开始输出大段文字:明厅设计采用落地窗搭配浅色木地板,采光充足;厨房以白色橱柜和原木台面为主,强调功能性……用户逐行阅读,在脑海中自行拼凑画面。

然而,面对长篇文字仅靠脑补来构建画面,确实较为吃力。同样的信息,其实可以换一种更直观的呈现方式:

纯文本输出的信息密度虽然很高,但前提是读者需要具备提炼核心要点的能力,才能流畅阅读。更何况,文本本身存在固有局限——缺乏层次结构、没有视觉锚点、也无法实现交互。

上图展示了 Generative UI(GenUI)在纯文本输出中的融合效果。LLM 的输出不再是一段需要自行消化的文字,而是一个可以直接浏览、点击、交互的界面。用户从返回内容中获取信息的效率,存在量级上的差异。

但纯 HTML 也行不通。尽管 LLM 对 HTML 非常精通,在聊天模式下,工程上很难对产物进行有效约束。

一个重要的判断是:未来很长一段时间内,人机交互的形式既不会是纯 HTML,也不会是实时渲染,而是介于两者之间的一种中间态——既保留 Markdown 的简洁性和 LLM 的熟悉度,又能承载结构化组件的交互能力。

有人可能会想到 MDX。这确实是一种可行的选型,但在我们的场景中,最终选择了输出和解析都更简单的 Markdown + Spec 模式:用 Markdown 组织文本和推理过程,用代码块承载结构化的组件规格(Specification)。

Spec 比 HTML 语义更单一、结构更扁平,LLM 输出更稳定,前端解析也更简单。作为协议层,它恰好处于“足够表达组件语义”和“足够简单可控”之间的理想中间状态。

下面就以 vtu-cmpts 的 ImageGallery(画廊)组件为例,按数据流顺序,走完从 LLM 输出到可交互界面的完整旅程。

从流到组件:ImageGallery 的完整数据之旅

整个数据流分为五个层级:

LLM Stream → chunk-processor → Spec 提取 → VtuRenderer → 用户交互 → 新一轮对话

组件库架构设计

结构化的输出需要一套预定义的组件库。组件库用什么框架编写其实不重要,关键在于组件必须是状态驱动的自包含单元——每个组件通过 Props 接收数据,通过事件对外通信,不依赖外部的 context 或状态树。

Headless 模式组件库比较适合这个场景,但由于我们使用 Vue 技术栈,扫了一圈市场上没有合适的现成库,因此基于 assistant-ui 重新编写了一套 Vue 组件。

在 Vue 项目中,采用经典的桶模式组织组件:

index.ts   // 组件入口,连接组件与工程
index.vue  // 组件界面骨架,整合依赖与内部逻辑
meta.jsonc // 组件定义,兼顾人类阅读与机器理解
states/*.ts // 组件状态及其驱动的方法
config/*.ts // 组件所需的数据或配置

桶模式的优势在于结构扁平,LLM 可以一步到位理解组件全貌,无需先读入口文件,再追踪 import 链去翻阅其他模块。

index.ts 或目录下如果存在 spec.ts,即为组件的 Spec 定义。以 ImageGallery 为例,其 Props 定义如下:

export const ImageGalleryItemSchema = z.object({
  id: z.string().min(1),
  src: z.url(),
  // ...
});
export type ImageGalleryItem = z.infer;/* ... */export interface ImageGalleryProps {
  id: string;
  images: ImageGalleryItem[];
  onImageClick?: (imageId: string, image: ImageGalleryItem) => void;
  // ...
}

可以看到,该组件使用 Zod 作为唯一的 Spec 来源。type ImageGalleryItem = z.infer 从 Zod Schema 派生出 TypeScript 类型——一份定义,同时服务于编译时类型检查和运行时校验,是非常高效的胶水层。

关于这个组件库,它名为 vtu-cmpts,包含表格、画廊、图表等展示类组件,以及表单、审核卡片等交互组件,合计二十余种,基本覆盖了当前的使用场景。

类别组件
数据展示Article、DataTable、Chart、StatsDisplay、WeatherWidget
代码相关CodeBlock、CodeDiff、Terminal
社交/内容ContactCard、Citation、XPost
表单/输入ApprovalCard、OptionList、ParameterSlider、PreferencesPanel
流程展示QuestionFlow、Plan、ProgressTracker、OrderSummary、GeoMap

Spec 协议设计

LLM 不会直接输出组件代码,而是输出一种中间协议——Spec。在实际操作中,Spec 以 Markdown 代码块的形式嵌入对话:

2026年的销售数据和转化率如下:
```json
{
  "root": "main",
  "elements": {
    "main": { "type": "Stack", "props": { "direction": "row" }, "children": ["card-1", "card-2"] },
    "card-1": { "type": "StatCard", "props": { "title": "总销售额", "value": "120万" } },
    "card-2": { "type": "StatCard", "props": { "title": "转化率", "value": "3.2%" } }
  }
}
```

Spec 采用 Vercel json-render 的 Flat Map 协议。组件在 elements 对象中以扁平列表形式输出,而非嵌套结构。扁平 Spec 具备两个天然优势:结构简单,不会给 LLM 增加嵌套复杂度;数据易于增量更新,适合流式渲染。

chunk-processor:数据碎片处理器

数据流从 Markdown 开始处理。Markdown 解析器方面,我们选择了开源组件 markstream-vue,因为它提供了自定义标签与高级组件功能,同时具备高性能的节点缓存,能直接接管从 MD 到混合输出的渲染过程。

chunk-processor 是 SSE streaming 到交给 markstream-vue 中间的数据处理层,负责将 JSON Spec 提取出来并交给渲染引擎。具体步骤如下:

  1. 检测——扫描流式文本,找到 ```json spec 代码块
  2. 提取——使用 jsonrepair 修复不完整的 JSON(缺少闭合括号、字符串未写完等)
  3. 验证——检查是否符合 vtu-cmpts Spec 结构,即 { "root": ..., "elements": ... }
  4. 转换成文本模板如 ,后者通过自定义标签在 markstream-vue 中注册

LLM 生成到一半的内容可能是这样的:

{"root":"root","elements":{"root":{"type":"Col

括号未闭合,字符串未结束,直接 JSON.parse 必然报错。chunk-processor 在流式上下文中持续修复这些碎片,确保下游渲染器始终能获得合法的 JSON。

除了提取以外,chunk-processor 还通过中间件形式提供对 Spec 的增强功能,比如下一小节提到的 Spec 流式渲染能力。

流式渲染:边生成边展示

如果等 JSON 全部生成完再渲染,用户将面对几秒钟的空白,然后界面突然弹出。流式渲染要解决的核心问题是:如何在数据尚未完整时,就给出有意义的界面反馈。

目前一个稳定的方案是在提示词中引导 LLM 按固定顺序生成 Spec 属性,比如先输出 typeidtitle 等元信息,再输出数据密集型字段。这样即使 images 数组尚未到达,渲染器已经能画出组件骨架:

{
  "root": "main",
  "elements": {
    "main": {
      "type": "ImageGallery",
      "title": "2026北欧自然风景精选",
    },
  }
}

此时 ImageGallery 以空状态或 loading 状态呈现,用户知道内容正在生成。随着流继续,当第一个完整的 ImageItem 到达时,触发节点激活,图片开始逐张渲染:

{
  "root": "main",
  "elements": {
    "main": {
      "type": "ImageGallery",
      "title": "2026北欧自然风景精选",
      "images": [
        {
          "id": "img-1",
          "src": "https://picsum.photos/seed/nordic1/800/600",
          "title": "明厅设计",
          "caption": "落地窗 + 浅色木地板"
        },
      ]
    },
  }
}

因此,第一个要介绍的 chunk-processor 增强插件就是 data-trigger,它支持为不同组件设置不同的数据触发节点。

举个例子,Chart 允许按 Series 的数据项触发渲染;文档(Markdown inside markdown)允许按行刷新等等。手动实现触发节点可以达到极为精确的控制效果。

当然,回退到简单策略也可行——例如每固定间隔 100ms 对当前 Spec 做一次补全,并触发组件重渲染。

最终,流结束后,ImageGallery 的 images 数组完整,界面从骨架蜕变为可交互的完整画廊。

用户交互:从点击到闭环

Generative UI 如果只能看不能点,那只是花哨的 Markdown。

传统界面通过事件绑定执行特定函数,但 GenUI 的理想情况是事件不交给 LLM 去配置——LLM 并不理解整个交互上下文。我们把交互按复杂度分层:纯文本、可播放媒体、按钮等单点交互、表单等有状态容器、弹窗、长时任务。在展示类场景中,交互复杂度通常控制在表单及以下层级。

单点交互的处理比较直接:给 LLM 提供上下文即可。比如用户点击 ImageGallery 的某张图片,系统捕获到「用户点击了图片:明厅设计」,将其拼接到下一次对话的上下文中,LLM 基于这个信息生成新的回复。

点击 DataTable 表格行的“详情”按钮或 OptionList 的多选同理,见以下图片:

表单类组件带有状态,稍微复杂一些,需要渲染器具备注入能力。

注入可以是状态(Vue 的响应式值,可与 localStorage 绑定),也可以是事件。VTU 组件暴露特定事件,因此可以注入特定处理函数,也可以通过 DOM 监听统一捕获组件内部的点击、切换等交互。

只要事件能被捕获,操作空间就很大了。我们可以决定拼接什么上下文、是否立即发送进入下一轮对话、还是等待用户补充更多信息。如果输出包含多个 Spec,多个组件的交互上下文可以同时被收集——比如用户勾选了 DataTable 的几行,再从 OptionList 中选择了"查看价格",最终的上下文会同时携带表格行选择信息和按钮点击信息。

这些被捕获并拼接成带上下文的数据,被添加到下一次用户提问中。LLM 根据这个上下文继续生成新的回复,问答场景形成闭环。

容错设计:应对不稳定的输出

最后简单提一下容错机制。LLM 输出不稳定是常态,即使 Prompt 写得很详细,输出也可能遇到不合格的状态——缺字段、类型错误、甚至混入自然语言。

组件层、渲染层、交互层都需要相应的容错和恢复机制。组件层通过 Zod Schema 做运行时校验,不合法的 Spec 会在渲染之前被拦截。渲染层对不完整的 JSON 做降级或增强处理。交互层则确保事件捕获和上下文拼接的鲁棒性,不会因为某个组件的异常而影响整个对话流程。

未尽之路:挑战与展望

这篇文章涵盖了 GenUI 落地的六个核心问题:组件库设计、协议设计、流式碎片提取、中间态渲染、交互回传、异常容错。

每一个环节都还有深入探索的空间,但将它们串联起来,已经能够勾勒出一幅从 LLM 文本流到可交互界面的全景图。

时间成本也非常低,整个 Demo 从 Agent 到组件库和前端,仅花费约 40 人日——放在以前纯手工开发时代,很难想象需要写多少代码才能把各个环节串联起来。

不过在实践中,我们也遇到了一些尚未找到解决思路的问题。

一是聊天页的虚拟滚动。每条消息都包含工具调用、思考过程和最终回答等不同聊天块。这些块高度差异极大,精确计算高度比较困难。但开不开启虚拟滚动,却直接影响长对话的性能表现。如何处理长对话是一个亟待解决的问题。

二是自定义组件的可能性。结论是可行的,但需要一个足够强壮的 Renderer 和规范层。如果做过低代码平台,应该能理解这里的挑战。

站在更宏观的视角看,LLM 的输出形态从文本向界面演进是必然趋势。当 AI 的回答不再是一段需要自行提炼的文字,而是一个可以直接操作的界面时,人与 AI 的协作模式将发生根本变化。未来有机会将继续分享相关内容。

来源:https://juejin.cn/post/7646408175776120832
上一篇vxe-gantt甘特图在Nuxt.js中的集成与使用全过程详解教程 下一篇uni-app小程序样式隔离实践指南与核心原理分析
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
如何在JavaScript中实现基于旋转视野的FOV射线绘制详解
前端开发 · 2026-07-01

如何在JavaScript中实现基于旋转视野的FOV射线绘制详解

如果用一句话概括核心,那就是:在 RayCasting 游戏开发中,绘制动态视野边界线(FOV)最可靠的方式是在逻辑层通过数学公式将坐标“算”出来,而不是依赖 Canvas 绘图上下文的旋转操作。 在实现类似 Doom 风格的 RayCasting 游戏时,动态视野(Field of View, F

TypeScript后端数据正确映射为前端接口类型的方法
前端开发 · 2026-07-01

TypeScript后端数据正确映射为前端接口类型的方法

在后端数据与前端类型之间来回转换,几乎是每位 TypeScript 开发者都无法回避的常态。后端返回的 car_brand、reg_number,和前端接口中定义的 brand、govtNumber,命名风格常常对不上号。此时,如果为了省事直接用 as 类型断言“强行”指认类型,那就踩进了常见的陷阱

动态HTML表格按层级条件合并单元格的JavaScript实现
前端开发 · 2026-07-01

动态HTML表格按层级条件合并单元格的JavaScript实现

本文详细讲解一种递归式 JavaScript 合并单元格方法,用于按列优先级(如前3列)智能合并表格行:仅当前一列已合并的前提下,才允许后续列合并相同值,从而精准实现多级分组与层级表格合并效果。 在动态生成的 HTML 表格中,按业务逻辑合并重复行是常见需求。然而,简单地对单列分别遍历合并——例如先

Next.js 13+重定向后滚动失效解决方案
前端开发 · 2026-07-01

Next.js 13+重定向后滚动失效解决方案

在 Next js App Router 的日常开发中,有一个令人颇为困扰的异常现象——当服务端执行 `redirect()` 跳转后,目标页面竟然无法正常滚动。没错,页面已经渲染完成,内容也完整显示,但垂直滚动条仿佛凭空消失。这个问题在 Next js 13 5 4 版本中尤为突出。 先给出结论:

WebGL图像加载延迟的纹理初始化时立即显示方法
前端开发 · 2026-07-01

WebGL图像加载延迟的纹理初始化时立即显示方法

本文详细介绍如何利用 Promise 与 async await 重构 WebGL 纹理加载流程,彻底解决首次渲染显示蓝色占位色、需要手动交互才能刷新的问题,实现文件导入后四张纹理平面即时正确渲染。 实际上,这个坑在 WebGL 开发中相当常见——纹理异步加载的小陷阱,说起来不大,但第一次遇到确实令