RAG系统实战:从PDF到智能问答
RAG系统实战:从PDF到智能问答
大纲
背景
在AI时代,Retrieval-Augmented Generation (RAG) 系统已成为构建智能问答应用的热门技术。它结合了信息检索和生成式AI的优势,能够从海量文档中提取相关信息,并生成准确、自然的回答。本文聚焦于基于PDF文档的RAG系统实战,使用LangChain框架、OpenAI的LLM模型和Chroma向量数据库,实现从PDF加载到智能问答的全流程。
RAG系统的兴起源于传统LLM的局限性:它们基于训练数据生成响应,但无法处理实时或私有知识。通过RAG,我们可以将外部知识库(如PDF文档)集成到AI系统中,提升回答的准确性和相关性。这在企业知识管理、法律咨询、教育等领域有广泛应用。
原理
RAG系统的核心原理是“检索-增强-生成”三步走:
- 检索(Retrieval):将用户查询转换为向量 embedding,使用相似度搜索从知识库中检索相关文档片段。
- 增强(Augmentation):将检索到的上下文注入到LLM的提示中,提供额外知识。
- 生成(Generation):LLM基于增强提示生成最终回答。
关键组件:
- 文档加载与切分:从PDF加载文本,并切分为小块(chunks)以便嵌入。
- 向量嵌入:使用OpenAI的embedding模型将文本转换为向量。
- 向量数据库:Chroma存储和管理向量,支持快速相似度搜索。
- LLM集成:OpenAI的GPT模型生成响应。
RAG的优势包括减少幻觉(hallucination)、支持私有数据和易于更新知识库。
系统架构图
系统流程图
完整可运行代码
前提:已安装依赖并设置好 OPENAI_API_KEY
pip install langchain openai pypdf chromadb langchain-openai langchain-chroma
export OPENAI_API_KEY=your_key_here
# 导入必要库
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 加载整个文件夹:
from langchain.document_loaders import DirectoryLoader
loader = DirectoryLoader("pdf_folder/", glob="**/*.pdf", loader_cls=PyPDFLoader)
Q: RAG 和纯LLM的最大区别?
A: RAG能引用真实文档,答案可追溯;纯LLM只能靠“背书”,容易产生幻觉。
Q: 想换成本地免费模型可以吗?
A: 可以,使用 HuggingFacePipeline 或 Ollama:
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,然后执行:
chmod +x run_rag.sh && ./run_rag.sh
#!/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=== 运行完成!==="