实战-AI智能伴侣-流式输出

实战-AI智能伴侣-流式输出

2026年9月18日·#编程学习/python学习笔记Python/Streamlit/AI·1336 字 7 分钟
浏览量加载中...
AI 摘要

让大模型的回复逐字显示(打字机效果):stream 参数、chunk 解析与 st.empty 占位更新

什么是流式输出#

非流式:服务端把整段回答生成完,一次性返回,用户要盯着空白页面等好几秒。

流式:服务端生成一个字就返回一个字,用户看到的是文字逐渐蹦出来的效果(像 ChatGPT 那样),等待体验明显更好。

对比非流式流式
参数stream=Falsestream=True
返回值一个完整的响应对象一个可迭代对象,逐段产出 chunk
取内容response.choices[0].message.contentchunk.choices[0].delta.content
展示st.chat_message(...).write(完整内容)st.empty() 占位 + 循环覆盖写入
Note

两种方式拿到的内容是一样的,区别只在”什么时候拿到”。所以同一份 messages 数据可以自由切换。

代码对比#

非流式(上一版的写法)#

response = client.chat.completions.create(
model="deepseek-chat",
messages=[...],
stream=False
)
st.chat_message("assistant").write(response.choices[0].message.content)

流式(本节的写法)#

response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": system_prompt},
*st.session_state.messages
],
stream=True
)
# 创建一个空的组件, 用于展示大模型返回的结果
response_message = st.empty()
full_response = ""
for chunk in response:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
full_response += content
response_message.chat_message("assistant").write(full_response)
# 保存大模型返回的结果
st.session_state.messages.append({"role": "assistant", "content": full_response})

关键点拆解#

1. 为什么取值的路径变了#

结构含义
response.choices[0].message.content非流式:message 是完整消息
chunk.choices[0].delta.content流式:delta 表示”这一小段增量”

delta 就是”增量”的意思,每次循环拿到的是新增的那几个字,不是全文。

2. 为什么要判断 is not None#

if chunk.choices[0].delta.content is not None:

流式返回的最后几个 chunk 里,delta.contentNone(只带结束标记等元信息,没有文字)。如果不判断就直接字符串拼接,会报 TypeError

3. st.empty() 的作用#

response_message = st.empty()
...
response_message.chat_message("assistant").write(full_response)
  • st.empty() 先在页面上占一个空位
  • 之后每次循环都往这个空位里重新写一次 full_response(累积后的完整文字),覆盖掉上一次的内容
  • 效果就是同一个气泡里的文字不断变长,而不是每加几个字就新建一个气泡
Warning

如果不用 st.empty(),而是每轮循环都调用 st.chat_message("assistant").write(...),页面上会不停新增几十个聊天气泡,每个气泡只装一小段文字。

4. 为什么还要 full_response 累加#

因为 delta.content 只是增量。要保存到 session_state.messages 里的是完整回答(下次发给模型时必须是完整的 assistant 消息),所以需要边拼边存。

补充:更简洁的官方写法#

Streamlit 提供了 st.write_stream(),专门用来展示流式输出,可以省掉手写循环:

response = client.chat.completions.create(..., stream=True)
st.chat_message("assistant").write_stream(response)

它要求传入的是”文本片段的可迭代对象”。课程里手写 chunk 循环的价值在于:看清楚流式数据到底长什么样,以及 deltamessage 的区别。理解了原理之后,实际项目可以直接用 write_stream

Tip

想看原始 chunk 结构,可以在循环里临时加一行 print(chunk),观察每个 chunk 的 choices[0].delta 与最后一个 chunk 的 finish_reason

相关#

练习题#

一、回忆填空(写完再展开对答案)#

  1. 开启流式:client.chat.completions.create(..., stream=____)
  2. 流式里每次循环拿到的是”增量”,取值路径是 chunk.choices[0].____.content
  3. 非流式取的是 message.content,流式取的是 ____.content(“增量”的意思)
  4. 循环里要判断 content is not ____,因为最后几个 chunk 只带结束标记、没有文字
  5. 占位组件:response_message = st.____(),之后可以反复往这个空位里覆盖写入
  6. 边拼边存:full_response ____ content
  7. 官方更简洁的写法:st.chat_message("assistant").____(response)
填空答案(做完再点开)
  1. True 2. delta 3. delta 4. None 5. empty 6. += 7. write_stream

二、裸写题#

  • 2-1 看清流式数据长什么样 发一次流式请求,在循环里打印每个 chunk 的 delta,观察:文字是怎么一段段来的?最后一个 chunk 里是什么?

    提示(先自己想,实在想不出再点开)

    一级 · 思路:不要急着拼字符串,先 print(chunk) 看清楚结构 二级 · 方法for chunk in response: + 打印 chunk.choices[0].delta 三级 · 骨架:注意有些 chunk 的 delta.contentNone,要先判断

    参考答案(做完再点开)
    import os
    from openai import OpenAI
    client = OpenAI(api_key=os.environ.get("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com")
    response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "从1数到5"}],
    stream=True,
    )
    for chunk in response:
    print(chunk) # 先看整体结构
    # 看清楚了再换成:print(chunk.choices[0].delta)
    # 观察结论:最后一个 chunk 的 delta.content 是 None(只带结束信息),
    # 所以拼接前必须判断 is not None
  • 2-2 打字机效果st.empty() 实现:同一个气泡里的文字不断变长,而不是每加几个字就新建一个气泡。

    提示

    一级 · 思路:先占一个空位,之后每次循环都往这个空位重写”累积后的全文” 二级 · 方法st.empty() / response_message.chat_message("assistant").write(full_response) 三级 · 骨架:不用 st.empty() 会怎样?试试直接 write,看页面上出现多少个气泡

    参考答案(做完再点开)
    import os
    import streamlit as st
    from openai import OpenAI
    st.title("打字机效果演示")
    client = OpenAI(api_key=os.environ.get("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com")
    prompt = st.chat_input("请输入您要问的问题")
    if prompt:
    st.chat_message("user").write(prompt)
    response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": prompt}],
    stream=True,
    )
    response_message = st.empty() # 先占一个空位,之后反复覆盖它
    full_response = ""
    for chunk in response:
    if chunk.choices[0].delta.content is not None:
    full_response += chunk.choices[0].delta.content
    response_message.chat_message("assistant").write(full_response)
    # 对比:把 st.empty() 那两行换成每轮直接 st.chat_message("assistant").write(...),
    # 页面上会冒出几十个气泡,每个只装几个字

评论区

[ 标签 ]
# AI37# AI 编程2# AI工具1# Ajax2# Apifox1# AstrBot3# Astro2# CC Switch1# CDN2# Claude Code1# claudecode2# ClaudeCode1# Cloudflare2# CloudFlare2# CloudFlare-ImgBed3# coc3# CSS6# DeepSeek6# deepseek2# DELETE1# Docker1# EdgeOne3# Gist1# git1# GitHub1# hexo-circle-of-friends1# HTML6# HTTP5# ImageManager1# Java23# java13# JavaScript5# JDBC3# JSON2# JUnit1# LangChain25# Logback1# Maven6# Muse Spark1# Mybatis1# MyBatis4# MySQL28# MySql1# NapCat1# Node.js1# obsidian2# Obsidian5# OpenCode4# ORM1# PathVariable1# PicGo1# PyCharm1# Python65# RequestBody1# RequestMapping1# RESTful风格1# skills1# Slf4j1# SpringBoot11# SQL2# Streamlit5# Svelte2# TailwindCSS1# Telegram3# Tlias2# Vercel1# vscode2# Vue7# Waline3# WebDAV1# Web基础6# Web开发6# WinSCP1# YAML1# 三层架构1# 中二宣言1# 书籍1# 使用文档10# 写作1# 函数2# 刷步数1# 前端32# 动态1# 动漫1# 包1# 单词2# 博客7# 博客工作流1# 博客开发2# 参数接收1# 友链1# 反思2# 图床6# 地图1# 备份2# 大模型1# 奇思妙想1# 存储1# 学习方法6# 学校1# 宝塔面板3# 宝宝10# 对象1# 导航栏1# 工具2# 开发1# 开发工具1# 开发规范1# 开心1# 异常处理1# 影视2# 微信1# 性能优化2# 总结1# 想法15# 感受1# 感悟11# 指南1# 提示词工程1# 插件5# 故障排除1# 效率工具2# 教程10# 数据分析9# 数据库27# 数据结构1# 文件操作2# 斩神1# 日常92# 日志框架1# 朋友圈1# 朱元璋1# 模块1# 模板1# 正则表达式2# 测试1# 游戏2# 爬虫7# 生活迁移1# 电影2# 电脑1# 碎碎念1# 视觉识别1# 类1# 类型注解1# 网络基础2# 网络教室1# 羊毛2# 脚本2# 脚本工具1# 自动化2# 蓝奏云1# 订阅推荐2# 记录2# 评论系统1# 词根1# 词缀1# 说说1# 足迹1# 跑步2# 路径参数1# 转载2# 运动1# 部落冲突1# 配置1# 随机图1# 面向对象5# 音乐3# 音标1# 饮食1# 驼峰命名1# 高德地图1
[ 公告 ]

如果你喜欢,那么欢迎来到我的世界!

了解更多
[ 音乐 ]
封面

音乐

暂未播放

0:000:00
暂无歌词
找不到相关结果。
[ contents ]
[ 全部文章 ]