RAG系统实战:从PDF到智能问答

9 min

RAG系统实战:从PDF到智能问答

大纲

背景

在AI时代,Retrieval-Augmented Generation (RAG) 系统已成为构建智能问答应用的热门技术。它结合了信息检索和生成式AI的优势,能够从海量文档中提取相关信息,并生成准确、自然的回答。本文聚焦于基于PDF文档的RAG系统实战,使用LangChain框架、OpenAI的LLM模型和Chroma向量数据库,实现从PDF加载到智能问答的全流程。

RAG系统的兴起源于传统LLM的局限性:它们基于训练数据生成响应,但无法处理实时或私有知识。通过RAG,我们可以将外部知识库(如PDF文档)集成到AI系统中,提升回答的准确性和相关性。这在企业知识管理、法律咨询、教育等领域有广泛应用。

原理

RAG系统的核心原理是“检索-增强-生成”三步走:

  1. 检索(Retrieval):将用户查询转换为向量 embedding,使用相似度搜索从知识库中检索相关文档片段。
  2. 增强(Augmentation):将检索到的上下文注入到LLM的提示中,提供额外知识。
  3. 生成(Generation):LLM基于增强提示生成最终回答。

关键组件:

  • 文档加载与切分:从PDF加载文本,并切分为小块(chunks)以便嵌入。
  • 向量嵌入:使用OpenAI的embedding模型将文本转换为向量。
  • 向量数据库:Chroma存储和管理向量,支持快速相似度搜索。
  • LLM集成:OpenAI的GPT模型生成响应。

RAG的优势包括减少幻觉(hallucination)、支持私有数据和易于更新知识库。

系统架构图

正在加载图表...

系统流程图

正在加载图表...

完整可运行代码

前提:已安装依赖并设置好 OPENAI_API_KEY

bash
pip install langchain openai pypdf chromadb langchain-openai langchain-chroma
export OPENAI_API_KEY=your_key_here
python
# 导入必要库
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA

# 步骤1: 加载PDF文档
loader = PyPDFLoader("example.pdf")  # 替换为你的PDF路径
documents = loader.load()

# 步骤2: 切分文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
texts = text_splitter.split_documents(documents)

# 步骤3: 创建向量嵌入和数据库
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(texts, embeddings, collection_name="pdf_rag")

# 步骤4: 设置检索器(返回最相似的3个片段)
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})

# 步骤5: 定义提示模板
prompt_template = """使用以下上下文回答问题。如果上下文不足以回答,就说不知道。

上下文: {context}

问题: {question}

回答:"""

PROMPT = PromptTemplate(template=prompt_template, input_variables=["context", "question"])

# 步骤6: 创建RetrievalQA链(推荐使用ChatOpenAI)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)  # 或 gpt-3.5-turbo

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    chain_type_kwargs={"prompt": PROMPT},
    return_source_documents=True
)

# 步骤7: 查询示例
query = "PDF文档的主要内容是什么?"
result = qa_chain.invoke({"query": query})

print("回答:", result["result"])
print("\n来源文档:")
for i, doc in enumerate(result["source_documents"]):
    print(f"{i+1}. page {doc.metadata.get('page', '未知')} - {doc.page_content[:200]}...")

性能优化

优化方面策略预期效果
检索速度使用Chroma内存模式或添加HNSW索引查询延迟 < 100ms
准确性chunk_size 500-1500,overlap 200-300召回率提升至 90%+
资源消耗批量嵌入、缓存查询结果API成本降低 30%-50%
扩展性替换为FAISS、Pinecone、Qdrant支持百万级文档
减少幻觉HyDE、Self-Query Retriever、多轮检索准确率提升 10%-20%

其他高级技巧:HyDE(假设文档嵌入)、Metadata过滤、MMR多样性搜索等。

实际案例

案例1:企业内部知识库
科技公司将数百份技术手册PDF接入RAG,用户问“如何配置Kubernetes集群?”,系统5秒内返回完整步骤,准确率95%+。

案例2:法律咨询助手
律师事务所加载法律法规PDF,查询“违约金超过本金20%是否有效?”,系统引用最新司法解释,避免LLM法律幻觉。

案例3:教育平台
在线课程平台加载教材PDF,学生问“薛定谔方程的物理意义是什么?”,系统返回教材原文+简洁解释,并标注页码。

FAQ

Q: 需要哪些前提条件?
A: OpenAI API密钥、Python 3.9+、上述pip依赖、一个可读的PDF文件。

Q: 如何处理多个PDF?
A: 使用 DirectoryLoader 加载整个文件夹:

python
from langchain.document_loaders import DirectoryLoader
loader = DirectoryLoader("pdf_folder/", glob="**/*.pdf", loader_cls=PyPDFLoader)

Q: RAG 和纯LLM的最大区别?
A: RAG能引用真实文档,答案可追溯;纯LLM只能靠“背书”,容易产生幻觉。

Q: 想换成本地免费模型可以吗?
A: 可以,使用 HuggingFacePipelineOllama

python
from langchain_huggingface import HuggingFacePipeline
llm = HuggingFacePipeline.from_model_id(model_id="Qwen2.5-7B-Instruct", ...)

Q: 系统安全吗?
A: 知识库保存在本地或私有向量数据库,敏感数据不会上传到OpenAI(仅embedding和查询会发送)。

Q: 支持DOCX、TXT等其他格式吗?
A: 支持,换成对应的Loader即可(TextLoader、Docx2txtLoader等)。

一键启动脚本

保存为 run_rag.sh,然后执行:

bash
chmod +x run_rag.sh && ./run_rag.sh
bash
#!/bin/bash
echo "=== RAG系统一键启动脚本 ==="

# 1. 设置你的OpenAI Key(请提前替换)
export OPENAI_API_KEY="sk-your-real-key-here"

# 2. 安装依赖
pip install langchain openai pypdf chromadb langchain-openai langchain-chroma --quiet

# 3. 下载一个真实可用的测试PDF(2页标准样例)
echo "正在下载测试PDF..."
wget -q https://www.africau.edu/images/default/sample.pdf -O example.pdf

# 4. 运行完整RAG代码
python3 - <<EOF
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA

print("正在加载和处理PDF...")
loader = PyPDFLoader("example.pdf")
documents = loader.load()

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
texts = text_splitter.split_documents(documents)

embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(texts, embeddings, collection_name="pdf_rag")
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

prompt_template = """使用以下上下文回答问题。如果不知道,就说不知道。

上下文: {context}

问题: {question}

回答:"""
PROMPT = PromptTemplate(template=prompt_template, input_variables=["context", "question"])

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm, chain_type="stuff", retriever=retriever,
    chain_type_kwargs={"prompt": PROMPT}, return_source_documents=True
)

query = "这个PDF讲了什么内容?"
print("\n查询问题:", query)
result = qa_chain.invoke({"query": query})
print("\n回答:", result["result"])
print("\n来源页码:", [doc.metadata["page"] for doc in result["source_documents"]])
EOF

echo "\n=== 运行完成!==="
分享:

评论