新闻详情

新闻详情

首页 / 资讯中心 / 详情

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

发布时间:2026/11/12 6:20:41来源:尧图网络
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 区域仅截取首个{之后的内容输入修复管道。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【外设】之大彩串口显示屏 2026/11/11 13:52:51

【外设】之大彩串口显示屏

大彩串口屏初步使用 1 .官网下载 STM32 屏幕 GUI 设计资料 http://www.gz-dc.com/category/typeid/4112 找到 STM32 Keil 工程,移植相关代码因项目而异进行移植,由于项目简单,本人只对用到的指令接口进行修改。 比如:注意事项&…

阅读更多 →
无法下载Windows系统iso文件 2026/11/10 19:42:40

无法下载Windows系统iso文件

当我遇到这个问题的时候,我打开了一个网站: 登录 然后我打算下载的时候: 突然那个官方的连接就可以下载了:

阅读更多 →
【清华代码熊】DeepSeek V4.1 Flash 后训练详解 2026/11/11 9:31:44

【清华代码熊】DeepSeek V4.1 Flash 后训练详解

📌 上期解析了 DeepSeek V4.1 Flash 模型架构改进,本期解析 DeepSeek V4.1 Flash 预训练/后训练技术: 🌟 预训练:45T 文本 多模态混合语料、直接训练 sparse attention(取消 DeepSeek V4 的 dense 冷启动&…

阅读更多 →
Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ... 2026/11/11 5:36:08

Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ...

文章主要内容和创新点 主要内容 本文聚焦于多模态大语言模型(MLLM)强化学习(RL)训练中的效率问题,提出了一个名为Shuffle-R1的框架。研究发现,当前RL训练存在两个关键缺陷: 优势值坍缩(Advantage Collapsing):批次中大多数优势值集中在零附近,导致有效梯度信号被淹…

阅读更多 →
PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction 2026/11/11 15:37:39

PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction

一、文章主要内容总结 本文聚焦于利用大型语言模型(LLMs)实现个人身份信息(PII)脱敏的研究,旨在解决传统脱敏方法(如基于规则的系统、领域特定命名实体识别(NER)模型)泛化能力差、跨格式/跨语境适应性弱的问题。 研究通过全面评估多种LLM架构(包括密集型LLM(D-LLM…

阅读更多 →
LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model 2026/11/10 17:14:10

LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model

文章主要内容和创新点 主要内容 本文聚焦于二进制图像-文本相关性评估任务(判断图像与文本“相关”或“不相关”),针对该任务中文本格式多样、相关性定义随场景变化等挑战,提出了基于多模态大语言模型(MLLM)的解决方案LLaVA-RE。 模型设计:LLaVA-RE基于LLaVA 1.5架构,…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉