#Next.js#React

源码解读:LLM 流式输出的前端实现

"流式输出是 AI 应用的体感生死线——用户盯着一秒一个字冒出来 vs 等十秒一次性蹦出来,体验天差地别。本文从协议到渲染讲透前端流式:SSE/ReadableStream 怎么解析、React 怎么增量渲染不卡、AI SDK 的 useChat 怎么简化,以及中断/错误的处理。"

本文要点

  • 主题聚焦:Next.js、React
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年1月8日·更新于 2026年7月23日

AI 应用有个反直觉的体感规律:1 秒首字 + 流式输出 5 秒,比"等 3 秒一次性返回"体验好得多——哪怕前者总时间更长。原因:流式让用户看到"在动",消除等待焦虑;首字时间比总时间更决定体感。

所以流式不是可选项,是 AI 应用的标配。这篇从协议到渲染讲透前端流式怎么做。

一、流式的底层:Server-Sent Events

LLM 流式的事实标准是 SSE(Server-Sent Events)——服务端单向、持续推送数据给浏览器。它本质是一个长连接的 HTTP 响应,Content-Type: text/event-stream,按 data: ...\n\n 分块。

后端(Next.js Route Handler / Server Action)调模型时用流式接口,把 token 边生成边写进响应:

ts
// app/api/chat/route.ts
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: anthropic("claude-sonnet-5"),
    messages,
  });
  return result.toDataStreamResponse();  // 自动转成 SSE 流
}

toDataStreamResponse() 把模型的流式 token 包成 AI SDK 的 data stream 协议(SSE 之上,带类型标记),前端用配套解析。

二、前端:手写一个流式接收

不用任何 SDK,先看裸实现,理解原理:

ts
async function streamChat(input: string, onToken: (t: string) => void) {
  const res = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ messages: [{ role: "user", content: input }] }),
  });

  const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += value;
    // SSE 按 \n\n 分块
    const parts = buffer.split("\n\n");
    buffer = parts.pop()!;  // 最后一段可能不完整,留到下次
    for (const part of parts) {
      const line = part.split("\n").find(l => l.startsWith("data: "));
      if (!line) continue;
      const data = line.slice(6);
      if (data === "[DONE]") return;
      onToken(JSON.parse(data).text);  // 解析出 token,回调
    }
  }
}

核心是 ReadableStream + TextDecoderStream:边读边解码、按 SSE 分块边界切、解析每块的 data:、回调增量 token。

三、React 渲染:增量但不卡

收到 token 就 setState 追加,听起来简单,但有坑:

tsx
function Chat() {
  const [text, setText] = useState("");
  return <div>{text}</div>;
}
// streamChat(input, t => setText(prev => prev + t))

问题:长输出(几千 token)下,每个 token 一次 setState + 重新渲染整个 markdown,会卡。优化:

1. 批量更新。 别每个 token 一次 setState,攒一个小 buffer(如每 30ms 或每 5 个 token)刷一次。减少渲染次数。

2. markdown 渲染别在主线程重算。 流式过程中每帧重新 parse 完整 markdown 会卡。要么流式期间用纯文本预览、结束后再渲染 markdown;要么用流式友好的 markdown 库(markdown-to-jsx 之类,或 AI SDK 的内置)。

3. 用 useRef + requestAnimationFrame。 把增量 token 存 ref,用 rAF 合并刷到 state,避免高频 setState。

四、AI SDK 的 useChat:把上面全简化

实际上,你不用手写上面这些。AI SDK 的 useChat 把"调接口 + 流式接收 + 状态管理 + 渲染优化"全包了:

tsx
"use client";
import { useChat } from "ai/react";

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    <>
      {messages.map(m => <div key={m.id}>{m.content}</div>)}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
      </form>
    </>
  );
}

useChat 内部:

  • 管 messages 状态(用户消息、助手消息)。
  • 流式接收助手回复,增量更新最后一条 messages。
  • 处理 loading、error、abort。

99% 的场景,直接用 useChat 就够。手写那套是帮你理解它在干什么、出问题时能调。

五、中断与错误(容易漏的)

流式比一次性返回多了几个状态要处理:

1. 用户主动中断。 长输出用户不想等了,要能"停止生成"。useChat 提供 stop(),内部用 AbortController 取消 fetch。手写时记得把 AbortController.signal 传给 fetch,中断时主动 close reader。

2. 连接中断。 网络断了,reader 会抛错。catch + 给用户"重试"按钮,别让界面卡在"加载中"。

3. 流中途出错。 模型流式生成中途报错(限流、内容审核),SSE 会推一个 error 事件。解析时要识别 error,显示"生成中断"+ 保留已生成内容。

4. 空流。 偶尔模型一个 token 都没生成就结束,别让界面留个空消息。

六、几个体验细节

  • 首字时间最关键。优化后端让首 token 尽快出(prompt caching、不要在生成前做重计算)。首字 < 1s 是黄金线。
  • 打字机速度可选。有些产品故意限制输出速度模拟打字机(视觉更舒服),但别慢于真实生成——纯等没意义。
  • 滚动跟随。流式输出时自动滚到底(但要尊重用户主动上滚——用户往上翻看时别强制拉回底部)。
  • 可复制。流式过程中允许复制已生成部分,别等到结束。

小结

流式输出是 AI 应用的体感生死线。技术上它是一套组合:

  • 协议:SSE(服务端单向流)。
  • 后端:模型流式接口 → 转成 SSE 响应(AI SDK 的 toDataStreamResponse)。
  • 前端:ReadableStream 解析增量(手写理解原理 / 用 useChat 直接搞定)。
  • 渲染:批量更新 + 流式友好的 markdown,别每 token 重渲染卡顿。
  • 健壮性:中断(AbortController)、断线重试、中途错误、空流。

记住一条:首字时间比总时间更决定体感。把首字优化到 < 1s,配上流式渲染,你的 AI 应用体感立刻从"等"变成"在动"——这一项的体验提升,往往比模型升级一档还明显。

Related

相关推荐