多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AI 流式输出不完整 JSON 怎么解析?partial-json + 插件化架构实现丝滑渲染(React 实战)

AI 流式输出不完整 JSON 怎么解析?partial-json + 插件化架构实现丝滑渲染(React 实战) 在上一篇文章中,我们成功让 AI 画出了地图 —— 这是一个激动人心的开始!但当你把这套方案推向生产环境时,一系列隐藏的工程挑战开始浮现…你是否遇到过这些问题:每新增一个功能,就得去修改主组件的渲染函数?AI 偶尔会幻觉出错误数据,导致前端直接白屏?在 AI 生成数据的过程中,用户只能盯着 Loading,感受不到任何反馈?今天我们来解决这些痛点,从Demo 级别升级到生产级别! 回顾与反思先看看第一篇文章中的实现方式:// 第一篇中的硬编码逻辑片段{msg.mapData(MapDisplay points{msg.mapData.points}/)}这种方式虽然快速跑通了流程,但在实际业务中存在明显的瓶颈:第一代架构的问题扩展性极差渲染逻辑充斥着大量的 if/else 每新增功能都要改主逻辑,代码耦合度高数据校验缺失AI 会幻觉出错误数据 之前的代码中无法感知数据是否正确用户体验生硬必须等待 AI 完整生成整个 JSON 后,组件才会弹出 用户感受不到即时反馈的丝滑❌ 问题一:扩展性极差(耦合度高)问题描述:渲染逻辑充斥着大量的if/else// 每增加一个功能就得改这里{msg.mapDataMapDisplay/}{msg.weatherDataWeatherCard/}{msg.stockDataStockChart/}{msg.ticketDataTicketBooking/}// ... 无限膨胀影响:违反了开闭原则 —— 对扩展开放,对修改关闭代码变得难以维护,改一个功能可能影响其他功能团队协作困难,容易产生代码冲突❌ 问题二:数据校验缺失(脆弱性)问题描述:AI 会幻觉出错误数据// AI 可能生成这样的数据{points:[{name:故宫,position:{latitude:999,longitude:-999}}// ← 把故宫画到了海里!]}影响:前端组件轻则渲染异常(地图显示空白)重则直接白屏崩溃(解析错误导致整个应用挂掉)用户体验极差,会怀疑你的产品稳定性❌ 问题三:用户体验生硬(交互断层)问题描述:必须等待 AI 完整生成整个 JSON 后,组件才会弹出用户输入: 帮我规划去西湖的路线 ↓ AI 开始生成... ↓ 【用户体验】盯着 Loading,等待 3-5 秒 ↓ 【突然】地图组件弹出来!影响:用户感受不到即时反馈的丝滑以为是应用卡死了,可能直接关闭页面无法在生成过程中展示进度,降低用户信心️ 架构升维:Lobe Chat为了解决扩展性问题,我们模仿Lobe Chat等成熟产品,将 AI 工具与 UI 组件彻底解耦。硬编码 vs 插件化维度硬编码方案插件化方案新增功能需要修改主组件的渲染函数只需在注册表中注册新插件代码耦合高,所有逻辑写在一起低,插件独立开发维护难度难,牵一发而动全身易,插件互不影响扩展性差,需要修改核心代码强,热插拔,无需修改主逻辑核心思想:不要在runSmartAgent里写业务判断构建一个插拔式的插件注册中心:// plugins/index.tsxconstAIPluginRegistry{render_map_itinerary:{component:MapDisplay,// 对应的 UI 组件validate:(args)args.points.every(pp.position.latitude0),// 数据校验skeleton:()MapSkeleton/,// 加载态占位组件},// 新增功能只需在此处注册,无需改动 runSmartAgent 主逻辑!get_weather:{component:WeatherCard,validate:(args)args.city.length0,skeleton:()WeatherSkeleton/,},// ... 无限扩展} 插件系统的优势维度硬编码方案插件化方案扩展性❌ 每新增功能都要改主逻辑✅ 只需注册插件维护性❌ 代码耦合,难以维护✅ 完全解耦协作性❌ 容易产生代码冲突✅ 独立开发,互不影响可测试性❌ 测试困难,牵一发而动全身✅ 每个插件独立测试 插件注册表的数据结构interfaceAIPluginTany{component:React.ComponentTypeT// 对应的 React 组件validate?:(args:T){// 可选的数据校验函数success:booleanerror?:string}skeleton?:React.ComponentType// 可选的加载态占位组件}typeAIPluginRegistryRecordstring,AIPlugin️ 改造地图组件:增强容错能力现在让我们改造MapDisplay组件,让它具备业务自愈能力:// plugins/MapDisplay.tsximport{z}fromzod// 第一步:用 Zod 定义 SchemaexportconstMapItinerarySchemaz.object({summary:z.string(),transportation:z.enum([walking,driving,transit]),points:z.array(z.object({name:z.string(),position:z.object({latitude:z.number(),longitude:z.number()}),}),),})// TypeScript 类型推导exporttype IMapItineraryz.infertypeofMapItinerarySchema// ️ 第二步:改造地图组件,支持 Partial JSON 和错误处理exportconstMap:React.FCIMapItinerary{isPartial?:boolean}({points,isPartial}){// 业务自愈:过滤掉非法坐标constvalidPointspoints.filter(ptypeofp?.position?.latitudenumbertypeofp?.position?.longitudenumberp.position.latitude-90p.position.latitude90p.position.longitude-180p.position.longitude180)constpositionsvalidPoints.map(p[p?.position?.latitude,p?.position?.longitude]as[number,number])// 如果没有有效坐标,显示加载态if(positions.length0){return(divdiv classNameh-[200px] flex items-center justify-center bg-gray-100 rounded-lg正在获取坐标数据.../div{isPartial(p classNametext-xs text-blue-500 mt-2AI正在努力计算位置.../p)}/div);}// ✅ 有有效坐标,正常渲染return(divMapContainer center{positions[0]}zoom{13}{/* 地图内容 */}/MapContainer{isPartial(p classNametext-xs text-blue-500AI正在规划剩余路径.../p)}/div)} 业务自愈的三重保护类型检查:确保坐标是number类型范围检查:确保纬度在 [-90, 90],经度在 [-180, 180]空值处理:过滤掉null或undefined这样即使 AI 生成了错误数据,组件也不会崩溃!⚡ 交互进化:破碎 JSON 的流式解析如何解决地图数据渲染过程中的等待问题?核心思想:当 AI 刚吐出{points: [{name: 故宫时,我们就让地图开始打桩!流式输出的碎片化 JSON第1包: {delta: {tool_calls: [{function: {arguments: {\summary\: \西湖}}}]}} 第2包: {delta: {tool_calls: [{function: {arguments: 一日游\, \points\: [{}]}}}} 第3包: {delta: {tool_calls: [{function: {arguments: \name\: \断桥\}]}}} 第4包: {delta: {tool_calls: [{function: {arguments: \position\: {\lat}}]}} ...解决方案:使用partial-json库// utils/json.tsimport{parse,Allow}frompartial-json/** * 安全解析不完整的 JSON 字符串 * param str 可能不完整的 JSON 字符串 * returns 解析结果,失败返回 null */exportconstsafePartialParseTany(str:string):T|null{try{// 尝试闭合未完成的 JSON 字符串并解析returnparse(str,Allow.ALL)asT}catch(e){returnnull}} 在runSmartAgent中集成 Partial JSON 逻辑// 核心:新增的 Partial JSON 处理逻辑letactivePluginName// 当前激活的插件名称letpartialArgsRaw// 累积的工具参数字符串constlastArgsRefuseRef()// 上一次解析的参数(用于去重)forawait(constchunkofstream){gatheredgathered?(gathered.concat(chunk)asAIMessageChunk):chunk// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━// A. 处理文本流(打字机效果)// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━if(chunk.content){fullContentchunk.contentupdateLastMessage({content:fullContent})}// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━// B. 处理工具调用参数流 (Partial JSON Logic)// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━consttoolCallChunkchunk.tool_call_chunks?.[0]// 1. 记录当前激活的插件名称(通常只在第一个 chunk 中)if(toolCallChunk?.name){activePluginNametoolCallChunk.name}// 2. 累积工具参数字符串if(toolCallChunk?.args){partialArgsRawtoolCallChunk.args// 3. 尝试解析不完整的 JSONconstpartialArgssafePartialParse(partialArgsRaw)// 4. 只有当解析结果发生变化时才更新 UI(避免重复渲染)if(partialArgsJSON.stringify(partialArgs)!lastArgsRef.current){lastArgsRef.currentJSON.stringify(partialArgs)// 5. 从注册表中查找对应的插件constpluginAIPluginRegistry[activePluginName]if(plugin){// 6. 实时更新 UI,显示 Partial 状态updateLastMessage({activePlugin:{Component:plugin.component,args:partialArgs},isPartial:true// ← 标记为 Partial 状态})}}}} UI 层:动态渲染插件组件现在我们需要修改 UI 渲染逻辑,让它支持动态加载插件组件:// 旧版本:硬编码条件渲染{msg.mapData(MapDisplay points{msg.mapData.points}/)}// 新版本:动态插件渲染{msg.activePlugin(div style{{marginTop:10px}}msg.activePlugin.Component{...msg.activePlugin.args}isPartial{msg.isPartial}//div)} 逻辑闭环:自愈型 Agentic Loop前端不应只是被动接收者。如果模型返回的数据有误,我们要利用Tool Calling 的闭环特性,将错误信息隐式发回给 AI,让它自我修正! 自愈机制工作原理↓ AI 收到错误 → 自动修正 → 重新调用工具// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━// 工具执行 自愈逻辑// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━consttoolCallsgathered?.tool_calls||[]if(gathered)currentMessages.push(gathered)if(toolCalls.length0){consttoolMessages[]for(constcalloftoolCalls){constpluginAIPluginRegistry[activePluginName]if(!plugin)continue// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━// 第一步:业务校验自愈// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━if(plugin.validate){const{success,error}plugin.validate(call.args)if(!success){// 校验失败:返回错误信息给 AI,让它自我修正toolMessages.push(newToolMessage({tool_call_id:call.id!,content:Error:${error}。请修正数据后重新调用。}))continue// 跳过后续步骤}}// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━// 第二步:更新 UI(确保拿到最终稳定的数据)// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━updateLastMessage({activePlugin:{Component:plugin.component,args:call.args},isPartial:false// ← 关闭 isPartial 标记})// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━// 第三步:返回成功消息给 AI// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━toolMessages.push(newToolMessage({tool_call_id:call.id!,content:Success: 组件已展示}))}// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━// 第四步:将所有 ToolMessage 返回给 AI,继续对话// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━currentMessages.push(...toolMessages)} 自愈机制演示场景:AI 生成了错误坐标用户: 规划北京路线 ↓ AI 调用工具: { points: [{ name: 故宫, position: { lat: 999, lng: -999 } }] } ↓ 前端校验: 纬度 999 超出范围 [-90, 90] ❌ ↓ 返回错误: Error: 坐标超出范围,请修正。纬度应在 -90 到 90 之间。 ↓ AI 收到错误,自我修正 ↓ AI 重新调用: { points: [{ name: 故宫, position: { lat: 39.9, lng: 116.4 } }] } ↓ 前端校验: 通过 ✅ ↓ 渲染地图组件 ↓ 返回成功: Success: 组件已展示 ↓ AI 生成最终回复: 已为您规划好北京路线,请查看地图! 完整架构对比:旧 vs 新 关键技术点总结插件化注册表- 解决扩展性差问题AIPluginRegistry { ... }业务自愈- 解决数据校验缺失问题validate: (args) { ... }Partial JSON- 解决流式渲染断层问题safePartialParse(partialArgsRaw)动态组件加载- 解决硬编码条件判断问题msg.activePlugin.Component /自愈型 Loop- 解决错误无法修正问题ToolMessage返回错误信息 常见问题Q1: Partial JSON 解析会失败吗?A:partial-json库非常健壮,即使 JSON 严重不完整,也会返回部分解析结果。只有完全无法解析时才返回null。Q2: 自愈机制会无限循环吗?A: 不会。你可以设置最大重试次数:letretryCount0constMAX_RETRY3if(!success){if(retryCountMAX_RETRY){// 放弃,返回友好提示updateLastMessage({content:抱歉,无法生成有效数据,请稍后重试。})return}retryCount// 返回错误给 AI}Q3: 如何调试 Partial JSON 解析?A: 添加日志:if(toolCallChunk?.args){partialArgsRawtoolCallChunk.args console.log(Partial JSON:,partialArgsRaw)// ← 调试日志constpartialArgssafePartialParse(partialArgsRaw)console.log(Parsed:,partialArgs)// ← 解析结果} 推荐资源partial-json 文档Lobe Chat 架构设计插件化架构模式React 动态组件加载 总结从第一篇的能画组件,到这一篇的丝滑渲染与健壮架构,我们完成了一次从Demo 到工程化的飞跃!✅ 核心成就插件化系统—— 彻底解耦业务逻辑,支持无限扩展Partial JSON 解析—— 实现丝滑的流式渲染自愈型 Loop—— 自动修正 AI 错误,提升系统健壮性现在才算真正推开了AI 原生交互(AI-Native UI)的大门!AI 不再是一个只会打字的对话框,它是一个能根据思考流实时构建动态界面的指挥官。
返回列表