前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吗?有什么好用的组件推荐?评论区聊聊 👇