在过去的2023年,人工智能,chatgpt 着实火遍全网,大公司都在纷纷发布自己的大语言模型,小点的公司都在基于这些模型来开发上层应用,各类应用层出不穷。而在实现这些应用的过程中,其中有一个小功能就是如何在回答问题时,将结果逐词逐字的显示出来,有种真实在对话的感觉。这种方式可以快速给予用户响应,避免用户在经过漫长的等待之后,一次性给用户展示整段文本。
流式响应
要实现这种响应结果逐词逐句显示,则需要服务端每生成一点内容就往客户端推送一点内容,客户端接收一点内容就展示一点内容。而相关的技术也不是在2023年才有的,像轮询,websocket 等都可以实现这种效果。只是以往的web应用基本用不到这样的功能,所以接口请求都是在完整接收数据之后现处理数据,展示数据。即使是社交通信工具的对话文本也是整段发送显示的。导致有不少开发同学只是听说过流式响应,而并没有实际应用过。前段时间看到有不少人在问如何实现类似 chatgpt 那种对话效果。正好2023我们也做过这样的应用,也需要做这样的效果,就把我们公司的实现方法拿来讲一下
我们的项目后端选用的midway.js 前端则是 vue3
代码
在服务端将Readable的一个实例对象作为响应返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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]) { readable.push(result[i]); setTimeout(push.bind(null, i + 1), 100); } else { readable.push(null); } } setTimeout(push.bind(null, 0), 100); return readable; } }
|
在前端,使用 fetch 发送请求,从响应获取reader对象,并从中读取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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(); async function read() { const {done, value} = await reader.read(); if (done) { return; } content += new TextDecoder().decode(value); resultContainer.innerText = content; setTimeout(read); } read(); })
|
如果想要使用 XMLHttpRequest 也是可以的,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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 配置其它的什么把服务端分次返回的数据进行缓存合并,然后才发送给前端页面
1 2 3 4
| location / { proxy_buffering off; proxy_pass http://127.0.0.1:7001/; }
|
我这里演示所有,所有路径的代理都关闭代理缓冲功能
原本这是半年前的做的功能了,最近又有人来问这个问题,所以在这里重写一下
Gitalk 加载中 ...