实现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,为举例
相关推荐
JarvanMo11 分钟前
Swift 应用在安卓系统上会怎么样?
前端
LinXunFeng24 分钟前
Flutter - 详情页 TabBar 与模块联动?秒了!
前端·flutter·开源
萌萌哒草头将军24 分钟前
Oxc 最新 Transformer Alpha 功能速览! 🚀🚀🚀
前端·javascript·vue.js
Justinc.1 小时前
HTML5新增属性
前端·html·html5
1024小神2 小时前
nextjs项目build导出静态文件
前端·javascript
阿聪_2 小时前
createContext 还是 useSyncExternalStore?一文讲清场景与选型
前端
Linsk2 小时前
当我把前端条件加载做到极致
前端·前端工程化
_辉2 小时前
大模型构建表单与数据结构
前端
祝鹏2 小时前
动态表单生成
前端
luckyJian2 小时前
React深入浅出理解
前端