前端长链接架构设计:WebSocket数据流实现AI对话页

背景

最近新增了一个产品项目“今天炒什么”,这是一款面向短线用户的热点选股工具。展示每日精选的热点话题,及相关话题近一个月的热度值。有一个“热点解读”功能,需要调用大模型接口实现一个AI对话页。其中大模型接口为流式响应,所以决定采用 WebSocket 技术实现前后端实时通信,确保为用户提供流畅自然的对话体验。

1 架构设计

1.1 前端对话页功能设计

  • 建立 WebSocket 连接,确认连接状态
  • 发送用户消息
  • 接收并实时渲染 AI 回复
  • 实现对话打字机效果

1.2 通信流程

  1. 连接建立
    • 通过 new WebSocket('ws://xxxxxx') 创建实例,绑定事件处理函数并建立连接
  2. 消息发送
    • 用户输入消息后,前端通过 ws.send() 发送 JSON 格式消息
  3. 流式响应
    • 大模型接口每生成一个字符,立即通过 WebSocket 推送给前端
    • 前端收到字符后,实时更新界面,实现打字机效果
  4. 连接管理
    • 通过 onmessage 事件接收后端推送的数据,实时更新界面
    • 前端监听 onopenonmessageoncloseonerror 事件,管理连接状态

2 核心代码解析

2.1 前端 WebSocket 连接


useEffect(() => {
  const ws = new WebSocket('ws://127.0.0.1:3000');
  wsRef.current = ws;

  ws.onopen = () => {...};
  ws.onclose = () => {...};
  ws.onerror = (error) => {...};

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    // 处理不同类型的消息:start、token、end、error
  };
  return () => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.close();
    }
  };
}, []);

2.2 建立 WebSocket 连接

在前端,我们通过 new WebSocket() 创建一个 WebSocket 实例,并绑定事件处理函数:


const ws = new WebSocket('ws://xxxxxx');

ws.onopen = () => {
  console.log('WebSocket连接已建立');
  setIsConnected(true);
};

ws.onclose = () => {
  console.log('WebSocket连接已关闭');
  setIsConnected(false);
};

ws.onerror = (error) => {
  console.error('WebSocket错误:', error);
  setIsConnected(false);
};

2. 3 发送消息

用户输入消息后,前端通过 ws.send() 发送 JSON 格式数据:


// 输入框提交文本事件
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  if (!inputValue.trim() || !isConnected) return;
	// 用户提问信息
  setMessages(prev => [...prev, { role: 'user', content: inputValue }]);
 	// 像大模型接口发送提问消息
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({
      type: 'message',
      content: inputValue
    }));
  }
  // 清空输入框
  setInputValue('');
};

2.4 接收流式响应

前端通过 onmessage 事件接收后端推送的数据,并根据消息类型实时更新界面:


ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  
  switch (data.type) {
    case 'start':
      // 开始新的AI响应,新增一条机器人消息
      setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
      setIsTyping(true);
      break;
      
    case 'token':
      // 接收新的token并更新消息
      setMessages((prev: Message[]) => {
        const newMessages = [...prev];
        const lastIndex = newMessages.length - 1;
        if (lastIndex >= 0 && newMessages[lastIndex].role === 'assistant') {
          return [
            ...newMessages.slice(0, lastIndex),
            {
              ...newMessages[lastIndex],
              content: newMessages[lastIndex].content + data.token
            }
          ];
        }
        return newMessages;
      });
      break;
      
    case 'end':
      // 响应结束
      setIsTyping(false);
      break;
      
    case 'error':
      // 处理错误
      console.error('WebSocket错误:', data.error);
      setIsTyping(false);
      break;
  }
};

3 其他弃用方案

在方案的设计过程中,后端提出了一种方案:自己调用大模型接口,等文本全部返回后再返回给前端。这样前端就可以直接渲染整段文本,不需要接收实时渲染的数据。

核心逻辑就是使用typed.js 插件渲染文本,实现打字机效果,关键代码如下:


import Typed from 'typed.js';
const TypedReact = (text) => {
  const typedElement = useRef(null);
  const typed = useRef(null);

  useEffect(() => {
    if (typedElement.current) {
      // 初始化 Typed 实例
      typed.current = new Typed(typedElement.current, {
        strings: [text],
        typeSpeed: 100,
        startDelay: 500,
        showCursor: true,
        cursorChar: '|',
        autoInsertCss: true,
        onComplete: (self) => {
          // 打字完成后的回调
        }
      });
    }
    // 组件卸载时清理 Typed 实例
    return () => {
      if (typed.current) {
        typed.current.destroy();
      }
    };
  }, [text]); // 当这些属性变化时重新初始化
  return (
    <div className='typed-container'>
      <span ref={typedElement}></span>
    </div>
  );
};

由于大模型接口答案较长,在全部响应完成之前loading时间过长,所以最终决定弃用此方案。

posted @ 2025-06-11 15:28  Justus-  阅读(465)  评论(0)    收藏  举报