首页/新闻资讯/正文详情

Spring AI 流式输出时的 JSON 截断与增量结构化补全

发布时间:2026/9/23 15:46:01 来源:云帆数科 栏目:资讯中心
Spring AI 流式输出时的 JSON 截断与增量结构化补全
Spring AI 流式输出时的 JSON 截断与增量结构化补全大模型在处理结构化数据提取或复杂表单生成时流式输出Streaming Output能极大降低前端用户的首字等待延迟TTFT。然而在基于 Spring AI 或底层 Reactor Flux 对接 LLM 流式流Server-Sent Events的过程中一个常见的痛点是大模型下发的 JSON 片段在传输过程中是逐 Token 输出的。前端或后端消费方若想在流式传输阶段就实时渲染已经生成的字段必须面对“不完整 JSON 字符串”的反序列化失败问题。如果在中间管道中直接调用 Jackson 的ObjectMapper.readValue()不可避免会抛出JsonParseException: Unexpected end-of-input。等待整个流全部结束再一次性解析又完全失去了流式体验的交互优势。这里我们将深入探讨在 Spring AI 流式调用场景下如何通过增量状态机与括号栈修复算法实现流式 JSON 的实时截断修复与安全结构化消费。流式 JSON 的结构痛点与异常现场当要求模型输出 JSON 格式时如提取用户信息、报表数据模型在生成过程中会经历如下典型的 Token 切片Chunk 1: {name: 张三, scores: [85, 9 Chunk 2: 2, 78], profile: {age: 3 Chunk 3: 0, city: 杭州}}在 Chunk 1 到达时内容为{name: 张三, scores: [85, 9。此时数组scores缺少闭合方括号]。整个根对象缺少闭合花括号}。最后一个数字9尚未完成后续可能是92或9。如果在后端中间流处理中希望将中间态推送给下游 WebSocket 或实时计算逻辑解析器会直接报错崩溃com.fasterxml.jackson.core.io.JsonEOFException: Unexpected end-of-input in numeric value at [Source: (String){name: 张三, scores: [85, 9; line: 1, column: 31] at com.fasterxml.jackson.core.base.ParserMinimalBase._reportInvalidEOF(ParserMinimalBase.java:160) at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._finishNumberMinus(ReaderBasedJsonParser.java:1520)业务诉求很明确我们需要一个低延迟、轻量级的增量修复器Incremental JSON Repairer能够在任意 Token 截断点将不完整的 JSON 串临时补齐为合法可解析的 JSON 对象同时标记未稳定字段。增量补全状态机设计修复截断 JSON 的核心是符号栈Bracket Stack与词法状态分析。通过单遍扫描当前缓冲区字符串跟踪以下状态字符串状态inString是否处于未闭合的双引号内部并处理转义字符\。符号嵌套栈stack记录当前处于{还是[的层级。键值分隔符colonPending识别是否出现key:但值还未开始输出的情况。悬挂标识与不完整字面量如截断在trutrue、falfalse、nulnull或未完成的数字上。状态机扫描与动态补全逻辑[原始缓冲区字符流] │ ▼ [词法状态扫描] ─── 处于未闭合字符串? ──► 自动追加闭合双引号 │ ▼ [检查末尾悬挂符号] ── 处于 key: 悬挂? ──► 补齐占位值 null │ ▼ [遍历符号栈] ── 依据栈内剩余 [ / { ──► 倒序追加 ] 和 } │ ▼ [输出合法临时 JSON] ──► Jackson 安全反序列化核心实现流式 JSON 增量补全器下面给出一个高性能的 Java 实现。该类在每次接收到新 Chunk 时能够以极低开销输出当前阶段的最优修复 JSON 字符串。package com.example.ai.stream; import java.util.ArrayDeque; import java.util.Deque; public class IncrementalJsonRepairer { public static String repair(String partialJson) { if (partialJson null || partialJson.trim().isEmpty()) { return {}; } String input partialJson.trim(); StringBuilder sb new StringBuilder(input); DequeCharacter stack new ArrayDeque(); boolean inString false; boolean escape false; for (int i 0; i input.length(); i) { char c input.charAt(i); if (escape) { escape false; continue; } if (c \\) { if (inString) { escape true; } continue; } if (c ) { inString !inString; continue; } if (!inString) { if (c { || c [) { stack.push(c); } else if (c }) { if (!stack.isEmpty() stack.peek() {) { stack.pop(); } } else if (c ]) { if (!stack.isEmpty() stack.peek() [) { stack.pop(); } } } } // 1. 如果截断在字符串内部先补全双引号 if (inString) { sb.append(\); } // 2. 清理末尾处于悬挂状态的分隔符 String current sb.toString().trim(); while (current.endsWith(:) || current.endsWith(,)) { current current.substring(0, current.length() - 1).trim(); // 如果去掉冒号后变成了未完成的 key再次尝试平衡 if (current.endsWith(\)) { // 保留为合法的 key: null 结构 current current : null; break; } } sb new StringBuilder(current); // 3. 处理可能截断的字面量 (true/false/null) trimIncompleteLiterals(sb); // 4. 根据当前栈深度倒序闭合括号 while (!stack.isEmpty()) { char openBracket stack.pop(); if (openBracket {) { sb.append(}); } else if (openBracket [) { sb.append(]); } } return sb.toString(); } private static void trimIncompleteLiterals(StringBuilder sb) { String text sb.toString(); String[] literals {true, false, null}; for (String literal : literals) { for (int len 1; len literal.length(); len) { String sub literal.substring(0, len); if (text.endsWith( sub) || text.endsWith(: sub) || text.endsWith(, sub)) { int idx text.lastIndexOf(sub); sb.delete(idx, sb.length()); sb.append(null); return; } } } } }结合 Spring AI ChatClient 的管道集成在 Spring AI 中ChatClient.prompt().stream().chatResponse()返回的是FluxChatResponse。我们可以在 Reactor 响应式流的操作链中注入增量修复器将原本离散的字符片段转换为实时可解析的 DTO 中间态对象。package com.example.ai.controller; import com.example.ai.stream.IncrementalJsonRepairer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.ai.chat.client.ChatClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import java.util.concurrent.atomic.AtomicReference; RestController public class StreamStructureController { private final ChatClient chatClient; private final ObjectMapper objectMapper; public StreamStructureController(ChatClient.Builder builder, ObjectMapper objectMapper) { this.chatClient builder.build(); this.objectMapper objectMapper; } GetMapping(value /api/v1/stream/profile, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxJsonNode streamProfileExtraction(RequestParam String userPrompt) { StringBuilder buffer new StringBuilder(); AtomicReferenceString lastValidJson new AtomicReference({}); return chatClient.prompt() .user(请提取以下内容的用户画像严格以 JSON 格式输出\n userPrompt) .stream() .content() .map(chunk - { buffer.append(chunk); String raw buffer.toString(); // 清理 Markdown 代码块包裹符如 json ... String cleanJson extractJsonContent(raw); return IncrementalJsonRepairer.repair(cleanJson); }) .filter(repairedJson - !repairedJson.equals(lastValidJson.get())) .map(repairedJson - { try { JsonNode node objectMapper.readTree(repairedJson); lastValidJson.set(repairedJson); return node; } catch (Exception e) { // 遇到边缘未闭合场景返回上一有效帧 try { return objectMapper.readTree(lastValidJson.get()); } catch (Exception ex) { return objectMapper.createObjectNode(); } } }); } private String extractJsonContent(String raw) { String trimmed raw.trim(); if (trimmed.startsWith(json)) { trimmed trimmed.substring(7); } else if (trimmed.startsWith()) { trimmed trimmed.substring(3); } if (trimmed.endsWith()) { trimmed trimmed.substring(0, trimmed.length() - 3); } return trimmed.trim(); } }生产落地踩坑与防御机制数值截断抖动当模型正在输出数字12345时前序帧可能是12、123、1234。在前端做图表或实时统计渲染时数值会发生连续跃变。对于关键数值字段建议在前端或 DTO 层增加校验标记或由后端在反序列化时判定字段是否处于当前最后一个 Key避免引发前端视觉闪烁。大对象内存开销随着输出内容增加buffer.append()的字符串变长每一帧执行repair和objectMapper.readTree()会产生较多中间临时对象。在高并发网关场景下可以引入**帧节流Throttle/Debounce**机制限制每 50ms~100ms 触发一次反序列化解析既能保证视觉流畅度又能节省 70% 以上的 GC 开销。Markdown 围栏与前置思考文本部分推理模型如带有think标签或前置 Markdown 描述的模型会在输出 JSON 之前输出一段解释性文本。在进入修复器之前必须通过状态过滤剔除前置非 JSON 区域仅截取首个{之后的内容输入修复管道。

相关推荐

MDX Deck 快速上手指南:基于 Markdown 与 React 构建演示文稿
MDX Deck 快速上手指南:基于 Markdown 与 React 构建演示文稿

MDX Deck 快速上手指南:基于 Markdown 与 React 构建演示文稿 【免费下载链接】mdx-deck ♠️ React MDX-based presentation decks 项目地址: https://gitcode.com/gh_mirrors/md/mdx-deck MDX Deck 是一个基于 React 与 MDX 的演示文稿(Present… · 2026/9/23 15:46:01

TRC20 USDT支付插件:服务端SDK快速集成指南
TRC20 USDT支付插件:服务端SDK快速集成指南

简介:这是一款专为彩虹易支付系统定制的USDT-TRC20链上收款插件,面向PHP开发者及中小型网站支付功能拓展需求者,解决传统易支付不支持稳定币直收、资金需经第三方中转的痛点。插件部署后可新增独立支付方式,调用标识为usdt&#x… · 2026/9/23 15:45:54

Kornia 下载缓存卫生机制解析:拒绝的新下载如何避免污染缓存(4367)
Kornia 下载缓存卫生机制解析:拒绝的新下载如何避免污染缓存(4367)

Kornia 下载缓存卫生机制解析:拒绝的新下载如何避免污染缓存(#4367) 【免费下载链接】kornia 🐍 空间人工智能的几何计算机视觉库 项目地址: https://gitcode.com/kornia/kornia 导读 Kornia 在模型权重与预训练检查点的下… · 2026/9/23 15:45:54

在 video-use 中用 manim-video Skill 生产 3Blue1Brown 风格数学动画:从文本提示到成片的完整管线
在 video-use 中用 manim-video Skill 生产 3Blue1Brown 风格数学动画:从文本提示到成片的完整管线

AI 技能/插件音视频视频处理人工智能 【免费下载链接】video-use Edit videos with coding agents 项目地址: https://gitcode.com/GitHub_Trending/vid/video-use 点击查看 免费下载 本文是 manim-video Skill 的实战技术指南。该 Skill 以 Manim Community Editi… · 2026/9/24 2:30:17

EMQX 客户端属性初始化:为 `mqtt.client_attrs_init` 表达式新增 `cert_common_name` 与 `cert_subject` 证书变量别名
EMQX 客户端属性初始化:为 `mqtt.client_attrs_init` 表达式新增 `cert_common_name` 与 `cert_subject` 证书变量别名

后端物联网消息队列通信 【免费下载链接】emqx The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles 项目地址: https://gitcode.com/gh_mirrors/em/emqx 点击查看 免费下载 mqtt.client_attrs_init 是 EMQX 在客户端连接阶段用… · 2026/9/24 2:30:17

PX4、Pixhawk、ArduPilot和APM到底什么关系?一文讲透飞控软硬件选型
PX4、Pixhawk、ArduPilot和APM到底什么关系?一文讲透飞控软硬件选型

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

Linux系统调试课(CPU篇)CPU频率调节与功耗策略
Linux系统调试课(CPU篇)CPU频率调节与功耗策略

文章目录 一、概述 二、为什么需要频率调节 2.1 功耗与频率的关系 2.2 DVFS 调频链路 三、基本概念 3.1 Governor 策略 3.2 OPP 表 四、源码解析 4.1 cpufreq-dt 驱动初始化 4.2 schedutil governor 五、RK3506 OPP 表 5.1 设备树 OPP 定义 六、关键特性与约束 七、调试命令与实… · 2026/9/24 2:29:46

wp-calypso 中 Automattic for Agencies(A4A)前端的本地开发环境搭建与运行指南
wp-calypso 中 Automattic for Agencies(A4A)前端的本地开发环境搭建与运行指南

前端CMS 【免费下载链接】wp-calypso The JavaScript and API powered WordPress.com 项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso 点击查看 免费下载 Automattic for Agencies(简称 A4A)是 wp-calypso 仓库中面向代理合作伙伴… · 2026/9/24 2:29:40

LTspice导入厂商SPICE模型:.lib与.sub文件操作及常见报错排查
LTspice导入厂商SPICE模型:.lib与.sub文件操作及常见报错排查

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

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码