学习目标
- 学会流式输出,让 AI 边思考边说话,不用等它憋完 (搞定!✅)
- 理解 stream=True 是怎么工作的(搞定!✅)
- 搞懂 chunk、delta 这些名词是啥意思(搞定!✅)
一、普通输出 vs 流式输出
普通输出(前两天干的)
你问一句,AI 憋半天,然后一次性吐出一大段:
你说:介绍Python
(等待...等待...等待...)
AI:Python是一种高级编程语言,以其简洁的语法、强大的功能和跨平台的特性,广泛应用于数据科学、人工智能、Web开发等多个领域。
缺点:等得心慌,不知道 AI 在干嘛,是不是卡死了。
流式输出(今天要学的)
你问一句,AI 边想边说,一个字一个字蹦出来:
你说:介绍Python
AI:Python是...一种高级...编程语言...以其简洁的语法...
优点:
- 看着舒服,像真人聊天一样
- 知道 AI 在干活,没卡死
- 可以提前看到内容,不用等完
二、核心原理:stream=True
普通请求的代码
data = {
"model": "deepseek-ai-qwen-32b",
"messages": messages
# 没有 stream 参数,默认是 False
}
response = requests.post(url, headers=headers, json=data)
result = response.json() # 等完整响应,一次性拿到
流式请求的代码
data = {
"model": "deepseek-ai-qwen-32b",
"messages": messages,
"stream": True # 开启流式!
}
# 注意:requests 也要加 stream=True
response = requests.post(url, headers=headers, json=data, stream=True)
# 一块一块读,不是一次性读
for line in response.iter_lines():
# 处理每一块数据
区别就两处:
- 请求体加
"stream": True - requests.post 加
stream=True,然后用iter_lines()循环读
三、数据是怎么一块一块来的
普通响应(一次性)
{
"choices": [
{
"message": {
"role": "assistant",
"content": "Python是一种高级编程语言..."
}
}
]
}
一个完整的 JSON,content 里是全部内容。
流式响应(一块一块)
每一块叫 chunk,格式是这样的:
data: {"choices":[{"delta":{"content":"Python"}}]}
data: {"choices":[{"delta":{"content":"是一种"}}]}
data: {"choices":[{"delta":{"content":"高级编程"}}]}
data: {"choices":[{"delta":{"content":"语言"}}]}
data: [DONE]
特点:
- 每行开头是
data: - 内容在
delta.content里(不是message.content了) - 最后有个
data: [DONE]表示结束
四、名词解释
| 词 | 是啥 |
|---|---|
| stream | 流式开关,True=边生成边吐,False=憋完再吐 |
| chunk | 每一块数据,流式响应就是一堆 chunk 组成的 |
| delta | 增量,每个 chunk 只包含新生成的那一点点内容 |
| iter_lines() | 一行一行读响应的方法 |
delta vs message:
- 普通响应用
message.content(完整内容) - 流式响应用
delta.content(增量内容,每次只有一点点)
五、完整代码
# -*- coding: utf-8 -*-
import requests
import sys
import json
sys.stdout.reconfigure(encoding='utf-8')
url = "http://xxx:port/v1-openai/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer your-api-key"
}
messages = [{"role": "user", "content": "介绍Python"}]
data = {
"model": "deepseek-ai-qwen-32b",
"messages": messages,
"stream": True # 开启流式
}
# requests 也要 stream=True
response = requests.post(url, headers=headers, json=data, stream=True)
print("AI:", end="", flush=True)
full_reply = ""
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
# 跳过结束标记
if line == "data: [DONE]":
continue
# 解析 chunk
if line.startswith("data: "):
json_str = line[6:] # 去掉 "data: " 前缀
try:
chunk = json.loads(json_str)
# 处理 choices 为空的情况
if "choices" in chunk and len(chunk["choices"]) > 0:
delta = chunk["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
print(content, end="", flush=True) # 立即打印
full_reply += content # 收集完整内容
except:
pass
print() # 换行
# 存到 messages(跟以前一样)
messages.append({"role": "assistant", "content": full_reply})
六、代码拆解
6.1 两处 stream=True
# 请求体里的 stream
data = {
"stream": True # 告诉 API:我要流式输出
}
# requests 里的 stream
response = requests.post(..., stream=True) # 告诉 requests:我要边接收边读
两个都要加!漏一个都不行。
6.2 iter_lines() 循环
for line in response.iter_lines():
# 每一行是一个 chunk
iter_lines() 会等待服务器推送每一行数据,来了就处理,不用等全部到齐。
6.3 立即打印
print(content, end="", flush=True)
end="":不换行,接着打印flush=True:立即显示,不等缓冲区满
6.4 收集完整回复
full_reply = ""
full_reply += content # 每个 chunk 的内容都累加
为什么要收集?因为最后要存到 messages 里,下次对话要用。
流式输出时,每块只有一点点,必须拼起来才是完整回复。
6.5 json 模块
import json
chunk = json.loads(json_str) # 把 JSON 字符串转成 Python 字典
json 是 Python 内置模块,用来处理 JSON 数据:
| 功能 | 示例 |
|---|---|
| 解析 JSON 字符串 | json.loads('{"name": "小明"}') → {"name": "小明"} |
| 把对象转成 JSON | json.dumps({"name": "小明"}) → '{"name": "小明"}' |
| 读取 JSON 文件 | json.load(open("data.json")) |
不用安装,Python 自带,直接 import json 就能用。
6.6 try-except 异常处理
try:
chunk = json.loads(json_str) # 尝试解析 JSON
except:
pass # 出错了就跳过,不报错
含义:
try:尝试执行这段代码except:如果出错了,执行这里pass:什么都不做,跳过
为什么要有这个?
流式输出的数据不一定每行都是有效 JSON:
data: {"choices":[{"delta":{"content":"你好"}}]} ← 能解析
data: {"choices":[]} ← choices 为空,但能解析
data: [DONE] ← 不是 JSON,解析会出错
如果不用 try-except,遇到解析失败的行程序就崩了。
用 except: pass 就是:解析失败就跳过,继续处理下一行。
七、跑起来试试
python week1/03_stream_chat.py
效果:
你说:介绍Python
AI:Python是一种高级编程语言,以其简洁的语法...
(字一个个蹦出来,像真人打字一样)
你说:quit
拜拜!
八、对比总结
| 特性 | 普通输出 | 流式输出 |
|---|---|---|
| 等待体验 | 憋完再吐,等得心慌 | 边想边说,看着舒服 |
| 响应格式 | 一个完整 JSON | 一堆 chunk(data: {...}) |
| 内容字段 | message.content | delta.content |
| 读取方式 | response.json() | iter_lines() 循环 |
| 代码复杂度 | 简单 | 稍复杂,要拼完整回复 |
九、踩坑记录
Q1: 没有流式效果,还是一次性输出
咋回事:漏了 flush=True,或者 requests 没加 stream=True
咋办:检查两处 stream=True 都加了,print 要有 flush=True
Q2: 报错解析 JSON 失败
咋回事:有些 chunk 格式不标准,或者有空行
咋办:加 try-except,跳过解析失败的行
Q3: 中文乱码
咋回事:又是 Windows 控制台的问题
咋办:别忘了 sys.stdout.reconfigure(encoding='utf-8')
Q4: messages 存不进去,下次对话没历史
咋回事:流式输出时忘了收集完整回复
咋办:用 full_reply += content 把每块内容拼起来,最后存到 messages
Q5: 用 eval() 解析 JSON,报错或没输出
咋回事:eval() 不是正经的 JSON 解析方法,遇到特殊格式会出错
咋办:用 json.loads() 代替 eval()
# 错误写法
chunk = eval(json_str) # 不安全,容易报错
# 正确写法
import json
chunk = json.loads(json_str) # 标准的 JSON 解析方法
为啥 eval() 不行:
eval()是执行 Python 代码,不是解析 JSON- JSON 格式和 Python 格式不完全一样(比如
truevsTrue) eval()有安全风险,可能执行恶意代码
Q6: 报错 "list index out of range"
咋回事:有些 chunk 的 choices 是空数组,访问 choices[0] 就报错
咋办:加个长度检查
# 错误写法
delta = chunk["choices"][0].get("delta", {}) # choices 为空就报错
# 正确写法
if "choices" in chunk and len(chunk["choices"]) > 0:
delta = chunk["choices"][0].get("delta", {})
十、今日总结
今天干了啥:
✅ 学会了流式输出,AI 边想边说,体验更丝滑
✅ 理解了 stream=True、chunk、delta 这些概念
✅ 做了一个支持流式输出的聊天程序
明天干嘛:试试调整参数,让 AI 更有创意(temperature)或者更老实(max_tokens)。
十一、参考学习网站
- OpenAI API参考文档 | OpenAI开发文档|OpenAI中文官方文档 - 官方讲流式输出
- requests 流式响应 - requests 官方文档
记于 2026-04-05,AI 学习第三天,流畅度拉满!