前面的课程给出了数据、模型和 API 的实现路径。这一课说明怎样用 Streamlit 把它们连接成可交互界面:读者能搜索论文、查看详情,并看到请求失败时的提示。实际演示前,需要先确认各个依赖已经运行,并准备可复查的数据与调用记录。
前置阅读:本课程全部前 24 课。本课代码调用系列五的 API,不直接碰模型和数据库——前端只是 API 的客户端。
应用定位:先想清楚给谁演示什么
动手前先回答产品问题。这个应用的观众是面试官/同行,演示动线是:5 分钟内看到”数据规模 → 检索智能 → 模型能力 → 趋势洞察”四个递进。由此定四个页面:
- 总览页:数据规模、领域分布、增长趋势(M1 + EDA 成果)。
- 语义检索页:输入自然语言,返回相关论文(M3 + M4 能力)。
- 自动分类页:贴标题摘要,实时返回领域预测与置信度(M2 能力)。
- 主题地图页:UMAP 交互图,按主题着色(系列三第 4 课成果)。
演示动线决定页面顺序,页面顺序决定导航结构——先有叙事,再有界面。
页面骨架:多页应用与共享状态
# app.py —— 入口与导航
import streamlit as st
st.set_page_config(page_title="ResearchHub", layout="wide")
page = st.sidebar.radio("功能", ["总览", "语义检索", "自动分类", "主题地图"])
st.sidebar.markdown("---")
st.sidebar.caption("数据版本: m1.0 · 模型: bge-large + LR")
if page == "总览":
from pages_app import overview; overview.render()
elif page == "语义检索":
from pages_app import search; search.render()
# ...
侧栏底部的”数据版本 + 模型版本”标签是个小心思:演示时被问”这是基于多少数据跑的”,答案直接印在页面上——版本意识是工程成熟度的外显。
总览页:缓存是生命线
总览页的统计查询扫全库,没有缓存的话每次切换页面都重新算几秒——Streamlit 的交互模型是”任何控件变化都重跑整个脚本”,不缓存就卡死:
import streamlit as st
import requests
API = "http://localhost:8000/api/v1"
@st.cache_data(ttl=3600) # 统计结果缓存 1 小时
def load_overview():
stats = requests.get(f"{API}/stats/overview", timeout=10).json()
trend = requests.get(f"{API}/stats/yearly-trend", timeout=10).json()
return stats, trend
def render():
st.header("平台总览")
stats, trend = load_overview()
cols = st.columns(4)
cols[0].metric("收录论文", f"{stats['total']:,}")
cols[1].metric("覆盖领域", stats["fields"])
cols[2].metric("最早年份", stats["year_min"])
cols[3].metric("今日新增", stats["today_new"]) # 昨日管线产物
import plotly.express as px
import pandas as pd
df = pd.DataFrame(trend)
st.plotly_chart(px.area(df, x="year", y="papers", color="field",
title="各领域年度论文趋势"),
use_container_width=True)
@st.cache_data 按参数缓存返回值:参数没变就直接用缓存。什么时候清缓存想明白:统计数据 TTL 一小时合理;但检索结果不该按查询文本缓存(用户可能想看最新),演示场景下首屏数据缓存是刚需。
语义检索页:演示的核心亮点
这是”哇塞时刻”页面。设计要点是让智能看得见:
def render():
st.header("语义检索")
query = st.text_input("描述你的研究兴趣",
placeholder="例如:parameter efficient fine-tuning for LLMs")
col1, col2 = st.columns([3, 1])
field_filter = col1.selectbox("限定领域", ["全部", "cs.CL", "cs.CV", "cs.LG"])
topk = col2.slider("返回数量", 5, 50, 10)
if query:
with st.spinner("检索中..."):
resp = requests.post(f"{API}/search/semantic", json={
"query": query,
"field": None if field_filter == "全部" else field_filter,
"topk": topk,
}, timeout=30).json()
for hit in resp:
with st.container(border=True):
st.markdown(f"**{hit['title']}**")
st.caption(f"{hit['year']} · {hit['field']} · "
f"相似度 {hit['similarity']:.3f} · {hit['venue'] or '预印本'}")
with st.expander("摘要"):
st.write(hit["abstract"])
体验细节:显示相似度分数(透明的系统更可信)、spinner 提示(检索要 1-2 秒,没有反馈用户会以为卡死)、摘要折叠(默认页面干净,想看细节再展开)。演示时现场输入一个面试官感兴趣的方向,检索结果的”懂语义”是任何口述都替代不了的。
自动分类页:把 M2 的置信度机制演出来
分类页展示的不只是”能分类”,还有第 2 课设计的置信度降级:
def render():
st.header("领域自动分类")
title = st.text_area("论文标题")
abstract = st.text_area("摘要", height=150)
if st.button("分类", type="primary") and title:
resp = requests.post(f"{API}/classify",
json={"title": title, "abstract": abstract},
timeout=30).json()
conf = resp["confidence"]
if conf >= 0.6:
st.success(f"预测领域:**{resp['field']}**(置信度 {conf:.2f})")
else:
st.warning(f"置信度 {conf:.2f} 低于阈值,建议人工确认。"
f"最可能:{resp['field']}")
st.bar_chart(resp["top5_distribution"]) # Top-5 类别概率分布
低置信时黄色警告而不是硬给答案——这个细节演示时一定要讲:它体现的是模型评估一课的服务水平设计,而不只是调了个 API。Top-5 分布条形图让”模型在哪些类别间犹豫”可视化,是错误分析(相邻领域混淆)的产品化表达。
主题地图页:预计算 + 静态化
UMAP 地图十万点的计算不可能实时做。策略是离线预计算、前端只渲染:
@st.cache_data
def load_topic_map():
return pd.read_parquet("assets/topic_map.parquet") # 系列三第4课的产物
def render():
df = load_topic_map()
if df.empty:
st.info("主题地图尚无数据,请先完成数据管线。")
return
fig = px.scatter(df.sample(n=min(20000, len(df)), random_state=42), x="x", y="y", color="topic_name",
hover_data=["title"], title="研究主题地图")
fig.update_traces(marker=dict(size=3, opacity=0.6))
st.plotly_chart(fig, use_container_width=True)
后端能力(UMAP)变成静态资产(Parquet),前端零计算——演示应用的正确架构是”重活在线下”。这个原则反过来约束流水线:主题地图成为每日管线的周期性产物(比如每周重算一次)。
部署与演示 checklist
应用用 Docker 一起打包(沿用 M4 的 compose,加一个 streamlit 服务)。演示前的自查清单:
- 冷启动时间测过(首次加载各页面都在 3 秒内)。
- 断网兜底:每个 API 调用有 try/except + 友好错误页,演示现场网络抖动不死。
- 数据版本标签显示正常。
- 准备三条演示查询:一个热门口径(LLM 微调)、一个交叉口径(医学影像的 Transformer)、一个刁钻口径(精确术语)——展示系统的能力和诚实的边界。
踩坑排查
| 症状 | 原因 | 处理 |
|---|---|---|
| 页面切一次卡几秒 | 统计查询没缓存 | @st.cache_data 加 TTL |
| 控件交互导致数据重取 | Streamlit 重跑模型 | 区分缓存粒度;表单用 st.form 批量提交 |
| 主题地图渲染卡死 | 前端一次画十万点 | 抽样 2 万 + marker 调小 |
| API 挂了页面白屏 | 没有异常处理 | try/except + st.error 友好提示 |
| 缓存了过期数据 | TTL 太长 | 统计数据 TTL 1h;管线更新后主动清 |
| 演示现场检索超时 | 冷模型首次推理慢 | 启动后先打一发 warmup 请求 |
作品集证据
本课目标是做出可演示的四页应用,把 M1–M4 四个里程碑接入界面;通过功能、异常路径和版本核对后,再将应用交付记为 M5。页面代码与课文不等同于已经部署的应用,演示地址、录屏和运行环境需要实际补齐。
练习
- 实现四个页面并完成本地串联演示,录屏 3 分钟。
- 给所有 API 调用加异常兜底与超时,断网测试各页面行为。
- 测量冷启动耗时,把首屏时间压到 3 秒内。
- 准备三条演示查询并写出每条的”展示点”解说词。
面试常问
Q:Streamlit 的定位和局限? 定位:数据/AI 应用的快速演示界面,Python 全栈、零前端成本。局限:重跑模型带来的状态管理反直觉、复杂交互吃力、多人并发性能有限。所以它是”演示与内部工具”的答案,不是 C 端产品的答案。
Q:演示应用的架构原则? 重活在线下(预计算静态化)、前端只做渲染与交互、缓存分粒度设计、外部依赖全部有兜底。演示场景的第一优先级是”稳”,不是”全”。
Q:怎么处理 Streamlit 的重跑机制? 任何控件变化触发整个脚本重跑。应对:@st.cache_data 缓存昂贵计算、st.session_state 存跨重跑状态、st.form 把多控件输入批量提交。不理解重跑模型,Streamlit 应用必然卡。
Q:这个项目里哪些决策体现了产品思维? 演示动线先于界面设计;置信度降级可视化(展示系统的自知);相似度分数透明展示;准备覆盖能力与边界的演示查询。产品思维 = 始终从”观众 5 分钟内看懂什么”倒推。
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。