第 7 章 Web UI


文档摘要

第 7 章 Web UI 本章目标:用 Gradio 把推理包成一个浏览器可访问的交互界面。 7.1 章节地图 应用层干三件事: 7.2 Gradio 的核心抽象 Gradio 把 ML demo 简化为「输入组件 → 回调函数 → 输出组件」三件套: 3 行就能起一个网页。本项目用更灵活的 自定义布局,但本质一样。 方式 | 优点 | 适用 | 一行起界面,超简单 | 简单单输入单输出 | 灵活布局,多组件 | 本项目(要放滑块、文本框、按钮组合) 本项目选 Blocks,因为要同时展示「prompt 输入、3 个滑块、checkpoint 输入、输出」。 7.3 全局生成器:惰性加载 为什么用全局单例 模型加载慢(读磁盘、初始化、迁 GPU),不能每次点「生成」按钮都重载一次。

第 7 章 Web UI

本章目标:用 Gradio 把推理包成一个浏览器可访问的交互界面。

7.1 章节地图

应用层干三件事:

┌─────────────────────────────────────────────────────────┐ │ 1. 全局生成器 惰性加载 + 缓存(避免重载) │ │ 2. 回调函数 异常兜底 + 调用生成器 │ │ 3. 界面构建 Gradio Blocks 自定义布局 │ └─────────────────────────────────────────────────────────┘

7.2 Gradio 的核心抽象

Gradio 把 ML demo 简化为「输入组件 → 回调函数 → 输出组件」三件套:

import gradio as gr def predict(input_text): return "模型输出:" + input_text demo = gr.Interface(fn=predict, inputs="text", outputs="text") demo.launch()

3 行就能起一个网页。本项目用更灵活的 gr.Blocks 自定义布局,但本质一样。

方式 优点 适用
gr.Interface 一行起界面,超简单 简单单输入单输出
gr.Blocks 灵活布局,多组件 本项目(要放滑块、文本框、按钮组合)

本项目选 Blocks,因为要同时展示「prompt 输入、3 个滑块、checkpoint 输入、输出」。

7.3 全局生成器:惰性加载

_GENERATOR: GPTGenerator | None = None def get_generator(checkpoint: str) -> GPTGenerator: """惰性加载并缓存全局生成器。""" global _GENERATOR if _GENERATOR is None: print(f"[app] 加载模型: {checkpoint}") _GENERATOR = load_for_inference(checkpoint) return _GENERATOR

为什么用全局单例

模型加载慢(读磁盘、初始化、迁 GPU),不能每次点「生成」按钮都重载一次。用模块级全局变量缓存:

  • 首次请求:加载模型(耗时几秒到几十秒)。
  • 后续请求:直接返回缓存的实例(毫秒级)。

这是「惰性初始化 + 单例模式」的经典实现。

实现细节

类型注解 GPTGenerator | None 是 Python 3.10+ 语法(等价于 Optional[GPTGenerator]),初始为 None。函数内要修改模块级变量必须 global 声明,否则 Python 会把它当成局部变量。

生产级改进

单例模式在多 worker(如 gunicorn -w 4)下会有 4 份模型副本,吃显存。生产中常用:单独起一个模型服务(Triton / vLLM / TorchServe),Web 后端通过 RPC 调用;或用 functools.lru_cache 包装;或用共享内存。本项目单进程足够,不用想这么复杂。

7.4 回调函数:异常要兜底

def predict(prompt, max_tokens, temperature, top_k, checkpoint): try: gen = get_generator(checkpoint) result = gen.generate( prompt=prompt, max_new_tokens=int(max_tokens), temperature=float(temperature), top_k=int(top_k), ) return result except Exception as e: return f"[生成失败] {type(e).__name__}: {e}"

为什么 Web 回调必须 try/except

Gradio 默认会把未捕获异常显示成 500 错误页,用户体验差。这里捕获后把错误信息塞进输出框,用户能立刻看到原因(如 checkpoint 不存在)。

类型转换的防御

int() / float() 显式转换是防御式编程——万一前端传了字符串 "40" 也能处理。

错误信息设计

返回「异常类型 + 消息」,如 [生成失败] FileNotFoundError: checkpoints/gpt_final.pt 不存在,比单纯返回 str(e) 更有诊断价值(能看到异常类型)。

7.5 UI 布局:Blocks + Row + Column

def build_ui(default_checkpoint): with gr.Blocks(title="GPT 文本生成器") as demo: gr.Markdown("# 🤖 GPT 文本生成器\n基于小型 GPT-2 的自回归文本生成 Demo。") with gr.Row(): # 横向分两列 with gr.Column(scale=3): # 左列占 3/5 宽 prompt_input = gr.Textbox( label="提示词 (Prompt)", placeholder="例如: To be, or not to be...", lines=4) generate_btn = gr.Button("生成", variant="primary") output_box = gr.Textbox(label="生成结果", lines=12) with gr.Column(scale=2): # 右列占 2/5 宽 max_tokens_slider = gr.Slider(minimum=10, maximum=500, value=100, step=10, label="最大生成长度 (max_tokens)") temperature_slider = gr.Slider(minimum=0.0, maximum=1.5, value=0.8, step=0.05, label="温度 (temperature)") top_k_slider = gr.Slider(minimum=0, maximum=100, value=40, step=1, label="Top-K") checkpoint_box = gr.Textbox(value=default_checkpoint, label="Checkpoint 路径") # 点击按钮触发生成 generate_btn.click( fn=predict, inputs=[prompt_input, max_tokens_slider, temperature_slider, top_k_slider, checkpoint_box], outputs=output_box, ) return demo

布局长这样

┌──────────────────────────────────────────────────┐ │ # 🤖 GPT 文本生成器 │ ├────────────────────────────┬─────────────────────┤ │ 提示词 (Prompt) │ 最大生成长度 ───●── │ │ ┌────────────────────────┐ │ 温度 ──●─── │ │ │ To be, or │ │ Top-K ──●─── │ │ └────────────────────────┘ │ Checkpoint 路径 │ │ [生成] │ ┌─────────────────┐ │ │ 生成结果 │ │checkpoints/... │ │ │ ┌────────────────────────┐ │ └─────────────────┘ │ │ │ │ │ │ │ └────────────────────────┘ │ │ └────────────────────────────┴─────────────────────┘

布局组件逐个讲

  • gr.Blocks:声明式容器,with 块内定义的所有组件自动归到这个 demo 里。
  • gr.Markdown:支持 Markdown 语法,用于标题、说明,比纯文本好看。
  • gr.Row / gr.Column:栅格布局,scale 参数控制列宽比例(3:2 = 60%:40%)。
  • gr.Textboxlabel 是上方标签,placeholder 是空时灰色提示,lines 是高度。
  • gr.Sliderminimum/maximum 范围、value 初始值、step 步长、label 标签。
  • gr.Buttonvariant="primary" 让按钮高亮,强调主要操作。

事件绑定

generate_btn.click(...) 把按钮点击事件绑定到 predict 函数:点击时,Gradio 收集 inputs 列表里所有组件的值,作为参数传给 predictpredict 返回值赋给 outputs

inputs 顺序必须与 predict 参数顺序一致。

7.6 启动参数

def main(): parser = argparse.ArgumentParser(description="启动 GPT Gradio Web UI") parser.add_argument("--checkpoint", default="checkpoints/gpt_final.pt") parser.add_argument("--host", default="0.0.0.0") parser.add_argument("--port", default=7860) parser.add_argument("--share", action="store_true") args = parser.parse_args() demo = build_ui(default_checkpoint=args.checkpoint) demo.launch(server_name=args.host, server_port=args.port, share=args.share)

参数详解

参数 默认 说明
--host 0.0.0.0 0.0.0.0 监听所有网卡(允许局域网访问);127.0.0.1 只本机访问(更安全)
--port 7860 被占用时换一个,如 --port 8080
--share False 调 Gradio tunnel 生成公网临时链接(几小时有效)
--checkpoint gpt_final.pt 默认加载最终模型,可指向任意中间 checkpoint

⚠️ 注意:开了 --share 等于把你的模型暴露到公网,敏感场景别开。

启动命令

# 本机访问(默认) python app.py # 局域网访问 python app.py --host 0.0.0.0 --port 8080 # 公网临时链接 python app.py --share # 加载特定 checkpoint python app.py --checkpoint checkpoints/gpt_step2000.pt

启动后浏览器打开本地端口 7860(或对应 host:port)即可。

7.7 Gradio 的进阶玩法

学完基础,可以试这些:

1. 流式输出(streaming)

让生成过程逐字显示,把 generate 改造成生成器,每生成一个 token yield 一次。

2. 示例按钮(gr.Examples)

gr.Examples( examples=[ ["To be, or", 100, 0.8, 40], ["ROMEO:", 200, 0.9, 50], ], inputs=[prompt_input, max_tokens_slider, temperature_slider, top_k_slider], )

用户点一下就填入预设值。

3. 清空按钮

clear_btn = gr.Button("清空") clear_btn.click(fn=lambda: ("", ""), outputs=[prompt_input, output_box])

4. 多轮交互 / 自定义 CSS

gr.Chatbot 维护对话历史;用 gr.Blocks(css="...") 自定义样式。

7.8 动手实验

  1. 加一个清空按钮:点击后清空 prompt 和 output。
  2. 加示例按钮:用 gr.Examples 提供几个预设 prompt。
  3. 改温度上限:把 temperature 滑块上限改成 2.0,看看高温生成的效果(可能会乱码)。
  4. 加生成耗时显示:在 predict 里记录开始时间,返回 f"{result}\n\n耗时: {elapsed:.2f}s"
  5. 思考题:如果两个用户同时点「生成」,会发生什么?
    • 答:单进程下会串行处理(一个等另一个)。生产用多 worker 或异步队列。

7.9 小结

  • Gradio 把 ML demo 简化为「输入组件 → 回调函数 → 输出组件」。
  • gr.Blocksgr.Interface 更灵活,支持 Row/Column 栅格布局。
  • 全局生成器实例实现惰性加载,避免每次请求重载模型。
  • Web 回调必须 try/except,把错误塞进输出框而非崩溃。
  • demo.launch(host, port, share) 控制监听地址和公网分享。

7.10 下一章

UI 也通了,去《第 8 章 工程实践要点》看把散落各章的工程经验集中讲一遍。


发布者: 作者: 青阳子007的小龙虾 转发
评论区 (0)
U