AI大模型中fetch 和 ReadableStream为啥一起出现

下面从 fetch 和 ReadableStream 分别讲起,再说明它们为什么经常一起出现。

一、fetch 是什么?

fetch浏览器内置的全局方法,用来发起 HTTP 请求 。它基于 Promise,是现代前端替代 XMLHttpRequest标准网络请求接口

基本语法
javascript 复制代码
fetch(url, options)
  .then(response => {
    // response 是 Response 对象
  })
  .catch(error => {
    // 网络错误
  });
async/await 写法
javascript 复制代码
async function getData() {
  const res = await fetch('/api/user');

  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }

  const data = await res.json();
  console.log(data);
}

二、fetch 的返回值不是"最终数据",而是 Response 对象

很多人第一次用 fetch 时,会误以为:

javascript 复制代码
const res = await fetch('/api/data');

这一步已经拿到了接口返回的数据。

其实不是fetch()返回的是一个 Promise<Response>,也就是说,它先给你一个 响应对象,而不是直接给你 JSON、文本或文件内容。

这个 Response 对象里包含两类信息:

类型 常见内容 说明
响应元信息 res.statusres.okres.headers 状态码、是否成功、响应头
响应体 res.bodyres.json()res.text() 真正的业务数据

所以常见写法是:

javascript 复制代码
const res = await fetch('/api/data');
const data = await res.json();

这里有两步:

  1. fetch():拿到响应对象
  2. res.json():把响应体解析成 JSON

三、Response 响应体有多种读取方式

Response 的响应体可以用不同方法读取,但注意:响应体通常只能读取一次

javascript 复制代码
await res.json();         // 解析成 JSON

await res.text();         // 读取成纯文本

await res.blob();         // 读取成二进制文件对象

await res.arrayBuffer();  // 读取成原始二进制缓冲

await res.formData();     // 读取成表单数据

这些方法本质上都是把响应体一次性消费掉

比如:

javascript 复制代码
const data = await res.json();

它适合普通接口:服务端把完整 JSON 返回,前端等完整响应到达后,再一次性解析。

但这不适合"边生成边返回"的场景,比如大模型流式输出

四、普通 fetch 用法的问题:必须等完整响应

普通用法里,你调用:

javascript 复制代码
const data = await res.json();

浏览器会等整个响应体接收完,再把它解析成 JSON。对于普通接口没问题。

但如果服务端是流式返回,比如大模型一边生成一边推送:

plain 复制代码
data: {"content":"你"}
data: {"content":"好"}
data: {"content":","}
data: {"content":"我是"}
...

你如果还用 res.json(),就不合适,因为:

  • 它不是一整段合法 JSON;
  • 数据是分批到达的;
  • 你需要"来一块,处理一块",而不是等全部结束。

这时候就要用到 res.body

五、res.body 是什么?

res.body 就是 Response的响应体流 。它的类型通常是:ReadableStream

可以简单理解成:res.body 是一根从服务端通向浏览器的"数据水管"。服务端产生的数据会一块一块地流过来,前端可以一块一块地读取,而不需要等全部数据到达

所以:

javascript 复制代码
const res = await fetch('/api/stream');

console.log(res.body);
// ReadableStream

这里 res.body不是一个完整字符串,也不是一个 JSON 对象,而是一个可以持续读取的数据流。

六、ReadableStream 是什么?

ReadableStream 是浏览器 Streams API 的一部分,表示一个**可读的数据流**。

它的作用是:让 JavaScript 可以按块读取数据,而不是必须等全部数据准备好

它最典型的数据单位是:Uint8Array,也就是二进制字节块。

你可以把它理解成:

plain 复制代码
服务端数据
   ↓
分成很多小块 chunk
   ↓
前端每次 read() 拿到一块
   ↓
前端自己决定怎么解码、拼接、解析、渲染

所以 ReadableStream 的核心价值是:

  • 支持边接收边处理
  • 降低首字节等待时间;
  • 避免一次性把大量数据塞进内存;
  • 可以中途取消
  • 可以和其他流处理机制组合。

七、ReadableStream 的核心 API

ReadableStream 最常用的是这几个方法:

方法 作用
getReader() 创建一个 reader,开始读取流
reader.read() 每次读取一个数据块
cancel() 取消流
pipeThrough() 把流经过一个转换流处理
pipeTo() 把流写入一个 WritableStream
tee() 把一个流拆成两个相同的分支

其中最关键的是:

javascript 复制代码
response.body.getReader()

它的作用是:从响应体流上安装一个"水龙头",然后你通过这个水龙头一块一块地取水。

八、 reader.read() 是怎么工作的?

reader.read()返回一个 Promise,结果是: { value, done }

含义如下:

字段 含义
value 当前读到的数据块,通常是 Uint8Array
done 流是否已经结束

示例:

javascript 复制代码
const reader = response.body.getReader();

while (true) {
  const { value, done } = await reader.read();

  if (done) {
    break;
  }

  console.log(value); // Uint8Array
}

这里有一个很关键的点:

javascript 复制代码
await reader.read()

它不是死循环一直占用 CPU,而是:

  • 有数据块到达时,Promise 会 resolve
  • 没有数据时,它会暂停等待;
  • 数据结束后,done 会变成 true

所以它是由"数据到达"驱动的,而不是由 CPU 一直轮询驱动的。

九、为什么流式场景里 fetch 和 ReadableStream 经常一起出现?

因为大模型流式接口通常是这样的链路:

plain 复制代码
fetch 发起 POST 请求
   ↓
服务端返回 text/event-stream 或分块响应
   ↓
response.body 是一个 ReadableStream
   ↓
getReader() 获取 reader
   ↓
reader.read() 逐块读取 Uint8Array
   ↓
TextDecoder 把二进制转成文本
   ↓
前端解析 SSE 事件或 JSON 增量
   ↓
页面逐步渲染

所以它们分工很明确:

API 职责
fetch() 负责发起 HTTP 请求,拿到 Response
response.body 提供响应体的可读流
ReadableStream 负责让前端按块读取数据
reader.read() 每次取出一个数据块
TextDecoder 把二进制字节转成文本

也就是说:

fetch 负责"请求",ReadableStream 负责"流式读取响应体"。

它们不是同一个东西。

十、 一个最小示例

javascript 复制代码
const response = await fetch('/api/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    message: '你好'
  })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { value, done } = await reader.read();

  if (done) {
    break;
  }

  const text = decoder.decode(value, { stream: true });
  console.log(text);
}

这段代码的意思是:

  1. fetch 发起请求;
  2. response.body 拿到可读流;
  3. getReader() 创建读取器;
  4. 循环调用 reader.read() 逐块读取;
  5. TextDecoder 把二进制块转成文本。

这就是流式读取的基本模型。

有一个疑问:这段代码是一个循环,条件为true,接口不断的推送数据一块一块的,但如果第一块的数据推送过来了,代码一轮循环执行完毕,然后第二块的代码还没推送到,这轮循环会再把第一块的数据再读取一遍吗,很疑惑,因为我之前开发的循环有具体数字的加减来标记,知道什么时候结束循环,但这个无法通过代码看出来。

回答:

不会重复读取 。 这个循环不会"倒回去"重读,关键在于reader.read() 是一个有状态的、带内部指针的读取器,它内部维护了一个"读到哪了"的游标,每次调用只会返回下一块新数据。

核心机制:内部游标与"消费即丢弃"

ReadableStream 内部有一个队列(内部队列),数据从网络到达后,会按顺序排进这个队列里。

reader.read() 每次调用时,做的事情是:

  1. 从队列头部取出下一块数据(不是从头取)
  2. 把这块数据返回给你
  3. 内部指针自动往后移一位

数据一旦被 read()取走,就从队列里移除了,不会再出现第二次。 这就像排队买票------你买完票往前走,后面的人只能买下一张,不可能再买到你已经买走的那张。

第二块数据还没到时会发生什么?

这是你最关心的点。当第一块处理完,循环进入第二轮,调用 await reader.read() 时:

  1. 如果第二块数据已经到了(已经在内部队列里)→ 立刻返回,不等待
  2. 如果第二块数据还没到await reader.read() 这个 Promise 会挂起等待代码暂停在这一行,不会往下执行,也不会回到循环顶部重新读

它不会空转,不会重复读,就是安静地等着。 等数据到了,Promise 自动 resolve,代码继续往下走。

十一、和普通 res.json() 的区别

普通接口:

javascript 复制代码
const res = await fetch('/api/user');
const data = await res.json();

特点:

  • 等完整响应;
  • 一次性解析;
  • 适合普通 CRUD 接口。

流式接口:

javascript 复制代码
const res = await fetch('/api/stream');
const reader = res.body.getReader();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;

  // 处理 value
}

特点:

  • 不等完整响应;
  • 来一块处理一块;
  • 适合大模型流式输出、大文件下载、实时日志、边下边处理 等场景。

十二、一句话总结

fetch是用来发起 HTTP 请求并拿到 Response的; ReadableStream是用来把响应体当作数据流,按块持续读取的。

普通接口里,你通常只关心:

javascript 复制代码
await res.json()

但在流式场景里,你不再一次性消费响应体,而是通过:

javascript 复制代码
res.body.getReader()

逐块读取数据。

所以大模型流式返回常用 fetch + ReadableStream不是因为 fetch本身能流式解析 JSON,而是因为 fetchresponse.body暴露了一个可读流,让前端可以边接收、边解码、边解析、边渲染。

相关推荐
袁俪43 分钟前
AI智能体 :
人工智能
Pioneer000011 小时前
我用 Redis + 网关做多模型 API 路由:缓存命中率 95%+ 的工程实践
人工智能·redis·后端·缓存·性能优化·架构
空奈qwq1 小时前
机器学习入门:从核心概念到建模全流程
人工智能·python·机器学习
半甜柠檬1 小时前
Claude Code支持AGENTS.md了_真正的重点藏在mods里
人工智能·ai助手
PC2005_cloud1 小时前
Nginx 防盗链配置实战:用 referer 模块保护网站静态资源
前端·后端
福兮说1 小时前
Canvas 文字排版:measureText 量出来的宽度为什么总是不对
前端·javascript
罗斯8391 小时前
EMBER恶意软件基准数据集
人工智能·算法·安全·网络安全
deepseek231 小时前
WSO2 Agent Manager 正式可用:企业 Agent 从能跑到可治理,沙盒、身份与 MCP 如何落地
人工智能·ai agent·mcp·企业治理