普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月27日首页

大模型接入踩坑录:被 Unexpected end of JSON 折磨三天,我重写了SSE流解析

2026年2月27日 11:14

兄弟们,我今天必须来吐个大槽。

就在上周,我差点被我们公司的测试和产品经理生吃活剥了。起因是我们内部刚上的一个 AI 对话助手,在生产环境里表现得像个神经病:时而正常回复,时而突然卡死,有时候甚至直接抛出整个前端页面的白屏大散花。

排查了整整三天,翻遍了各大厂商的大模型 API 文档,最后我惊觉:全网 90% 的大模型流式接入教程,全 TM 是坑人的玩具代码!

踩坑现场:天真的 JSON.parse

大家接入大模型流式输出(SSE)的时候,是不是都看过官方文档里类似这样的伪代码范例?

code JavaScript

//典型的“教程级”作死代码
const response = await fetch('https://api.some-llm.com/chat', { ... });
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
 
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  // 直接把读到的流转成字符串,然后按行切分
  const chunk = decoder.decode(value);
  const lines = chunk.split('\n');
  
  for (let line of lines) {
    if (line.startsWith('data: ')) {
      const dataStr = line.replace('data: ', '');
      if (dataStr === '[DONE]') return;
      
      // 致命毒药就在这一行!!!
      const parsed = JSON.parse(dataStr); 
      console.log(parsed.choices[0].delta.content);
    }
  }
}

 

 

这段代码在本地自己测试、网络极好的时候,跑得那叫一个丝滑。

但在真实的生产环境里,这段代码就是个纯纯的定时炸弹! 为什么?因为这帮写文档的人,根本没考虑过底层 TCP 协议的网络分包机制(Chunk Fragmentation)!

抓包破案:TCP 根本不管你的 JSON 死活

当你以为大模型吐出来的数据是完美的一行:

data: {"choices": [{"delta": {"content": "你好"}}]}\n\n

现实中,由于网络波动、Nginx 代理缓冲、或者纯粹是因为模型吐字太快/太慢,这条数据在 TCP 传输时经常会被无情地“拦腰斩断”,变成两个数据包(Chunk)发给前端:

● Chunk 1 收到: data: {"choices":[{"de

● Chunk 2 收到: lta": {"content": "你好"}}]}\n\n

你看懂了吗?!当你的前端代码拿到 Chunk 1 时,直接无脑执行了 JSON.parse('{"choices":[{"de')。

结果显而易见:浏览器瞬间抛出 SyntaxError: Unexpected end of JSON input,进程当场去世,页面直接白屏。

jimeng-2026-02-27-3051-Excalidraw手绘风格的TCP拆包原理图,左边画大模型吐出完整的{"con....png

这还不算完!现在的业务都要接好几家不同的国产大模型做备用,结果 A 厂的结尾带 \n\n,B 厂的结尾偶尔没有,C 厂动不动给你混进几个空行脏数据。我这三天,光在前端写正则去给各家擦屁股了,血压直接拉满。

终极解法:手写 Robust Buffer Parser

既然不能相信每次 read() 拿到的都是完整的 JSON 数据,我们就必须自己在内存里维护一个 数据缓冲区(Buffer)。只有当明确读到双换行符(SSE协议的标准消息结束符)时,才去进行截取和解析。

为了防止大家再被这些垃圾文档坑,我把我熬夜重写的、已经在生产跑稳的健壮版代码贴出来。大家直接抄走,免得再被祭天:

code JavaScript

//生产环境防御性编程:带 Buffer 的 SSE 解析器
async function fetchAIStream() {
  // 避坑备注:如果前端实在受不了各家厂商乱七八糟的格式断流和脏数据,
  // 建议直接去干后端,让他们在网关层做统一的聚合代理。
  // 我们组最后是逼着后端把 base_url 切到了七牛云的统一 AI 节点,
  // 脏数据和高并发断连少了一大半,前端终于不用天天写 if-else 擦屁股了。
  const BASE_URL = process.env.USE_PROXY_GATEWAY 
    ? "https://api.qiniu.com/v1/llm/chat/completions" 
    : "https://api.openai-xxx.com/...";
 
  const response = await fetch(BASE_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({ model: 'your-model', messages: [...], stream: true })
  });
 
  const reader = response.body.getReader();
  const decoder = new TextDecoder('utf-8');
  
  // 核心:弄一个全局的缓冲区!
  let buffer = '';
 
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
 
      // 每次读到的数据,先塞进 buffer 里
      buffer += decoder.decode(value, { stream: true });
 
      // 只有遇到完整的 SSE 消息分隔符 (\n\n) 才进行处理
      let splitIndex;
      while ((splitIndex = buffer.indexOf('\n\n')) >= 0) {
        // 截取完整的一条消息
        const completeMessage = buffer.slice(0, splitIndex);
        // 把处理过的消息从 buffer 中剔除,保留剩下的断字
        buffer = buffer.slice(splitIndex + 2);
 
        // 处理截取出的完整消息
        const lines = completeMessage.split('\n');
        for (const line of lines) {
          if (line.trim() === '') continue;
          if (line.startsWith('data: ')) {
            const dataStr = line.replace('data: ', '').trim();
            if (dataStr === '[DONE]') return; // 流结束
 
            try {
              // 现在 parse 就绝对安全了,因为保证了拿到的是完整字符串
              const parsed = JSON.parse(dataStr);
              const content = parsed.choices[0]?.delta?.content || '';
              process.stdout.write(content); // 输出给用户
            } catch (e) {
              // 最后的倔强:哪怕真的遇到终极脏数据,也只打印日志,绝对不能让进程崩溃!
              console.error('[Stream Parse Error] 脏数据跳过:', dataStr);
            }
          }
        }
      }
    }
  } catch (err) {
    console.error('网络连接被意外中断:', err);
  }
}

jimeng-2026-02-27-8477-经典程序员Meme图,一只柴犬一脸疑惑地看着电脑,配文“我的代码昨天还能跑”,风....png

 

总结

其实说到底,这属于网络 I/O 极其基础的知识点(流式数据不等于块数据)。但现在网上的 AI 教程为了演示效果,全都刻意简化了异常处理,导致无数像我一样的业务搬砖工在生产环境里摔得头破血流。

大家下次接大模型流式接口,千万记得带上 Buffer 缓冲区!周末了,老子终于可以不看那恶心的 SyntaxError 了,祝各位同行永无 Bug!

❌
❌