AI图鉴

AI图鉴首页 › AI 资讯 › 模型吐出半截JSON,前端如何边收边渲染?2026年AI应用必须闯过的工程关

模型吐出半截JSON,前端如何边收边渲染?2026年AI应用必须闯过的工程关

来源:掘金 · 发布时间:10-26 · 由 AI图鉴每小时自动聚合

一、先分清:你处理的是两层完全不同的JSON 很多刚接触流式结构化输出的开发者会混淆一件事:“我在SSE里不是已经用buffer处理过半截数据了吗?” 你处理的是外层信封,不是内层业务JSON。 外层

你做的AI功能,还在等JSON完全吐完才渲染吗?

模型生成一段完整的结构化JSON往往需要几百个Token、耗时5到15秒。如果你在reader.read()循环里傻等到最后一个}才调用JSON.parse(),用户就得对着空白卡片干等十几秒——流式传输的意义被你自己亲手抹掉了。

但“边收边渲染”不是加一行partial-json就能搞定的。它有三个具体的工程关卡:半截JSON怎么补、补出来怎么不闪烁、补出来的数据怎么不被误用。

一、先分清:你处理的是两层完全不同的JSON

很多刚接触流式结构化输出的开发者会混淆一件事:“我在SSE里不是已经用buffer处理过半截数据了吗?”

你处理的是外层信封,不是内层业务JSON。

外层是SSE传输信封,一行一条,收到换行符\n就完整了,几毫秒的事:

data: {"choices":[{"delta":{"content":"{\"title\":"}}]}

data: {"choices":[{"delta":{"content":"\"降噪耳"}}]}

内层是把所有delta.content拼起来之后的业务JSON,模型可能停在任何一个字符上:

第1刻:{"title":

第2刻:{"title":"降噪耳

第3刻:{"title":"降噪耳机"}

外层用buffer按\n切行、JSON.parse一行一行解析,是对的。但内层不能等——你必须在模型只吐出半截JSON的时候,就把它“补”成一个合法对象,然后渲染。

二、半截JSON怎么“补”?核心是一个栈

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

所以你需要一个“尽力而为”的补全器。它的工作可以拆成三步:去烂尾、补引号、补括号。

去烂尾:削掉悬空的尾巴

模型可能停在各种尴尬的位置。补全器需要先判断当前截断的状态,然后做对应的“清理”:

补括号:用一个栈记住欠了什么

怎么知道最后该补]还是}]?答案是一个简单的栈:

从头扫描字符,维护两个状态:

- 是否在字符串内:遇到未转义的"就切换状态。字符串里的{、[不参与括号计数。

- 括号栈:在字符串外部,每遇到{就往栈里压一个},遇到[就压一个];遇到闭合的}或]就弹栈。

需要渲染时,先把末尾悬空的逗号、冒号、半截Key削掉;如果停在字符串值中间就补一个";最后把栈里剩下的括号倒序拼到末尾。

下面这段35行的极简代码就是核心原理,生产环境直接用partial-json,但看懂这35行你就知道为什么有些字段会提前出现、有些字段会被暂时丢弃:

function miniCompleteJson(partialStr) {

let inString = false;

let escaped = false;

const closers = [];

for (const ch of partialStr) {

if (inString) {

if (escaped) escaped = false;

else if (ch === '\\') escaped = true;

else if (ch === '"') inString = false;

continue;

}

if (ch === '"') inString = true;

else if (ch === '{') closers.push('}');

else if (ch === '[') closers.push(']');

else if (ch === '}' || ch === ']') closers.pop();

}

let fixed = partialStr;

if (inString) {

if (escaped) fixed = fixed.slice(0, -1);

fixed += '"';

}

fixed = fixed.replace(/,\s*"[^"]*"?\s*:?\s*$/, '').replace(/[,:\s]+$/, '');

return fixed + closers.reverse().join('');

}

三、补出来之后,怎么让页面“不闪、不抖、不卡”?

能解析出半截对象只是第一步。如果直接把半成品丢给UI,你会遇到三个体验问题:组件白屏报错、数字疯狂闪烁、长JSON解析卡死主线程。

问题一:半截数字闪烁

price字段从{"price":变成{"price":1再变成{"price":12再变成{"price":123,如果每一帧都渲染,用户会看到价格疯狂跳动。

解法:在补全器配置中排除半截数字。partial-json的Allow位掩码可以只允许字符串、对象、数组处于半截状态,数字必须等到完整才输出。

问题二:每来一个chunk就setState,主线程被吃满

模型可能吐出几万个chunk。实测数据很直观:

解析次数从27000次降到500多次,从“卡死”变成“丝滑”。

节流策略按场景选:

- 动画敏感型(普通卡片) :用requestAnimationFrame对齐屏幕刷新率,60Hz下最多16ms解析渲染一次

- 重组件/虚拟DOM框架(React/Vue大表格) :用50ms到80ms的定时器节流,每秒只更新12到20次,人眼看起来依然连贯,框架重渲染开销再降70%

问题三:组件在字段缺失时崩溃

半截数据里items可能还不存在,summary可能是空字符串。UI层必须对每个字段做降级默认值,而不是直接访问data.items.map()。

一个可用的模式是写一个toCardView(partialObj)的适配函数,在每个字段缺失或类型不对时返回一个安全的占位值,UI只消费适配后的结果。

四、开箱即用:纯JS核心,原生/Vue/React通用

把逻辑拆成两层:底层是零框架依赖的纯JS流消费函数,上层是不到10行的UI接入代码。

// partialStream.js —— 原生、Vue、React、小程序通用

import { parse, Allow } from 'partial-json';

function extractJsonBody(raw) {

const fenceMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*)/i);

if (fenceMatch) return fenceMatch[1].replace(/\s*```\s*$/, '');

const firstBracket = raw.search(/[{[]/);

return firstBracket === -1 ? '' : raw.slice(firstBracket);

}

export async function consumePartialJson(body, onUpdate, throttleMs = 40) {

const reader = body.pipeThrough(new TextDecoderStream()).getReader();

let rawText = '';

let timerId = null;

const flush = () => {

timerId = null;

const jsonCandidate = extractJsonBody(rawText);

if (!jsonCandidate.trim()) return;

try {

const partialObj = parse(jsonCandidate, Allow.STR | Allow.OBJ | Allow.ARR);

onUpdate({ data: partialObj, done: false });

} catch { /* 字符太少,等下一拍 */ }

};

while (true) {

const { done, value } = await reader.read();

if (done) break;

rawText += value;

if (timerId === null) timerId = setTimeout(flush, throttleMs);

}

if (timerId !== null) clearTimeout(timerId);

const finalJson = JSON.parse(extractJsonBody(rawText)); // 最终完整校验

onUpdate({ data: finalJson, done: true });

}

React接入:

const [state, setState] = useState({ data: {}, done: false });

async function generate() {

const res = await fetch('/api/product-card', { method: 'POST' });

await consumePartialJson(res.body, setState);

}

Vue 3接入同理,onUpdate直接传给一个ref的赋值函数即可。

五、工具怎么选?一张表对号入座

不用纠结重复造轮子,按项目情况直接选:

六、三条生产安全底线:半截数据“长得太像真的”

这是全文最容易被忽略、但后果最严重的一部分。补全器吐出来的对象看起来和完整数据几乎没有区别,但里面可能少了一半字段。

底线一:半截数据只做展示,绝不提交

网络中断、用户点“停止生成”、模型触发最大长度截断——这些情况下补全器依然会给你一个合法的JSON对象。写数据库、提交表单、触发下一步接口,必须以done === true且通过原生JSON.parse或zod.safeParse完整校验为唯一标准。

底线二:Agent工具调用参数,完整前绝不执行

模型正在生成一个删除目录的工具调用,半截参数{"path": "/data补全后是合法的{"path": "/data"},但模型原本要写的是/data/tmp/cache。流式阶段解析出的参数只能用于UI预览,真正执行工具必须等流结束并校验通过。

底线三:结构化不等于安全

如果卡片里有link、avatar等URL字段,渲染前必须校验协议是否为https://,防止模型被诱导输出javascript:alert(1)伪协议。普通文本字段坚持走文本插值(textContent、{{ title }}、{title}),不要直接塞进innerHTML或v-html。

写在最后

2026年,AI应用的前端已经不再只是“聊天框”了。模型正在返回结构化数据——商品卡片、流式表格、表单填充、Agent工具调用参数。而流式结构化输出有一个逃不掉的工程现实:在最后一个}到达之前,你手里永远只有半截JSON。

把这半截JSON变成可渲染的UI,需要三件事:一个能去烂尾、补括号的补全器;一道按帧或按毫秒的节流阀;一套“半截数据只展示不提交”的安全纪律。

流式传输的意义不是“快点传完”,而是“让用户更早看到有用的东西”。 如果你的渲染还在等JSON闭合,那流式传输对你来说,只是把等待从服务端搬到了客户端。

参考链接:

本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。

相关 AI 工具

相关免费羊毛

同源资讯

阅读原文(juejin.cn)

标题、摘要与正文均来自该信源的公开页面,版权归原作者所有;本站自动聚合与翻译,仅作出处跳转。

← 返回 AI 资讯列表AI工具图鉴首页

返回 AI图鉴首页 · 提交工具 · 商务合作 · 关于本站 · 591 款 AI 工具分类导航 · 每日 AI 热门项目与动态