在对接大模型流式输出的过程当中,真的是心力交瘁。
今天再来分享一个笔者真实的踩坑经历。故事背景是笔者高高兴兴的解决了后端关于大模型流式输出的相应问题之后,满心期待的验证结果的时候发现前端接收到的Markdown原文直接格式错乱了。
期待的理想的类似于Deepseek等主流AI问答界面的效果没出来,直接把原文弄出来了。该缩进的没有缩进,该换行的没有换行,海拔原文完完整整的给我呈现了一遍,看的我真是不知道咋形容。
踩坑过程
按照现实的现象来说,就是后端给我推送的空格以及换行符直接丢失了。导致我前端拼接token的时候直接把原文的markdown格式给破坏掉了。
所以,想也没想,直接在前端加了换行的\n转<br>的逻辑以及检测到是空的token转成空格。发现并没有什么用。前端调试了一下发现确实接收到的后端的关于空格以及换行符的token是一个空格,但是后端调试发现推送的token是没问题的,就是一个标准的换行符和空格。
接下来就是漫长的查资料的过程。。。
脱坑方案
问题原因
其实问题的原因SSE协议造成的:后端在和LLM交互的过程中接收到token片段之后,会原封不动的响应给前端。但是在SSE标准的传输过程中就会丢失,因为SSE协议的标准数据结构是data: token \n\n,我们传输一个\n就跟协议文案冲突了,导致响应成了空格,如果传输的是空格,就会导致直接空格丢失
解决方案
知道了原因就很好解决了。我们只需要在后端接收到token的时候,做一下格式转换,比如把空格转成[SPACE],把换行符转成[LF],前端接收到之后在转回来就可以了。
前端代码示例:
const restoredData = data.replace(/[LF]/g, "\n").replace(/[SPACE]/g, " ");
话外题
最后,再分享一个前端使用vditor包来做markdown格式渲染组件的一个注意事项。
如果我们的前端使用的是vditor来做的markdown文案预览组件,在模拟打字机输出效果的时候,需要按照下面示例的格式来编写:
let streamBuffer = "\n\n";
currentStream.value = streamChat(`url`, {
text: text
}, data => {
streamBuffer += data;
const renderText = streamBuffer
.replace(/[LF]/g, "\n")
.replace(/[SPACE]/g, " ");
vditor.value.setValue(existBlogContent + renderText)
}, () => {
abortStream()
}, error => {
abortStream()
})
否则换行还是会不生效。