实现AI聊天-核心技术点详解

实现chatGpt的效果,包含以下几个技术点:

1. 流式获取

要实现像chatgpt一个字一个字回复的效果,涉及到流式获取数据SSE(Server - Sent Events)。

sse利用向客户端声明,接下来要发送的是流信息的机制,向浏览器连续不断地推送信息

这完全不同于与平常我们所用的一次性加载方式。在传统方式中,前端发起请求后,需要等待后端准备好完整数据才能一次性返回;而流式方式则是后端逐步返回数据片段(chunk),前端逐块接收和处理,实现"边接收边处理"的效果。

如何实现:

  1. 后端实现

后端设置响应头:(记得添加charset=utf-8,声明编码格式,防止中文乱码

vbnet 复制代码
text/event-stream;charset=utf-8

响应体格式:

每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。

markdown 复制代码
[field]: value\n

上面的field可以取四个值。

  • data
  • event
  • id
  • retry

返回示例:

vbnet 复制代码
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

data: {"username": "bobby", "time": "02:34:23"}

data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}

# Server-Sent Events 教程-阮一峰- 4.1 数据格式

MDN-使用服务器发送事件-事件流格式

用nodejs,express实现一个简易的sse接口:

js 复制代码
const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;

// 配置CORS选项
const corsOptions = {
  origin: true, // 或指定域名如 'http://example.com'
  methods: ['GET', 'OPTIONS'], // SSE通常使用GET
  allowedHeaders: ['Content-Type'],
  credentials: true, // 如果需要凭证
};

// 应用CORS中间件
app.use(cors(corsOptions));

app.get('/sse', (req, res) => {
  const text = '随便写一句话,看看效果';
  const speed = 100;

  res.writeHead(200, {
    'Content-Type': 'text/event-stream;charset=utf-8',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    // 添加cors
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Credentials': true,
  });

  let index = 0;
  const timer = setInterval(() => {
    if (index < text.length) {
      res.write(`data: ${text[index]}\n\n`);
      index++;
    } else {
      clearInterval(timer);
      res.write('event: end\ndata: stream ended\n\n');
      res.end();
    }
  }, speed);

  req.on('close', () => {
    clearInterval(timer);
    res.end();
  });
});

app.listen(port, () => {
  console.log(`SSE Typewriter server running at http://localhost:${port}`);
});

如果是nginx部署,需注意添加接口配置

关闭代理缓冲 proxy_buffering off;详见处理 EventStream 不能流式返回的问题:Nginx 配置优化

  1. 前端实现:

利用fetch api处理,示例如下:

js 复制代码
<!DOCTYPE html>
<html>
  <head>
    <title>获取流式数据 Demo</title>
    <style>
      #output {
        font-family: monospace;
        font-size: 24px;
        min-height: 100px;
        border: 1px solid #ccc;
        padding: 10px;
        white-space: pre-wrap;
      }
    </style>
  </head>
  <body>
    <button id="startBtn">获取流式数据</button>
    <div id="output"></div>

    <script>
      document
        .getElementById('startBtn')
        .addEventListener('click', async () => {
          const output = document.getElementById('output');
          output.innerHTML = ''; // 清空输出

          try {
            const response = await fetch('http://localhost:3000/sse', {
              method: 'GET',
              headers: {
                'Content-Type': 'application/json',
              },
            });

            if (!response.ok) {
              throw new Error(`HTTP error! status: ${response.status}`);
            }

            // 获取可读流
            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            // 递归读取流
            const readChunk = async () => {
              const { done, value } = await reader.read();

              if (done) {
                return;
              }

              // 解码并显示字符
              const chunk = decoder.decode(value);
              const text = chunk.split('data: ')[1].replace('\n\n', '');
              console.log(text);

              output.innerHTML += text || '';

              // 继续读取下一个字符
              readChunk();
            };

            readChunk();
          } catch (error) {
            console.error('Error:', error);
            output.textContent = 'Error: ' + error.message;
            cursor.remove();
          }
        });
    </script>
  </body>
</html>

这里前端处理方式还有eventSoure对象,但有局限,只支持get请求。以及相关库实现@microsoft/fetch-event-source。流式处理前端方案对比详见:juejin.cn/post/747810... juejin.cn/post/747849...

2. 体验优化

  1. 暂停控制:通过AbortController中断流

示例:

js 复制代码
const controller = new AbortController(); //创建AbortController对象

async function streamData() {
  try {
    const response = await fetch('/stream-endpoint', {
      signal: controller.signal // 添加signal
    });
    
    const reader = response.body.getReader();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      console.log('收到数据块:', value);
    }
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('流已中断');
    } else {
      console.error('流错误:', err);
    }
  }
}

// 中断流
function cancelStream() {
  controller.abort(); // 在合适时机触发abort,取消请求
}
  1. 智能滚动:仅当用户未手动滚动时自动滚动到底部
js 复制代码
// 智能滚动:仅当用户未手动滚动时生效
if (isNearBottom()) container.scrollTop = container.scrollHeight;

 // 判断是否接近底部
  isNearBottom() {
    const { scrollTop, scrollHeight, clientHeight } = this.container;
    return scrollHeight - (scrollTop + clientHeight) < this.threshold;
  }
  
this.threshold = 10; // 距离底部的阈值(px),10,为举例
相关推荐
Pedantic1 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘1 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆1 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师2 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆2 小时前
VSCode自动格式化三要素
前端
爱勇宝3 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen4 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端