实现接口流式响应数据

在过去的2023年,人工智能,chatgpt 着实火遍全网,大公司都在纷纷发布自己的大语言模型,小点的公司都在基于这些模型来开发上层应用,各类应用层出不穷。而在实现这些应用的过程中,其中有一个小功能就是如何在回答问题时,将结果逐词逐字的显示出来,有种真实在对话的感觉。这种方式可以快速给予用户响应,避免用户在经过漫长的等待之后,一次性给用户展示整段文本。

流式响应

要实现这种响应结果逐词逐句显示,则需要服务端每生成一点内容就往客户端推送一点内容,客户端接收一点内容就展示一点内容。而相关的技术也不是在2023年才有的,像轮询,websocket 等都可以实现这种效果。只是以往的web应用基本用不到这样的功能,所以接口请求都是在完整接收数据之后现处理数据,展示数据。即使是社交通信工具的对话文本也是整段发送显示的。导致有不少开发同学只是听说过流式响应,而并没有实际应用过。前段时间看到有不少人在问如何实现类似 chatgpt 那种对话效果。正好2023我们也做过这样的应用,也需要做这样的效果,就把我们公司的实现方法拿来讲一下

我们的项目后端选用的midway.js 前端则是 vue3

代码

在服务端将Readable的一个实例对象作为响应返回

typescript 复制代码
import { Controller, Get } from '@midwayjs/core';
import { Readable } from 'stream';

@Controller('/api')
export class APIController {
  @Get('/ask')
  async ask() {
    const result = [...'人间四月芳菲尽,山寺桃花始盛开,长恨春归无觅处,不知转入此中来。'];

    const readable = new Readable({ read: () => {} });
    function push(i: number) {
      if (result[i]) {
        // 还有字符则继续 push
        readable.push(result[i]);
        // 100ms 之后读下一个字符
        setTimeout(push.bind(null, i + 1), 100);
      } else {
        // 没有字符之后 push 参数设为 null 标记服务端内容响应结束
        readable.push(null);
      }
    }
    setTimeout(push.bind(null, 0), 100);
    return readable;
  }
}

在前端,使用 fetch 发送请求,从响应获取reader对象,并从中读取数据

typescript 复制代码
let content = '';
const btn = document.querySelector('.btn');
const resultContainer = document.querySelector('.res');

btn.addEventListener('click', async () => {
  const response = await fetch('/api/ask');
  const reader = response.body.getReader();
  // 不停的调用 reader.read 方法,写法有很多,能实现就行
  async function read() {
    const {done, value} = await reader.read();
    if (done) {
      // 服务端的响应结束,不会再有后续的响应了
      return;
    }
    // 进行解码获取文本
    content += new TextDecoder().decode(value);
    resultContainer.innerText = content;
    // 不停的调用 read, 读取后续内容,
    setTimeout(read);
  }
  read();
})

如果想要使用 XMLHttpRequest 也是可以的,代码如下

js 复制代码
const btn = document.querySelector('.btn');
const resultContainer = document.querySelector('.res');

btn.addEventListener('click', async () => {
  const xhr = new XMLHttpRequest();
  xhr.open('get', '/api/ask');

  xhr.addEventListener('readystatechange', () => {
    if (xhr.readyState === xhr.DONE) {
      // 响应已经全部拿到了
    }
  })

  xhr.addEventListener('progress', (e) => {
    resultContainer.innerText = e.target.responseText;
  })

  xhr.send();
})

如果你在开发的时候是正常的,响应数据分次返回,逐词显示,但是部分到线上之后就又变成了长时间等待之后一次性展示的话,不要怀疑你的代码,毕竟在本机是正常的。这时候很有可以是你的生产环境造成,比如 nginx 配置其它的什么把服务端分次返回的数据进行缓存合并,然后才发送给前端页面

nginx 复制代码
location / {
  proxy_buffering off;
  proxy_pass http://127.0.0.1:7001/;
}

我这里演示所有,所有路径的代理都关闭代理缓冲功能


原本这是半年前的做的功能了,最近又有人来问这个问题,所以在这里重写一下

相关推荐
噢,我明白了12 小时前
JavaScript 中处理时间格式的核心方式
前端·javascript
纸上的彩虹13 小时前
半年一百个页面,重构系统也重构了我对前端工作的理解
前端·程序员·架构
be or not to be14 小时前
深入理解 CSS 浮动布局(float)
前端·css
LYFlied14 小时前
【每日算法】LeetCode 1143. 最长公共子序列
前端·算法·leetcode·职场和发展·动态规划
老华带你飞14 小时前
农产品销售管理|基于java + vue农产品销售管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
小徐_233314 小时前
2025 前端开源三年,npm 发包卡我半天
前端·npm·github
GIS之路15 小时前
GIS 数据转换:使用 GDAL 将 Shp 转换为 GeoJSON 数据
前端
JIngJaneIL15 小时前
基于springboot + vue房屋租赁管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
天天扭码15 小时前
以浏览器多进程的角度解构页面渲染的整个流程
前端·面试·浏览器
你们瞎搞15 小时前
Cesium加载20GB航测影像.tif
前端·cesium·gdal·地图切片