在近期的项目中,对接了大模型,这个过程中踩了一些坑,和大家分享。
整体分为两块内容:SSE的简单介绍 和 大模型流式数据请求与解析,大家可以重点关注请求与解析。
最终效果gif形式附在文章末尾,供参考。
一、SSE 简单介绍
流式输出,即服务器持续地将数据推送到客户端,而不是一次性全部发送完。
这种模式下,一旦连接建立,服务器可以实时发送数据更新,客户端接收后进行处理。
简单来说,服务器会将完整数据拆分为多个小块,分批次发送,客户端实时接收每个小批次数据。
这种流式传输形式在大模型交互中非常常见,且具有明显的优势:
- 提升用户体验:因为大模型生成结果通常需要较长时间,流式传输像“打字机”一样逐步输出结果,能够实时展现部分生成内容,增强交互感和体验的实时性。
- 减少等待时间:相比传统接口模式需要等待结果生成完毕才返回,流式传输减少了中间的等待时间,用户体验更流畅。
当下流式输出几乎已成为大模型交互中的标准做法。
二、大模型流式数据请求与解析
接下来分享踩的两个坑,重点是解决方案。
1. 数据请求
在流式输出的实现中,第一个遇到的问题就是网络请求,这个问题耗费了我小半天的时间。
目前项目中我们基本都在用 Axios 发起请求,Axios 默认使用的是 XMLHttpRequest (XHR) ,而 Axios 对 XHR 的封装中并没有处理流式数据。也就是说,默认情况下 Axios 并不能很好地支持 SSE 这种实时流式传输。
为了解决这个问题,我们有两个选择:
- 直接使用 Fetch:Fetch 原生支持流式数据。
- 在 Axios 中切换到 Fetch:在 Axios 1.7.0 版本中,增加了对 Fetch 的适配器支持(今年5月下旬发布)。因此,也可以通过切换 Axios 使用 Fetch 来支持流式数据。切换比较简单,代码如下:
const instance = axios.create({
// other config
adapter: ['fetch' , 'xhr' , 'http']
});
// axios 的默认配置优先级是
// adapter: ['xhr' , 'http', 'fetch']
以上是切换了Axios的适配器优先级。
简单来说,Axios内部会自动适配node和browser环境。对于browser环境,有两种选择:xhr、fetch。默认情况下优先使用xhr。
具体可以参考以下Axios源码: github.com/axios/axios…
github.com/axios/axios…
2. 数据解析
在成功请求流式数据后,接下来就遇到了 流式数据解析 的问题。
为了更好地理解问题,我们可以在浏览器中查看流式数据的响应格式。通常流式响应会有一些特定的标志,比如 data:
字段标识每一块数据。在解析时,必须逐步读取这些数据块并即时处理,而不是等到所有数据接收完才处理。
这一步相当重要,因为解析不当可能导致数据的拼接出错或者数据延迟展示,影响整体效果。实际可以参考以下gif图片
从上图中只能简易看出这种数据特点前两个特点:
- 分段返回
- 每段有多条类似json数据
- 每段的最后一条json数据有可能不完整
针对这种数据解析,暂时没有找到成熟方案以下是我自己实现,供参考:
const chartRequest = async (messages: str) => {
const response = await agentChat({
type: 'CHAT',
sessionId,
query: messages
}).catch(() => {
return DefaultError();
})
if (!(response instanceof ReadableStream)) {
return DefaultError();
}
const reader = response.getReader() as ReadableStreamDefaultReader;
const decoder = new TextDecoder('utf-8');
const encoder = new TextEncoder();
let jsonBuffer = ''
const readableStream = new ReadableStream({
async start(controller) {
function push() {
reader
.read()
.then(({ done, value }) => {
if (done) {
controller.close();
console.log('数据流解析-------- 连接关闭')
return;
}
// 1、流返回的块数据
const chunk = decoder.decode(value, { stream: true });
console.log('数据流解析-------- 当前返回块', chunk);
// 2、更新到缓存区
jsonBuffer += chunk;
// 3、尝试分片解析json
let boundaryIndex = 0;
// 当前片内容
let result = '';
while ((boundaryIndex = jsonBuffer.indexOf('\n')) >= 0) {
// 3.1 数据块切片
const jsonString = jsonBuffer.slice(0, boundaryIndex);
// 3.2 更新缓存区
jsonBuffer = jsonBuffer.slice(boundaryIndex + 2);
console.log('数据流解析-------- 缓存区剩余数据', jsonBuffer)
try {
const jsonStr = jsonString.replace('data:', '');
console.log('数据流解析-------- 将要解析的json字符串', jsonStr)
const jsonObject = JSON.parse(jsonStr); // 解析 JSON
console.log('数据流解析-------- json字符串转换为对象', jsonObject);
// 处理可识别内容 - 伪代码,根据实际对象处理
const content = jsonObject?.data?.content;
controller.enqueue(encoder.encode(content));
// 解析结束 - 我们业务是根据此字段标识,根据实际情况调整
if (jsonObject?.data?.isEnd === true) {
console.log('数据流解析-------- 解析数据流结束');
// 清空缓存区
jsonBuffer = '';
break;
}
} catch (error) {
console.log('数据流解析-------- json解析出错', error)
}
}
// 处理缓冲区中剩余的数据(这里冗余设计,可以考虑去掉,只是为了观察每块数据的不完整json串)
if (jsonBuffer) {
console.log('数据流解析-------- 缓存区剩余内容', jsonBuffer)
try {
const jsonObject = JSON.parse(jsonBuffer);
console.log('缓存区剩余内容:解析成功', jsonObject);
} catch (error) {
console.log('数据流解析-------- 处理缓存区剩余内容出错,可能需要等待下一块流数据,缓存区剩余数据', jsonBuffer);
}
}
push();
})
.catch((err) => {
console.log('数据流解析-------- 读取流中的数据时发生错误', err);
controller.error(err);
});
}
push();
},
});
return new Response(readableStream);
}
注释较多,不再赘述。
总结
我们简单介绍了sse请求方式,一种长链接,服务端可以持续的向客户端推送数据。之后讨论了sse如何使用axios请求,以及如何解析这种数据。希望以上对您有所收获。我的微信:l592816909(备注掘金),欢迎探讨。