资讯动态

从Markdown到生成式UI:AI前端流式渲染的工程实践与优化

发布时间:2026/10/18 9:43:42 来源:尧图企业网站定制
聊到AI应用的前端体验绕不开一个细节那行字是一个字一个字蹦出来的。第一次做完这个效果我和团队小伙伴都挺激动用户反馈说“感觉像有生命一样”。但再往深走一步问题就来了——当内容不再是纯文本而是带着表格、代码块、甚至复杂结构化卡片的时候流式渲染就不是“打字机效果”那么简单了。这篇帖子我把自己从 Markdown 流式渲染一路折腾到生成式 UI 的完整过程整理出来包括方案取舍、解析思路、性能优化和踩坑实录希望给正在做 AI 前端渲染的朋友一点参考。1. 为什么会需要流式渲染1.1 等待一个完整回答是糟糕的交互先说一个反直觉的事实大模型生成完整回答再一次性返回其实技术实现更简单但产品体验会变得非常“死板”。早期给某跨平台系统做 AI 助手的时候后端接口就是普通 POST 请求模型吐完整个 Markdown 之后才把结果抛给前端渲染。那样做有一个很明显的尴尬——用户在页面上干等。有时候模型思考慢一些15 秒甚至更久才看到内容用户会下意识刷新页面或者直接怀疑服务挂了。后来对比过业内几个主流 AI 产品的交互形态发现“边说边显示”已经成了默认选项。不仅仅是“看起来高级”背后有真实的用户心理流式输出让用户感知到系统在运作降低不确定感也允许用户提前阅读前面部分整体等待时间主观上缩短了差不多一半。所以流式渲染不是加分项而是现代 AI 应用的交互底线。1.2 流式的技术基础SSE 与 token 流流式输出的技术路线当前最通用的是 SSE全称 Server-Sent Events。它跑在 HTTP 之上服务端可以向客户端持续推送文本数据浏览器原生支持EventSource。比起 WebSocketSSE 更轻量、自动重连、只做服务端到客户端的单向推送——这在 AI 对话场景里恰好够用。从大模型的角度看每次生成的最小单位是 token。一个 token 不一定是一个完整的字有时是半个词、几个字符由模型的 tokenizer 决定。也就是说前端必须处理“内容不完整”的状态并随时准备用新的数据增量更新界面。如果还要保持 Markdown 样式、结构化组件渲染就需要对这些“半成品内容”做额外的容错处理。提示流式渲染第一个要接受的事实是你拿到的永远是一段尚未完成的文本。前端不能等所有内容齐了再渲染而是要用最少量的计算把当前片段呈现出来同时不破坏用户已经看到的内容。2. Markdown 流式渲染的第一步2.1 为什么 Markdown 是默认方案绝大多数大模型默认输出就是 Markdown。它简单、轻量、人类可读也能表达标题、列表、代码块、表格等结构。早期方案里我们把模型返回的 Markdown 原文接到一个 Markdown 渲染器里一次性输出带格式的 HTML。流式改造后这个模型必须升级为“累加式解析”。Markdown 流式渲染的核心难点在于片段本身可能是语法不完整的。比如模型已经输出了| 姓名 | 年龄 |但分隔行| --- | --- |还没生成再比如代码块的三重反引号只出现了一个此时不应该立即渲染成普通文本。如果完全按“半截语法”渲染UI 会一闪一闪地被不断重新设置视觉效果极差。2.2 我在实践里的解析策略分块与标脏在模拟项目 X 里我最终采用的做法不是对全量文本做语法重分析而是把流式文本按行拆分建立“块级状态机”。每一步只解析新增行并用“标脏机制”更新受影响的块。具体来说核心步骤是这三个维护一个lines数组记录从流接口收到的全部行文本。每收到新的文本片段不做整段的 Markdown 解析只把它追加到最后一个“未闭合块”。把内容分成两类块完整块语法闭合和非完整块待定状态。只对完整块做渲染非完整块以原始文本或半渲染状态展示。举一个实际的例子模型正生成一个 JSON 代码块已经输出了前三行但还没输出结尾的三个反引号。此时“代码块未开始”语义已经触发了只是未闭合。如果强行整体渲染成正文用户会看到一行行类似{、name:的文字被当成普通段落展示这就是一种常见错乱。更好的做法是识别到第一个反引号后把从那里开始的所有行归入“待定代码块”用等宽字体、灰底背景做提示性渲染等闭合符出现后再正式激活代码块样式。2.3 表格与列表的流式“地震”表格是 Markdown 流式渲染里体验问题最突出的结构。原因在于表格的渲染依赖“表头行 分隔行 数据行”全部到位后才能确定列数和对齐方式。模型生成表格时通常会一口气输出多行但网络传输可能被拆成多次如果每收到一行就重算表格结构用户会看到表格列宽跳来跳去。我的处理经验是引入“最小稳定窗口”表头 分隔行至少要凑齐两行才把其所在区域标记为可渲染的表格。如果只来了表头行就先按普通文本或者“临时表格标题”展示。代码上可以用一个pendingTable状态去暂存一旦下一行确认是分隔行就触发完整表格段的渲染。列表的问题稍微轻一点但也容易踩坑嵌套列表的前缀空格数量是语义的一部分。模型如果先输出- 第一项下一行输出- 子项前端必须重新统计缩进级别。我的建议是列表块只确认闭合当出现非空且非列表前缀的新行在此之前把新行追加到当前列表项同时通过缩进对比动态调整子父级关系。3. 从 Markdown 走向生成式 UI3.1 Markdown 的边界固定模板表达不了交互意图做了半年 Markdown 流式渲染后我越来越意识到一个问题Markdown 是“内容描述格式”不是“交互描述格式”。它可以表达文字结构但表达不了按钮、进度条、折线图、表单输入框这类组件。AI 应用一旦超过“生成文档”的范畴进入“生成应用”阶段就需要一种更灵活的输出协议让模型能在回复中间插入交互组件。我第一次尝试生成式 UI是在给某 AI 报表助手做改版的时候。当时用户会让助手“给一个带趋势图的季度销售汇总”再配一个筛选器。如果只返回 Markdown图片和图表得靠前端写死逻辑去匹配完全没有扩展性。后来重新设计了输出协议让模型返回一段 JSON描述页面上应该渲染哪些组件每个组件的数据是什么。3.2 数据结构设计一个可增量解析的 JSON 格式生成式 UI 的本质是让模型的输出从“文本内容”变成“UI 描述结构”。在我们团队的设计里最小协议是这样的{ type: ai_page, children: [ { type: heading, level: 2, props: { text: 季度销售分析 } }, { type: markdown, props: { content: 这里是普通说明支持流式追加 } }, { type: chart, props: { chartType: line, data: {{stream_data}} } } ] }这里一个关键设计是markdown块保留了流式文本的入口而chart、form这类组件则用结构化数据承载。模型输出过程中markdown块的内容是持续追加的chart块则等data字段完整后再渲染。实际工程中我们并没有让模型直接生成一整个大 JSON。原因很简单大 JSON 在流式传输过程中极难做到增量解析。模型输出的 JSON 往往是一层层嵌套的一旦某个字符串中途被截断整个 JSON.parse 就会失败而等到全部生成完再解析又回到了“非流式”的老路中间交互组件无法及时呈现。3.3 增量解析 JSON三套方案的取舍针对“JSON 流式解析”这个问题我试验过三条路各有优劣字符串状态机逐字符解析自己写一个轻量的 JSON 扫描器跟踪当前处于哪个 key、哪个 value、哪个括号层级。优点是完全可控不依赖框架缺点是边界条件极多字符串里出现花括号、转义字符、Unicode都要正确处理维护成本高。输出分块协议约定模型先输出一个“组件布局骨架”再逐块输出每个组件的内容。比如先输出children[0]再输出children[1]每个块之间用特殊分隔符切开。前端收到完整块才渲染对应组件。这是目前我们的主力方案实现稳定也不用写复杂的扫描器。代价是需要对大模型做输出格式约束保证它不“违规乱插”。使用支持流式的解析库某些解析库能把 JSON.parse 改成流式的读取一个片段就解析一部分内存占用也更低。但我实测下来这类库通常体积偏大而且对模型输出的容忍度仍有限如果模型产生了格式轻微不合法的情况照样罢工。用到生产环境我会给它加一层恢复机制。3.4 组件渲染的白名单与安全边界生成式 UI 和 Markdown 渲染最大的不同是前端需要执行一份“UI 描述”并创建实体 DOM。如果让模型任意指定组件类型很容易出现恶意或者错误的设计。我们的做法是一张组件白名单只允许渲染团队预先封装好的组件包括标题、段落、列表、表格、图片、图表、按钮、表单、卡片、进度指示条。模型输出不在白名单里的type时渲染器会降级成一个兜底块按“未知组件文本展示”来处理避免整个应用崩溃。事实证明这一步极其重要。模型有时会输出一些奇怪的组件名例如pie_chart_3d或者custom_map白名单机制保证系统不会因为一个命名问题而白屏。4. 流式 UI 渲染的性能优化4.1 高频更新引起的界面卡顿流式渲染自带“高频更新”属性。如果每次收到网络数据块都触发一次 React 状态更新、重算整棵组件树页面必然卡顿。我第一次做性能测试时一个 5 秒生成的回答页面基本处于“打字”模式用户滚动一下都卡得不行。后来做优化先从更新频率入手。大模型输出令牌的速度大概在每秒几十到几百 token没必要每次都渲染。我们设置了一个“渲染时钟”每 50ms 检查一次是否有新内容如果有才把内容交给渲染线程。实测下来这个阈值在流畅度和实时性之间最平衡。4.2 虚拟列表只渲染看得见的内容变长后整个页面会变成一条很长的流式内容流。如果不做任何处理DOM 节点会越来越多内存和布局计算压力都会上升。我们的方案是把内容区域虚拟化只渲染视口附近的内容块视口之外的块替换成占位元素滚动时动态回收和创建。具体的做法是把整条内容流拆成一个个“渲染块”每个渲染块对应一个最小高度占位。滚动容器内维护一个可视区间的块引用超出区间范围的块从 DOM 中移除。这一招可以把长回复场景的 DOM 节点数量降到接近常量页面滚动手感也回来了。4.3 缓存与重放调试流式界面的好帮手流式渲染的调试有点特殊问题往往出现在“内容进行到一半”的中间状态而这个状态稍纵即逝。为了能稳定复现我为流式渲染器加了一个“录制重放”功能把每次流式请求收到的片段按时间戳保存下来之后可以回放这个过程。在排查“表格列数跳变”“代码块先被渲染成文本后又被修正”这类问题时回放机制非常有效。另一方面本地缓存也可以帮用户恢复场景刷新页面后把已经产生的会话内容重新用流式方式演示一遍而不是一次性静态展示。这听起来像炫技但确实把“AI 生成过程”本身变成了可回放的内容后续无论做分析还是做演示都方便很多。5. 方案选择什么时候用 Markdown什么时候升级生成式 UI5.1 四种典型场景的取舍结合我在不同项目里的实践我总结了一个粗略的选型参考。如果你的 AI 应用主要在做“信息输出”例如聊天问答、文档草稿、要点总结那么 Markdown 流式渲染足够而且成本低、稳定。如果应用要做“信息操作”比如生成一份待确认的订单表单、一个可筛选的图表报表、一组可点击的操作卡片那必须引入生成式 UI。我自己做过几个形态的对照场景推荐方案原因通用问答、知识总结Markdown 流式渲染简单稳定内容样式的表达基本足够数据分析、报表生成混合模式用 Markdown 显示结论用 chart 组件展示图表表单填写、步骤引导生成式 UI需要按钮、输入框、校验逻辑文本表达不了复杂工作台、多组件页面全量结构化协议需要多个组件协同内容本身仍是流式拼装实际做某 AI 报表工具时我用的就是混合模式。主体说明仍以 Markdown 流式输出但遇到“图表节点”时通过约定的标记切换成结构化组件。用户可以先看到文字分析逐渐展开紧接着图表以一个流畅的动画出现体验比单纯 Markdown 或纯 JSON UI 都要自然。5.2 用协议优先还是代码优先还有一个战略层面的选择到底应该是“让模型遵循协议”还是“让代码适配模型的随意输出”我的体会是协议优先更可控。做生成式 UI 时模型需要在输出里按约定好的组件树结构来生成。为了帮助模型稳定输出我们在提示词里不仅仅描述“你要输出 JSON”还会给一份缩略的组件 schema 示例和“禁止输出某些字段”的约束。实践结果表明如果模型本身支持结构化输出约束那么准确率能显著提高如果只是口头约定偶尔会跑偏但配合白名单和降级渲染机制也能兜底。6. 常见问题与排查技巧实录6.1 网络层背锅SSE 数据来了但界面没动有一段时间用户反馈 AI 回答像“卡住了”偶尔几十秒才蹦出一行字。后端明明说 SSE 正常推送前端也没报错。最后排查发现问题出在网关层的缓冲设置某个中间代理默认开启了缓冲把 SSE 片段攒在一起统一转发导致前端看到的输出节奏完全被破坏。排查这个问题的技巧很简单在浏览器开发者工具里看网络面板的 EventStream 标签逐条查看消息接收时间。如果多条消息的时间戳挤在同一秒大概率是代理缓冲导致。解决方式是在代理层关闭 SSE 路径的缓冲配置让数据真正“流”出去。6.2 Markdown 与 JSON 夹杂交错的混乱用混合模式并行渲染时模型偶尔会把 Markdown 内容和 JSON 内容交错输出比如 Markdown 段落里突然插进一段{type:chart}。在协议上我们要求它严格区分但模型还是可能“自由发挥”。后来我在渲染器里加了一个预处理在 Markdown 块渲染前先用正则检测可能存在的 JSON 片段如果命中就把这部分截取出来按结构化组件渲染剩余部分仍按 Markdown。这也算是一种容错设计虽然不优雅但确实避免了白屏。6.3 内容跳动带来的视觉眩晕流式渲染最常见的视觉问题就是“内容跳动”。用户读着第一行第二行进入时第一行的排版突然变了或者表格宽度变了看起来很不舒服。解决思路是给每个渲染块设定最小高度但更根本的是尽量让“已输出的内容不可变”。我维护了一个原则——一旦一个 Markdown 块被标记为“完整”就不再重新解析它的内容后续内容只会追加到新的块。这相当于把一段流式文本切分成多个独立的“只读区”已经展示给用户的文字绝不回溯修改跳动感自然大幅减少。7. 最后还想说的几点我个人在实际操作中的体会是流式渲染不是单纯的“打字机”而是一整套围绕“不完整内容”构建的工程体系包括块级解析、增量更新、协议设计、虚拟列表和回放调试。如果你正在做一个 AI 应用建议从一开始就把“流式”作为默认架构而不是留到后期再补。另外有一个小技巧给做生成式 UI 的朋友与其让模型直接输出一个复杂的 JSON schema不如在提示词里附上一个“极简示例”只给三层嵌套的小结构。模型对这种“形态示范”的跟随能力远比一份冗长的字段说明更好。我们实测中附上示例后输出合规率提升了不止一倍。这个方向还远没有到终点。流式渲染再往下走是让页面上的组件不仅能在生成过程中渐进出现还能在用户与页面交互后局部更新。想象一下你在 AI 生成的表单里改了一个字段AI 现场重新计算并只更新那一个数字的场景。那种体验也许就是 AI 应用的下一阶段。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑