LLM流式输出完全解析之socket

最近在面试,在整理之前做过的项目,整理的过程中我会整理相关技术栈的实现,本篇是任何对话式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 对话场景中,我们需要:

  1. 逐字输出:像 ChatGPT 一样实时显示生成的文本
  2. 低延迟:用户输入后立即看到响应
  3. 实时性:AI 每生成一个 token 就立即推送
  4. 良好体验:避免长时间等待,提供视觉反馈

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 心跳检测机制

为什么需要心跳检测?

  1. 检测死连接:客户端异常断开时,服务器可能无法立即感知
  2. 保持连接活跃:防止中间代理或防火墙关闭空闲连接
  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 流式输出的核心要点

  1. 技术选型

    • ✅ WebSocket 提供双向实时通信能力
    • ✅ 相比 HTTP 轮询和 SSE,延迟更低、功能更强大
    • ✅ 适合 AI 流式对话、实时数据推送等场景
  2. 架构设计

    • ✅ 单端口集成 HTTP + WebSocket
    • ✅ 类型安全的消息协议
    • ✅ 完善的连接生命周期管理
    • ✅ 心跳检测和自动重连
  3. AI 集成

    • ✅ LangChain 流式 API 集成
    • ✅ 逐 token 推送到客户端
    • ✅ 完整的错误处理和状态通知
    • ✅ 连接中断时的优雅降级
  4. 前端实现

    • ✅ React Hooks 管理 WebSocket 状态
    • ✅ 流式内容累积显示
    • ✅ 实时 Markdown 渲染
    • ✅ 资源清理和错误处理
  5. 生产级实践

    • ✅ 性能优化(消息压缩、连接池)
    • ✅ 安全性(身份验证、速率限制)
    • ✅ 监控与日志(结构化日志、性能指标)
    • ✅ 部署方案(负载均衡、Docker)

适用场景

  • 🤖 AI 对话系统:ChatGPT 风格的流式对话
  • 📊 实时数据可视化:股票、监控数据实时更新
  • 💬 即时通讯:聊天应用、协作工具
  • 🔔 推送通知:系统通知、消息提醒
  • 📋 日志流:实时日志监控、部署日志

📚 参考资源

相关推荐
ObjectX前端实验室4 小时前
ChatGPT流式输出完全解析之SSE
前端·人工智能
又是忙碌的一天4 小时前
前端学习 JavaScript(2)
前端·javascript·学习
2501_915106324 小时前
JavaScript编程工具有哪些?老前端的实用工具清单与经验分享
开发语言·前端·javascript·ios·小程序·uni-app·iphone
GISer_Jing4 小时前
计算机基础——浏览器、算法、计算机原理和编译原理等
前端·javascript·面试
我的xiaodoujiao4 小时前
从 0 到 1 搭建 Python 语言 Web UI自动化测试学习系列 8--基础知识 4--常用函数 2
前端·python·测试工具·ui
蓝瑟5 小时前
React 项目实现拖拽排序功能,如何在众多库中选对 “它”
前端·javascript·react.js
万少5 小时前
开发者注意了 DevEco Studio 6 Release 开放了,但是我劝你慎重升级6应用
前端
小刘不知道叫啥6 小时前
React 源码揭秘 | 合成事件
前端·javascript·react.js
ziyue75756 小时前
vue修改element-ui的默认的class
前端·vue.js·ui