入门 Eva J Patel(freeCodeCamp) 2026-09-10 16:14:02 · 1 阅读

第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 步:避免不必要地发送超大文档

目前这个简单实现会把整篇文档直接发给模型。作为学习项目没问题,但扩展性很差。

更好的做法是:

  1. 将文档分块

  2. 生成嵌入

  3. 存储嵌入

  4. 检索相关分块

  5. 只将相关上下文发给模型

第 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 不可用
    文档格式损坏
    

    针对每种情况,想清楚用户应该看到什么。

    部署毕业项目

    应用在本地跑通之后:

    1. 创建 Space

    2. 添加 app.py

    3. 添加 requirements.txt

    4. 配置密钥

    5. 部署

    6. 查看日志

    7. 测试公开访问的应用

    项目在你笔记本上能跑通并不算完成,只有用户能稳定可靠地使用,才算真正完成。

    毕业项目清单

    你的应用最终应该能够:

    • [ ] 上传文档。

    • [ ] 校验上传内容。

    • [ ] 提取文本。

    • [ ] 显示预览。

    • [ ] 计算文档统计信息。

    • [ ] 存储处理后的数据。

    • [ ] 将文档切分为 chunks。

    • [ ] 检索相关 chunks。

    • [ ] 就文档内容提问。

    • [ ] 维护对话历史。

    • [ ] 生成摘要。

    • [ ] 处理模型错误。

    • [ ] 保护 API 密钥。

    • [ ] 提供重置机制。

    • [ ] 成功完成部署。

    这个项目教会你什么

    最大的收获不是怎么创建一个 Textbox,而是各部分如何衔接在一起。

    一个真正的应用,由多个小系统组成。

    界面负责收集信息,Python 协调工作流,模型执行专项任务,状态管理保存临时数据;存储处理持久化信息,部署让应用可访问,安全保护应用和用户。

    好的工程实践,在于有意识地打通这些环节。

    评论 (0)