AI 流式传输工程指南:模型吐出半截 JSON,前端如何边收边渲染?

本文是通俗实战版。保留全部工程精华、性能实测对比与跨框架(原生 JS / Vue 3 / React)开箱即用代码,8 分钟即可读完并直接落地到项目中。

做普通 AI 聊天时,模型吐出的是纯文本或 Markdown,前端收到一个字就往屏幕上追加一个字(打字机效果)。

但现在越来越多的 AI 功能要求模型直接返回 JSON 结构化数据,让前端渲染成交互组件:

模型生成一段完整的 JSON 往往需要几百个 Token、耗时 5~15 秒。如果傻等到最后一个 } 才调用 JSON.parse(),用户就得对着白屏干等十几秒——流式传输也就失去了意义。

要在模型只吐出“半截 JSON”时就实时渲染 UI,全链路其实只需要闯过三关:

很多刚接触流式开发的同学会问:“我在写 SSE 流式接收时,不是已经用 buffer 处理过半截 JSON 了吗?”

其实在流式结构化输出里,存在外层信封和内层业务数据两层完全不同的 JSON:

浏览器原生的 JSON.parse 是个“全有或全无”的严格解析器——只要末尾少一个引号或括号,它立刻抛 SyntaxError,绝不会把前面已经解析好的字段还给你:

所以在整个生成过程中,除了最后一刻,直接调 JSON.parse 100% 会失败。我们需要一个**“尽力而为”的补全器**。

生产环境里我们直接调用成熟小库(如 partial-json),但花 2 分钟看懂它的原理,你就能明白为什么有些字段会提前出现、有些字段会被暂时丢弃。

模型可能在任意字符处停下,补全器的工作其实就是**“去烂尾、补引号、补括号”**三步:

计算机是怎么知道最后该补 ]} 还是 }]} 的?答案是一个简单的栈(Stack):

下面这 35 行极简代码就能跑通核心流程(帮你吃透原理的演示版):

能解析出半截对象只是第一步。如果直接把半成品丢给页面渲染,往往会遇到组件白屏报错、数字疯狂闪烁、长 JSON 越滚越卡三大问题。

流式传输刚开始时,解析出来的可能是 {},下一秒是 { title: "降噪" },数组 specs 还是 undefined。如果在模板里直接写 data.specs.map(...),第一帧就会报错崩溃。

标准做法:写一个纯函数 toCardView,把“任何字段都可能缺”的半成品,转换成结构固定的安全对象:

流式卡片最影响观感的就是画面乱跳,记住这四条实战经验:

在控制台观察一帧一帧的真实产出,数据是这样“长”出来的:

大模型吐 Token 极快,10 毫秒内可能连发四五个 chunk。如果每收到一个 chunk 就立刻全量补全 + JSON.parse + 刷新 UI,当 JSON 达到几十 KB(例如长表格)时,页面会越来越卡。

在一份 107 KB(1500 行表格,约 2.7 万个 chunk)的真实基准测试中,不同策略的耗时差距高达 60 倍:

所以在前端落地上,千万不要在 reader.read() 循环里直接调 parse 和更新状态,而是加一道节流阀:

为了让代码在任何项目里都能直接复制使用,我们把逻辑拆成两层:

先安装轻量库:npm i partial-json

💡 为什么这里默认用了 setTimeout(..., 40) 而不是固定死 requestAnimationFrame? 在原生 DOM 里用 rAF(约 16ms 一次)很轻快;但在 React / Vue 等框架中,16ms 触发一次响应式更新意味着每秒要走 60 次组件重渲染(Diff)。设为 40ms ~ 50ms 节流(每秒更新 20~25 次),肉眼看打字机依然丝滑,同时能把 React / Vue 的渲染负担降低 60% 以上。

来源:稀土掘金 原文