llm 的流式请求和普通请求
理解这两种请求模式的区别,最直观的比喻是:“写信”与“打电话”。
在大语言模型(LLM)的场景下,这两种请求方式带来了完全不同的底层运行逻辑和用户体验。我们把它们拆开对比:
1. 普通请求(非流式,stream=False)
比喻:写信。你把问题寄给大模型,它在后台苦思冥想,等把几千字的回答全部写完、装进信封后,才一次性交给你。
- 运行逻辑:
- 你的代码发送请求。
- 程序卡在
client.chat.completions.create(...)这一行(阻塞等待)。 - 服务器在后台把所有字都算完(可能需要 5-30 秒)。
- 服务器将完整的长文本一次性通过网络返回。
- 你的代码拿到结果,瞬间把所有文字打印到屏幕上。
- 优点:代码极其简单,直接拿最终的完整字符串(
response.choices[0].message.content)用就行,不用操心拼接碎片。 - 缺点:首字节时间(TTFB)极长。 如果模型要写一篇 2000 字的文章,用户可能对着白屏干等 20 秒,会以为程序死机了。这种“等待焦虑”对用户体验是毁灭性的。
2. 流式请求(流式,stream=True)
比喻:打电话。大模型脑子里蹦出一个词,嘴里就说出一个词,你立刻就能听到。
- 运行逻辑:
- 你的代码发送请求。
- 只要大模型算出了第一个字(通常不到 1 秒),服务器立刻把这个字包装成一个微小的
chunk数据包发给你。 - 随着模型持续计算,数据包像水管里的水滴一样源源不断地传过来。
- 你的代码通过
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) |
动态的增量变化 | “这是刚才蹦出来的一个字(增量片段)。” |

浙公网安备 33010602011771号