
本文提供 Streamlit 流式聊天界面滚动异常(如跳顶、抖动)的标准化修复方案,核心是弃用 st.empty() + write() 的手动刷新模式,改用官方推荐的 st.chat_message + st.write_stream() 组合,确保跨平台(尤其 Linux)稳定滚动。
本文提供 streamlit 流式聊天界面滚动异常(如跳顶、抖动)的标准化修复方案,核心是弃用 `st.empty()` + `write()` 的手动刷新模式,改用官方推荐的 `st.chat_message` + `st.write_stream()` 组合,确保跨平台(尤其 linux)稳定滚动。
在构建类 ChatGPT 的 Streamlit 聊天应用时,许多开发者会遇到一个典型 UI 问题:当 LLM 响应流式输出时,聊天窗口频繁“跳回顶部”或滚动卡顿——尤其在 Linux 系统上更为明显。根本原因在于:手动使用 st.empty().write() 触发高频重渲染,破坏了 Streamlit 内置的滚动锚点机制。浏览器无法可靠维持滚动位置,导致视觉抖动。
✅ 正确解法:遵循 Streamlit 官方对话式应用指南,采用声明式流式渲染范式:
始终在 st.chat_message 容器内调用 st.write_stream()
st.write_stream() 是专为流式内容设计的 API,它会在首次渲染后持续追加文本,不触发整块重绘,从而保留 DOM 结构与滚动锚点。
CentOS Stream 9下载CentOS Stream 9是基于RHEL 9技术路线的持续交付版本,适合需要贴近RHEL 9生态的软件开发、系统集成和测试环境。它相比传统CentOS Linux更靠近上游开发过程,用户可以更早看到RHEL 9后续小版本中的软件包变化。CentOS Stream 9仍是当前可用的官方版本线之一,适合对稳定性和新功能之间有平衡需求的团队使用。
避免 st.empty() + 循环 write() + st.rerun()
原代码中 response_placeholder.write(streamed_response) 每次都重建整个消息块,且后续 st.rerun() 强制全页面刷新,直接干扰浏览器滚动行为。统一消息数据结构字段名
官方 st.chat_message 期望 role 和 content 字段(非 message),保持结构一致可避免渲染逻辑错位。
以下是重构后的生产级示例(兼容 Windows/Linux/macOS):
import streamlit as st
from ollama import chat # 示例使用 Ollama;适配其他 LLM 时替换生成器即可
def llm_stream_generator(prompt_input):
"""封装流式响应生成器,返回 content 字符串流"""
stream = chat(
model="llama3.2",
messages=[{"role": "user", "content": prompt_input}],
stream=True,
)
for chunk in stream:
yield chunk["message"]["content"]
def general_chat(user_input):
# 构建上下文与安全指令(保持原逻辑)
key_data = ["politics", "religion", "violence"]
blocked_topics = "\n".join([f"Do not answer any questions about: {kw}" for kw in key_data])
history_context = "\n".join(f"{msg['role']}: {msg['content']}" for msg in st.session_state.chat_history)
prompt_input = (
f"{history_context}\n"
f"Instructions:\n{blocked_topics}\n"
f"User: {user_input}\n"
f"Assistant:"
)
try:
# ✅ 关键:在 st.chat_message 容器内直接流式写入
with st.chat_message("assistant"):
response = st.write_stream(llm_stream_generator(prompt_input))
# ✅ 使用 'content' 字段(与 st.chat_message 保持一致)
st.session_state.chat_history.append({"role": "assistant", "content": response})
except Exception as e:
error_msg = "Server busy. Please try again later."
st.session_state.chat_history.append({"role": "assistant", "content": error_msg})
st.error(error_msg)
def chat_page():
st.title("ThinkBot")
st.write("Welcome to ThinkBot! I am an AI Chatbot.")
# 初始化历史记录(使用 'content' 字段)
if "chat_history" not in st.session_state:
st.session_state.chat_history = [
{"role": "assistant", "content": "How can I help you?"}
]
# ✅ 声明式渲染全部历史消息(无手动 rerun)
for msg in st.session_state.chat_history:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# ✅ 实时响应用户输入(无中间状态标记)
if user_input := st.chat_input(placeholder="Your message"):
# 立即显示用户消息
st.session_state.chat_history.append({"role": "user", "content": user_input})
with st.chat_message("user"):
st.markdown(user_input)
# 同步调用流式响应(无需 st.rerun)
general_chat(user_input)
? 关键注意事项:
- 不要添加任何自定义 CSS 强制禁用滚动锚点(如 .stAppScrollToBottomContainer { overflow-anchor: none }),这会破坏 Streamlit 底层滚动优化;
- 若使用非 Ollama 的 LLM(如 OpenAI、LiteLLM),只需将 llm_stream_generator 替换为对应 SDK 的流式迭代器(返回字符串即可);
- st.write_stream() 自动处理空格、换行与 Markdown 渲染,无需额外 strip() 或格式化;
- 所有 st.chat_message 必须成对出现在 with 语句块中,否则容器结构失效导致滚动异常。
该方案已在 Ubuntu 24.04、Windows 11 和 macOS Sonoma 上验证通过,彻底消除抖动与跳顶现象,同时提升代码可维护性与性能。记住:Streamlit 的流式 UI 本质是「声明式」而非「命令式」——让框架管理 DOM,而非手动干预。










