文章目录
-
-
- [**1. 引言:为什么需要流式 AI 响应?**](#1. 引言:为什么需要流式 AI 响应?)
- [**2. 技术核心:如何实现流式传输?**](#2. 技术核心:如何实现流式传输?)
-
- [**2.1 整体架构流程图**](#2.1 整体架构流程图)
- [**2.2 关键技术:Server-Sent Events**](#2.2 关键技术:Server-Sent Events)
- [**3. 实战:构建一个流式 AI 聊天应用**](#3. 实战:构建一个流式 AI 聊天应用)
-
- [**3.1 项目结构**](#3.1 项目结构)
- [**3.2 后端实现**](#3.2 后端实现)
- [**3.3 前端实现**](#3.3 前端实现)
- [**4. 深入解析:流式传输的数据流**](#4. 深入解析:流式传输的数据流)
- [**5. 其他流式技术方案**](#5. 其他流式技术方案)
- [**6. 总结**](#6. 总结)
-
1. 引言:为什么需要流式 AI 响应?
传统的 AI 接口调用模式是 "一问一答" 或 "同步响应":
- 用户发送问题。
- 客户端等待 AI 模型处理整个问题,生成完整的答案。
- 服务器将完整的答案一次性返回给客户端。
- 客户端渲染最终结果。
这种模式的痛点非常明显:
- 用户体验差:如果生成一段长文本需要 10-20 秒,用户会面对一个空白页面或加载动画,无法感知到任何进展,容易产生焦虑,甚至认为请求失败。
- 网络超时风险:长时间的 HTTP 连接保持容易因网络波动或代理设置而中断。
- 首字节时间慢:用户需要等待整个内容生成完毕才能看到第一个字。
流式响应 完美地解决了这些问题。它的工作方式类似于电影放映:
- 用户发送问题。
- AI 模型开始生成内容,每生成一个"词元"就立即发送出去。
- 客户端持续接收这些"词元碎片"并实时拼接、渲染。
- 用户几乎在提问后的一瞬间就能看到 AI 开始"思考"和"输出"。
2. 技术核心:如何实现流式传输?
实现流式 AI 响应的技术栈主要包含两部分:服务器与 AI 模型的流式交互 和 客户端与服务器的流式通信。
2.1 整体架构流程图
为了宏观理解整个流程,我们来看下面的交互图:
用户/客户端 Web 服务器 AI 模型 API (如 OpenAI) 1. 发送提问请求 携带提问内容、参数等 2. 发起流式请求 设置 stream=true 返回 Server-Sent Events (SSE) 数据流 转发/直接返回 SSE 数据流 实时渲染收到的词元 loop [3. 持续流式响应] 4. 流结束标志 [DONE] 转发结束标志 关闭连接,渲染完成 用户/客户端 Web 服务器 AI 模型 API (如 OpenAI)
2.2 关键技术:Server-Sent Events
如流程图所示,SSE 是实现此类应用的绝佳选择,原因如下:
- 单向数据流:完美契合 AI 模型生成内容并推送的场景。
- 基于 HTTP:无需像 WebSocket 那样进行复杂的协议升级,简单易用。
- 自动重连:内置机制提高了应用健壮性。
- 标准格式 :
data: {...}的格式易于解析。
3. 实战:构建一个流式 AI 聊天应用
我们将使用 Node.js + Express 作为后端,通过 OpenAI API 作为 AI 模型服务,前端使用原生 JavaScript 的 EventSource 来接收流。
3.1 项目结构
stream-ai-demo/
├── server.js # 后端服务器
├── public/
│ └── index.html # 前端页面
└── package.json
3.2 后端实现
后端充当了一个代理角色,接收客户端请求,然后以流式方式调用 OpenAI API,并将流数据转发给客户端。
1. 安装依赖
bash
npm install express openai
2. server.js 代码
javascript
const express = require('express');
const { OpenAI } = require('openai');
const path = require('path');
const app = express();
const port = 3000;
// 初始化 OpenAI 客户端 (请替换为你的真实 API Key)
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY || '你的-api-key-here', // 强烈建议使用环境变量
});
// 提供静态文件服务
app.use(express.static('public'));
// 解析 JSON 请求体
app.use(express.json());
// 流式聊天端点
app.post('/chat/stream', async (req, res) => {
const { message } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
console.log(`收到用户消息: "${message}"`);
// 1. 设置 SSE 响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
// 对于 SSE,通常不需要额外的 CORS 头,但确保连接不跨域或服务器已配置 CORS
});
try {
// 2. 向 OpenAI 发起流式请求
const stream = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: message }],
stream: true, // !!!核心:开启流式输出 !!!
temperature: 0.7,
max_tokens: 500,
});
// 3. 处理从 OpenAI 返回的流
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
// 如果当前块有内容,则发送给客户端
if (content) {
// 构建符合 SSE 格式的数据
// 注意:这里发送的是纯文本,但也可以发送 JSON
const data = JSON.stringify({
type: 'content',
content: content
});
res.write(`data: ${data}\n\n`); // SSE 格式要求以 \n\n 结束
}
}
// 4. 发送流结束标志
const endEvent = JSON.stringify({ type: 'end', content: '[DONE]' });
res.write(`data: ${endEvent}\n\n`);
// 5. 结束响应
res.end();
} catch (error) {
console.error('调用 OpenAI API 出错:', error);
// 发送错误信息给客户端 (同样遵循 SSE 格式)
const errorEvent = JSON.stringify({
type: 'error',
content: `服务出错: ${error.message}`
});
res.write(`data: ${errorEvent}\n\n`);
res.end();
}
});
app.listen(port, () => {
console.log(`流式 AI 服务器运行在 http://localhost:${port}`);
});
3.3 前端实现
前端负责创建 SSE 连接,监听服务器推送的消息,并实时渲染到页面上。
public/index.html 代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>流式 AI 聊天演示</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.chat-container {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
#chatBox {
height: 400px;
overflow-y: auto;
padding: 20px;
border-bottom: 1px solid #eee;
}
.message {
margin-bottom: 15px;
line-height: 1.5;
}
.user-message {
text-align: right;
}
.user-message .bubble {
background: #007bff;
color: white;
border-radius: 18px 18px 4px 18px;
display: inline-block;
padding: 10px 16px;
max-width: 80%;
}
.ai-message .bubble {
background: #e9ecef;
color: #333;
border-radius: 18px 18px 18px 4px;
display: inline-block;
padding: 10px 16px;
max-width: 80%;
text-align: left;
}
#aiThinking {
color: #6c757d;
font-style: italic;
padding: 10px 20px;
display: none;
}
.input-area {
display: flex;
padding: 20px;
background: #f8f9fa;
}
#userInput {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 24px;
outline: none;
font-size: 16px;
}
#userInput:focus {
border-color: #007bff;
}
#sendBtn {
background: #007bff;
color: white;
border: none;
border-radius: 24px;
padding: 0 24px;
margin-left: 10px;
cursor: pointer;
font-size: 16px;
transition: background 0.2s;
}
#sendBtn:hover {
background: #0056b3;
}
#sendBtn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.cursor {
display: inline-block;
width: 8px;
height: 16px;
background: #007bff;
margin-left: 2px;
animation: blink 1s infinite;
vertical-align: middle;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
</style>
</head>
<body>
<div class="chat-container">
<h2 style="text-align: center; padding: 20px; margin: 0; background: #007bff; color: white;">流式 AI 聊天演示</h2>
<div id="chatBox">
<div class="message ai-message">
<div class="bubble">
你好!我是一个 AI 助手。请问有什么可以帮你的吗?
</div>
</div>
</div>
<div id="aiThinking">AI 正在思考中<span class="cursor"></span></div>
<div class="input-area">
<input type="text" id="userInput" placeholder="输入你的问题..." autocomplete="off">
<button id="sendBtn">发送</button>
</div>
</div>
<script>
const chatBox = document.getElementById('chatBox');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const aiThinking = document.getElementById('aiThinking');
let currentEventSource = null;
let currentAIMessageElement = null;
// 发送消息函数
function sendMessage() {
const message = userInput.value.trim();
if (!message) return;
// 添加用户消息到聊天框
addMessage(message, 'user');
userInput.value = '';
sendBtn.disabled = true;
// 显示 AI 正在思考的指示器
aiThinking.style.display = 'block';
scrollToBottom();
// 准备 AI 消息的占位元素
currentAIMessageElement = createAIMessageElement();
chatBox.appendChild(currentAIMessageElement);
// 如果存在之前的连接,先关闭
if (currentEventSource) {
currentEventSource.close();
}
// 创建新的 EventSource 连接 (注意:这里使用 POST 请求的变通方式)
// 由于 EventSource 只支持 GET,我们这里使用 Fetch API 来实现 POST + 流式响应
fetch('/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message })
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
// 读取流数据
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
// 流结束
aiThinking.style.display = 'none';
sendBtn.disabled = false;
userInput.focus();
return;
}
// 解码数据并添加到缓冲区
buffer += decoder.decode(value, { stream: true });
// 按行处理缓冲区中的数据
const lines = buffer.split('\n');
buffer = lines.pop(); // 最后一行可能不完整,放回缓冲区
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6); // 去掉 "data: " 前缀
try {
const data = JSON.parse(dataStr);
if (data.type === 'content') {
// 实时追加 AI 回复内容
currentAIMessageElement.querySelector('.bubble').textContent += data.content;
scrollToBottom();
} else if (data.type === 'end') {
// 流结束
aiThinking.style.display = 'none';
sendBtn.disabled = false;
userInput.focus();
} else if (data.type === 'error') {
// 处理错误
currentAIMessageElement.querySelector('.bubble').textContent = `错误: ${data.content}`;
aiThinking.style.display = 'none';
sendBtn.disabled = false;
userInput.focus();
}
} catch (e) {
console.error('解析 SSE 数据出错:', e, '原始数据:', dataStr);
}
}
}
// 继续读取下一块数据
return read();
});
}
return read();
})
.catch(error => {
console.error('请求失败:', error);
if (currentAIMessageElement) {
currentAIMessageElement.querySelector('.bubble').textContent = `请求失败: ${error.message}`;
}
aiThinking.style.display = 'none';
sendBtn.disabled = false;
userInput.focus();
});
}
// 添加消息到聊天框
function addMessage(content, sender) {
const messageElement = document.createElement('div');
messageElement.className = `message ${sender}-message`;
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble.textContent = content;
messageElement.appendChild(bubble);
chatBox.appendChild(messageElement);
scrollToBottom();
}
// 创建 AI 消息元素
function createAIMessageElement() {
const messageElement = document.createElement('div');
messageElement.className = 'message ai-message';
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble.textContent = ''; // 初始为空,内容将通过流式响应逐步添加
messageElement.appendChild(bubble);
return messageElement;
}
// 滚动到底部
function scrollToBottom() {
chatBox.scrollTop = chatBox.scrollHeight;
}
// 事件监听
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
userInput.addEventListener('input', () => {
sendBtn.disabled = !userInput.value.trim();
});
</script>
</body>
</html>
4. 深入解析:流式传输的数据流
让我们更细致地看一下从 AI 模型到客户端浏览器的数据流动过程:
OpenAI API Stream Chunk → 你的服务器 → SSE Format → 浏览器 EventSource → DOM 更新
↓ ↓ ↓ ↓ ↓
{ "choices": [{ res.write(`data: data: {"type": event.data element
"delta": { ${JSON.stringify( "content":"嗨"} → JSON.parse() .textContent += "嗨"
"content": "嗨" {type:'content', \n\n`} → {type:'content',
} content:'嗨'} content:'嗨'}
}]} )}\n\n`)
关键点:
- Chunk: OpenAI API 返回的每个数据块都是一个完整的 JSON 对象,但只包含生成内容的一小部分。
- SSE 格式化 : 服务器将这些小块数据包装成 SSE 格式 (
data: ...\n\n)。 - 客户端解析: 浏览器接收到 SSE 数据后,解析 JSON,并实时更新 UI。
5. 其他流式技术方案
除了 SSE,还有其他实现流式传输的方案:
-
WebSocket
- 优点:真正的双向通信,延迟极低。
- 缺点:实现相对复杂,需要协议升级,对于单纯的 AI 响应推送有点"杀鸡用牛刀"。
-
Fetch API + ReadableStream
- 如上例所示,使用 Fetch API 的 Response.body 获取可读流,然后逐步读取和处理。
- 比 SSE 更灵活,可以支持 POST 请求和自定义数据处理逻辑。
6. 总结
流式 AI 响应通过将内容"化整为零",极大地提升了用户体验。其技术核心在于:
- 后端:利用 AI 提供商(如 OpenAI)的流式 API,并通过 SSE 或 HTTP 流将数据块实时转发给客户端。
- 前端 :使用
EventSource或Fetch API + Stream来接收这些数据块,并实时更新界面。
通过本文的详细讲解和完整代码示例,你应该能够理解并实现自己的流式 AI 应用。这种技术已经成为现代 AI 应用的标配,掌握它将使你能构建出体验更出色的下一代 Web 应用。
