🏆 本篇文章带你了解作者在工作中开发 AI Agent CUI 的过程中关于「流式输出」品尝到的酸甜苦辣~🙌 如文章有误,恳请评论区指正,谢谢!💖 写作不易,「点赞」+「在看」+「转发」 谢谢支持!
前言:流式输出的"隐形陷阱"
我们期望的丝滑 AI 回复体验
在前端接入 ChatGPT、OCR 或任何大模型能力时,"流式返回"(Streaming)几乎是标配。服务端一边算,前端一边把字打出来,用户体验非常丝滑。
这看起来是一个非常标准的技术,但在实际工程中,很多开发者会遇到一个隐蔽却致命 的怪象: 同样是流式请求,有的实现稳如泰山,有的却总是少字、断句、乱码,甚至完全无法复现 Bug。
实际工程中经常遇到的"丢字断句"现象
很多时候,我们会把锅甩给"网络抖动"或"浏览器 Bug",但真相往往更扎心:问题不出在网络,而出在你对"流"的理解层级上。
本文将抛开晦涩的术语,用最通俗的语言告诉你:为什么手写的 fetch 读取流极其容易"弄丢"你的数据,以及为什么 fetchEventSource 才是正解。
一、现场对比:手写版 vs 专业版
为了搞清楚问题,我们先看两段代码。它们都在做同一件事:接收服务端推过来的 AI 回复。
❌ 方式一:裸用 fetch + reader(相当于"手接水管")
csharp
// 伪代码演示
const reader = response.body.getReader();
while (true) {
const { value } = await reader.read();
// 💀 灾难现场:
// 1. 手动解码二进制数据
// 2. 手动用正则去匹配 data:、RECORD_ID 等字段
// 3. 手动拼接破碎的字符串
// 4. 手动处理换行符...
}
特点: 代码里充满了复杂的正则替换(replace)、拼接逻辑。开发者实际上是在手动重新发明一个"协议解析器" 。
✅ 方式二:使用 fetchEventSource(相当于"收快递")
javascript
await fetchEventSource(url, {
onmessage(msg) {
// 🎁 优雅现场:
// 直接拿到完整数据,不用管它是怎么传过来的
if (msg.event === 'message') {
console.log(msg.data);
}
}
});
特点: 代码极简,没有复杂的拼接逻辑。你不需要关心数据是怎么传输的,只关心拿到的结果。
二、核心原理:是"接水管"还是"收快递"?
手动处理原始数据流 vs 接收封装好的完整事件
为什么代码差别这么大?因为这两种方式工作的层级完全不同。
我们可以用 "接收信件" 来打个比方。
1. SSE 协议:互联网的写信规范
服务端给前端推数据,遵循一种叫 SSE (Server-Sent Events) 的规范。它就像写信:
data:是信的内容。\n\n(两个换行) 是信封封口的标记,代表"这一封信写完了"。
服务端发出来的数据是这样的:
data: 你好,我是AI。\n\n data: 今天天气不错。\n\n
2. fetch + reader = "碎纸机"
当你直接使用 reader.read() 时,你是在最底层的网络层(TCP层)工作。 网络传输不关心原本的"信"长什么样,它只负责把数据运过去。为了运输效率,它经常会把数据切碎 或者粘在一起。
这就是导致"丢数据"的根本原因:
- 场景 A(被切碎):
- 服务端发了:
data: RecordID: 100 - 你的 reader 第一次读到:
data: Record(断了!) - 你的 reader 第二次读到:
ID: 100 - 后果: 你的代码如果此时用正则去匹配
RecordID,第一次匹配不到,第二次也匹配不到。这条数据就这样"消失"了。 - 场景 B(粘在一起):
- 服务端发了两条:
hello和world - 你的 reader 一口气读到:
data: hello\n\ndata: world - 后果: 你的代码如果只写了简单的替换逻辑,很可能直接把中间的换行符当垃圾清理掉了,结果变成了
helloworld,丢失了分句信息。
3. fetchEventSource 的优势:它是"私人秘书"
fetchEventSource 这个库,就像一个懂行的私人秘书 。 它虽然底层也用 fetch,但它在中间多做了一层关键工作:缓存与拼接。
- 它会帮你在门口守着,拿到碎片(
data: Record)时不急着给你。 - 它会等到下一片(
ID: 100)到了,帮你拼好。 - 直到它看到了信封封口标记(
\n\n),确认这是一封完整的信 ,才会敲门把信交给你(触发onmessage)。
总结对比:
| 维度 | fetchEventSource (秘书) | fetch + reader (你自己) |
|---|---|---|
| 拿到的东西 | 完整的事件 (语义级) | 随机的字节碎片 (传输级) |
| 处理碎片/粘包 | ✅ 自动处理,帮你拼好 | ❌ 你得自己写复杂的算法去拼 |
| 乱码风险 | ✅ 内部处理了 UTF-8 解码 | ❌ 遇到中文被切成两半时直接乱码 |
| 丢数据概率 | 极低 | 极高 (尤其是网络波动时) |
三、避坑指南:到底该怎么选
读到这里,答案已经很明显了。
✅ 什么时候必须用 fetchEventSource?
只要你的场景是 AI 对话、流式文字、OCR 识别 ,或者任何服务端声明了 Content-Type: text/event-stream 的接口。请务必使用 fetchEventSource。 不要试图挑战手写解析器,因为你无法预判网络会把你的数据切成什么奇怪的形状。
⚠️ 什么时候才用 fetch + reader?
只有当你明确知道自己在下载文件 、处理二进制流(比如视频流、音频流),或者你需要处理一种服务端自定义的非标准协议时,才需要下沉到这个层级去手动处理。
💡 总结

- fetch + reader 是在网络管道里捞碎片,极其容易划伤手(丢数据)。
- fetchEventSource 是坐在办公桌前收完整的信件,安全又优雅。
下次再做 AI 流式输出,别再自己造轮子了,让专业的库来做专业的事。

最后
我是 Smoothzjc,致力于产出更多且不仅限于前端方面的优质文章
写作不易,「点赞」+「收藏」+「转发」 谢谢支持💖