如何在 SSE 流式返回中提取 JSON 数组对象?

538 阅读3分钟

如何在 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 在结构未闭合时会直接抛错,你根本拿不到数据。

即便尝试正则匹配 { ... } 块,也只能匹配闭合结构,无法提取“收集减也”这样的不完整数据块


目标拆解

我们想要:

  1. "steps": [ 开始的位置截取出后续数据;
  2. "steps" 数组中的每一项(即以 "need_search" 开头的对象)逐个分块
  3. 对每个分块做字段提取(不要求完整闭合);
  4. 至少有一个字段存在,就认为这是一个有效的 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 数据时需要更细致地设计处理逻辑。