《60天AI学习计划启动 | Day 04: 流式响应实现 - 打造流畅的AI对话体验》
Day 04: 流式响应实现 - 打造流畅的AI对话体验
📋 学习目标
📚 核心学习内容
1. 流式响应 vs 普通响应
普通响应(阻塞式):
用户发送消息 → 等待AI完整生成 → 一次性返回全部内容
问题:等待时间长,用户体验差
流式响应(Streaming):
用户发送消息 → AI逐字生成 → 实时返回每个字符
优势:即时反馈,体验流畅,类似ChatGPT
对比:
// 普通响应:等待10秒后一次性返回
response: "这是一个很长的回答..."
// 流式响应:每秒返回一部分
chunk1: "这是"
chunk2: "一个"
chunk3: "很长的"
chunk4: "回答..."
2. Server-Sent Events (SSE)
什么是 SSE?
- 服务器主动向客户端推送数据
- 基于 HTTP 长连接
- 单向通信(服务器 → 客户端)
- 比 WebSocket 更简单
SSE vs WebSocket:
SSE: 单向,HTTP协议,自动重连,简单易用
WebSocket: 双向,TCP协议,需要手动处理,功能强大
SSE 格式:
data: 这是第一段内容\n\n
data: 这是第二段内容\n\n
data: [DONE]\n\n
3. OpenAI Stream API
流式调用:
const stream = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: messages,
stream: true // 关键:启用流式
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
// 实时处理每个chunk
console.log(content);
}
}
🏗️ 实践作业
作业1:后端实现流式接口
src/routes/chat.js(添加流式路由):
import express from 'express';
import { streamChatWithAI } from '../services/openai.js';
import { logger } from '../utils/logger.js';
export const chatRouter = express.Router();
// POST /api/chat/stream - 流式聊天接口
chatRouter.post('/stream', async (req, res) => {
try {
const { message, conversationHistory = [] } = req.body;
// 参数验证
if (!message || typeof message !== 'string') {
return res.status(400).json({
success: false,
error: '消息内容不能为空'
});
}
logger.info(`收到流式请求: ${message.substring(0, 50)}...`);
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
// 调用流式 AI 服务
await streamChatWithAI(message, conversationHistory, {
onChunk: (content) => {
// 发送数据块
res.write(`data: ${JSON.stringify({ content })}\n\n`);
},
onComplete: (usage) => {
// 发送完成信号
res.write(`data: ${JSON.stringify({ done: true, usage })}\n\n`);
res.end();
},
onError: (error) => {
// 发送错误信息
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
} catch (error) {
logger.error('流式接口错误:', error);
if (!res.headersSent) {
res.status(500).json({
success: false,
error: error.message || '流式服务异常'
});
}
}
});
作业2:实现流式 OpenAI 服务
src/services/openai.js(添加流式方法):
import OpenAI from 'openai';
import { logger } from '../utils/logger.js';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
/**
* 流式聊天
* @param {string} message - 用户消息
* @param {Array} conversationHistory - 对话历史
* @param {Object} callbacks - 回调函数
*/
export async function streamChatWithAI(
message,
conversationHistory = [],
callbacks = {}
) {
const { onChunk, onComplete, onError } = callbacks;
try {
// 构建消息列表
const messages = [
{
role: 'system',
content: '你是一个友好的AI助手,擅长回答各种问题。'
},
...conversationHistory,
{
role: 'user',
content: message
}
];
logger.info(`开始流式请求,消息数: ${messages.length}`);
// 创建流式请求
const stream = await openai.chat.completions.create({
model: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
messages: messages,
temperature: parseFloat(process.env.TEMPERATURE || '0.7'),
max_tokens: parseInt(process.env.MAX_TOKENS || '2000'),
stream: true // 启用流式
});
let fullContent = '';
let usage = null;
// 处理流式数据
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
const content = delta?.content || '';
if (content) {
fullContent += content;
// 实时回调每个chunk
if (onChunk) {
onChunk(content);
}
}
// 记录使用情况
if (chunk.usage) {
usage = chunk.usage;
}
}
logger.success(`流式响应完成,总长度: ${fullContent.length}`);
// 完成回调
if (onComplete) {
onComplete(usage || {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0
});
}
return {
content: fullContent,
usage
};
} catch (error) {
logger.error('流式请求失败:', error);
if (onError) {
onError(error);
}
throw error;
}
}
作业3:前端实现 SSE 接收
utils/api.js(添加流式方法):
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:3000/api',
timeout: 60000
});
/**
* 流式聊天
* @param {string} message - 用户消息
* @param {Array} conversationHistory - 对话历史
* @param {Object} callbacks - 回调函数
*/
export function streamChatWithAI(message, conversationHistory = [], callbacks = {}) {
const { onChunk, onComplete, onError } = callbacks;
return new Promise((resolve, reject) => {
// 使用 fetch + ReadableStream 实现 SSE
fetch('http://localhost:3000/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
conversationHistory
})
})
.then(response => {
if (!response.ok) {
throw new Error('请求失败');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function readStream() {
reader.read().then(({ done, value }) => {
if (done) {
if (onComplete) {
onComplete();
}
resolve();
return;
}
// 解码数据
buffer += decoder.decode(value, { stream: true });
// 处理 SSE 格式数据
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留不完整行
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
if (data.error) {
if (onError) {
onError(new Error(data.error));
}
reject(new Error(data.error));
return;
}
if (data.done) {
if (onComplete) {
onComplete(data.usage);
}
resolve(data);
return;
}
if (data.content && onChunk) {
onChunk(data.content);
}
} catch (e) {
console.error('解析数据失败:', e);
}
}
}
// 继续读取
readStream();
}).catch(error => {
if (onError) {
onError(error);
}
reject(error);
});
}
readStream();
})
.catch(error => {
if (onError) {
onError(error);
}
reject(error);
});
});
}
作业4:Vue 组件实现流式显示
ChatComponent.vue:
<template>
<div class="chat-container">
<div class="messages" ref="messagesContainer">
<div
v-for="(msg, index) in messages"
:key="index"
:class="['message', msg.type]">
<div class="message-content">
{{ msg.content }}
<span v-if="msg.streaming" class="cursor">|</span>
</div>
<div v-if="msg.usage" class="message-usage">
Token: {{ msg.usage.total_tokens }}
</div>
</div>
</div>
<div class="input-area">
<textarea
v-model="inputMessage"
@keydown.enter.exact.prevent="sendMessage"
@keydown.shift.enter.exact="handleShiftEnter"
placeholder="输入消息(Enter发送,Shift+Enter换行)..."
:disabled="loading"
rows="3" />
<button
@click="sendMessage"
:disabled="loading || !inputMessage.trim()">
{{ loading ? '发送中...' : '发送' }}
</button>
</div>
</div>
</template>
<script>
import { streamChatWithAI } from '@/utils/api';
export default {
data() {
return {
messages: [],
inputMessage: '',
loading: false,
conversationHistory: [],
currentStreamingMessage: null
};
},
methods: {
async sendMessage() {
if (!this.inputMessage.trim() || this.loading) return;
const userMessage = this.inputMessage.trim();
this.inputMessage = '';
// 添加用户消息
this.messages.push({
type: 'user',
content: userMessage,
timestamp: Date.now()
});
// 更新对话历史
this.conversationHistory.push({
role: 'user',
content: userMessage
});
// 创建AI消息占位
const aiMessageIndex = this.messages.length;
this.messages.push({
type: 'ai',
content: '',
streaming: true,
timestamp: Date.now()
});
this.loading = true;
this.currentStreamingMessage = this.messages[aiMessageIndex];
try {
await streamChatWithAI(
userMessage,
this.conversationHistory,
{
onChunk: (content) => {
// 实时更新AI消息内容
this.$set(this.messages[aiMessageIndex], 'content',
this.messages[aiMessageIndex].content + content
);
// 自动滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
},
onComplete: (usage) => {
// 完成流式输出
this.$set(this.messages[aiMessageIndex], 'streaming', false);
this.$set(this.messages[aiMessageIndex], 'usage', usage);
// 更新对话历史
this.conversationHistory.push({
role: 'assistant',
content: this.messages[aiMessageIndex].content
});
this.loading = false;
this.currentStreamingMessage = null;
},
onError: (error) => {
this.messages[aiMessageIndex].content =
error.message || '请求失败,请稍后重试';
this.messages[aiMessageIndex].streaming = false;
this.loading = false;
this.currentStreamingMessage = null;
}
}
);
} catch (error) {
this.messages[aiMessageIndex].content =
error.message || '请求失败,请稍后重试';
this.messages[aiMessageIndex].streaming = false;
this.loading = false;
this.currentStreamingMessage = null;
}
},
handleShiftEnter() {
// Shift+Enter 换行,不做处理
},
scrollToBottom() {
const container = this.$refs.messagesContainer;
if (container) {
container.scrollTop = container.scrollHeight;
}
}
}
};
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
margin-bottom: 20px;
padding: 12px;
border-radius: 8px;
}
.message.user {
background: #e3f2fd;
margin-left: 20%;
}
.message.ai {
background: #f5f5f5;
margin-right: 20%;
}
.message-content {
white-space: pre-wrap;
word-break: break-word;
}
.cursor {
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.message-usage {
font-size: 12px;
color: #999;
margin-top: 8px;
}
.input-area {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
}
.input-area textarea {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: none;
}
.input-area button {
padding: 10px 20px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.input-area button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
作业5:优化用户体验
添加加载动画:
<div v-if="loading && !currentStreamingMessage" class="loading">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
<span>AI正在思考...</span>
</div>
添加错误重试:
async sendMessage(retryCount = 0) {
try {
// ... 发送逻辑
} catch (error) {
if (retryCount < 3) {
// 自动重试
setTimeout(() => {
this.sendMessage(retryCount + 1);
}, 1000 * (retryCount + 1));
} else {
// 显示错误
this.showError('请求失败,请稍后重试');
}
}
}
⚠️ 遇到的问题
问题1:SSE 连接断开
错误信息: EventSource connection closed
解决方案:
// 使用 fetch + ReadableStream 更稳定
// 添加心跳检测
setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
问题2:数据乱码
问题: 中文显示乱码
解决方案:
// 确保使用 UTF-8 编码
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
// 前端正确解码
const decoder = new TextDecoder('utf-8');
问题3:流式响应中断
问题: 网络波动导致中断
解决方案:
// 添加错误处理和重连机制
eventSource.onerror = (error) => {
console.error('SSE错误:', error);
// 实现重连逻辑
};
问题4:内存泄漏
问题: 长时间运行内存占用增加
解决方案:
// 限制对话历史长度
if (this.conversationHistory.length > 20) {
this.conversationHistory = this.conversationHistory.slice(-20);
}
// 清理定时器
beforeDestroy() {
if (this.streamTimer) {
clearInterval(this.streamTimer);
}
}
📊 学习总结
今日收获
- ✅ 理解流式响应原理
- ✅ 掌握 SSE 实现方式
- ✅ 实现 OpenAI Stream API
- ✅ 前端实时显示 AI 回复
- ✅ 优化用户体验
关键知识点
- 流式响应提升体验,即时反馈,类似 ChatGPT
- SSE 适合单向推送,比 WebSocket 更简单
- 逐字显示更自然,用户感觉更流畅
- 错误处理很重要,网络波动需要重连机制
- 性能优化,限制历史长度,避免内存泄漏
技术对比
普通响应:等待时间长,用户体验差
流式响应:即时反馈,体验流畅 ✅
前端开发者的优势
- ✅ 熟悉异步编程(Promise/async-await)
- ✅ 理解事件流(Event Stream)
- ✅ 擅长 UI 交互优化
- ✅ 熟悉状态管理
🔧 代码优化建议
1. 添加流式控制
// 可以暂停/恢复流式输出
let isPaused = false;
onChunk: (content) => {
if (!isPaused) {
// 更新内容
}
}
2. 添加性能监控
const startTime = Date.now();
onComplete: (usage) => {
const duration = Date.now() - startTime;
console.log(`响应时间: ${duration}ms`);
console.log(`Token使用: ${usage.total_tokens}`);
}
3. 添加缓存机制
// 缓存常见问题的回答
const cache = new Map();
if (cache.has(message)) {
// 直接返回缓存
} else {
// 调用API并缓存
}
📅 明日计划
明天将学习:
期待明天的学习! 🚀
📚 参考资源
💻 代码仓库
项目已更新:
- ✅ 实现流式响应接口
- ✅ 前端 SSE 接收
- ✅ 实时显示 AI 回复
- ✅ 用户体验优化
GitHub 提交: Day 04 - 流式响应实现
💭 写在最后
今天完成了流式响应的实现,让 AI 对话体验更加流畅自然。虽然技术实现有一定复杂度,但效果非常值得!明天将整合所有功能,打造一个完整的聊天机器人项目。
继续加油! 💪
✅ 快速检查清单
完成这些,第四天就达标了! ✅
标签: #AI学习 #SSE #流式响应 #用户体验 #学习笔记

浙公网安备 33010602011771号