
把训练好的 AI 模型变成一个能交互访问的 Web 应用是算法工程师、数据分析师在模型落地时遇到的第一道工程门槛。写 Flask 当然可以但输入校验、页面样式、文件上传、运行进度、结果展示都要从零拼接前期投入并不小。Streamlit 和 Gradio 正是为这个场景设计的两个 Python 框架它们把前端组件和交互逻辑封装成接近普通 Python 的 API让模型推理过程在几小时内变成可操作页面。这篇文章围绕同一个文本分类任务分别使用两个框架完成最小可运行应用再补充缓存、启动参数、真实模型替换、问题排查和部署边界。阅读后可快速决定什么场景用 Streamlit什么场景用 Gradio以及怎样把示例换成自己的模型。1. 先拆清楚问题模型 Web 化到底做了什么很多人在模型训练阶段思路很清晰到了部署阶段却不知道从哪里开始。原因不是“不会前端”而是没有把“模型推理”和“Web 交互”拆成两个独立问题。1.1 模型本质上是一个推理函数无论模型是 PyTorch、TensorFlow、scikit-learn还是通过远程 API 调用站在调用方视角看它都是一个输入输出函数输入一组文本、图片、特征向量或结构化参数。处理模型权重加载、预处理、前向计算、后处理。输出标签、概率、坐标、生成文本或 JSON 结构。可以把一切 Web 化工作看成给这个函数加一个“浏览器外壳”。外壳要负责用输入控件收集用户数据。把数据转成模型函数要求的格式。调用函数并等待结果。把结果渲染成人类可读的页面。处理异常、空输入、长任务和并发访问。def predict(text: str) - dict: # 这里可以是 scikit-learn pipeline # 可以是 transformer 模型的 forward 结果 # 也可以是远程 API 的 JSON 响应 return {label: 正向, score: 0.93, detail: []}把函数写好再把前端壳接上去技术风险就会小很多。相反如果一开始就写页面再去想模型怎么接很容易被框架细节带偏。1.2 Web 化不只有 UI还要考虑状态和重跑写普通 Python 脚本时程序是从头执行到尾。但 Web 应用不是这样用户每点一次按钮可能只会执行某个回调刷新页面后内存中的局部变量可能已经丢失两个用户同时访问时同一个模型权重不能被重复加载。这些是浏览器交互特有的问题。Streamlit 的解决思路是“脚本每次交互都重新执行”Gradio 的解决思路是“事件驱动回调”。两者都能完成 AI 模型演示但适合的场景和心智模型不同。1.3 Streamlit 与 Gradio 的定位差异对比维度StreamlitGradio主要定位数据应用、内部工具、仪表盘AI 模型演示、接口封装、分享体验开发方式按 Python 脚本书写页面随脚本执行生成Interface 极简封装Blocks 提供自定义布局交互模型每次操作触发整页脚本重跑事件回调组件与函数绑定输入组件文本框、上传、滑块、日期等丰富组件面向模型输入输出设计的组件覆盖文本、图像、音频缓存内置 st.cache_data、st.cache_resource模型加载通常自己写模块级变量或缓存API 开放不是重点页面为主启动后自带接口文档方便自动化调用学习成本低上手快Interface 更低Blocks 稍高典型场景数据查询面板、模型分析后台、报表给业务方试用模型、做 POC、输出可调用接口一个项目里也可以同时使用内部数据看板用 Streamlit模型对外演示和接口试用用 Gradio。这里没有绝对优劣只有场景匹配。2. 环境准备先用虚拟环境把依赖链路固定下来这部分看起来简单却是多数入门项目浪费时间的起点。如果直接在当前系统 Python 里安装依赖很容易和已有包冲突最后出现“这里能跑、换个环境就不能跑”的尴尬问题。2.1 Python 版本与核心库安装Streamlit 和 Gradio 都依赖较新的 Python 特性建议使用 Python 3.10 或更高版本。因为这些库的版本升级较快正式安装前请以官方当前文档的版本要求为准。mkdir ai_web_demo cd ai_web_demo # 创建虚拟环境 python -m venv .venv # Windows 激活 .venv\Scripts\activate # Linux / macOS 激活 source .venv/bin/activate # 升级 pip 并安装两个框架 python -m pip install --upgrade pip pip install streamlit gradio安装完成后检查版本streamlit version python -c import gradio; print(gradio.__version__)如果版本命令正常输出说明安装链路没有问题。以后在其他机器复现时不要靠命令行一条条装直接导出一份锁定的依赖清单pip freeze requirements.txt注意requirements.txt 应该来自干净虚拟环境。生产环境安装时用pip install -r requirements.txt避免手工安装导致版本漂移。2.2 建议的项目目录结构示例代码建议按模块拆分不要让页面文件和模型代码混在一起ai_web_demo/ |-- .venv/ |-- requirements.txt |-- model_predict.py # 模型加载与推理函数 |-- streamlit_app.py # Streamlit 页面 |-- gradio_app.py # Gradio 页面 |-- models/ # 本地模型文件目录示例可留空 |-- README.md这样设计的核心原因是同一份模型推理逻辑可以被 Streamlit 和 Gradio 共用页面文件只负责交互。替换真实模型时只需要修改model_predict.py不需要动前端代码。3. Streamlit 示例把文本情感分类做成可交互页面3.1 先写一个可替换的推理函数为了让示例不依赖外部模型下载先用关键词规则模拟一个文本情感分类器。真实项目中把函数内部替换成模型加载与推理即可函数签名保持不变。# model_predict.py NEG_WORDS [卡顿, 崩溃, 闪退, 难用, 缓慢, 糟糕, 不推荐] POS_WORDS [流畅, 好用, 清晰, 稳定, 推荐, 满意] def predict_text(text: str) - dict: if not text or not text.strip(): raise ValueError(输入文本不能为空) neg_hits [word for word in NEG_WORDS if word in text] pos_hits [word for word in POS_WORDS if word in text] delta len(pos_hits) - len(neg_hits) if delta 0: label 正向 score 0.5 min(0.45, delta * 0.15) elif delta 0: label 负向 score 0.5 min(0.45, -delta * 0.15) else: label 中性 score 0.5 return { label: label, score: round(score, 4), pos_hits: pos_hits, neg_hits: neg_hits, }这里的输入是普通字符串输出是包含标签、置信度和命中词的字典。真实模型中同样可以用这个字典协议与前端交互。3.2 用 st.cache_resource 管理模型单例模型加载通常是 Web 应用中开销最大的操作。Streamlit 的特点是每次交互都会重新执行脚本如果不做缓存每次点击按钮都可能重新读权重。对本地模型来说这是完全不可接受的。# 以本地模型目录为例只做结构示意 import joblib import streamlit as st st.cache_resource def load_pipeline(model_path: str): # sklearn joblib 方式 return joblib.load(model_path) st.cache_resource def load_big_model(model_dir: str): # PyTorch / TensorFlow 方式 # 例如 tokenizer 和 model ... raise NotImplementedError(换成你的模型加载逻辑)关键点是st.cache_resource缓存的是不可变对象例如模型、数据库连接、客户端实例。对于 DataFrame、文件读取结果优先用st.cache_data。两种缓存不要搞混否则可能出现缓存读写不稳定或占用内存异常的问题。3.3 写页面交互与结果展示新建streamlit_app.pyimport streamlit as st from model_predict import predict_text st.set_page_config(page_title文本情感识别 Demo, layoutcentered) st.title(文本情感识别 Demo) st.markdown(输入一段中文模型会判断情感极性并给出置信度。) user_input st.text_area( 待分析文本, value, height150, placeholder例如这个页面加载很流畅推荐大家使用。, ) if st.button(开始分析, typeprimary): if not user_input.strip(): st.warning(请先输入文本。) else: try: with st.spinner(模型推理中...): result predict_text(user_input) col1, col2, col3 st.columns(3) col1.metric(情感标签, result[label]) col2.metric(置信度, f{result[score]:.2%}) col3.metric(正向词命中, len(result[pos_hits]) len(result[neg_hits])) with st.expander(查看结构化结果): st.json(result) except Exception as exc: st.error(f推理失败{exc})代码要点st.text_area负责收集文本st.button触发分析动作。st.spinner给长时间运行的模型提供视觉反馈。st.json展示结构化输出适合调试阶段查看完整结果。显式捕获异常避免模型内部错误导致页面白屏。3.4 启动与验证在项目根目录执行streamlit run streamlit_app.py启动后终端会输出本地访问地址。浏览器打开类似http://localhost:8501输入文本并点击按钮。预期结果文本包含“流畅”“好用”等词时标签显示“正向”。文本包含“崩溃”“闪退”等词时标签显示“负向”。两个方向的词都不命中时显示“中性”。如果发现页面一直转圈先回到终端看日志可能是模型函数抛异常或耗时过长。Streamlit 每次交互都会重跑页面配合st.cache_resource后模型只需要首次加载一次。4. Gradio 示例面向模型演示的另一种写法4.1 Interface几行代码完成最小闭环Gradio 最简用法是gr.Interface它只需要三个核心参数处理函数、输入组件、输出组件。新建gradio_app.pyimport gradio as gr from model_predict import predict_text def predict_for_demo(text: str) - str: result predict_text(text) return f{result[label]}置信度 {result[score]:.2%} demo gr.Interface( fnpredict_for_demo, inputsgr.Textbox( lines5, placeholder输入一句中文文本, label待分析文本, ), outputstext, titleAI 文本情感识别, description输入一段文本观察模型输出, examples[ 这个页面加载很流畅推荐大家使用。, 软件频繁崩溃体验很糟糕。, 今天天气不错。, ], ) if __name__ __main__: demo.launch(server_name127.0.0.1, server_port7860)examples参数特别适合 AI 模型演示。业务方打开页面后不需要自己构造测试数据直接点击示例就能看到效果。这是 Gradio 在模型评测场景中流行的原因之一。4.2 Blocks需要自定义布局时用事件回调当页面需要多个输入输出或者需要按钮点击后才触发而不是一输入就触发可以使用gr.Blocksimport gradio as gr from model_predict import predict_text def analyze(text: str): result predict_text(text) return result[label], f{result[score]:.2%}, str(result[pos_hits]) with gr.Blocks(titleAI 文本情感识别) as demo: gr.Markdown(## 文本情感识别) with gr.Row(): text_input gr.Textbox( lines4, label输入文本, placeholder例如页面很流畅功能稳定。, ) result_label gr.Label(label预测结果) score_text gr.Textbox(label置信度, interactiveFalse) hit_text gr.Textbox(label命中关键词, interactiveFalse) analyze_btn gr.Button(开始分析, variantprimary) analyze_btn.click( fnanalyze, inputstext_input, outputs[result_label, score_text, hit_text], ) gr.Examples( examples[页面很流畅功能稳定。, 频繁闪退不推荐。], inputstext_input, ) if __name__ __main__: demo.launch(server_name0.0.0.0, server_port7860)这里有一个必须注意的规则输出组件列表要和函数返回值一一对应。analyze返回了三个值outputs就写三个组件。如果数量不一致页面会报 “Cannot unpack” 或类似错误。4.3 给演示页面加一层基础身份验证实际部署时如果页面需要暴露到内网给不固定人员访问但暂时没有统一登录系统可以使用 Gradio 的基础认证参数demo.launch( server_name0.0.0.0, server_port7860, auth(admin, change-me-password), )浏览器访问时会出现账号密码弹窗。需要说明的是这只是一个非常基础的身份校验不是安全方案。正式环境仍然建议放在认证网关或反向代理后面不要直接裸暴露公网。4.4 Gradio 的接口能力Gradio 应用启动后不仅提供可视化页面还提供了一个 HTTP 接口层方便脚本调用和自动化测试。具体路径在不同版本间略有变化但通常可以在启动后访问带有/gradio_api/openapi.json的地址查看接口定义。如果需要做接口自动化先打开 OpenAPI 文档确认当前版本的请求体字段再用 requests 或 curl 请求实际接口路径。不要把网上旧教程里的接口地址直接套到自己项目上Gradio 版本升级后路径容易变。5. 把示例函数替换成真实 AI 模型示例中的关键词规则只用于理解流程并不具备真正的模型能力。把它替换成真实模型只需要专注修改model_predict.py。5.1 本地模型把加载动作移到模块级或缓存中如果你的模型是用 scikit-learn 训练后保存的加载逻辑可以写成import joblib _pipeline None def load_model_once(): global _pipeline if _pipeline is None: _pipeline joblib.load(models/pipeline.joblib) return _pipeline def predict_text(text: str) - dict: if not text or not text.strip(): raise ValueError(输入文本不能为空) pipeline load_model_once() label pipeline.predict([text])[0] proba pipeline.predict_proba([text])[0] return { label: str(label), score: round(float(max(proba)), 4), detail: proba.tolist(), }关键的加载逻辑使用模块级单例避免每次调用都重新读取权重。如果在 Streamlit 项目中使用可以进一步用st.cache_resource包住加载函数。处理 PyTorch、TensorFlow 或其他模型时只需要替换joblib.load部分让函数返回一个具备predict能力的对象即可。加载模型时建议使用本地目录而不是每次启动临时下载。如果模型文件不在项目目录要通过环境变量或配置文件指定路径不要写在页面代码里。5.2 远程模型 API把 HTTP 调用封装成同一个输出结构如果模型实际上运行在别的服务上前端框架不需要知道细节。你只需要把 HTTP 调用封装到同一个predict_text函数中import os import requests REMOTE_API_URL os.getenv(MODEL_API_URL, https://your-model-service.example/api/predict) REMOTE_API_KEY os.getenv(MODEL_API_KEY, ) def predict_text(text: str) - dict: resp requests.post( REMOTE_API_URL, headers{Authorization: fBearer {REMOTE_API_KEY}}, json{text: text, task: sentiment}, timeout30, ) resp.raise_for_status() data resp.json() return { label: data[label], score: float(data[score]), detail: data.get(detail, {}), }API 地址和密钥必须通过环境变量或配置中心传入不要硬编码。生产环境还要处理超时、重试、上游不可用时的降级提示。前端页面只关心predict_text返回结构底层是本地模型还是远程 API对 UI 完全透明。注意远程模型接口调用要考虑数据合规。业务数据是否允许发送到外部服务由项目自身的数据安全规范决定。5.3 输入输出协议要先固定再开发无论使用哪种模型强烈建议先把输入输出字段写成固定协议# 建议的统一输出协议示例 { label: 负向, score: 0.87, detail: { pos_hits: [], neg_hits: [崩溃, 闪退] } }前端展示、日志记录、接口测试都围绕这个协议展开。模型替换时只是内部实现变化页面不需要改动。6. 启动、验证与运行检查清单Web 应用的“能跑”不只是打开一个页面而是页面、接口、异常路径都符合预期。6.1 页面验证检查项预期结果输入合法文本后点击按钮显示标签和置信度输入空文本出现警告或错误提示不崩溃连续点击多次第二次之后应明显更快缓存生效刷新页面页面能恢复不出现残留状态错乱浏览器控制台没有明显的 JS 资源加载错误6.2 命令和日志验证Streamlit 启动时可以通过--server.headless true静默运行streamlit run streamlit_app.py --server.headless true --server.port 8501Gradio 启动后注意日志中输出的地址。如果需要从另一台机器访问服务端必须监听0.0.0.0同时保证防火墙和云安全组开放对应端口python gradio_app.py6.3 并发和性能预检AI 模型推理往往比普通 Web 请求慢。上线前要明确几个问题单次推理耗时是多少秒两个用户同时访问时模型对象是否线程安全模型权重占用多少内存一台机器最多能承载几个并发是否需要把请求排队避免模型被并发调用打崩Gradio 内置了队列机制可以在启动前先调用demo gr.Blocks() demo.queue(default_concurrency_limit4)这个参数用于控制并发队列具体字段名和版本有关使用前先看当前版本的 API 文档。Streamlit 的线程模型更特殊重模型加载务必走cache_resource否则每个会话都可能尝试重新加载权重。7. 常见问题与排查路径7.1 页面一直转圈没有结果显示可能原因推理函数抛异常但没有被捕获。模型加载超时。st.cache_resource中加载了不可序列化却需要反复修改的对象。Gradio 回调函数返回值与输出组件数量不匹配。检查顺序回终端看应用日志是否有 Traceback。单独在 Python 环境调用一次predict_text确认函数本身能跑通。去掉缓存装饰器做对照测试排查是不是缓存导致。检查输入数据和模型期望的格式是否一致。7.2 端口被占用Streamlit 默认端口是 8501Gradio 默认端口是 7860。启动时若提示address already in use先查占用# Linux / macOS lsof -i :8501 # Windows netstat -ano | findstr :8501确认占用进程后释放端口或换端口启动streamlit run streamlit_app.py --server.port 8510 ./.venv/bin/python gradio_app.py7.3 其他机器访问不到页面原因通常是服务只监听了127.0.0.1或服务器防火墙没有放行端口。Streamlit 监听地址可以通过参数修改streamlit run streamlit_app.py --server.address 0.0.0.0Gradio 则在launch中传入server_name0.0.0.0。注意这不是推荐的生产暴露方式只适合开发联调。直接暴露到公网会带来明显的安全风险。7.4 常见问题速查表问题现象常见原因检查方式处理建议页面首次慢后续快模型首次加载未缓存看日志是否有加载耗时使用 cache_resource 或模块级单例每次点击都很慢模型被重复加载在加载函数里加 print核查缓存是否真实命中避免每轮重跑模型加载失败模型路径写错或目录不存在打印绝对路径用 os.path.abspath 核对路径Gradio 输出报 unpack 错误返回值数量与输出组件不一致数返回值和 outputs 列表让函数返回结构清晰或统一一个 JSON 输出页面能打开但接口调不通版本路径变化查看 openapi.json按当前版本的接口字段调用上传的文件超出默认限制框架默认限制查看错误提示在框架配置中调整上传大小限制按版本文档操作8. 从本机 Demo 到服务器项目的工程底线8.1 学习环境与生产环境差异环境需要做的事情学习环境虚拟环境安装依赖跑通页面验证函数边界测试环境使用真实模型或真实 API验证字段映射、日志、异常生产环境配置外置、反向代理、进程守护、日志采集、监控告警、回滚方案不要把一个本机 demo 直接拷贝到生产服务器然后只改 IP 就对外提供服务。至少还应检查依赖是否锁定、模型文件是否随发布带上、密钥是否已外置、日志是否能排查问题。8.2 使用 systemd 或 Docker 管理进程以 Streamlit 为例用 systemd 管理服务时可以写一个类似下面的 unit 文件[Unit] DescriptionStreamlit Sentiment App Afternetwork.target [Service] Userwebuser WorkingDirectory/opt/ai_web_demo ExecStart/opt/ai_web_demo/.venv/bin/streamlit run streamlit_app.py --server.port 8501 --server.address 127.0.0.1 Restartalways RestartSec3 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target建议先监听127.0.0.1前面再放 Nginx 做反向代理和 HTTPS。如果直接监听0.0.0.0应用等同于你自己同时兼职了防火墙、TLS 和访问控制错误风险很高。使用 Docker 时可以准备一个最小 DockerfileFROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8501 CMD [streamlit, run, streamlit_app.py, --server.port, 8501, --server.address, 0.0.0.0, --server.headless, true]8.3 反向代理需要处理升级请求Streamlit 底层使用 WebSocket 与浏览器通信。使用 Nginx 代理时除了普通 HTTP 头还需要设置升级请求头否则页面无法实时更新server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:8501; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }普通请求代理方式无法满足 Streamlit 的实时更新机制。Gradio 也有类似的长连接和事件接口需求上线前要在代理层完整测试页面交互而不是只看首页能不能打开。8.4 最终实践建议如果目标只是给同事演示分析过程Streamlit 更合适开发效率高、页面信息密度大。如果目标是让非技术用户反复测试同一套模型效果Gradio 的 Interface、示例样例和内置 API 更顺手。替换真实模型前先固定输入输出协议上线前把页面、接口、异常字段、超时时间都验证一遍进入服务器后用 systemd 或 Docker 管理进程并让应用通过反向代理对外提供服务。对新手来说最有价值的练习不是把两个框架都用到很花哨而是完成一次最小闭环先跑通规则示例再换成本地模型或远程 API最后加上缓存和异常处理。这个过程走完模型到 Web 应用之间的最后一公里就真正打通了。