
本文提供一种符合 streamlit 最佳实践的流式聊天实现方式,通过 st.chat_message + st.write_stream 替代手动 st.empty() 刷新,彻底解决 linux/windows 下滚动跳动、页面重置等渲染 glitch 问题。
本文提供一种符合 streamlit 最佳实践的流式聊天实现方式,通过 st.chat_message + st.write_stream 替代手动 st.empty() 刷新,彻底解决 linux/windows 下滚动跳动、页面重置等渲染 glitch 问题。
在构建 Streamlit 聊天应用时,频繁使用 st.empty() 配合 .write() 动态更新内容,是导致滚动异常(如页面突然跳回顶部、消息闪烁、滚动位置丢失)的根本原因。尤其在 Linux 系统上,浏览器渲染引擎(如 Chromium 的 GTK 后端)对 DOM 频繁重绘更敏感,加剧了这一问题。根本症结在于:每次 response_placeholder.write() 都会触发整个容器重渲染,破坏
✅ 正确做法是完全放弃手动占位符更新,转而采用 Streamlit 官方推荐的声明式流式输出模式:
CentOS Stream 9是基于RHEL 9技术路线的持续交付版本,适合需要贴近RHEL 9生态的软件开发、系统集成和测试环境。它相比传统CentOS Linux更靠近上游开发过程,用户可以更早看到RHEL 9后续小版本中的软件包变化。CentOS Stream 9仍是当前可用的官方版本线之一,适合对稳定性和新功能之间有平衡需求的团队使用。
✅ 推荐方案:st.chat_message + st.write_stream
def general_chat(user_input):
try:
# 构建 prompt(注意:session_state 中 message 字段应统一为 'content')
key_data = ["politics", "religion", "violence"]
blocked_topics = "\n".join([f"Do not answer any questions about: {k}" for k 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}
Instructions:
{blocked_topics}
User: {user_input}
Assistant:"""
# ✅ 关键:在固定 chat_message 容器内流式写入,不触发重排
with st.chat_message("assistant"):
# 假设 generator 返回字符串流(每 chunk 为 str)
response = st.write_stream(llm_stream_generator(prompt_input))
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)
其中 llm_stream_generator 需返回一个生成器(yield 每个 token/chunk):
def llm_stream_generator(prompt):
# 示例:适配 Ollama
from ollama import chat
stream = chat(model="llama3.2", messages=[{"role": "user", "content": prompt}], stream=True)
for chunk in stream:
yield chunk["message"]["content"]
# 或适配其他 LLM(如 OpenAI、LiteLLM):
# for chunk in client.chat.completions.create(..., stream=True):
# if chunk.choices[0].delta.content:
# yield chunk.choices[0].delta.content
✅ 页面渲染逻辑优化(无 rerun!)
def chat_page():
st.title("ThinkBot")
# 初始化历史记录(统一用 '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"])
# ✅ 输入即响应:用户发送后立即渲染 user 消息,再调用 general_chat
if user_input := st.chat_input("Your message"):
st.session_state.chat_history.append({"role": "user", "content": user_input})
with st.chat_message("user"):
st.markdown(user_input)
general_chat(user_input) # 内部完成 assistant 消息渲染
⚠️ 关键注意事项
- 禁止 st.rerun():在聊天流中调用 st.rerun() 会清空当前 DOM 状态,导致滚动重置。Streamlit 的 st.chat_message 已内置滚动锚定(auto-scroll-to-bottom),无需手动干预。
- 字段命名一致性:确保 st.session_state.chat_history 中所有消息均使用 "content" 键(而非 "message"),否则 history_context 拼接会出错。
- CSS 覆盖慎用:.stAppScrollToBottomContainer { overflow-anchor: none !important; } 会禁用浏览器原生滚动锚定,反而加剧问题,应删除。
- Linux 兼容性:该方案在 Wayland/X11 环境下均稳定,因底层依赖的是 Streamlit 渲染器的声明式 diff 更新,而非强制 DOM 操作。
✅ 总结
滚动 glitch 的本质是命令式 DOM 操作(st.empty().write() + st.rerun())与 Streamlit 声明式范式的冲突。采用 st.chat_message 容器 + st.write_stream 流式写入,既符合官方最佳实践,又利用了 Streamlit 内置的滚动智能管理(自动锚定最新消息),从根源上消除平台差异导致的渲染异常。重构后代码更简洁、可维护性更高,且支持任意 LLM 流式 API。










