核心问题:为什么传统搜索在技术文档上表现不佳
你有没有试过从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队友。