最近在面试,在整理之前做过的项目,整理的过程中我会整理相关技术栈的实现,本篇是任何对话式ai应用都会遇到的流式输出协议的其中之一socket,全双工协议

项目代码Learn-LLM
一、WebSocket 技术详解
1.1 什么是 WebSocket?
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
核心特性:
特性 | 说明 |
---|---|
🔄 双向通信 | 客户端和服务器可以同时发送和接收消息 |
🚀 低延迟 | 建立连接后无需重复握手,延迟极低 |
💡 实时性 | 服务器可主动推送数据,无需轮询 |
📦 轻量级 | 相比 HTTP 长轮询,开销更小 |
🔌 持久连接 | 一次握手,长期保持连接 |
1.2 WebSocket vs HTTP
lua
HTTP 请求-响应模型:
客户端 ---请求---> 服务器
客户端 <--响应--- 服务器
(每次通信都需要新的请求)
WebSocket 双向通信:
客户端 <=========> 服务器
(建立连接后可双向实时通信)
对比表:
维度 | HTTP | WebSocket |
---|---|---|
通信方式 | 请求-响应 | 双向推送 |
连接状态 | 无状态 | 有状态 |
开销 | 每次请求都有 HTTP 头 | 握手后开销极小 |
实时性 | 需要轮询 | 主动推送 |
适用场景 | 传统 Web 应用 | 实时通信、流式输出 |
1.3 WebSocket 工作原理
握手过程:
http
# 1. 客户端发起升级请求
GET /api/websocket HTTP/1.1
Host: localhost:3000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
# 2. 服务器响应升级
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
# 3. 连接建立,开始双向通信
消息帧格式:
lua
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
二、为什么选择 WebSocket 实现流式输出?
2.1 流式输出的需求场景
在 AI 对话场景中,我们需要:
- 逐字输出:像 ChatGPT 一样实时显示生成的文本
- 低延迟:用户输入后立即看到响应
- 实时性:AI 每生成一个 token 就立即推送
- 良好体验:避免长时间等待,提供视觉反馈
2.2 技术方案对比
方案一:HTTP 轮询 ❌
javascript
// 客户端不断请求
setInterval(() => {
fetch('/api/status').then(res => res.json())
}, 1000); // 每秒请求一次
缺点:
- ❌ 延迟高(轮询间隔限制)
- ❌ 服务器压力大(大量无效请求)
- ❌ 浪费带宽(重复的 HTTP 头)
方案二:Server-Sent Events (SSE) ⚠️
javascript
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (event) => {
console.log(event.data);
};
优点:
- ✅ 服务器主动推送
- ✅ 实现简单
缺点:
- ❌ 单向通信(只能服务器推送)
- ❌ 不支持二进制数据
- ❌ 连接数限制(浏览器限制 6 个)
方案三:WebSocket ✅
javascript
const ws = new WebSocket('ws://localhost:3000/api/websocket');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 处理流式数据
};
优点:
- ✅ 双向实时通信
- ✅ 低延迟(无 HTTP 开销)
- ✅ 支持二进制数据
- ✅ 无连接数限制
- ✅ 完整的连接控制
2.3 WebSocket 在流式输出中的优势
rust
传统 HTTP 流式输出流程:
用户输入 -> HTTP 请求 -> 等待完整响应 -> 一次性显示
⏰ 延迟 5-30 秒 ⏰
WebSocket 流式输出流程:
用户输入 -> WebSocket 发送 -> AI 生成 token 1 -> 立即推送 -> 显示
-> AI 生成 token 2 -> 立即推送 -> 显示
-> AI 生成 token 3 -> 立即推送 -> 显示
⚡ 每个 token 延迟 < 100ms ⚡
三、架构设计
3.1 整体架构
bash
┌─────────────────────────────────────────────────────────────┐
│ 客户端 (React) │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ UI 组件 │ │ WebSocket │ │ 状态管理 │ │
│ │ - 输入框 │→ │ 连接管理 │→ │ - 流式内容累积 │ │
│ │ - 消息显示 │← │ 消息处理 │← │ - 连接状态 │ │
│ └────────────┘ └─────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ ws://localhost:3000/api/websocket
┌─────────────────────────────────────────────────────────────┐
│ 集成服务器 (Next.js + WebSocket) │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ HTTP 服务 │ │ WebSocket │ │ AI 处理 │ │
│ │ - Next.js │ │ - 连接管理 │ │ - LangChain 集成 │ │
│ │ - API 路由 │ │ - 消息路由 │ │ - 流式生成 │ │
│ │ │ │ - 心跳检测 │ │ - OpenAI 调用 │ │
│ └────────────┘ └─────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ API 调用
┌─────────────────────────────────────────────────────────────┐
│ OpenAI API / LangChain │
│ (GPT-3.5/4 等) │
└─────────────────────────────────────────────────────────────┘
3.2 核心设计原则
1. 单端口集成
typescript
// HTTP 和 WebSocket 共用 3000 端口
const server = createServer(nextHandler);
const wss = new WebSocket.Server({
server, // 附加到同一个 HTTP 服务器
path: '/api/websocket',
});
优势:
- ✅ 简化部署(只需暴露一个端口)
- ✅ 避免跨域问题
- ✅ 统一的服务管理
2. 类型安全的消息协议
typescript
interface WebSocketMessage {
type: 'chat' | 'chat-stream' | 'data-stream' | 'notification' | ...;
payload: any;
}
// 使用 TypeScript 确保消息格式正确
const message: WebSocketMessage = {
type: 'chat-stream',
payload: { message: '你好', modelName: 'gpt-3.5-turbo' }
};
3. 连接生命周期管理
typescript
interface ClientConnection {
ws: WebSocket;
id: string; // 唯一标识
connectedAt: number; // 连接时间
lastPing: number; // 最后心跳时间
}
const clients = new Map<string, ClientConnection>();
四、核心实现
4.1 服务器端 - WebSocket 服务器搭建
完整的服务器初始化:
typescript
import { createServer } from 'http';
import next from 'next';
import WebSocket from 'ws';
const app = next({ dev: true });
const handle = app.getRequestHandler();
app.prepare().then(() => {
// 1. 创建 HTTP 服务器
const server = createServer(async (req, res) => {
const parsedUrl = parse(req.url!, true);
await handle(req, res, parsedUrl);
});
// 2. 创建 WebSocket 服务器
const wss = new WebSocket.Server({
server,
path: '/api/websocket',
perMessageDeflate: { // 消息压缩
threshold: 1024, // 大于 1KB 才压缩
concurrencyLimit: 10, // 并发限制
},
});
// 3. 处理 WebSocket 连接
wss.on('connection', (ws: WebSocket, request) => {
console.log('✅ 新客户端连接');
// 连接处理逻辑...
});
// 4. 启动服务器
server.listen(3000, () => {
console.log('🚀 服务器运行在 http://localhost:3000');
console.log('📡 WebSocket: ws://localhost:3000/api/websocket');
});
});
4.2 连接管理
客户端注册与管理:
typescript
// 存储所有活跃连接
const clients = new Map<string, ClientConnection>();
wss.on('connection', (ws: WebSocket) => {
// 生成唯一 ID
const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 创建连接对象
const client: ClientConnection = {
ws,
id: clientId,
connectedAt: Date.now(),
lastPing: Date.now(),
};
// 注册客户端
clients.set(clientId, client);
console.log(`✅ 客户端 ${clientId} 已连接,总连接数: ${clients.size}`);
// 发送欢迎消息
sendMessage(ws, {
type: 'status',
payload: {
message: '🎉 欢迎连接到 WebSocket 服务器',
clientId,
serverTime: new Date().toISOString(),
},
});
// 监听消息
ws.on('message', async (data) => {
const message = JSON.parse(data.toString());
await handleMessage(client, message);
});
// 监听关闭
ws.on('close', (code, reason) => {
console.log(`🔚 客户端 ${clientId} 断开: ${code}`);
clients.delete(clientId);
});
// 监听错误
ws.on('error', (error) => {
console.error(`❌ 客户端 ${clientId} 错误:`, error);
clients.delete(clientId);
});
});
4.3 心跳检测机制
为什么需要心跳检测?
- 检测死连接:客户端异常断开时,服务器可能无法立即感知
- 保持连接活跃:防止中间代理或防火墙关闭空闲连接
- 资源清理:及时释放无效连接占用的资源
实现方式:
typescript
// 1. 启动心跳定时器
function startHeartbeat() {
setInterval(() => {
const now = Date.now();
const timeout = 60000; // 60 秒超时
clients.forEach((client, clientId) => {
// 检查超时
if (now - client.lastPing > timeout) {
console.log(`💀 客户端 ${clientId} 心跳超时,断开连接`);
client.ws.terminate();
clients.delete(clientId);
}
// 发送心跳
else if (client.ws.readyState === WebSocket.OPEN) {
client.ws.ping();
console.log(`💓 向客户端 ${clientId} 发送心跳`);
}
});
}, 30000); // 每 30 秒检查一次
}
// 2. 监听心跳响应
ws.on('pong', () => {
client.lastPing = Date.now();
console.log(`💓 收到客户端 ${clientId} 心跳响应`);
});
// 3. 处理客户端主动心跳
async function handlePing(client: ClientConnection, payload: any) {
client.lastPing = Date.now();
sendMessage(client.ws, {
type: 'pong',
payload: {
timestamp: Date.now(),
originalTimestamp: payload?.timestamp,
latency: Date.now() - payload?.timestamp,
},
});
}
4.4 消息路由系统
typescript
// 消息类型定义
interface WebSocketMessage {
type: 'ping' | 'chat' | 'chat-stream' | 'data-stream'
| 'notification' | 'log-stream' | 'broadcast' | 'custom';
payload: any;
}
// 消息处理路由
async function handleMessage(
client: ClientConnection,
message: WebSocketMessage
): Promise<void> {
const { type, payload } = message;
try {
switch (type) {
case 'ping':
await handlePing(client, payload);
break;
case 'chat':
await handleChatMessage(client, payload);
break;
case 'chat-stream':
await handleStreamingChat(client, payload);
break;
case 'data-stream':
await handleDataStream(client, payload);
break;
case 'notification':
await handleNotification(client, payload);
break;
case 'log-stream':
await handleLogStream(client, payload);
break;
case 'broadcast':
await handleBroadcast(client, payload);
break;
case 'custom':
await handleCustomMessage(client, payload);
break;
default:
sendMessage(client.ws, {
type: 'error',
payload: { message: `未知消息类型: ${type}` },
});
}
} catch (error) {
console.error(`❌ 处理消息 ${type} 时出错:`, error);
sendMessage(client.ws, {
type: 'error',
payload: {
message: `处理失败: ${error.message}`,
},
});
}
}
五、AI 流式对话集成
5.1 LangChain 集成架构
scss
用户消息
→ WebSocket 接收
→ 消息路由 (chat-stream)
→ LangChain 处理链
→ OpenAI 流式 API
→ 逐 token 生成
→ WebSocket 推送
→ 前端实时显示
5.2 流式 AI 对话实现
完整实现代码:
typescript
import { ChatOpenAI } from '@langchain/openai';
import {
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
} from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
interface ChatPayload {
message: string;
system?: string;
temperature?: number;
modelName?: string;
}
async function handleStreamingChat(
client: ClientConnection,
payload: ChatPayload
): Promise<void> {
const {
message,
system = 'You are a helpful AI assistant. Please respond in Chinese.',
temperature = 0.7,
modelName = 'gpt-3.5-turbo',
} = payload;
console.log(`🌊 开始流式 AI 对话:`, { clientId: client.id, message });
// 1. 验证环境变量
if (!process.env.OPEN_API_KEY) {
sendMessage(client.ws, {
type: 'chat-error',
payload: { message: '❌ 服务器未配置 OpenAI API 密钥' },
});
return;
}
// 2. 发送开始状态
sendMessage(client.ws, {
type: 'chat-start',
payload: { message: '🤖 正在思考您的问题...' },
});
try {
// 3. 初始化 ChatOpenAI(流式模式)
const llm = new ChatOpenAI({
openAIApiKey: process.env.OPEN_API_KEY!,
modelName: modelName,
temperature: temperature,
maxTokens: 2000,
streaming: true, // 🔥 关键:启用流式输出
configuration: {
baseURL: process.env.OPEN_API_BASE_URL,
},
});
// 4. 创建聊天提示模板
const chatPrompt = ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(system),
HumanMessagePromptTemplate.fromTemplate('{userMessage}'),
]);
// 5. 创建处理链
const chain = chatPrompt.pipe(llm).pipe(new StringOutputParser());
// 6. 流式调用 LLM
const stream = await chain.stream({
userMessage: message,
});
let totalTokens = 0;
let chunkCount = 0;
let fullResponse = '';
// 7. 逐块处理流式响应
for await (const chunk of stream) {
// 检查连接状态
if (client.ws.readyState !== WebSocket.OPEN) {
console.log(`⚠️ 客户端 ${client.id} 连接已断开,停止流式传输`);
break;
}
chunkCount++;
totalTokens += chunk.length;
fullResponse += chunk;
// 8. 发送流式内容到客户端
sendMessage(client.ws, {
type: 'chat-stream',
payload: {
content: chunk, // 本次生成的内容片段
chunkCount, // 已发送块数
totalTokens, // 总 token 数
},
});
// 添加小延迟,模拟打字效果(可选)
await new Promise((resolve) => setTimeout(resolve, 30));
}
console.log(
`✅ 流式响应完成: ${chunkCount} chunks, ${totalTokens} tokens`
);
// 9. 发送完成状态
sendMessage(client.ws, {
type: 'chat-complete',
payload: {
message: '✅ 回答生成完成',
fullResponse, // 完整回复内容
stats: {
chunks: chunkCount,
tokens: totalTokens,
model: modelName,
},
},
});
} catch (error) {
console.error(`❌ 流式聊天错误:`, error);
// 10. 发送错误消息
sendMessage(client.ws, {
type: 'chat-error',
payload: {
message: `流式 AI 处理失败: ${error.message}`,
originalMessage: message,
},
});
}
}
5.3 关键技术点解析
1. 流式输出的核心配置
typescript
const llm = new ChatOpenAI({
streaming: true, // 🔥 必须设置为 true
// ...其他配置
});
// 使用 stream() 而不是 invoke()
const stream = await chain.stream({ userMessage: message });
// 使用 for await 循环处理流式数据
for await (const chunk of stream) {
// 每个 chunk 是一小段文本
console.log(chunk); // "你", "好", ",", "我", "是"...
}
2. 连接状态检测
typescript
// 在流式循环中检查连接状态
for await (const chunk of stream) {
// 如果客户端断开,立即停止生成
if (client.ws.readyState !== WebSocket.OPEN) {
console.log('客户端已断开,停止流式传输');
break;
}
// 发送数据...
}
3. 错误处理和状态通知
typescript
// 发送开始状态
sendMessage(ws, { type: 'chat-start', ... });
// 流式发送内容
sendMessage(ws, { type: 'chat-stream', payload: { content: chunk } });
// 发送完成状态
sendMessage(ws, { type: 'chat-complete', ... });
// 发送错误状态
sendMessage(ws, { type: 'chat-error', ... });
5.4 消息流转时序图
lua
客户端 WebSocket 服务器 LangChain/OpenAI
| | |
|--1. 发送聊天请求-------->| |
| {type:'chat-stream'} | |
| | |
|<--2. 开始状态消息--------| |
| {type:'chat-start'} | |
| | |
| |--3. 调用 LLM 流式 API--->|
| | |
| |<--4. token: "你" --------|
|<--5. 流式消息------------| |
| {type:'chat-stream', | |
| content: "你"} | |
| | |
| |<--6. token: "好" --------|
|<--7. 流式消息------------| |
| {content: "好"} | |
| | |
| |<--8. token: "," --------|
|<--9. 流式消息------------| |
| | |
| ... (循环继续,直到生成完成) ... |
| | |
| |<--10. 生成完成 ----------|
|<--11. 完成状态消息-------| |
| {type:'chat-complete'} | |
| | |
六、前端实现
6.1 React WebSocket 客户端
完整的 React Hook 实现:
tsx
'use client';
import { useState, useRef, useEffect } from 'react';
export default function WebSocketChat() {
// 状态管理
const [isConnected, setIsConnected] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<
'disconnected' | 'connecting' | 'connected' | 'error'
>('disconnected');
const [streamingContent, setStreamingContent] = useState('');
const [messages, setMessages] = useState<any[]>([]);
const [inputMessage, setInputMessage] = useState('');
// WebSocket 引用
const wsRef = useRef<WebSocket | null>(null);
// 组件卸载时清理
useEffect(() => {
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, []);
// 连接 WebSocket
const connectWebSocket = () => {
if (isConnected || connectionStatus === 'connecting') {
return;
}
setConnectionStatus('connecting');
setStreamingContent('');
setMessages([]);
// 创建 WebSocket 连接
const wsUrl = `ws://${window.location.host}/api/websocket`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
// 连接打开
ws.onopen = () => {
console.log('✅ WebSocket 连接已建立');
setConnectionStatus('connected');
setIsConnected(true);
addMessage('info', '🚀 已连接到 WebSocket 服务器');
};
// 接收消息
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (error) {
console.error('消息解析错误:', error);
}
};
// 连接错误
ws.onerror = (error) => {
console.error('❌ WebSocket 错误:', error);
setConnectionStatus('error');
};
// 连接关闭
ws.onclose = (event) => {
console.log('🔚 WebSocket 连接已关闭:', event.code);
setConnectionStatus('disconnected');
setIsConnected(false);
addMessage('info', `🔚 连接已关闭 (${event.code})`);
};
};
// 处理 WebSocket 消息
const handleWebSocketMessage = (data: any) => {
const { type, payload } = data;
switch (type) {
case 'status':
addMessage('info', payload?.message || '状态更新');
break;
case 'chat-start':
addMessage('info', '🤖 AI 开始思考...');
setStreamingContent(''); // 清空之前的内容
break;
case 'chat-stream':
// 🔥 关键:累积流式内容
if (payload?.content) {
setStreamingContent((prev) => prev + payload.content);
}
break;
case 'chat-complete':
addMessage('success', '✅ 回答生成完成');
if (payload?.stats) {
addMessage(
'info',
`📊 统计: ${payload.stats.chunks} 块, ${payload.stats.tokens} tokens`
);
}
break;
case 'chat-error':
addMessage('error', payload?.message || '❌ AI 聊天出错');
break;
default:
addMessage('data', JSON.stringify(data));
}
};
// 发送聊天消息
const sendChatMessage = () => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
alert('WebSocket 未连接');
return;
}
if (!inputMessage.trim()) {
return;
}
const message = {
type: 'chat-stream',
payload: {
message: inputMessage,
system: 'You are a helpful AI assistant. Please respond in Chinese.',
temperature: 0.7,
modelName: 'gpt-3.5-turbo',
},
};
wsRef.current.send(JSON.stringify(message));
addMessage('info', `📤 发送消息: ${inputMessage}`);
setInputMessage('');
};
// 添加消息到历史
const addMessage = (type: string, content: string) => {
setMessages((prev) => [
...prev,
{
id: Date.now(),
type,
content,
timestamp: Date.now(),
},
]);
};
// 断开连接
const disconnectWebSocket = () => {
if (wsRef.current) {
wsRef.current.close();
}
};
return (
<div className="p-6 space-y-6">
{/* 连接控制 */}
<div className="flex gap-4">
<button
onClick={connectWebSocket}
disabled={isConnected}
className="px-6 py-2 bg-blue-600 text-white rounded-md disabled:bg-gray-400"
>
{connectionStatus === 'connecting' ? '连接中...' : '连接'}
</button>
<button
onClick={disconnectWebSocket}
disabled={!isConnected}
className="px-6 py-2 bg-red-600 text-white rounded-md disabled:bg-gray-400"
>
断开
</button>
<div className="flex items-center gap-2">
<span>状态:</span>
<span className={`px-3 py-1 rounded ${
connectionStatus === 'connected' ? 'bg-green-100 text-green-800' :
connectionStatus === 'connecting' ? 'bg-yellow-100 text-yellow-800' :
connectionStatus === 'error' ? 'bg-red-100 text-red-800' :
'bg-gray-100 text-gray-800'
}`}>
{connectionStatus}
</span>
</div>
</div>
{/* AI 对话展示区 */}
<div className="border rounded-lg p-6 bg-white">
<h3 className="text-lg font-bold mb-4">🤖 AI 对话</h3>
<div className="min-h-[300px] max-h-[500px] overflow-y-auto border rounded p-4 bg-gray-50">
{streamingContent ? (
<div className="prose prose-sm max-w-none">
{/* 使用 Markdown 渲染库或简单显示 */}
<pre className="whitespace-pre-wrap">{streamingContent}</pre>
</div>
) : (
<div className="text-center text-gray-500 py-20">
<div className="text-4xl mb-4">🤖</div>
<p>点击"连接"开始 AI 对话</p>
<p className="text-xs mt-2">AI 回复将在这里实时显示</p>
</div>
)}
</div>
</div>
{/* 输入区 */}
<div className="flex gap-2">
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendChatMessage()}
placeholder="输入要对话的内容..."
className="flex-1 px-4 py-2 border rounded-md"
disabled={!isConnected}
/>
<button
onClick={sendChatMessage}
disabled={!isConnected || !inputMessage.trim()}
className="px-6 py-2 bg-green-600 text-white rounded-md disabled:bg-gray-400"
>
发送
</button>
</div>
{/* 消息历史 */}
<div className="border rounded-lg p-4 bg-gray-50">
<h4 className="font-medium mb-2">消息历史</h4>
<div className="h-64 overflow-y-auto space-y-2">
{messages.map((msg) => (
<div
key={msg.id}
className="p-2 bg-white rounded shadow-sm text-sm"
>
<span className="text-gray-500 text-xs">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
<p>{msg.content}</p>
</div>
))}
</div>
</div>
</div>
);
}
6.2 关键技术点
1. 流式内容累积
tsx
// 错误做法:直接替换
case 'chat-stream':
setStreamingContent(payload.content); // ❌ 只会显示最后一个字符
// 正确做法:累积追加
case 'chat-stream':
setStreamingContent((prev) => prev + payload.content); // ✅ 逐字累积
2. 连接状态管理
tsx
// 使用状态机模式
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
// 根据状态控制 UI
{connectionStatus === 'connected' && <ChatInterface />}
{connectionStatus === 'connecting' && <LoadingSpinner />}
{connectionStatus === 'error' && <ErrorMessage />}
3. 资源清理
tsx
// 组件卸载时清理 WebSocket
useEffect(() => {
return () => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
};
}, []);
6.3 实时 Markdown 渲染
如果 AI 返回 Markdown 格式,可以使用 streamdown
库实现流式渲染:
tsx
import { Streamdown } from 'streamdown';
<Streamdown
parseIncompleteMarkdown={true} // 支持不完整的 Markdown
className="prose prose-sm max-w-none"
>
{streamingContent}
</Streamdown>
效果:
makefile
AI 正在输出: "# 标题\n\n这是一段..."
实时渲染为:
# 标题
这是一段...
🎯 总结
WebSocket 流式输出的核心要点
-
技术选型
- ✅ WebSocket 提供双向实时通信能力
- ✅ 相比 HTTP 轮询和 SSE,延迟更低、功能更强大
- ✅ 适合 AI 流式对话、实时数据推送等场景
-
架构设计
- ✅ 单端口集成 HTTP + WebSocket
- ✅ 类型安全的消息协议
- ✅ 完善的连接生命周期管理
- ✅ 心跳检测和自动重连
-
AI 集成
- ✅ LangChain 流式 API 集成
- ✅ 逐 token 推送到客户端
- ✅ 完整的错误处理和状态通知
- ✅ 连接中断时的优雅降级
-
前端实现
- ✅ React Hooks 管理 WebSocket 状态
- ✅ 流式内容累积显示
- ✅ 实时 Markdown 渲染
- ✅ 资源清理和错误处理
-
生产级实践
- ✅ 性能优化(消息压缩、连接池)
- ✅ 安全性(身份验证、速率限制)
- ✅ 监控与日志(结构化日志、性能指标)
- ✅ 部署方案(负载均衡、Docker)
适用场景
- 🤖 AI 对话系统:ChatGPT 风格的流式对话
- 📊 实时数据可视化:股票、监控数据实时更新
- 💬 即时通讯:聊天应用、协作工具
- 🔔 推送通知:系统通知、消息提醒
- 📋 日志流:实时日志监控、部署日志