不写前端,也能做出像样的 AI 产品

为什么选 Streamlit

到第 10 篇为止,我们积累的所有能力都是脚本级的——命令行跑一跑看看输出。要把这些能力给真实用户用,必须有 UI。对 Python 开发者而言,这里有三种主流选择:

  • Streamlit——声明式,把 Python 脚本自动变网页,10 分钟搞定聊天页。适合内部工具、原型、数据分析应用
  • Gradio——AI/ML 社区默认,组件预置了各种多媒体(语音、图像),适合模型 demo
  • FastAPI + 前端——生产级,但要你掌握 HTML/JS,学习成本高

Streamlit 对本系列目标读者最合适:零前端知识、十几分钟出成品、够用到部署上线。生产级需求超出 Streamlit 能力范围再升级到 FastAPI + React 也来得及。

最小聊天页

安装:

pip install streamlit

下面这 30 行完整实现一个带流式输出的多轮聊天页:

# app.py
import streamlit as st
from ai import chat_stream, DEFAULT_MODEL  # 沿用 02 篇封装

st.set_page_config(page_title="Python AI 聊天助手", page_icon=":robot_face:")
st.title("Python AI 聊天助手")

# session_state 是 Streamlit 存持久状态的地方,每个用户会话独立
if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "你是一位简洁专业的 Python 助手。"}
    ]

# 把历史消息渲染出来(跳过 system)
for msg in st.session_state.messages:
    if msg["role"] == "system":
        continue
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

# 底部输入框
if user_input := st.chat_input("有什么我可以帮你的?"):
    st.session_state.messages.append({"role": "user", "content": user_input})
    with st.chat_message("user"):
        st.markdown(user_input)

    # 流式渲染助手回复
    with st.chat_message("assistant"):
        placeholder = st.empty()
        full = ""
        for piece in chat_stream(st.session_state.messages):
            full += piece
            placeholder.markdown(full + "▌")  # 光标效果
        placeholder.markdown(full)

    st.session_state.messages.append({"role": "assistant", "content": full})

运行:

streamlit run app.py

浏览器会自动打开 http://localhost:8501,一个功能完整的聊天页就跑起来了——支持多轮上下文、流式打字机效果、会话持久。整个过程你没写一行 HTML/CSS/JS。

理解 Streamlit 的执行模型

Streamlit 有一个非常独特的执行模型,理解它能避免 80% 的诡异 bug:每次用户交互,整个脚本从头到尾重新执行一遍。不是执行某个回调函数,而是把你的 app.py 从第一行重新跑一次。

为什么它看起来还能"保留状态"——全靠 st.session_state。这是一个 dict-like 对象,跨 rerun 保留用户会话的数据。上面代码里的 st.session_state.messages 就是对话历史能持续累积的原因。

这个模型带来几个实用推论:

  • 初始化放在 if "xxx" not in st.session_state——保证只在第一次运行时执行
  • 昂贵操作用 @st.cache_data@st.cache_resource 缓存——模型加载、数据库连接等只执行一次
  • 不要在全局作用域做有副作用的事——会随 rerun 反复触发
# 缓存一个 embedding 模型,避免每次交互都重新加载
@st.cache_resource
def load_embedder():
    from sentence_transformers import SentenceTransformer
    return SentenceTransformer("BAAI/bge-m3")

embedder = load_embedder()  # 第一次真的加载,后续直接拿缓存

加侧边栏:模型切换、温度调节、清空对话

真实聊天产品的"配置面板"放在侧边栏里。Streamlit 的 st.sidebar 把任何组件塞进去就行:

with st.sidebar:
    st.header("设置")

    model = st.selectbox(
        "模型",
        ["deepseek-chat", "deepseek-reasoner", "qwen2.5:7b"],
        index=0,
    )

    temperature = st.slider("temperature", 0.0, 2.0, 0.7, 0.1)

    if st.button("清空对话"):
        st.session_state.messages = [st.session_state.messages[0]]  # 保留 system
        st.rerun()

st.rerun() 手动触发一次重新执行,用在"清空"这种需要立即刷新界面的场景。

modeltemperature 传给 chat_stream 就能实时生效。由于每次 rerun 都读最新的侧边栏状态,用户调整参数后下一轮对话自动使用新参数,不需要额外逻辑。

集成 RAG:让助手基于你的文档回答

把 06 篇的 RAG 管线接过来,用户上传文件后索引,提问时自动检索:

# app_rag.py
import streamlit as st
from pathlib import Path
import tempfile
from ai import chat_stream
from indexer import index_directory
from retriever import retrieve

st.set_page_config(page_title="知识库问答", page_icon=":books:")
st.title("知识库问答助手")

# 侧边栏:文件上传和索引
with st.sidebar:
    st.header("知识库")
    uploaded = st.file_uploader(
        "上传文档",
        type=["md", "pdf"],
        accept_multiple_files=True,
    )

    if uploaded and st.button("建立索引"):
        with st.spinner("正在索引..."):
            with tempfile.TemporaryDirectory() as tmpdir:
                tmp_path = Path(tmpdir)
                for f in uploaded:
                    (tmp_path / f.name).write_bytes(f.getbuffer())
                index_directory(tmp_path)
        st.success(f"已索引 {len(uploaded)} 个文档")

if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "你基于给定的参考资料回答问题,不知道就说不知道。"}
    ]

for m in st.session_state.messages:
    if m["role"] == "system":
        continue
    with st.chat_message(m["role"]):
        st.markdown(m["content"])

if user_input := st.chat_input("针对你的文档问我问题"):
    # 先做 RAG 检索
    hits = retrieve(user_input, top_k=3)
    context = "\n---\n".join(f"[来源:{h['source']}]\n{h['text']}" for h in hits)
    augmented = f"参考资料:\n{context}\n\n问题:{user_input}"

    st.session_state.messages.append({"role": "user", "content": augmented})
    with st.chat_message("user"):
        st.markdown(user_input)  # UI 只显示原问题

    with st.chat_message("assistant"):
        placeholder = st.empty()
        full = ""
        for piece in chat_stream(st.session_state.messages):
            full += piece
            placeholder.markdown(full + "▌")
        placeholder.markdown(full)

        # 展示引用来源
        with st.expander("查看引用的文档片段"):
            for h in hits:
                st.caption(f"{h['source']}  ·  相似度 {h['score']:.3f}")
                st.text(h["text"][:300] + "...")

    st.session_state.messages.append({"role": "assistant", "content": full})

这个版本已经覆盖了一个知识库问答产品的核心——文件上传、索引、检索、流式回答、引用展示。Python 代码约 60 行,没写一行前端。

注意:user 消息在对话里存的是"含上下文的 augmented 文本",但 UI 上显示的是原始问题。这是工程上常见的做法——让模型看到足够上下文,让用户看到自然问答。

会话持久化

上面的聊天页刷新浏览器就全没了,因为数据只存在 st.session_state 这个内存对象里。持久化很简单——写 JSON 文件或 SQLite:

import json
from pathlib import Path

SESSIONS_DIR = Path("./sessions")
SESSIONS_DIR.mkdir(exist_ok=True)


def save_session(session_id: str, messages: list[dict]):
    (SESSIONS_DIR / f"{session_id}.json").write_text(
        json.dumps(messages, ensure_ascii=False, indent=2)
    )


def load_session(session_id: str) -> list[dict]:
    path = SESSIONS_DIR / f"{session_id}.json"
    if path.exists():
        return json.loads(path.read_text())
    return []

侧边栏加一个"历史会话"下拉框,选中时 load_session 恢复;每次对话结束 save_session 存盘。几行代码就有了"ChatGPT 历史对话列表"的效果。

多用户场景要升级到数据库——sqlite3psycopg、或 supabase-py 都行。Streamlit 本身不管用户认证,多用户要么用 Streamlit Cloud 的内置登录,要么在前面加一层反向代理做 SSO。

部署

Streamlit Community Cloud——免费托管,连 GitHub 仓库直接部署,最适合小规模 demo。限制是计算资源有限,不适合本地模型。

自己部署——最简单方式是 Docker:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]

扔到任何云主机 / Kubernetes 上都能跑。Streamlit 进程自带健康检查端点 /_stcore/health,方便加负载均衡。

不要把 API Key 写进容器镜像——通过环境变量或 secret 管理器注入。Streamlit 自己也提供 .streamlit/secrets.toml 机制,云端部署时用密钥注入。

Streamlit 的边界

Streamlit 让简单 UI 极速上线,但它不是银弹。下面这些场景它做得很吃力:

  • 复杂交互——拖拽、画布、高级动画。Streamlit 的组件库有限
  • 极致的性能——rerun 模型有固定开销,高频交互(每秒多次)会卡顿
  • 自定义品牌设计——整站风格深度定制需要写 JS 注入,不如直接用 React
  • SEO——Streamlit 是动态应用不利于搜索引擎抓取

如果你的产品已经有了用户基础、PMF 明确、需要深度设计,就到了换技术栈的时候——通常是 FastAPI 后端 + Next.js 前端。但在验证阶段 Streamlit 的 ROI 几乎无敌。

本篇要点

  • Streamlit 让 Python 开发者零前端做出像样的 AI 聊天应用
  • 执行模型是"每次交互重跑整个脚本",状态用 st.session_state 保存
  • 昂贵操作(模型加载、DB 连接)用 @st.cache_resource 缓存
  • RAG 集成:上传 → 索引 → 检索 → 流式答 → 展示引用,组合已有模块即可
  • 部署首选 Streamlit Cloud(小项目)或 Docker(自主控制)
  • 原型阶段用 Streamlit,PMF 之后再换 FastAPI + 前端框架

下一篇

到这里主线 11 篇结束,你已经能独立做出一个完整的 AI 应用。第 12 篇是 进阶方向——LangChain/LlamaIndex 怎么选、评测怎么做、监控用什么工具、微调什么时候值得。把视野从"做出一个 demo"拉高到"把 AI 应用做成可运营的产品"。

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:用 Streamlit 构建 AI 聊天应用

本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/11-Streamlit聊天应用/

本文最后一次更新为 天前,文章中的某些内容可能已过时!