第二十三章 Gradio构建AI应用¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
用Python快速构建交互式AI应用界面
学习时间: 6-8小时 难度级别: ⭐⭐⭐ 中级 前置知识: Python基础、LLM API调用(第1-2章)、RAG基础(第5章) 学习目标: 掌握Gradio 4.x核心API,能独立构建聊天、RAG问答、图像生成等AI应用界面
📖 章节导读¶
大模型能力再强,也需要一个直观的界面让用户使用。Gradio让你用几行Python代码就能构建专业级AI应用界面,无需前端知识。本章从基础组件到完整项目,带你掌握Gradio的核心用法。
1. Gradio简介¶
1.1 为什么选择Gradio¶
构建AI应用界面有多种选择,以下是主流方案对比:
| 特性 | Gradio | Streamlit | Flask/FastAPI |
|---|---|---|---|
| 学习曲线 | ⭐ 极低 | ⭐⭐ 低 | ⭐⭐⭐ 中等 |
| AI组件支持 | ✅ 原生ChatInterface | ⚠️ 需配合st.chat | ❌ 需自建 |
| 分享功能 | ✅ 一键share链接 | ❌ 需部署 | ❌ 需部署 |
| HF Spaces | ✅ 原生支持 | ✅ 支持 | ⚠️ 需Docker |
| API自动生成 | ✅ 自动 | ❌ 无 | ✅ 手动定义 |
| 流式输出 | ✅ 原生 | ✅ 支持 | ⚠️ 需SSE |
| 自定义布局 | ✅ Blocks API | ⚠️ 有限 | ✅ 完全自由 |
| 适用场景 | ML Demo、AI应用 | 数据仪表盘 | 生产级API |
Gradio核心优势: - 专为AI/ML场景设计,内置聊天、图像、音频等组件 - 零前端知识即可构建专业界面 - 自动生成REST API,方便集成 - 一行代码生成公网分享链接
1.2 安装与快速上手¶
# 安装Gradio 4.x(需要Python 3.10+)
pip install "gradio>=4.0"
# 验证安装
python -c "import gradio as gr; print(gr.__version__)"
Hello World示例:
import gradio as gr
def greet(name: str) -> str:
return f"你好 {name}!欢迎使用Gradio构建AI应用 🎉"
# 最简单的Interface:一个输入,一个输出
demo = gr.Interface(
fn=greet,
inputs=gr.Textbox(label="你的名字", placeholder="请输入名字..."),
outputs=gr.Textbox(label="问候语"),
title="Gradio Hello World",
description="输入你的名字,获取个性化问候!",
)
demo.launch()
# 默认运行在 http://127.0.0.1:7860
运行后浏览器自动打开,你将看到一个带输入框、提交按钮和输出区域的完整界面。
1.3 核心概念:Interface、Blocks、Components¶
Gradio有三层抽象:
Components(组件) → 单个UI元素:Textbox, Image, Slider...
↓
Interface(接口) → 快速搭建:输入 → 函数 → 输出
↓
Blocks(区块) → 完全自定义布局与交互逻辑
- Interface:高级API,适合简单的 输入→处理→输出 场景
- Blocks:底层API,支持任意布局、多步交互、状态管理
- Components:构成界面的基本元素,既可作为输入也可作为输出
💡 经验法则:原型阶段用Interface快速验证,产品阶段用Blocks精细控制。
2. 基础组件¶
2.1 输入组件¶
import gradio as gr
def process_inputs(
text: str,
image, # PIL.Image
audio, # tuple(sample_rate, numpy_array)
file, # tempfile path
slider_val: float,
dropdown_val: str,
checkbox_val: bool,
):
return f"""
文本: {text}
图像尺寸: {image.size if image else 'None'}
音频采样率: {audio[0] if audio else 'None'}
文件: {file.name if file else 'None'}
滑块值: {slider_val}
下拉选择: {dropdown_val}
复选框: {checkbox_val}
"""
demo = gr.Interface(
fn=process_inputs,
inputs=[
gr.Textbox(label="文本输入", placeholder="输入任意文本...", lines=3),
gr.Image(label="上传图片", type="pil"),
gr.Audio(label="上传音频", type="numpy"),
gr.File(label="上传文件", file_types=[".pdf", ".txt", ".csv"]),
gr.Slider(minimum=0, maximum=100, value=50, step=1, label="参数调节"),
gr.Dropdown(
choices=["GPT-4o", "Claude-3.5", "Gemini-2.0"],
value="GPT-4o",
label="模型选择",
),
gr.Checkbox(label="启用流式输出", value=True),
],
outputs=gr.Textbox(label="处理结果", lines=10),
title="Gradio组件展示",
)
demo.launch()
2.2 输出组件¶
import gradio as gr
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
def generate_outputs(text: str):
# 文本输出
result_text = f"处理结果:{text.upper()}"
# DataFrame输出
df = pd.DataFrame({
"模型": ["GPT-4o", "Claude-3.5", "Gemini-2.0"],
"准确率": [0.92, 0.89, 0.91],
"延迟(ms)": [320, 280, 250],
})
# 图表输出
fig, ax = plt.subplots(figsize=(8, 4))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), label="sin(x)")
ax.plot(x, np.cos(x), label="cos(x)")
ax.set_title("示例图表")
ax.legend()
plt.tight_layout()
return result_text, df, fig
demo = gr.Interface(
fn=generate_outputs,
inputs=gr.Textbox(label="输入文本"),
outputs=[
gr.Textbox(label="文本结果"),
gr.DataFrame(label="数据表格"),
gr.Plot(label="可视化图表"),
],
)
demo.launch()
2.3 组件属性与事件¶
每个组件都有丰富的属性可配置:
import gradio as gr
# Textbox高级配置
text_input = gr.Textbox(
label="Prompt",
placeholder="输入你的问题...",
lines=5, # 显示行数
max_lines=20, # 最大行数
show_copy_button=True, # 显示复制按钮
autofocus=True, # 自动聚焦
elem_id="main-input", # CSS ID
elem_classes=["custom-textbox"], # CSS类
)
# Image高级配置
image_input = gr.Image(
label="上传图片",
type="pil", # "pil", "numpy", "filepath"
height=300,
sources=["upload", "clipboard"], # 图片来源
show_download_button=True,
)
# 组件事件
with gr.Blocks() as demo:
text = gr.Textbox(label="输入")
output = gr.Textbox(label="输出")
# change事件:输入变化时触发
text.change(fn=lambda x: x.upper(), inputs=text, outputs=output) # lambda匿名函数
# submit事件:按回车时触发
text.submit(fn=lambda x: f"提交了: {x}", inputs=text, outputs=output)
demo.launch()
3. 构建LLM聊天应用¶
3.1 gr.ChatInterface快速构建¶
gr.ChatInterface是Gradio 4.x专为聊天场景设计的高级组件:
import gradio as gr
from openai import OpenAI
client = OpenAI() # 自动读取OPENAI_API_KEY环境变量
def chat(message: str, history: list[dict]) -> str:
"""非流式聊天函数"""
messages = history + [{"role": "user", "content": message}]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
return response.choices[0].message.content
demo = gr.ChatInterface(
fn=chat,
type="messages", # Gradio 4.x推荐使用messages格式
title="AI聊天助手",
description="基于GPT-4o-mini的智能助手",
examples=["你好!", "解释一下什么是RAG", "写一首关于AI的诗"],
theme=gr.themes.Soft(),
)
demo.launch()
3.2 流式输出(Streaming)¶
流式输出大幅提升用户体验,让回答"打字机式"逐字显示:
import gradio as gr
from openai import OpenAI
client = OpenAI()
def chat_stream(message: str, history: list[dict]):
"""流式聊天函数——使用yield逐步返回"""
messages = history + [{"role": "user", "content": message}]
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True,
)
partial_message = ""
for chunk in stream:
if chunk.choices[0].delta.content is not None:
partial_message += chunk.choices[0].delta.content
yield partial_message # 每次yield更新界面
demo = gr.ChatInterface(
fn=chat_stream,
type="messages",
title="AI聊天助手(流式输出)",
)
demo.launch()
3.3 系统提示词配置¶
让用户自定义AI助手的行为:
import gradio as gr
from openai import OpenAI
client = OpenAI()
def chat_with_system_prompt(
message: str,
history: list[dict],
system_prompt: str,
temperature: float,
max_tokens: int,
):
messages = [{"role": "system", "content": system_prompt}]
messages += history
messages.append({"role": "user", "content": message})
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True,
temperature=temperature,
max_tokens=max_tokens,
)
partial = ""
for chunk in stream:
if chunk.choices[0].delta.content:
partial += chunk.choices[0].delta.content
yield partial # yield产出值,函数变为生成器
demo = gr.ChatInterface(
fn=chat_with_system_prompt,
type="messages",
title="可配置AI助手",
additional_inputs=[
gr.Textbox(
value="你是一个有帮助的AI助手,回答简洁准确。",
label="系统提示词",
lines=3,
),
gr.Slider(0, 2, value=0.7, step=0.1, label="Temperature"),
gr.Slider(64, 4096, value=1024, step=64, label="Max Tokens"),
],
additional_inputs_accordion=gr.Accordion("⚙️ 高级设置", open=False),
)
demo.launch()
3.4 多轮对话历史管理¶
Gradio 4.x使用OpenAI兼容的messages格式管理对话历史:
import gradio as gr
from openai import OpenAI
client = OpenAI()
def chat_with_memory(message: str, history: list[dict]):
"""
history格式(type="messages"):
[
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮你的?"},
...
]
"""
# 限制历史长度,防止token超限
max_history_turns = 20
recent_history = history[-(max_history_turns * 2):]
messages = [
{"role": "system", "content": "你是一个友好的AI助手。"},
*recent_history,
{"role": "user", "content": message},
]
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True,
)
partial = ""
for chunk in stream:
if chunk.choices[0].delta.content:
partial += chunk.choices[0].delta.content
yield partial
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🤖 AI聊天助手")
gr.Markdown("支持多轮对话,自动管理对话历史")
chatbot = gr.ChatInterface(
fn=chat_with_memory,
type="messages",
examples=[
"帮我解释什么是Transformer",
"用Python实现快速排序",
"比较RAG和微调的优缺点",
],
)
demo.launch()
3.5 完整代码:接入OpenAI API的聊天机器人¶
"""完整的聊天机器人应用 —— 支持流式输出、参数配置、对话管理"""
import gradio as gr
from openai import OpenAI
client = OpenAI()
MODELS = ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"]
PRESETS = {
"通用助手": "你是一个有帮助的AI助手,回答准确、简洁。",
"代码专家": "你是一个编程专家,擅长Python、JavaScript等语言。用代码示例回答问题。",
"翻译官": "你是一个专业翻译,将用户输入翻译为英文。如果输入已经是英文,则翻译为中文。",
"写作助手": "你是一个写作助手,帮助用户润色、改写和创作文本。",
}
def respond(
message: str,
history: list[dict],
system_prompt: str,
model: str,
temperature: float,
max_tokens: int,
):
messages = [{"role": "system", "content": system_prompt}]
# 保留最近20轮对话
messages += history[-(20 * 2):]
messages.append({"role": "user", "content": message})
try: # try/except捕获异常,防止程序崩溃
stream = client.chat.completions.create(
model=model,
messages=messages,
stream=True,
temperature=temperature,
max_tokens=max_tokens,
)
partial = ""
for chunk in stream:
if chunk.choices[0].delta.content:
partial += chunk.choices[0].delta.content
yield partial
except Exception as e:
yield f"❌ 调用失败: {e}"
def apply_preset(preset_name: str) -> str:
return PRESETS.get(preset_name, "")
with gr.Blocks(theme=gr.themes.Soft(), title="AI聊天助手") as demo:
gr.Markdown("# 🤖 AI聊天助手\n支持多模型、流式输出、预设角色")
with gr.Row():
with gr.Column(scale=4):
chat = gr.ChatInterface(
fn=respond,
type="messages",
additional_inputs=[
gr.Textbox(
value=PRESETS["通用助手"],
label="系统提示词",
lines=3,
key="system_prompt",
),
gr.Dropdown(MODELS, value="gpt-4o-mini", label="模型"),
gr.Slider(0, 2, value=0.7, step=0.1, label="Temperature"),
gr.Slider(64, 4096, value=1024, step=64, label="Max Tokens"),
],
additional_inputs_accordion=gr.Accordion("⚙️ 设置", open=False),
)
demo.launch()
4. 构建RAG问答应用¶
4.1 文件上传与处理¶
import gradio as gr
from pathlib import Path
def load_document(file) -> str:
"""加载上传的文档并提取文本"""
if file is None:
return ""
file_path = Path(file.name)
suffix = file_path.suffix.lower()
if suffix == ".txt":
return file_path.read_text(encoding="utf-8")
elif suffix == ".pdf":
# 使用PyMuPDF提取PDF文本
import pymupdf
doc = pymupdf.open(file_path)
text = ""
for page in doc:
text += page.get_text() + "\n"
return text
elif suffix == ".md":
return file_path.read_text(encoding="utf-8")
else:
return f"不支持的文件类型: {suffix}"
4.2 完整代码:简单RAG应用¶
"""简单RAG问答应用 —— 上传文档,基于文档内容回答问题"""
import gradio as gr
from pathlib import Path
from openai import OpenAI
import numpy as np
client = OpenAI()
# ---- 文档处理 ----
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""将文本切分为有重叠的块"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
def get_embeddings(texts: list[str]) -> list[list[float]]:
"""调用OpenAI Embedding API"""
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts,
)
return [item.embedding for item in response.data]
def cosine_similarity(a: list[float], b: list[float]) -> float:
a, b = np.array(a), np.array(b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
# ---- 全局状态 ----
knowledge_base: dict = {"chunks": [], "embeddings": []}
def process_document(file) -> str:
"""处理上传的文档,构建知识库"""
if file is None:
return "⚠️ 请先上传文件"
file_path = Path(file.name)
if file_path.suffix == ".txt":
text = file_path.read_text(encoding="utf-8")
elif file_path.suffix == ".pdf":
import pymupdf
doc = pymupdf.open(file_path)
text = "\n".join(page.get_text() for page in doc)
else:
return f"❌ 不支持的格式: {file_path.suffix}"
chunks = chunk_text(text)
embeddings = get_embeddings(chunks)
knowledge_base["chunks"] = chunks
knowledge_base["embeddings"] = embeddings
return f"✅ 已加载文档,共 {len(chunks)} 个文本块"
def retrieve(query: str, top_k: int = 3) -> list[str]:
"""检索最相关的文本块"""
if not knowledge_base["chunks"]:
return []
query_emb = get_embeddings([query])[0]
scores = [
(i, cosine_similarity(query_emb, emb))
for i, emb in enumerate(knowledge_base["embeddings"]) # enumerate同时获取索引和元素
]
scores.sort(key=lambda x: x[1], reverse=True) # 按相似度分数(x[1])降序排列
return [knowledge_base["chunks"][i] for i, _ in scores[:top_k]] # 取前K个,_丢弃分数
def rag_answer(message: str, history: list[dict]) -> str:
"""基于检索结果回答问题"""
if not knowledge_base["chunks"]:
return "⚠️ 请先上传文档再提问"
# 检索相关上下文
contexts = retrieve(message, top_k=3)
context_text = "\n---\n".join(contexts)
messages = [
{
"role": "system",
"content": (
"你是一个文档问答助手。根据以下检索到的文档内容回答用户问题。"
"如果无法从文档中找到答案,请如实告知。\n\n"
f"【检索到的文档内容】\n{context_text}"
),
},
*history,
{"role": "user", "content": message},
]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
return response.choices[0].message.content
# ---- Gradio界面 ----
with gr.Blocks(theme=gr.themes.Soft(), title="RAG问答应用") as demo:
gr.Markdown("# 📚 RAG文档问答")
gr.Markdown("上传文档后,基于文档内容回答你的问题")
with gr.Row():
with gr.Column(scale=1):
file_input = gr.File(
label="上传文档",
file_types=[".txt", ".pdf", ".md"],
)
process_btn = gr.Button("📥 加载文档", variant="primary")
status = gr.Textbox(label="状态", interactive=False)
process_btn.click(fn=process_document, inputs=file_input, outputs=status)
with gr.Column(scale=3):
gr.ChatInterface(
fn=rag_answer,
type="messages",
examples=["这篇文档的主要内容是什么?", "总结文档的关键要点"],
)
demo.launch()
5. 构建图像生成应用¶
5.1 文生图界面¶
"""图像生成应用 —— 使用DALL-E 3生成图片"""
import gradio as gr
from openai import OpenAI
client = OpenAI()
def generate_image(
prompt: str,
size: str,
quality: str,
style: str,
) -> str:
"""调用DALL-E 3生成图片"""
if not prompt.strip(): # 链式调用:strip去除空白
raise gr.Error("请输入描述文字")
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=size,
quality=quality,
style=style,
n=1,
)
return response.data[0].url
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎨 AI图像生成")
with gr.Row():
with gr.Column(scale=1):
prompt = gr.Textbox(
label="描述",
placeholder="描述你想生成的图片...",
lines=3,
)
size = gr.Dropdown(
["1024x1024", "1024x1792", "1792x1024"],
value="1024x1024",
label="尺寸",
)
quality = gr.Radio(["standard", "hd"], value="standard", label="质量")
style = gr.Radio(["vivid", "natural"], value="vivid", label="风格")
btn = gr.Button("🎨 生成", variant="primary")
with gr.Column(scale=2):
output_image = gr.Image(label="生成结果", type="filepath")
btn.click(
fn=generate_image,
inputs=[prompt, size, quality, style],
outputs=output_image,
)
gr.Examples(
examples=[
["一只戴墨镜的猫坐在海滩上看日落,水彩风格"],
["未来城市的鸟瞰图,赛博朋克风格,霓虹灯光"],
["一个安静的日式花园,春天,樱花飘落"],
],
inputs=prompt,
)
demo.launch()
5.2 图片画廊展示¶
使用gr.Gallery展示多张生成的图片:
import gradio as gr
from openai import OpenAI
import requests
from PIL import Image
from io import BytesIO
client = OpenAI()
def generate_variations(prompt: str, num_images: int) -> list:
"""生成多张图片(通过多次调用实现)"""
images = []
for i in range(int(num_images)):
response = client.images.generate(
model="dall-e-3",
prompt=f"{prompt} (variation {i+1})",
size="1024x1024",
quality="standard",
n=1,
)
url = response.data[0].url
img = Image.open(BytesIO(requests.get(url).content))
images.append((img, f"变体 {i+1}"))
return images
with gr.Blocks() as demo:
gr.Markdown("# 🖼️ 图片画廊")
prompt = gr.Textbox(label="描述")
num = gr.Slider(1, 4, value=2, step=1, label="生成数量")
btn = gr.Button("生成")
gallery = gr.Gallery(label="结果", columns=2, height="auto")
btn.click(fn=generate_variations, inputs=[prompt, num], outputs=gallery)
demo.launch()
6. Blocks高级布局¶
6.1 Row、Column布局¶
import gradio as gr
with gr.Blocks() as demo:
gr.Markdown("# 布局演示")
# 水平排列
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 左侧面板")
input1 = gr.Textbox(label="输入1")
with gr.Column(scale=2):
gr.Markdown("### 右侧面板(2倍宽)")
output1 = gr.Textbox(label="输出1")
# 嵌套布局
with gr.Row():
with gr.Column():
with gr.Row():
btn1 = gr.Button("按钮1")
btn2 = gr.Button("按钮2")
btn3 = gr.Button("按钮3")
demo.launch()
6.2 Tab分页¶
import gradio as gr
with gr.Blocks() as demo:
gr.Markdown("# 🧩 多功能工具")
with gr.Tabs():
with gr.Tab("💬 聊天"):
chatbot = gr.Chatbot(type="messages")
msg = gr.Textbox(label="输入消息")
with gr.Tab("📝 文本处理"):
text_input = gr.Textbox(label="输入文本", lines=5)
with gr.Row():
summarize_btn = gr.Button("摘要")
translate_btn = gr.Button("翻译")
text_output = gr.Textbox(label="结果", lines=5)
with gr.Tab("🎨 图像"):
img_input = gr.Image(label="上传图片")
img_output = gr.Textbox(label="图片描述")
demo.launch()
6.3 Accordion与Group¶
import gradio as gr
with gr.Blocks() as demo:
gr.Markdown("# 高级布局组件")
# Accordion折叠面板
with gr.Accordion("🔧 高级设置", open=False):
temperature = gr.Slider(0, 2, value=0.7, label="Temperature")
max_tokens = gr.Slider(64, 4096, value=1024, label="Max Tokens")
top_p = gr.Slider(0, 1, value=0.9, label="Top P")
# Group组件组
with gr.Group():
gr.Markdown("### 模型配置")
model = gr.Dropdown(["gpt-4o", "gpt-4o-mini"], label="模型")
api_key = gr.Textbox(label="API Key", type="password")
demo.launch()
6.4 自定义CSS¶
import gradio as gr
custom_css = """
#main-title {
text-align: center;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 2.5em;
}
.custom-container {
border: 2px solid #667eea;
border-radius: 12px;
padding: 20px;
margin: 10px 0;
}
"""
with gr.Blocks(css=custom_css) as demo:
gr.Markdown("# AI应用平台", elem_id="main-title")
with gr.Column(elem_classes=["custom-container"]):
gr.Textbox(label="输入")
gr.Button("提交", variant="primary")
demo.launch()
6.5 事件链(.then())¶
import gradio as gr
import time
def step1(text: str) -> str:
time.sleep(1)
return f"步骤1完成: {text.upper()}"
def step2(text: str) -> str:
time.sleep(1)
return f"步骤2完成: {text}(已翻转: {text[::-1]})"
def step3(text: str) -> str:
time.sleep(1)
return f"步骤3完成: 最终结果长度={len(text)}"
with gr.Blocks() as demo:
gr.Markdown("# 事件链演示")
input_text = gr.Textbox(label="输入")
btn = gr.Button("开始处理")
out1 = gr.Textbox(label="步骤1")
out2 = gr.Textbox(label="步骤2")
out3 = gr.Textbox(label="步骤3")
# 链式调用:step1 -> step2 -> step3
btn.click(fn=step1, inputs=input_text, outputs=out1) \
.then(fn=step2, inputs=out1, outputs=out2) \
.then(fn=step3, inputs=out2, outputs=out3)
demo.launch()
7. 状态管理与交互¶
7.1 gr.State状态保持¶
gr.State用于在多次交互间保持数据,每个用户独立:
import gradio as gr
def add_item(item: str, items: list) -> tuple[str, list, str]:
"""添加项目到列表"""
if item.strip():
items.append(item.strip())
display = "\n".join(f" {i+1}. {x}" for i, x in enumerate(items)) # enumerate提供索引,f-string格式化为编号列表,join用换行符拼接
return "", items, display # 清空输入, 更新状态, 显示列表
def clear_items(items: list) -> tuple[list, str]:
items.clear()
return items, "(列表已清空)"
with gr.Blocks() as demo:
gr.Markdown("# 📋 待办列表(State演示)")
state = gr.State(value=[]) # 每个用户独立的状态
item_input = gr.Textbox(label="新项目", placeholder="输入待办事项...")
with gr.Row():
add_btn = gr.Button("➕ 添加", variant="primary")
clear_btn = gr.Button("🗑️ 清空")
display = gr.Textbox(label="当前列表", lines=10, interactive=False)
add_btn.click(fn=add_item, inputs=[item_input, state], outputs=[item_input, state, display])
item_input.submit(fn=add_item, inputs=[item_input, state], outputs=[item_input, state, display])
clear_btn.click(fn=clear_items, inputs=state, outputs=[state, display])
demo.launch()
7.2 组件间联动¶
import gradio as gr
def update_options(category: str) -> gr.Dropdown:
"""根据类别动态更新下拉选项(Gradio 4.x用组件构造器替代gr.update)"""
options_map = {
"文本模型": ["gpt-4o", "gpt-4o-mini", "claude-3.5-sonnet"],
"图像模型": ["dall-e-3", "stable-diffusion-xl", "midjourney"],
"语音模型": ["whisper-large-v3", "tts-1", "tts-1-hd"],
}
options = options_map.get(category, [])
return gr.Dropdown(choices=options, value=options[0] if options else None)
with gr.Blocks() as demo:
gr.Markdown("# 组件联动演示")
category = gr.Dropdown(
["文本模型", "图像模型", "语音模型"],
label="模型类别",
value="文本模型",
)
model = gr.Dropdown(
["gpt-4o", "gpt-4o-mini", "claude-3.5-sonnet"],
label="具体模型",
value="gpt-4o",
)
category.change(fn=update_options, inputs=category, outputs=model)
demo.launch()
7.3 进度条与加载状态¶
import gradio as gr
import time
def long_task(text: str, progress=gr.Progress()) -> str:
"""带进度条的长时间任务"""
progress(0, desc="初始化...")
time.sleep(1)
results = []
steps = text.split()
for i, word in enumerate(progress.tqdm(steps, desc="处理中")):
time.sleep(0.5)
results.append(word.upper())
progress(1, desc="完成!")
return " ".join(results)
with gr.Blocks() as demo:
text = gr.Textbox(label="输入多个单词", value="hello world gradio python ai")
btn = gr.Button("处理")
output = gr.Textbox(label="结果")
btn.click(fn=long_task, inputs=text, outputs=output)
demo.launch()
7.4 错误处理¶
import gradio as gr
def safe_divide(a: float, b: float) -> float:
if b == 0:
raise gr.Error("除数不能为零!") # 在UI上显示错误提示
return a / b
def with_warning(text: str) -> str:
if len(text) > 100:
gr.Warning("文本过长,将只处理前100个字符")
text = text[:100]
if not text.strip():
gr.Info("提示:输入为空,将返回默认值")
return "默认输出"
return text.upper()
with gr.Blocks() as demo:
gr.Markdown("# 错误处理演示")
with gr.Row():
a = gr.Number(label="被除数", value=10)
b = gr.Number(label="除数", value=0)
result = gr.Number(label="结果")
btn = gr.Button("计算")
btn.click(fn=safe_divide, inputs=[a, b], outputs=result)
text = gr.Textbox(label="文本输入")
text_out = gr.Textbox(label="处理结果")
text.submit(fn=with_warning, inputs=text, outputs=text_out)
demo.launch()
8. 部署与分享¶
8.1 Gradio Share链接¶
最快捷的分享方式——一行代码生成72小时有效的公网链接:
⚠️ 注意:share链接通过Gradio代理服务器转发,不适合生产环境。72小时后自动失效。
8.2 Hugging Face Spaces部署¶
推荐用于演示和轻量级部署:
# 1. 安装huggingface_hub
pip install huggingface_hub
# 2. 登录
huggingface-cli login
# 3. 创建Space
huggingface-cli repo create my-ai-app --type space --space-sdk gradio
项目结构:
README.md(HF Space配置):
---
title: My AI App
emoji: 🤖
colorFrom: blue
colorTo: purple
sdk: gradio
sdk_version: "4.44.0"
app_file: app.py
pinned: false
---
# 4. 推送代码
cd my-ai-app
git init
git remote add origin https://huggingface.co/spaces/YOUR_USERNAME/my-ai-app
git add .
git commit -m "Initial commit"
git push origin main
8.3 Docker容器化部署¶
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Gradio默认端口7860
EXPOSE 7860
# 设置环境变量避免Gradio尝试分析
ENV GRADIO_SERVER_NAME="0.0.0.0"
ENV GRADIO_SERVER_PORT="7860"
CMD ["python", "app.py"]
# 构建并运行
docker build -t my-gradio-app .
docker run -p 7860:7860 -e OPENAI_API_KEY=$OPENAI_API_KEY my-gradio-app
8.4 生产部署建议¶
import gradio as gr
demo = gr.Blocks()
# 生产环境配置
demo.launch(
server_name="0.0.0.0", # 监听所有网卡
server_port=7860, # 指定端口
share=False, # 生产环境不开share
auth=("admin", "password"),# 基础认证(可选)
ssl_keyfile="key.pem", # HTTPS(可选)
ssl_certfile="cert.pem",
max_threads=40, # 并发线程数
show_error=False, # 不暴露错误详情
)
也可使用Nginx反向代理:
server {
listen 443 ssl;
server_name ai-app.example.com;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
location / {
proxy_pass http://127.0.0.1:7860;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # WebSocket支持
}
}
9. 实战项目:AI多功能助手¶
"""
AI多功能助手 —— 集成聊天、文档问答、图像理解
运行: python app.py
依赖: pip install gradio openai pymupdf numpy
"""
import gradio as gr
from openai import OpenAI
from pathlib import Path
import numpy as np
import base64
client = OpenAI()
# ==================== 工具函数 ====================
def get_embeddings(texts: list[str]) -> list[list[float]]:
resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
return [item.embedding for item in resp.data]
def cosine_sim(a, b):
a, b = np.array(a), np.array(b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def encode_image_base64(image) -> str:
"""将PIL Image编码为base64"""
from io import BytesIO
buffer = BytesIO()
image.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode("utf-8")
# ==================== Tab 1: 聊天 ====================
def chat_fn(message: str, history: list[dict], system_prompt: str, temperature: float):
messages = [{"role": "system", "content": system_prompt}]
messages += history[-(20 * 2):]
messages.append({"role": "user", "content": message})
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True,
temperature=temperature,
)
partial = ""
for chunk in stream:
if chunk.choices[0].delta.content:
partial += chunk.choices[0].delta.content
yield partial
# ==================== Tab 2: 文档问答 ====================
doc_store: dict = {"chunks": [], "embeddings": []}
def load_doc(file) -> str:
if file is None:
return "⚠️ 请上传文件"
path = Path(file.name)
if path.suffix == ".txt":
text = path.read_text(encoding="utf-8")
elif path.suffix == ".pdf":
import pymupdf
doc = pymupdf.open(path)
text = "\n".join(page.get_text() for page in doc)
elif path.suffix == ".md":
text = path.read_text(encoding="utf-8")
else:
return f"❌ 不支持: {path.suffix}"
# 切分(500字符块,50字符重叠)
chunk_size, overlap = 500, 50
chunks = []
for i in range(0, len(text), chunk_size - overlap):
chunks.append(text[i:i + chunk_size])
doc_store["chunks"] = chunks
doc_store["embeddings"] = get_embeddings(chunks)
return f"✅ 已加载 {path.name},共 {len(chunks)} 个文本块"
def doc_qa(message: str, history: list[dict]) -> str:
if not doc_store["chunks"]:
return "⚠️ 请先在左侧上传文档"
q_emb = get_embeddings([message])[0]
scores = [(i, cosine_sim(q_emb, e)) for i, e in enumerate(doc_store["embeddings"])]
scores.sort(key=lambda x: x[1], reverse=True)
top_chunks = [doc_store["chunks"][i] for i, _ in scores[:3]]
context = "\n---\n".join(top_chunks)
messages = [
{
"role": "system",
"content": f"根据以下文档内容回答问题。如果无法回答,请说明。\n\n{context}",
},
*history,
{"role": "user", "content": message},
]
resp = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
return resp.choices[0].message.content
# ==================== Tab 3: 图像理解 ====================
def analyze_image(image, question: str) -> str:
if image is None:
return "⚠️ 请上传图片"
if not question.strip():
question = "请描述这张图片的内容"
b64 = encode_image_base64(image)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": question},
{
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64}"},
},
],
}
],
max_tokens=1024,
)
return response.choices[0].message.content
# ==================== 组装界面 ====================
custom_css = """
#app-title {
text-align: center;
margin-bottom: 0.5em;
}
"""
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css, title="AI多功能助手") as demo:
gr.Markdown("# 🚀 AI多功能助手", elem_id="app-title")
gr.Markdown("集成聊天、文档问答、图像理解三大功能", elem_id="app-title")
with gr.Tabs():
# ---- Tab 1: 智能聊天 ----
with gr.Tab("💬 智能聊天"):
gr.ChatInterface(
fn=chat_fn,
type="messages",
additional_inputs=[
gr.Textbox(
value="你是一个有帮助的AI助手。",
label="系统提示词",
lines=2,
),
gr.Slider(0, 2, value=0.7, step=0.1, label="Temperature"),
],
additional_inputs_accordion=gr.Accordion("⚙️ 设置", open=False),
examples=["你好!", "用Python实现快速排序", "什么是RAG?"],
)
# ---- Tab 2: 文档问答 ----
with gr.Tab("📚 文档问答"):
with gr.Row():
with gr.Column(scale=1):
doc_file = gr.File(
label="上传文档",
file_types=[".txt", ".pdf", ".md"],
)
doc_btn = gr.Button("📥 加载文档", variant="primary")
doc_status = gr.Textbox(label="状态", interactive=False)
doc_btn.click(fn=load_doc, inputs=doc_file, outputs=doc_status)
with gr.Column(scale=3):
gr.ChatInterface(
fn=doc_qa,
type="messages",
examples=["文档的主要内容是什么?", "总结关键要点"],
)
# ---- Tab 3: 图像理解 ----
with gr.Tab("🖼️ 图像理解"):
with gr.Row():
with gr.Column(scale=1):
img_input = gr.Image(label="上传图片", type="pil")
img_question = gr.Textbox(
label="问题",
placeholder="关于这张图片你想问什么?",
value="请描述这张图片的内容",
)
img_btn = gr.Button("🔍 分析", variant="primary")
with gr.Column(scale=1):
img_output = gr.Textbox(label="分析结果", lines=10)
img_btn.click(
fn=analyze_image,
inputs=[img_input, img_question],
outputs=img_output,
)
gr.Markdown("---")
gr.Markdown("💡 Powered by OpenAI GPT-4o-mini | Built with Gradio")
if __name__ == "__main__":
demo.launch()
10. 最佳实践与FAQ¶
10.1 最佳实践¶
| 实践 | 说明 |
|---|---|
使用type="messages" | Gradio 4.x推荐的聊天历史格式,与OpenAI API兼容 |
流式输出用yield | 用generator函数实现流式,大幅提升体验 |
设置show_error=False | 生产环境不暴露错误堆栈 |
使用gr.Progress() | 长任务显示进度,避免用户以为卡死 |
gr.State管理会话数据 | 每个用户独立,不会串数据 |
| 限制对话历史长度 | 防止token超限,保留最近N轮即可 |
| 使用Blocks而非Interface | 复杂应用用Blocks更灵活 |
合理使用gr.update() | 动态更新组件属性(选项、可见性等) |
10.2 性能优化¶
import gradio as gr
# 1. 开启队列(高并发必需)
demo = gr.Blocks()
# queue()默认已启用(Gradio 4.x);可调整参数:
# demo.queue(max_size=20, default_concurrency_limit=5)
# 2. 缓存昂贵计算
import functools
@functools.lru_cache(maxsize=128)
def expensive_embedding(text: str) -> tuple:
"""缓存embedding计算结果"""
emb = get_embeddings([text])[0]
return tuple(emb) # lru_cache需要hashable参数
# 3. 异步支持
import asyncio # Python标准异步库
async def async_chat(message: str, history: list[dict]): # async def定义协程函数
"""Gradio 4.x原生支持async函数"""
# 使用异步客户端
from openai import AsyncOpenAI
aclient = AsyncOpenAI()
stream = await aclient.chat.completions.create( # await等待异步操作完成
model="gpt-4o-mini",
messages=[{"role": "user", "content": message}],
stream=True,
)
partial = ""
async for chunk in stream:
if chunk.choices[0].delta.content:
partial += chunk.choices[0].delta.content
yield partial
10.3 常见问题¶
Q: 如何在Gradio中使用环境变量?
import os
# 方法1:直接读取
api_key = os.environ.get("OPENAI_API_KEY", "")
# 方法2:用.env文件
from dotenv import load_dotenv
load_dotenv()
Q: 如何限制上传文件大小?
Q: 如何添加用户认证?
# 简单认证
demo.launch(auth=("username", "password"))
# 多用户认证
def auth_fn(username: str, password: str) -> bool:
users = {"admin": "admin123", "user": "user123"}
return users.get(username) == password
demo.launch(auth=auth_fn)
Q: 组件不更新/事件不触发?
# 常见原因1:忘记在outputs中声明要更新的组件
btn.click(fn=my_fn, inputs=[a], outputs=[b, c]) # b和c都会更新
# 常见原因2:函数返回值数量与outputs不匹配
def my_fn(a):
return "result_b", "result_c" # 必须返回与outputs数量一致的值
Q: 如何在Blocks中使用Examples?
with gr.Blocks() as demo:
inp = gr.Textbox(label="输入")
out = gr.Textbox(label="输出")
btn = gr.Button("提交")
# Examples需要在Blocks内部使用
gr.Examples(
examples=["例子1", "例子2", "例子3"],
inputs=inp,
)
btn.click(fn=my_fn, inputs=inp, outputs=out)
10.4 调试技巧¶
# 1. 启用详细日志
import logging
logging.basicConfig(level=logging.DEBUG)
# 2. 使用 gr.Warning/gr.Info 在UI上输出调试信息
def debug_fn(x):
gr.Info(f"收到输入: {x}") # 右上角toast提示
result = process(x)
gr.Info(f"处理完成: {result[:50]}")
return result
# 3. 热重载开发(命令行方式更好用)
# gradio app.py ← 自动检测代码变更并重载
📝 本章小结¶
| 知识点 | 掌握程度 |
|---|---|
| Gradio Interface快速原型 | ⭐⭐⭐ 必须掌握 |
| Blocks自定义布局 | ⭐⭐⭐ 必须掌握 |
| ChatInterface聊天应用 | ⭐⭐⭐ 必须掌握 |
| 流式输出(yield) | ⭐⭐⭐ 必须掌握 |
| 状态管理(gr.State) | ⭐⭐ 重要 |
| 组件联动与事件链 | ⭐⭐ 重要 |
| HF Spaces / Docker部署 | ⭐⭐ 重要 |
| 高级CSS自定义 | ⭐ 了解即可 |
📝 练习与面试¶
练习1:多轮对话Bot(⭐)¶
用 gr.ChatInterface 接入OpenAI API,实现一个支持流式输出、可调整temperature的聊天机器人。
练习2:文档问答UI(⭐⭐)¶
用 gr.Blocks 构建一个带文件上传、检索结果展示、回答生成的RAG问答界面。
练习3:多模态助手(⭐⭐⭐)¶
构建一个多Tab应用:文本聊天 + 图像理解 + 语音转写,包含状态管理和错误处理。
面试题¶
Q1: Gradio与Streamlit的核心区别是什么?各自适合什么场景?
Gradio更擅长ML模型的快速交互Demo,支持一键分享链接和HF Spaces部署,组件以I/O对为核心。Streamlit更适合数据展示和Dashboard,支持更复杂的页面布局和多页应用。简单模型Demo选Gradio,复杂数据应用选Streamlit。
Q2: Gradio 4.x中如何实现流式输出?
两种方式:① 在普通函数中用 yield 逐步返回结果。② 在 gr.ChatInterface 中,将 fn 设为generator函数,通过 yield 流式输出每个token。Gradio会自动处理前端渲染。
导航:上一章 22-结构化输出与函数调用 | 下一章 24-多模态RAG与向量数据库进阶 | 返回目录