用Streamlit给AI应用套个界面,10行代码出Web页面

前7篇搭了RAG、跑了Ollama、写了Agent、用了Dify------全是在终端里跑的。有读者问: "能不能有个界面?给老板演示总不能让他看终端吧?"

确实,终端里一堆日志输出,技术人员看着亲切,非技术人员看着懵。你跟老板说"你看这个AI回答多准",他看到的是满屏绿色的>>> Entering new AgentExecutor chain...

Streamlit就是干这个的------Python写的AI应用,10行代码变Web页面。 不用写HTML、不用写CSS、不用写JavaScript,纯Python搞定。

我花了一个下午把之前几篇文章的应用都套上了Streamlit界面,踩了3个坑。这篇把全过程写出来。


先说结论

维度 终端运行 Streamlit
交互方式 命令行输入/输出 网页聊天框
给老板看 ❌ 看不懂 ✅ 像产品
给客户用 ❌ 不会用终端 ✅ 打开浏览器就能用
开发量 0(本身就是终端运行的) 10-50行额外代码
前端知识 不需要 不需要

用Java人的理解:Streamlit ≈ 不用写前端,后端直接输出页面。比Thymeleaf还简单------Thymeleaf至少还得写HTML模板,Streamlit连模板都不用。


最简示例:10行代码出页面

python 复制代码
# app.py
import streamlit as st
​
st.title("智能问答助手")
st.write("基于RAG的文档问答系统,输入问题开始提问")
​
question = st.text_input("你的问题:")
if question:
    st.write(f"AI回答:这是一个示例回答,实际使用时接入你的RAG链即可。")

运行:

arduino 复制代码
streamlit run app.py

浏览器自动打开 http://localhost:8501,一个带标题、输入框、输出的网页就出来了。

10行代码,零前端知识。 这就是Streamlit。


实战:把第3篇的RAG应用套上界面

第3篇的终端版回顾

python 复制代码
# 终端版:命令行一问一答
while True:
    q = input("\n问:")
    if q.lower() == "q":
        break
    print(f"答:{rag_chain.invoke(q)}")

改成Streamlit版

python 复制代码
# app.py --- RAG问答Web版
import streamlit as st
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
​
# ============ 页面配置 ============
st.set_page_config(page_title="文档问答助手", page_icon="📖")
st.title("📖 文档问答助手")
st.caption("基于RAG,让AI读懂你的文档")
​
# ============ 初始化(只跑一次) ============
@st.cache_resource
def init_rag():
    API_KEY = "你的API Key"
    BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
​
    embeddings = OpenAIEmbeddings(model="text-embedding-v3", base_url=BASE_URL, api_key=API_KEY)
    vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
​
    model = ChatOpenAI(model="qwen-plus", base_url=BASE_URL, api_key=API_KEY)
    prompt = ChatPromptTemplate.from_template(
        """严格基于以下参考内容回答问题。如果参考内容中没有相关信息,回答"根据现有文档,无法回答该问题"。不要编造。
​
参考内容:
{context}
​
问题:{question}"""
    )
​
    retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 3, "fetch_k": 10})
    rag_chain = (
        {"context": retriever | (lambda docs: "\n\n".join(d.page_content for d in docs)), "question": RunnablePassthrough()}
        | prompt | model | StrOutputParser()
    )
    return rag_chain
​
rag_chain = init_rag()
​
# ============ 对话界面 ============
if "messages" not in st.session_state:
    st.session_state.messages = []
​
# 显示历史对话
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])
​
# 输入框
if question := st.chat_input("输入你的问题:"):
    # 显示用户消息
    st.session_state.messages.append({"role": "user", "content": question})
    with st.chat_message("user"):
        st.markdown(question)
​
    # 生成AI回答
    with st.chat_message("assistant"):
        with st.spinner("思考中..."):
            answer = rag_chain.invoke(question)
        st.markdown(answer)
​
    st.session_state.messages.append({"role": "assistant", "content": answer})

运行:

arduino 复制代码
streamlit run app.py

效果: 左上角标题"文档问答助手",中间聊天框,底部输入框,像ChatGPT一样的对话体验。历史记录保存在session里,刷新清零。

和终端版的核心区别只有一个: input()st.chat_input()print()st.markdown()


坑1:每次输入都重新初始化RAG,等30秒

翻车现场

我第一次写的时候没有加@st.cache_resource

ini 复制代码
# 每次用户输入问题,都重新初始化
rag_chain = init_rag()  # 加载向量库+构建链,耗时20-30秒
​
if question := st.chat_input("输入你的问题:"):
    answer = rag_chain.invoke(question)

结果:用户每输入一个问题,页面卡30秒------因为Streamlit每次交互都会重新执行整个脚本,向量库每次都重新加载。

正确做法:用@st.cache_resource缓存

ruby 复制代码
@st.cache_resource  # 关键:只初始化一次,后续复用
def init_rag():
    # ... 加载向量库、构建链
    return rag_chain
​
rag_chain = init_rag()  # 第一次慢,之后秒加载

Streamlit的缓存机制有三个装饰器,必须搞清楚:

装饰器 缓存什么 什么时候用
@st.cache_resource 对象(模型、向量库、连接池) AI应用必备,初始化只跑一次
@st.cache_data 数据(DataFrame、列表、字典) 数据查询结果缓存
不加 每次重新执行 默认行为,别用在这里

用Java人的理解:@st.cache_resource ≈ Spring的@Bean单例模式,@st.cache_data@Cacheable查询缓存。不加缓存 ≈ 每次请求new一个对象------能跑但效率极低。


坑2:流式输出不会写,等AI全部生成完才显示

翻车现场

大模型生成回答通常要5-15秒,如果等全部生成完再显示,用户体验很差:

ini 复制代码
# 等全部生成完才显示
answer = rag_chain.invoke(question)  # 卡5-15秒
st.markdown(answer)  # 一次性全出来

用户看到的效果:输入问题 → 等待15秒 → 突然冒出一大段文字。像死机了一样。

正确做法:用st.write_stream逐字显示

csharp 复制代码
# 流式输出:逐字显示,像ChatGPT一样
with st.chat_message("assistant"):
    def stream_response():
        for chunk in rag_chain.stream(question):
            yield chunk
    response = st.write_stream(stream_response)
​
st.session_state.messages.append({"role": "assistant", "content": response})

效果: 输入问题 → 1-2秒后开始逐字显示,像ChatGPT一样的打字效果。

关键改动:

  • rag_chain.invoke()rag_chain.stream() --- LangChain的流式接口
  • st.markdown()st.write_stream() --- Streamlit的流式输出

两个都得换,少换一个都不行。


坑3:侧边栏放配置,主区域放对话,布局一搞就乱

翻车现场

我把所有东西都堆在主区域:

css 复制代码
[标题]
[API Key输入框]
[模型选择下拉框]
[PDF上传按钮]
[对话区域]
[输入框]

一堆配置项和对话混在一起,用户分不清哪里是设置、哪里是聊天。

正确做法:侧边栏放配置,主区域只留对话

css 复制代码
# ============ 侧边栏:配置区 ============
with st.sidebar:
    st.header("⚙️ 配置")
​
    model_name = st.selectbox(
        "选择模型",
        ["qwen-plus", "qwen-turbo", "qwen-max"],
        index=0,
    )
​
    temperature = st.slider(
        "Temperature",
        min_value=0.0, max_value=1.0, value=0.1, step=0.1,
        help="越低越确定,越高越随机",
    )
​
    st.divider()
​
    # 上传文档
    uploaded_file = st.file_uploader("上传文档", type=["pdf"])
    if uploaded_file:
        with st.spinner("正在处理文档..."):
            # 处理上传的PDF,重建向量库
            process_pdf(uploaded_file)
        st.success("文档处理完成!")
​
    st.divider()
​
    # 清空对话
    if st.button("🗑️ 清空对话"):
        st.session_state.messages = []
        st.rerun()
​
    st.divider()
    st.caption("Java后端转大模型系列 · 荣码")
​
# ============ 主区域:对话区 ============
st.title("📖 文档问答助手")
# ... 对话逻辑

布局原则:

区域 放什么 原因
侧边栏 配置、设置、上传 不干扰主体验,需要时展开
主区域 标题 + 对话 + 输入 用户90%时间在这里

用Java人的理解:这和前后端分离一个道理------侧边栏是"设置页面",主区域是"主业务页面"。别把管理功能和用户功能混在一起。


完整代码:RAG问答Web应用(含侧边栏+流式输出)

python 复制代码
"""
Streamlit RAG问答应用 --- Web界面版
运行:streamlit run app.py
依赖:pip install streamlit langchain langchain-openai langchain-chroma langchain-community pypdf
"""
import streamlit as st
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
​
# ============ 页面配置 ============
st.set_page_config(page_title="文档问答助手", page_icon="📖", layout="wide")
​
# ============ 侧边栏 ============
with st.sidebar:
    st.header("⚙️ 配置")
​
    model_name = st.selectbox("模型", ["qwen-plus", "qwen-turbo", "qwen-max"], index=0)
    temperature = st.slider("Temperature", 0.0, 1.0, 0.1, 0.1)
​
    st.divider()
    st.caption("📚 文档已预加载")
    st.caption("Java后端转大模型系列 · 荣码")
​
    if st.button("🗑️ 清空对话"):
        st.session_state.messages = []
        st.rerun()
​
# ============ 初始化RAG ============
API_KEY = "你的API Key"
BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
​
@st.cache_resource
def init_rag(_model_name, _temperature):
    embeddings = OpenAIEmbeddings(model="text-embedding-v3", base_url=BASE_URL, api_key=API_KEY)
    vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
    model = ChatOpenAI(model=_model_name, base_url=BASE_URL, api_key=API_KEY, temperature=_temperature, streaming=True)
    prompt = ChatPromptTemplate.from_template(
        """严格基于以下参考内容回答问题。如果参考内容中没有相关信息,回答"根据现有文档,无法回答该问题"。不要编造。
​
参考内容:
{context}
​
问题:{question}"""
    )
    retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 3, "fetch_k": 10})
    rag_chain = (
        {"context": retriever | (lambda docs: "\n\n".join(d.page_content for d in docs)), "question": RunnablePassthrough()}
        | prompt | model | StrOutputParser()
    )
    return rag_chain
​
rag_chain = init_rag(model_name, temperature)
​
# ============ 对话界面 ============
st.title("📖 文档问答助手")
st.caption("基于RAG,让AI读懂你的文档 · 换模型和参数请在左侧调整")
​
if "messages" not in st.session_state:
    st.session_state.messages = []
​
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])
​
if question := st.chat_input("输入你的问题:"):
    st.session_state.messages.append({"role": "user", "content": question})
    with st.chat_message("user"):
        st.markdown(question)
​
    with st.chat_message("assistant"):
        def stream_response():
            for chunk in rag_chain.stream(question):
                yield chunk
        response = st.write_stream(stream_response)
​
    st.session_state.messages.append({"role": "assistant", "content": response})

和终端版相比,新增代码不到30行,但体验提升10倍。

运行:

arduino 复制代码
streamlit run app.py
# 自动打开浏览器 http://localhost:8501

3个坑的总结

# 错误做法 正确做法 一句话
1 每次输入重新初始化 不加缓存 @st.cache_resource 等于Spring的@Bean单例
2 等全部生成完才显示 invoke+markdown stream+write_stream 逐字输出才像ChatGPT
3 配置和对话混在一起 全堆主区域 侧边栏放配置,主区域放对话 设置是设置,聊天是聊天

Streamlit能做的远不止聊天界面

这篇只讲了最常用的聊天界面,Streamlit还能做这些:

功能 代码量 效果
数据表格展示 st.dataframe(df) 可排序、可搜索的表格
图表可视化 st.line_chart(data) 折线图、柱状图、散点图
文件上传 st.file_uploader() 拖拽上传,支持多种格式
多页面应用 多个.py文件 侧边栏自动生成导航
部署分享 streamlit run 或 Streamlit Cloud 免费云部署

我接下来的计划: 把第5篇的Embedding对比做成可视化Dashboard------选5个模型,跑测试,结果自动出图表。比Markdown里的表格直观10倍。


不想写Streamlit?还有一个更快的方案

如果你觉得Streamlit还要写代码,第7篇的Dify自带Web界面,发布就有一个可分享的链接。

方案 代码量 自定义程度 适合场景
Dify发布 0行 快速给非技术人员用
Streamlit 30-100行 需要自定义界面和交互
前后端分离 1000+行 极高 正式产品

我的选择:内部验证用Dify,对外展示用Streamlit,正式上线写前后端。 三个层次,按需选择。


你用过Streamlit吗?有什么好用的组件推荐?评论区聊聊 👇

相关推荐
SamDeepThinking1 小时前
Java微服务练习方式
java·后端·微服务
兵慌码乱11 小时前
基于Python+PyQt5+SQLite的药房管理系统实现:事务一致性与界面解耦全流程解析
python·sqlite·信号与槽·pyqt5·数据库设计·桌面应用开发·事务处理
朦胧之12 小时前
AI 编程-老项目改造篇
java·前端·后端
金銀銅鐵12 小时前
[Python] 体验用欧几里得算法计算最大公约数的过程
python·数学
FreakStudio16 小时前
W55MH32L-EVB 上手测评:硬件 TCP/IP 加持的以太网单片机,MicroPython 零门槛开发
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
程序猿大帅16 小时前
别再只当调包侠了:用 Spring AI 落地 Function Calling,我被大模型硬生生砸出了三个大坑
java
用户03321266636717 小时前
使用 Python 从零创建 Word 文档
python
程序员晓琪17 小时前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
Flittly17 小时前
【AgentScope Java新手村系列】(11)中断与恢复
java·spring boot·spring