部分 JSON 的增量解析:Function Calling 流式输出的前端执行策略
部分 JSON 的增量解析Function Calling 流式输出的前端执行策略一、流式 Function Calling 的首字节延迟为什么不能等完整 JSON大模型 Function Calling 的 arguments 字段是流式返回的。模型一边生成一边吐 Token前端收到的可能先是{city: 上几十毫秒后才补全为{city: 上海, days: 3}。如果前端等完整 JSON 再解析首字节延迟会被放大几百毫秒用户感觉模型变慢了。这事我见过太多团队栽进去——Function Calling 上线后首字节延迟从 200 毫秒涨到 800 毫秒业务方以为是模型变慢其实是前端在等完整 JSON。更糟的是某些工具调用参数很长模型生成 2 秒才完整前端干等 2 秒才显示「正在调用查天气」用户体验直接崩盘。正确的做法是增量解析。每收到一个 Token立刻尝试解析当前缓冲区提前暴露已完整的字段。模型刚吐出{city: 上海时前端就能渲染「即将调用查天气城市上海」让用户看到进展。等完整 JSON 后再执行真正的函数调用。但增量解析的核心难点是部分 JSON 无法直接JSON.parse。{city: 上这种字符串未闭合标准解析器会抛错。前端需要一套容错策略补全未闭合的引号与括号、移除尾部逗号、对解析失败做回退。同时还要维护一个增量 AST让已完整的字段能提前被消费。二、部分 JSON 的容错与增量 AST流式解析的底层机制部分 JSON 的核心问题是结构不完整。常见三种中断态字符串未闭合{city: 上、对象未闭合{city: 上海,、数组未闭合{tags: [a, b。每种都需要不同的补全策略。字符串未闭合时需要在缓冲区末尾补一个。但要注意转义如果末尾是反斜杠补会被当成转义引号需要先移除反斜杠或补两个字符。对象未闭合时需要移除末尾的逗号如果有再补}。数组同理补]。容错的本质是猜测模型还没生成完的内容。猜对了能提前暴露字段猜错了会展示错误值。所以增量解析必须设计成「乐观展示 完整后校验」流式阶段展示的值带 pending 标记完整后用标准JSON.parse覆盖。增量 AST 是另一层。每收到 Token更新当前解析状态机在「对象内」期待键或值在「字符串内」累积字符在「数组内」期待元素。状态机能提前暴露已完整的字段无需等整个对象闭合。某对话产品接入增量解析后Function Calling 的首字节延迟从 800 毫秒降到 180 毫秒。综上流式 JSON 增量解析以「乐观展示 完整后校验」为核心逐 Token 更新解析状态机字段就绪即触发回调让 UI 提前渲染完整对象闭合后再用标准 JSON.parse 覆盖兼顾低延迟与正确性。三、生产级流式 JSON 增量解析器实现下面给出一个可复用的增量解析器。它支持部分 JSON 容错补全、字段就绪回调、解析失败回退。interface ParseResult { /** 当前已能解析出的部分对象可能不完整 */ partial: Recordstring, unknown; /** 是否已收到完整 JSON */ complete: boolean; /** 本次新增的就绪字段名列表 */ newFields: string[]; } type FieldReadyCallback (field: string, value: unknown) void; /** * 流式 JSON 增量解析器。 * 为什么不直接用 JSON.parse 容错 * JSON.parse 每次都要重解析整个缓冲区且无法区分「本次新增了哪些字段」。 * 增量解析能精准触发字段就绪回调避免重复渲染。 */ export class StreamingJsonParser { private buffer ; private knownFields new Setstring(); private onComplete: ((parsed: Recordstring, unknown) void) | null null; private onFieldReady: FieldReadyCallback | null null; private aborted false; // 防止异常流撑爆内存64KB 覆盖 99% 的 Function Calling 场景 private maxBufferBytes 64 * 1024; constructor(opts: { onFieldReady?: FieldReadyCallback; onComplete?: (parsed: Recordstring, unknown) void; } {}) { this.onFieldReady opts.onFieldReady ?? null; this.onComplete opts.onComplete ?? null; } /** * 喂入新 Token 并尝试解析。 * 为什么每次都重新解析整个缓冲区而不是维护增量 AST * arguments 通常较短几百字节重解析成本远低于维护 AST 的复杂度。 * 真正的增量 AST 只在超长 arguments10KB场景才值得引入。 */ feed(token: string): ParseResult { if (this.aborted) { return { partial: {}, complete: false, newFields: [] }; } this.buffer token; // 缓冲区溢出保护异常流可能无限输出 if (this.buffer.length this.maxBufferBytes) { console.warn(流式 JSON 缓冲区溢出中止解析); this.aborted true; return { partial: {}, complete: false, newFields: [] }; } // 第一尝试直接解析完整 JSON 时命中 const direct this.tryParse(this.buffer); if (direct.ok) { const newFields this.detectNewFields(direct.value!); this.emitFields(direct.value!, newFields); this.onComplete?.(direct.value!); return { partial: direct.value!, complete: true, newFields }; } // 第二尝试容错补全后解析 const patched this.patchPartialJson(this.buffer); const patchedResult this.tryParse(patched); if (patchedResult.ok) { const newFields this.detectNewFields(patchedResult.value!); this.emitFields(patchedResult.value!, newFields); return { partial: patchedResult.value!, complete: false, newFields }; } // 解析失败保持上一次的 partial等更多 Token return { partial: {}, complete: false, newFields: [] }; } /** * 容错补全根据缓冲区末尾状态补全缺失的闭合符号。 * 为什么不直接拼接 ]} * 不同中断态需要不同补全盲目拼接会让 JSON.parse 报错而非返回部分结果。 */ private patchPartialJson(input: string): string { let s input.trimEnd(); if (s ) return {}; // 移除尾部不完整的逗号或冒号 if (s.endsWith(,)) s s.slice(0, -1); if (s.endsWith(:)) s s.slice(0, -1) :null; // 检查是否在字符串内部未闭合的字符串 const inString this.isInUnclosedString(s); if (inString) { // 末尾是奇数个反斜杠时补引号前要先补反斜杠抵消转义 const trailingBackslashes this.countTrailingBackslashes(s); if (trailingBackslashes % 2 1) { s \\; } s ; } // 统计未闭合的括号层级 const { braces, brackets } this.countUnclosed(s); s ].repeat(brackets); s }.repeat(braces); return s; } /** * 判断缓冲区是否处于未闭合字符串状态。 * 为什么用字符遍历而不是正则 * 嵌套转义如 \\\\下正则容易误判遍历更准确可控。 */ private isInUnclosedString(s: string): boolean { let inStr false; let escape false; for (let i 0; i s.length; i) { const ch s[i]; if (escape) { escape false; continue; } if (ch \\) { escape true; continue; } if (ch ) inStr !inStr; } return inStr; } private countTrailingBackslashes(s: string): number { let count 0; for (let i s.length - 1; i 0; i--) { if (s[i] \\) count; else break; } return count; } /** * 统计未闭合的 {} 与 []。 * 为什么简单计数不够 * 字符串内的括号不应计入必须跳过字符串内部。 */ private countUnclosed(s: string): { braces: number; brackets: number } { let braces 0, brackets 0; let inStr false, escape false; for (let i 0; i s.length; i) { const ch s[i]; if (escape) { escape false; continue; } if (ch \\) { escape true; continue; } if (ch ) { inStr !inStr; continue; } if (inStr) continue; if (ch {) braces; else if (ch }) braces--; else if (ch [) brackets; else if (ch ]) brackets--; } // 负值表示多余闭合符号按 0 处理容错 return { braces: Math.max(0, braces), brackets: Math.max(0, brackets) }; } private tryParse(s: string): { ok: boolean; value?: Recordstring, unknown } { try { const v JSON.parse(s); // 仅接受对象类型避免裸字符串/数字被误判 if (v typeof v object !Array.isArray(v)) { return { ok: true, value: v }; } return { ok: false }; } catch { return { ok: false }; } } private detectNewFields(obj: Recordstring, unknown): string[] { const news: string[] []; for (const k of Object.keys(obj)) { if (!this.knownFields.has(k)) { this.knownFields.add(k); news.push(k); } } return news; } private emitFields(obj: Recordstring, unknown, fields: string[]) { for (const f of fields) { this.onFieldReady?.(f, obj[f]); } } /** 中断解析用于用户切换会话或离开页面 */ abort() { this.aborted true; } /** 重置以复用实例 */ reset() { this.buffer ; this.knownFields.clear(); this.aborted false; } }关键点在于三处。其一双重尝试策略先直接解析命中完整 JSON失败后再容错补全兼顾性能与容错。其二字符串闭合判断用字符遍历而非正则能正确处理转义嵌套。其三缓冲区溢出保护防止异常流撑爆内存。某对话产品接入后Function Calling 首字节延迟稳定在 200 毫秒内用户感知「模型变快了」。四、增量解析的代价容错误判、内存累积、状态复杂度与适用边界增量解析不是没有代价。第一道代价是容错误判。补全策略本质是猜测猜错时会展示错误的字段值。例如模型生成{city: 上海, weather:时容错补全可能插入null前端展示「天气null」。这就是为什么必须用「pending 标记 完整后覆盖」策略不能把流式阶段的值当最终结果。某团队曾直接用流式解析结果触发函数调用结果补全的null被当成真实参数工具调用失败。第二道代价是内存累积。缓冲区随 Token 增长超长 arguments如生成代码、长文本会持续占用内存。必须设上限并在溢出时降级到「等完整」模式。64KB 是经验值覆盖 99% 的 Function Calling 场景。第三道代价是状态复杂度。容错逻辑分支多测试用例必须覆盖字符串中断、对象中断、数组中断、转义嵌套、嵌套对象、空数组、Unicode 字符等。任一场景漏测都会在线上偶发崩溃。适用边界Function Calling 频繁、参数较短10KB、对首字节延迟敏感的产品收益最高。一次性长文本生成、参数超大的场景等完整再解析反而更稳。五、总结流式 Function Calling 的工程核心是把部分 JSON 增量解析为可消费的字段提前暴露调用意图。落地建议第一双重尝试策略先直接解析命中完整 JSON失败后再容错补全。第二字符串闭合判断用字符遍历正确处理转义嵌套。第三缓冲区设上限溢出时降级到等待完整模式。第四流式阶段的值带 pending 标记完整后用标准解析覆盖禁止直接触发函数调用。最终在首字节延迟与解析正确性之间取得平衡。这条路在毫秒级流式响应下能跑通回报是值得的。

相关新闻

MSP430F22x2/F22x4超低功耗MCU实战:从架构解析到传感器系统设计

MSP430F22x2/F22x4超低功耗MCU实战:从架构解析到传感器系统设计

1. 从数据手册到实战:MSP430F22x2/F22x4 深度解析与应用指南 如果你正在为你的下一个电池供电项目寻找一颗“心脏”,或者正在为传感器信号调理和低功耗数据采集而烦恼,那么德州仪器的MSP430F22x2/F22x4系列微控制器很可能就是你寻找的答案。这…

2026/10/23 7:01:26 阅读更多 →
从压缩行列号到源码定位:前端错误监控的 Source Map 解析与聚合

从压缩行列号到源码定位:前端错误监控的 Source Map 解析与聚合

从压缩行列号到源码定位:前端错误监控的 Source Map 解析与聚合 一、线上报错只剩行列号:错误监控的解释性断层 生产环境的前端代码都是压缩混淆过的。错误堆栈里只能看到 app.min.js:1:23456,这个行列号对应的是打包产物,不是源码…

2026/10/25 6:56:37 阅读更多 →
SAM2Matting:复旦凭什么只用图片训练,就打败所有视频抠图模型

SAM2Matting:复旦凭什么只用图片训练,就打败所有视频抠图模型

一个把"追踪"和"抠图"彻底解耦的框架,零样本视频抠图,支持文字、点击、框选任意目标——这才是 SAM2 应该有的终点。 先聊聊视频抠图为什么这么难 不理解问题的复杂性,就不会知道这篇论文究竟解决了什么。 如果你用过…

2026/10/26 21:50:37 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/26 12:00:21 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/25 22:36:10 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/26 4:52:12 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/26 12:00:21 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/26 5:00:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/26 3:16:46 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 12:00:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 13:21:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 8:34:37 阅读更多 →