第1周 Day 3:前端转型AI,实现流式输出🎯

0 阅读6分钟

学习目标

  • 学会流式输出,让 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():
    # 处理每一块数据

区别就两处

  1. 请求体加 "stream": True
  2. 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": "小明"}
把对象转成 JSONjson.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
拜拜!

动画.gif

image.png


八、对比总结

特性普通输出流式输出
等待体验憋完再吐,等得心慌边想边说,看着舒服
响应格式一个完整 JSON一堆 chunk(data: {...}
内容字段message.contentdelta.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 格式不完全一样(比如 true vs True
  • 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)。


十一、参考学习网站


记于 2026-04-05,AI 学习第三天,流畅度拉满!

image.png