第 6 章:流式输出:像 ChatGPT 一样逐字返回
本章目标
这一章把普通响应改造成流式响应。用户不需要等完整答案生成完,而是可以边生成边阅读。
本章效果
下面的截图来自配套 Next.js 项目。回答区域使用 ReadableStream 持续追加模型输出,右侧展示问题结构化分析。

为什么需要流式输出
AI 接口常常比普通接口慢。如果用户点击发送后等 10 秒才看到结果,会觉得应用卡住了。
流式输出可以改善等待感:
请求开始 -> 收到第一个 token -> 持续追加文本 -> 完成
服务端流式 API
LangChain 模型通常支持 stream()。在 Next.js API Route 中,我们可以把模型输出转成 ReadableStream。
import { createChatModel } from "@/lib/ai/model";
export async function POST(request: Request) {
const body = await request.json();
const model = await createChatModel();
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
const modelStream = await model.stream([
{
role: "system",
content: "你是一个简洁可靠的 AI 应用开发助手。"
},
...body.messages
]);
for await (const chunk of modelStream) {
controller.enqueue(encoder.encode(chunk.text));
}
controller.close();
} catch (error) {
controller.enqueue(encoder.encode("\n[生成失败,请稍后重试]"));
controller.close();
}
}
});
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache"
}
});
}
前端读取流
前端用 response.body.getReader() 读取:
async function readStream(response: Response, onText: (text: string) => void) {
const reader = response.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
onText(decoder.decode(value));
}
}
发送消息时先插入一个空 assistant 消息,然后持续追加:
const assistantIndex = nextMessages.length;
setMessages([...nextMessages, { role: "assistant", content: "" }]);
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: nextMessages })
});
await readStream(response, (text) => {
setMessages((current) => {
const copy = [...current];
copy[assistantIndex] = {
...copy[assistantIndex],
content: copy[assistantIndex].content + text
};
return copy;
});
});
取消生成
使用 AbortController:
const controller = new AbortController();
await fetch("/api/chat", {
method: "POST",
signal: controller.signal,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages })
});
controller.abort();
前端可以提供“停止生成”按钮。生产环境还要考虑服务端是否继续消耗模型请求。
流式输出的 UI 细节
几个体验点很重要:
- assistant 消息生成中显示光标或 loading
- 生成时禁用重复发送
- 支持停止生成
- 自动滚动到底部,但用户向上翻历史时不要强行抢滚动
- 错误时保留用户输入,方便重新发送
实战任务
完成:
/api/chat返回ReadableStream- 前端逐块读取文本
- assistant 消息实时追加
- loading 和停止生成状态
常见坑
不要用 JSON 包完整个流式响应,普通 JSON 适合一次性返回,不适合 token 级输出。
不要假设每个 chunk 都是完整句子。chunk 可能只是一个字、一个词、甚至空字符串。
不要忘记处理 response.body 为空的情况。
本章小结
流式输出让 AI 应用体验上一个台阶。下一章我们会整理 Prompt,让模型回答更稳定、更符合知识库助手的定位。