第24章 Gradio 实战:构建 AI 文档智能助手
你现在已经掌握了足够的 Gradio 知识,可以动手做一个像样的项目了。
不再写小例子了,我们要把整本书的思路整合成一个完整的应用。
我们的毕业作品是一个文档智能助手。
用户可以:
上传文档
处理文档
预览内容
提问
保持对话上下文
生成摘要
分析文档统计信息
最终接入 AI 模型
具体用哪个模型,可以根据环境灵活替换。
我们要构建什么
应用分为几个板块。
首先是:
文档上传
然后是:
文档信息
再是:
AI 助手
接着是:
文档摘要
最后是:
统计信息
第一步:先规划再编码
动手写代码之前,先理清数据流。
我们需要:
上传文件
→ 提取文本
→ 存储文档
→ 用户提问
→ AI 回复
还需要:
文档
→ 摘要
以及:
文档
→ 统计信息
第二步:创建项目
一个简单的项目可以这样起步:
document-assistant/
├── app.py
├── requirements.txt
└── README.md
随着功能增多,可以把不同职责拆到独立模块里。
第三步:安装依赖
基础版本需要:
pip install gradio
如果要处理 PDF:
pip install pymupdf
如果用到 pandas:
pip install pandas
如果要接入某个具体模型,再安装它对应的 SDK 或库即可。
第 4 步:创建初始界面
从以下代码开始:
import gradio as gr
with gr.Blocks(
theme=gr.themes.Soft()
) as demo:
gr.Markdown(
"""
# Document Intelligence Assistant
Upload a document, analyze it, and ask questions about its contents.
"""
)
demo.launch()
在添加其他内容之前,先跑通这段代码。
没问题就继续往下走。
第 5 步:添加文档上传
添加:
file = gr.File(
label="Upload Document"
)
最初可以先把应用限制为只接受文本文件:
file = gr.File(
file_types=[".txt"],
label="Upload Text File"
)
整个工作流跑通后,再扩展到更多文件格式。
第 6 步:添加状态
需要个地方存放提取出来的文本。
document_text = gr.State("")
还需要会话历史。
根据 Chatbot 的具体实现方式,Chatbot 组件本身就能承载可见的对话历史,其他应用特有的信息则用额外的 state 来存。
第 7 步:提取文档
创建:
def extract_text(file):
if file is None:
return "", "Please upload a document."
try:
with open(
file.name,
"r",
encoding="utf-8"
) as f:
text = f.read()
return text, "Document processed successfully."
except UnicodeDecodeError:
return "", "The file is not valid UTF-8 text."
except Exception:
return "", "The document could not be processed."
第 8 步:添加预览
创建:
preview = gr.Textbox(
label="Document Preview",
lines=15
)
你大概不想把一个百万字符的文档原样全部显示出来。
改为截取前 5000 个字符:
preview_text = text[:5000]
然后返回:
return text, preview_text
第 9 步:添加处理按钮
process_button = gr.Button(
"Process Document",
variant="primary"
)
接上事件:
process_button.click(
fn=extract_text,
inputs=file,
outputs=[document_text, preview]
)
现在文档处理流程可以跑通了。
第 10 步:添加文档统计
先创建函数:
def document_stats(text):
if not text:
return "No document processed."
words = len(text.split())
characters = len(text)
return (
f"Words: {words}\n"
f"Characters: {characters}"
)
添加组件:
stats = gr.Textbox(
label="Document Statistics"
)
然后绑定事件:
process_button.click(
fn=document_stats,
inputs=document_text,
outputs=stats
)
不过要记住,事件依赖和输出更新需要仔细设计。
另一种做法是让一个处理函数返回所有初始文档输出,这样工作流会更容易理解。
第 11 步:整合文档处理
更简洁的写法可以是:
def process_document(file):
if file is None:
return "", "", "Please upload a document."
try:
with open(
file.name,
"r",
encoding="utf-8"
) as f:
text = f.read()
preview = text[:5000]
words = len(text.split())
characters = len(text)
stats = (
f"Words: {words}\n"
f"Characters: {characters}"
)
return text, preview, stats
except Exception:
return "", "", "Could not process the document."
这样一次事件就能更新多个输出。
第 12 步:添加 Chatbot
创建:
chatbot = gr.Chatbot(
label="Document Assistant"
)
然后:
question = gr.Textbox(
label="Question",
placeholder="Ask something about the document..."
)
再加:
ask_button = gr.Button(
"Ask"
)
步骤 13:构建问答函数
先不接入 AI 模型,把流程跑通。
def answer_question(document, question, history):
if not document:
return history + [
{
"role": "user",
"content": question
},
{
"role": "assistant",
"content": "Please process a document first."
}
]
if not question.strip():
return history
response = (
"A language model would analyze the document "
"and answer this question."
)
return history + [
{
"role": "user",
"content": question
},
{
"role": "assistant",
"content": response
}
]
history 的具体格式需与你使用的 Gradio 版本保持一致。
步骤 14:接入聊天机器人
ask_button.click(
fn=answer_question,
inputs=[
document_text,
question,
chatbot
],
outputs=chatbot
)
此时界面已具备完整的对话流程。
步骤 15:用真实模型替换占位逻辑
接下来接入真实的模型。
概念上:
def answer_question(document, question, history):
prompt = f"""
You are a document analysis assistant.
Use only the provided document.
DOCUMENT:
{document}
QUESTION:
{question}
If the answer cannot be found in the document,
clearly say so.
"""
response = model.generate(prompt)
...
模型可以是本地的,也可以是远程的。
步骤 16:添加摘要功能
创建函数:
def summarize_document(document):
if not document:
return "Please process a document first."
prompt = f"""
Summarize the following document.
DOCUMENT:
{document}
"""
return model.generate(prompt)
然后添加界面组件:
summary_button = gr.Button(
"Generate Summary"
)
summary = gr.Textbox(
label="Summary",
lines=12
)
将它们连接起来:
summary_button.click(
fn=summarize_document,
inputs=document_text,
outputs=summary
)
第 17 步:避免不必要地发送超大文档
目前这个简单实现会把整篇文档直接发给模型。作为学习项目没问题,但扩展性很差。
更好的做法是:
将文档分块
生成嵌入
存储嵌入
检索相关分块
只将相关上下文发给模型
第 18 步:添加分块
一个简单的分块函数示例:
def chunk_text(text, chunk_size=2000):
return [
text[i:i + chunk_size]
for i in range(0, len(text), chunk_size)
]
这只是最朴素的切法。实际检索系统通常按语义或结构边界来划分文本,而不是机械地每 N 个字符就一刀切。
第 19 步:添加检索
学习阶段可以先用简单的关键词检索。
def retrieve(chunks, question, top_k=3):
question_words = set(
question.lower().split()
)
scored = []
for chunk in chunks:
chunk_words = set(
chunk.lower().split()
)
score = len(
question_words & chunk_words
)
scored.append(
(score, chunk)
)
scored.sort(
key=lambda item: item[0],
reverse=True
)
return [
chunk
for score, chunk in scored[:top_k]
if score > 0
]
这不是真正的语义检索,但足以说明核心思路。
第 20 步:存储分块
添加:
chunks_state = gr.State([])
修改文档处理逻辑:
def process_document(file):
...
chunks = chunk_text(text)
return text, chunks, preview, stats
按钮的输出则包含:
outputs=[
document_text,
chunks_state,
preview,
stats
]
第 21 步:使用检索到的上下文
现在:
def answer_question(chunks, question):
relevant = retrieve(
chunks,
question
)
if not relevant:
return "I couldn't find relevant information in the document."
context = "\n\n".join(relevant)
prompt = f"""
Answer the question using only the context below.
CONTEXT:
{context}
QUESTION:
{question}
"""
return model.generate(prompt)
相比每次都发送整个文档,这种方式的可扩展性好得多。
第 22 步:添加重置按钮
用户应该能随时重新开始。重置流程可以清空:
文档状态
分块
预览
统计信息
摘要
聊天记录
例如:
def reset():
return "", [], "", "", "", []
然后:
reset_button.click(
fn=reset,
outputs=[
document_text,
chunks_state,
preview,
stats,
summary,
chatbot
]
)
注意返回值的数量和顺序必须与 outputs 完全对应。
第 23 步:整理界面布局
功能跑通之后,可以优化一下布局。
例如:
with gr.Row():
with gr.Column():
...
with gr.Column():
...
比如把文档操作放在左边,结果展示放在右边。
第 24 步:添加标签页
一个实用的结构可以是:
with gr.Tab("Document"):
...
with gr.Tab("Ask Questions"):
...
with gr.Tab("Summary"):
...
with gr.Tab("Statistics"):
...
这样可以避免界面显得过于繁杂。
第 25 步:添加高级设置
可以把一些参数暴露出来:
with gr.Accordion("Advanced Settings"):
top_k = gr.Slider(
minimum=1,
maximum=10,
value=3,
step=1,
label="Number of Retrieved Chunks"
)
这样高级用户就能自己控制检索行为了。
步骤 26:添加模型选择器
如果应用支持多个模型:
model_name = gr.Dropdown(
choices=[
"Model A",
"Model B"
],
label="Model"
)
推理函数可以据此选择对应的模型。
如果这个功能对用户没有实际价值,就不要暴露它。
步骤 27:处理模型故障
用 try-except 包裹外部调用:
def generate_response(prompt):
try:
return model.generate(prompt)
except Exception:
return (
"The AI service is currently unavailable. "
"Please try again later."
)
步骤 28:保护你的 API Key
应该用:
import os
API_KEY = os.getenv("API_KEY")
而不是:
API_KEY = "..."
步骤 29:添加文件校验
不要来者不拒。
例如:
file = gr.File(
file_types=[".txt", ".pdf"]
)
然后在处理阶段校验实际内容。
步骤 30:考虑隐私问题
文档助手可能处理敏感文件。
自问:
上传的文件存储在哪里?
文档内容是否会被发送到外部模型?
数据保留多久?
谁能访问这些数据?
日志里是否记录了文档内容?
其他用户能否访问同一份状态?
对于严肃的应用来说,这些问题不可回避。
简化的综合项目结构
最终成品可能包含:
import gradio as gr
def process_document(file):
...
def answer_question(chunks, question, history):
...
def summarize_document(document):
...
def get_statistics(document):
...
def reset():
...
with gr.Blocks(
theme=gr.themes.Soft()
) as demo:
gr.Markdown(
"""
# 文档智能助手
上传文档,利用 AI 探索内容。
"""
)
document_text = gr.State("")
chunks_state = gr.State([])
with gr.Tab("文档"):
file = gr.File(
label="上传文档"
)
process_button = gr.Button(
"处理文档",
variant="primary"
)
preview = gr.Textbox(
label="预览",
lines=15
)
stats = gr.Textbox(
label="统计信息"
)
with gr.Tab("提问"):
chatbot = gr.Chatbot(
label="助手"
)
question = gr.Textbox(
label="问题"
)
ask_button = gr.Button(
"提问"
)
with gr.Tab("摘要"):
summary_button = gr.Button(
"生成摘要"
)
summary = gr.Textbox(
label="摘要",
lines=15
)
reset_button = gr.Button(
"重置"
)
process_button.click(
fn=process_document,
inputs=file,
outputs=[
document_text,
chunks_state,
preview,
stats
]
)
summary_button.click(
fn=summarize_document,
inputs=document_text,
outputs=summary
)
ask_button.click(
fn=answer_question,
inputs=[
chunks_state,
question,
chatbot
],
outputs=chatbot
)
demo.queue().launch()
这就是骨架。
你可以把模型、PDF 处理、检索和生产基础设施作为独立层逐步叠加。
你构建了什么
完成这个项目后,你几乎把书中的每个核心概念都串到了一起:
Blocks组件
布局
事件
状态
文件
媒体
聊天机器人
AI 模型
检索
环境变量
部署
错误处理
生产环境注意事项
这就是毕业项目存在的意义。
目标不是背熟 Gradio 的语法,而是学会如何思考交互式 Python 应用的设计。
改进毕业项目
基础应用跑通之后,你可以逐个添加新功能。
可选的升级包括:
PDF 支持
DOCX 支持
CSV 支持
语义搜索
embeddings
引用标注
来源摘录
可下载的摘要
多模型切换
流式响应
用户认证
会话持久化
不要一口气全部实现。良好的工程流程应该是渐进式的。
测试毕业项目
先测试正常流程,再测试异常情况。
试试这些场景:
没有文件
空文件
不支持的文件格式
超大文件
空问题
超长问题
AI API 不可用
文档格式损坏
针对每种情况,想清楚用户应该看到什么。
部署毕业项目
应用在本地跑通之后:
创建 Space
添加
app.py添加
requirements.txt配置密钥
部署
查看日志
测试公开访问的应用
项目在你笔记本上能跑通并不算完成,只有用户能稳定可靠地使用,才算真正完成。
毕业项目清单
你的应用最终应该能够:
[ ] 上传文档。
[ ] 校验上传内容。
[ ] 提取文本。
[ ] 显示预览。
[ ] 计算文档统计信息。
[ ] 存储处理后的数据。
[ ] 将文档切分为 chunks。
[ ] 检索相关 chunks。
[ ] 就文档内容提问。
[ ] 维护对话历史。
[ ] 生成摘要。
[ ] 处理模型错误。
[ ] 保护 API 密钥。
[ ] 提供重置机制。
[ ] 成功完成部署。
这个项目教会你什么
最大的收获不是怎么创建一个 Textbox,而是各部分如何衔接在一起。
一个真正的应用,由多个小系统组成。
界面负责收集信息,Python 协调工作流,模型执行专项任务,状态管理保存临时数据;存储处理持久化信息,部署让应用可访问,安全保护应用和用户。
好的工程实践,在于有意识地打通这些环节。