我是如何构建一个能理解数千份PDF并像人类一样交流的自定义AI文档助手的

92 阅读5分钟

核心问题:为什么传统搜索在技术文档上表现不佳

你有没有试过从500多份PDF中找到某条特定信息?

甚至可能文件名是Report_Final_v2_NEW_Latest.pdf这样的,祝你好运。

搜索工具并不“理解”内容。它们只是做关键词匹配。这对于以下场景远远不够:

  • 从科学论文中提取见解

  • 总结法律合同

  • 对比产品规格

正因如此,我决定构建一个更智能的工具——一个能阅读、理解数千份PDF并回答问题的多模态RAG人工智能。

步骤1:规划项目目录结构

提前合理组织所有内容帮助我将项目从支持10份文档扩展到了支持10,000份。

ai-doc-assistant/

├── ingest/(数据摄入)

│ ├── extract_text.py

│ ├── extract_images.py

├── process/(处理)

│ ├── chunk_text.py(文本分块)

│ ├── embed_chunks.py(分块嵌入)

├── index/(索引)

│ └── vector_store.py(向量存储)

├── backend/(后端)

│ ├── qa_chain.py(问答链)

│ └── server.py(服务器)

├── interface/(界面)

│ └── ui.py

每个模块都有单一职责。

流程为:摄入→处理→索引→回答→展示。

步骤2:使用PyMuPDF提取PDF中的文本

文本是关键信息所在。所以我逐页提取文本,并保留元数据。

import fitz

def extract_text(file_path):
    doc = fitz.open(file_path)
    pages = []
    for i, page in enumerate(doc):
        text = page.get_text()
        pages.append({
            "file": file_path,
            "page": i + 1,
            "text": text
        })
    return pages

这让我拥有完全控制权:文件名、页面引用以及选择性纳入。

步骤3:提取并存储PDF中的图表

视觉内容承载着重要信息——尤其是在研究文献和产品手册中。所以我提取了所有嵌入的图片。

def extract_images(pdf_path, output_dir):
    doc = fitz.open(pdf_path)
    for page_index in range(len(doc)):
        images = doc[page_index].get_images(full=True)
        for img_index, img in enumerate(images):
            xref = images[img_index][0]
            base_image = doc.extract_image(xref)
            image_bytes = base_image["image"]
            image_filename = f"{output_dir}/{page_index}_{img_index}.png"
            with open(image_filename, "wb") as img_file:
                img_file.write(image_bytes)

之后,这些图表会通过CLIP模型进行嵌入,并与文本一同存储。

步骤4:对文本进行带重叠的分块,为RAG构建友好的上下文

大型语言模型无法处理完整文档——所以我们要对文本进行分块。

def chunk_text(text, size=500, overlap=100):
    chunks = []
    for i in range(0, len(text), size - overlap):
        chunk = text[i:i + size]
        chunks.append(chunk)
    return chunks

重叠部分确保上下文不会在分块之间断裂,这对于生成连贯的回答至关重要。

步骤5:为文本分块生成嵌入向量

接下来,我使用sentence-transformers将分块转换为向量。

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')

def embed_chunks(chunks):
    return model.encode(chunks)

这些向量代表的是“含义”,而不仅仅是关键词。因此,之后我们能检索到最相关的概念,即便表达方式不同。

8让我成为后端高手的Python库

从彻夜调试到无缝自动化——这些工具彻底改变了一切

步骤6:使用FAISS构建快速向量搜索引擎

我使用FAISS高效地存储和搜索嵌入向量。

import faiss
import numpy as np

def build_index(embeddings):
    dim = embeddings.shape[1]
    index = faiss.IndexFlatL2(dim)
    index.add(embeddings)
    return index

构建完成后,它能在一秒内完成10,000+份文档的搜索。

步骤7:添加CLIP嵌入以支持基于图像的搜索

为了让助手具备多模态能力,我用CLIP对图表进行了索引。

from transformers import CLIPProcessor, CLIPModel
from PIL import Image

clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

def embed_image(image_path):
    image = Image.open(image_path)
    inputs = processor(images=image, return_tensors="pt")
    with torch.no_grad():
        image_features = clip_model.get_image_features(**inputs)
    return image_features.squeeze().numpy()

现在,这个机器人可以回答诸如“展示系统重启的流程图”之类的查询了。

步骤8:使用LangChain构建检索+回答链

检索到最相关的文档后,我将它们输入到大型语言模型中。

from langchain.chains import RetrievalQA
from langchain.vectorstores import FAISS
from langchain.llms import Ollama

retriever = FAISS.load_local("index", embeddings=model)
llm = Ollama(model="mistral")

qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)

这样查询:

qa.run("Product X的保修范围是什么?")

然后一下就得到了像是人类一般的回答,还附带引用的文档。

步骤9:在回答中引用来源

为了增强可信度,我在每个回答后都附上来源。

def format_response(answer, docs):
    refs = "\n".join([f"{doc.metadata['file']}(第{doc.metadata['page']}页)" for doc in docs])
    return f"{answer}\n\n来源:\n{refs}"

这让它更像一名律师,而不是聊天机器人。因为现在每一个说法都可以追溯来源。

步骤10:创建用于集成的本地API

用FastAPI将整个流程封装起来:

from fastapi import FastAPI
from pydantic import BaseModel

class Query(BaseModel):
    question: str

app = FastAPI()

@app.post("/ask")
def ask(query: Query):
    response = qa.run(query.question)
    return {"answer": response}

现在可以将这个后端与Web应用、Slack甚至语音系统连接起来。

步骤11:用Gradio构建Web界面

为了向客户或团队成员演示,我们可以创建一个简洁的界面:

import gradio as gr

def answer_question(q):
    return qa.run(q)

gr.Interface(fn=answer_question, inputs="text", outputs="text", title="AI PDF Assistant").launch()

用公司手册中的实际问题进行测试,发现效果不错——这个机器人都答对了。

步骤12:处理文件上传和实时摄入

用户可以在界面上拖放新的PDF文件,这些文件会立即被处理并编入索引。

@app.post("/upload")
async def upload_pdf(file: UploadFile):
    save_path = f"./docs/{file.filename}"
    with open(save_path, "wb") as f:
        f.write(await file.read())
    # 提取、分块、嵌入并更新FAISS索引

这让助手实现了自动更新,新文档=新知识。

13一旦开始使用就会觉得神奇的Python特性

这些工具会让你小声嘀咕:“等等……Python还能这么玩?”

步骤13:使用Ollama + Docker进行本地部署

为了实现完全控制,我们将所有内容容器化。

Dockerfile:

FROM python:3.10
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "backend.server:app", "--host", "0.0.0.0", "--port", "8000"]

现在它可以在任何笔记本电脑上运行,无需依赖云服务。

步骤14:使用SQLite添加长期记忆功能

为助手添加记忆功能:

import sqlite3

def save_interaction(question, answer):
    conn = sqlite3.connect("history.db")
    conn.execute("INSERT INTO log (q, a) VALUES (?, ?)", (question, answer))
    conn.commit()

之后,我们用这些数据重新训练模型,以改进响应效果。

步骤15:最终思考——从搜索到推理

这不仅仅是搜索。这是对文档、图表和音频的推理。

它能够:

  • 对比产品规格

  • 总结手册中的章节

  • 根据文字描述找到对应的图片

  • 为用户提供个性化答案

我不只是构建了一个机器人,而是打造了一个AI队友。