如何在 SSE 流式返回中提取 JSON 数组对象?以 steps 字段为例的容错解析实践
在实际开发中,我们经常会使用 Server-Sent Events(SSE)来实现服务端向前端推送流式数据。尤其是在与大模型交互时(如 ChatGPT、Claude 等 API),服务端往往以逐字或逐字段的形式返回 JSON 内容。
这本身没有问题,直到你需要从这个不完整、未闭合的 JSON 字符串中,提前提取某些结构——比如一个对象数组中的每一项。
本文就以我的一个真实需求为例,详细讲解:如何从 SSE 返回的“未闭合 JSON 字符串”中,提取出 steps 数组中的每一个对象项,哪怕它是不完整的结构。
Tips: Best-effort JSON parser 这个插件处理的更好
场景背景
服务端返回的数据格式如下,是典型的结构化指令输出:
{
"locale": "zh-CN",
"thought": "用户需要一个医疗减肥方案,目前信息不足以直接给出方案,需收集相关信息。",
"title": "收集制定医疗减肥方案所需信息",
"steps": [
{
"need_search": true,
"title": "收集医疗减肥基础信息",
"description": "...",
"step_type": "research"
},
{
"need_search": true,
"title": "收集减也
注意最后一项:这是一段 SSE 正在流式返回的内容,steps 数组还没有返回完,甚至字段 "title": "收集减也 也没有闭合。
目标是:在不使用 JSON.parse 的前提下,尽可能多地提取出结构完整的 step 项,并且对未闭合但有价值的片段也尽可能解析字段。
常规做法行不通
很多人第一反应是:
const data = JSON.parse(buffer);
const steps = data.steps;
问题在于:JSON.parse 在结构未闭合时会直接抛错,你根本拿不到数据。
即便尝试正则匹配 { ... } 块,也只能匹配闭合结构,无法提取“收集减也”这样的不完整数据块。
目标拆解
我们想要:
- 从
"steps": [开始的位置截取出后续数据; - 将
"steps"数组中的每一项(即以"need_search"开头的对象)逐个分块; - 对每个分块做字段提取(不要求完整闭合);
- 至少有一个字段存在,就认为这是一个有效的
step,加入最终数组。
实现方案(纯字符串解析)
以下是最终验证可用的实现:
function extractStepsLoosely(buffer) {
const steps = [];
const stepsStart = buffer.indexOf('"steps": [');
if (stepsStart === -1) return steps;
const stepsPart = buffer.slice(stepsStart);
// 按每个 step 的 "need_search" 分割
const stepParts = stepsPart.split(/(?="need_search"\s*:)/g).slice(1);
for (const raw of stepParts) {
const getField = (field) => {
const match = raw.match(new RegExp(`"${field}"\s*:\s*"([^"]*)`));
return match ? match[1] : null;
};
const getBoolField = (field) => {
const match = raw.match(new RegExp(`"${field}"\s*:\s*(true|false)`));
return match ? match[1] === "true" : null;
};
const step = {
title: getField("title"),
description: getField("description"),
step_type: getField("step_type"),
need_search: getBoolField("need_search")
};
if (
step.title !== null ||
step.description !== null ||
step.step_type !== null ||
step.need_search !== null
) {
steps.push(step);
}
}
return steps;
}
使用效果演示
const buffer = `
"steps": [
{
"need_search": true,
"title": "收集医疗减肥基础信息",
"description": "...",
"step_type": "research"
},
{
"need_search": true,
"title": "收集减也
`;
const steps = extractStepsLoosely(buffer);
console.log(steps);
输出:
[
{
title: "收集医疗减肥基础信息",
description: "...",
step_type: "research",
need_search: true
},
{
title: "收集减也",
description: null,
step_type: null,
need_search: true
}
]
优点总结
| 特性 | 说明 |
|---|---|
| ✔️ 不依赖 JSON.parse | 纯字符串操作,适配 SSE 拼接中间态 |
| ✔️ 可提取不完整结构 | 字段缺失或 JSON 未闭合不影响 |
| ✔️ 兼容性强 | JS 环境通用,无依赖 |
| ✔️ 可拓展性强 | 可轻松加入更多字段,如 status, priority 等 |
应用场景扩展
除了 SSE,在以下场景也非常适用:
- WebSocket 推送结构化 JSON 片段
- AI 大模型 API 返回结构未闭合的思维链数据
- 日志/中间文件中逐行输出对象数组结构
- 流式生成 markdown/json 的编辑器
结语
SSE 是前端和服务端交互中一种极为常见的技术,但由于其非一次性返回完整结构的特性,我们在处理结构化 JSON 数据时需要更细致地设计处理逻辑。