llm 的流式请求和普通请求

理解这两种请求模式的区别,最直观的比喻是:“写信”“打电话”

在大语言模型(LLM)的场景下,这两种请求方式带来了完全不同的底层运行逻辑和用户体验。我们把它们拆开对比:

1. 普通请求(非流式,stream=False

比喻:写信。你把问题寄给大模型,它在后台苦思冥想,等把几千字的回答全部写完、装进信封后,才一次性交给你。

  • 运行逻辑
  1. 你的代码发送请求。
  2. 程序卡在 client.chat.completions.create(...) 这一行(阻塞等待)。
  3. 服务器在后台把所有字都算完(可能需要 5-30 秒)。
  4. 服务器将完整的长文本一次性通过网络返回。
  5. 你的代码拿到结果,瞬间把所有文字打印到屏幕上。
  • 优点:代码极其简单,直接拿最终的完整字符串(response.choices[0].message.content)用就行,不用操心拼接碎片。
  • 缺点首字节时间(TTFB)极长。 如果模型要写一篇 2000 字的文章,用户可能对着白屏干等 20 秒,会以为程序死机了。这种“等待焦虑”对用户体验是毁灭性的。

2. 流式请求(流式,stream=True

比喻:打电话。大模型脑子里蹦出一个词,嘴里就说出一个词,你立刻就能听到。

  • 运行逻辑
  1. 你的代码发送请求。
  2. 只要大模型算出了第一个字(通常不到 1 秒),服务器立刻把这个字包装成一个微小的 chunk 数据包发给你。
  3. 随着模型持续计算,数据包像水管里的水滴一样源源不断地传过来。
  4. 你的代码通过 for chunk in response: 循环,来一个字就打印一个字。
  • 优点极致的用户体验。 用户几乎瞬间就能看到模型开始回答(打字机效果),即使总共也需要 20 秒才能答完,用户的注意力也被跳动的文字吸引了,不会觉得慢。
  • 缺点:代码变复杂了。你不仅要处理异常(比如前面提到的 IndexError),如果要实现多轮对话,你还得自己准备一个空字符串,把这些碎文字拼接起来存进记忆列表里。

核心对比总结表

对比维度 普通请求 (Normal) 流式请求 (Streaming)
首字响应速度 极慢(需等待全部生成完毕) 极快(通常 < 1秒)
网络传输方式 一个庞大的 HTTP 响应包 利用 SSE 技术,拆分成数百个极小的碎片包返回
视觉效果 漫长白屏等待 \(\rightarrow\) 瞬间全篇展示 像人打字一样,一个字一个字蹦出来
代码复杂度 简单(直接拿最终字符串) 较高(需循环遍历、拼接数据块、处理异常块)
LLM 适用场景 机器处理(如后台提取 JSON、文章分类) 人机交互(如聊天机器人、客服对话框)

💡 为什么大模型天生适合流式请求?

这和 AI 的底层原理有关。大模型本质上是“文字接龙”游戏(自回归生成)。它无法在第 1 秒就知道整句话该怎么说,它是一个 Token(词元)接着一个 Token 算出来的。

流式请求完美契合了这种工作模式:算出一个词就发一个词,绝不让算好的词在服务器里闲置浪费,最大化了资源传递的效率。

delta 和 messages

chunk.choices[0].delta.content 中,为什么 api 中要加入 delta ?

这是一个非常深刻且直击 API 设计本质的问题!

要搞懂为什么在这里出现 delta,我们得先看它的英文本义,然后再看它在流式传输架构(Streaming)中的不可替代性。


1. delta 的英文本义与数学含义

  • 音标/ˈdel.tə/
  • 本义:希腊字母表的第四个字母 \(\Delta\)(大写)或 \(\delta\)(小写)。
  • 数学与科学含义:在数学、物理和计算机科学中,\(\Delta\)(Delta)永远用来表示“增量”“变化量”(即:后来的状态减去先前的状态所产生的那个差值)。

直观记忆

  • 在数学中:\(\Delta x\) 代表 \(x\) 的变化量(\(x_2 - x_1\))。
  • 在编程中:delta 就是指“只包含新增加的那一小撮变化数据”,而不是完整的全量数据。

2. 为什么普通请求用 message,而流式请求用 delta

为了让你一眼看出区别,我们把两种模式下 API 返回的 JSON 数据结构摆在一起对比:

① 普通请求(stream=False):返回的是 message

因为模型已经把整个句子都算完了,它给你的是一个完整的、静态的“消息”

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "佛法者,即非佛法,是名佛法。" // 完整的全量文本
      }
    }
  ]
}

  • 字段名:叫 message,因为它代表了一封完整书信

② 流式请求(stream=True):返回的是 delta

在流式传输中,每过几毫秒服务器就发过来一个数据块。它发给你的不是整句话,也不是从头到尾拼接好的历史文本,而仅仅是这一毫秒内新算出的“新增片段”

// 第 1 个 chunk
{ "choices": [ { "delta": { "role": "assistant", "content": "佛" } } ] }

// 第 2 个 chunk
{ "choices": [ { "delta": { "content": "法" } } ] }

// 第 3 个 chunk
{ "choices": [ { "delta": { "content": "者" } } ] }

  • 字段名:叫 delta,因为它代表的是新增增量

3. 为什么它没有被隐藏成内部目录,而是显式留给开发者?

你之前问过:“为什么 resources 被隐藏了,而 delta 却显式留着?”

这两者的性质有着本质的区别:

  • resources 是“代码存放在哪”(实现细节)
    对于开发者来说,代码放在 resources 文件夹还是 src 文件夹,完全不影响你调用的逻辑。所以 SDK 帮你在后台隐藏掉,让你少敲几次键盘。
  • delta 是“数据的物理属性”(数据语义)
    delta 显式留在 API 里,是在明确提醒开发者:“你现在拿到的这个 content 只是一个碎片(增量),而不是完整的结果!”

如果 OpenAI 强行把 delta 隐藏掉,也统一叫 message

# 如果 OpenAI 搞骚操作,强行把流式数据也改名叫 message:
chunk.choices[0].message.content

这会导致严重的语义混淆
开发者看到 message.content,可能会误以为这是“到目前为止生成的完整句子”,于是直接拿去更新UI,结果导致屏幕上不断覆盖掉前面的字,或者引发严重的逻辑 Bug。

因此,设计成 delta 是 API 规范中非常严谨的做法,它用属性名字告诉你:“这是增量,请你自己做加法(拼接)。”


💡 总结

字段 模式 语义 含义
message 普通请求 (stream=False) 静态的全量结果 “这是一封完整的信(全量文本)。”
delta 流式请求 (stream=True) 动态的增量变化 “这是刚才蹦出来的一个字(增量片段)。”
posted @ 2026-07-31 18:31  立体风  阅读(16)  评论(0)    收藏  举报