# 使用 LangChain 实现 PDF 文档问答系统
## 背景介绍
工作中,我们经常要和大量的 PDF 文档打交道。技术文档、论文报告、合同协议……从这些文件里找信息,真是件麻烦事。传统的关键词搜索听起来美好,但实际操作起来,要么搜不到想要的内容,要么返回一堆八竿子打不着的结果。
大语言模型出来以后,RAG(检索增强生成)架构给这个问题提供了一个新思路。让 AI 系统”读懂”文档内容,然后直接回答问题,这个场景终于变得可实现了。本文就手把手教你用 LangChain 框架,从零搭一个 PDF 问答系统。
## 问题描述
把 PDF 做成问答系统,坑不少。
PDF 本身是非结构化文档。文本可能分散在不同页面、不同栏目里,有些关键信息甚至是图片或表格形式存的,直接读文本根本抓不住。
再有,PDF 文档通常很长,LLM 根本没办法一次处理整个文件。必须把文档拆开,分块处理。
还有,用户提问的方式和文档原文的表达往往差别很大。关键词搜索肯定不好使,得用语义匹配才能找到真正相关的内容。
最后,直接把整个文档喂给 LLM,费用高、响应慢,根本不现实。
## 详细步骤
### 环境准备
先装必要的 Python 包,建个虚拟环境:
“`bash
python -m venv pdf-qa-env
source pdf-qa-env/bin/activate # Linux/Mac
# Windows: pdf-qa-env\Scripts\activate
pip install langchain langchain-community langchain-openai
pip install pypdf pydantic
pip install faiss-cpu openai
“`
### 第一步:加载 PDF 文档
LangChain 的 PyPDFLoader 用来加载 PDF:
“`python
from langchain_community.document_loaders import PyPDFLoader
def load_pdf(file_path):
loader = PyPDFLoader(file_path)
pages = loader.load()
return pages
# 加载文档
pages = load_pdf(“sample.pdf”)
print(f”共加载 {len(pages)} 页”)
print(f”第一页内容预览:{pages[0].page_content[:200]}”)
“`
### 第二步:文档分块
LLM 有上下文长度限制,得把文档拆成小块:
“`python
from langchain.text_splitter import RecursiveCharacterTextSplitter
def split_documents(pages, chunk_size=1000, chunk_overlap=200):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=[“\n\n”, “\n”, “。”, ” “, “”]
)
chunks = text_splitter.split_documents(pages)
return chunks
# 分块处理
chunks = split_documents(pages)
print(f”共分成 {len(chunks)} 个文本块”)
“`
### 第三步:创建向量存储
用文本嵌入模型把文档块转成向量,存到向量数据库里:
“`python
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
def create_vector_store(chunks, openai_api_key):
embeddings = OpenAIEmbeddings(
model=”text-embedding-3-small”,
openai_api_key=openai_api_key
)
vector_store = FAISS.from_documents(
documents=chunks,
embedding=embeddings
)
return vector_store
# 创建向量存储(需要设置 OPENAI_API_KEY 环境变量)
import os
os.environ[“OPENAI_API_KEY”] = “your-api-key-here”
vector_store = create_vector_store(chunks, os.environ[“OPENAI_API_KEY”])
vector_store.save_local(“faiss_index”)
“`
### 第四步:构建问答链
把检索和问答串起来:
“`python
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
def create_qa_chain(vector_store, openai_api_key):
llm = ChatOpenAI(
model=”gpt-3.5-turbo”,
temperature=0,
openai_api_key=openai_api_key
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type=”stuff”,
retriever=vector_store.as_retriever(
search_type=”similarity”,
search_kwargs={“k”: 3}
),
return_source_documents=True
)
return qa_chain
qa_chain = create_qa_chain(vector_store, os.environ[“OPENAI_API_KEY”])
“`
### 第五步:运行问答
提问,拿答案:
“`python
def ask_question(qa_chain, question):
result = qa_chain({“query”: question})
return result
# 示例问答
question = “这份文档的主要内容是什么?”
result = ask_question(qa_chain, question)
print(f”问题:{question}”)
print(f”答案:{result[\”result\”]}”)
print(“\n参考来源:”)
for i, doc in enumerate(result[“source_documents”]):
print(f”{i+1}. 第 {doc.metadata[\”page\”]} 页”)
“`
## 完整代码示例
完整的、可直接跑的示例:
“`python
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain.chains import RetrievalQA
class PDFQASystem:
def __init__(self, pdf_path, openai_api_key):
self.pdf_path = pdf_path
self.openai_api_key = openai_api_key
self.qa_chain = None
self.vector_store = None
def initialize(self):
# 1. 加载 PDF
loader = PyPDFLoader(self.pdf_path)
pages = loader.load()
# 2. 文档分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = text_splitter.split_documents(pages)
# 3. 创建向量存储
embeddings = OpenAIEmbeddings(
model=”text-embedding-3-small”,
openai_api_key=self.openai_api_key
)
self.vector_store = FAISS.from_documents(
documents=chunks,
embedding=embeddings
)
# 4. 创建问答链
llm = ChatOpenAI(
model=”gpt-3.5-turbo”,
temperature=0,
openai_api_key=self.openai_api_key
)
self.qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type=”stuff”,
retriever=self.vector_store.as_retriever(k=3),
return_source_documents=True
)
def ask(self, question):
result = self.qa_chain({“query”: question})
return result
# 使用示例
if __name__ == “__main__”:
os.environ[“OPENAI_API_KEY”] = “your-api-key”
qa_system = PDFQASystem(
pdf_path=”sample.pdf”,
openai_api_key=os.environ[“OPENAI_API_KEY”]
)
qa_system.initialize()
questions = [
“文档的核心观点是什么?”,
“有哪些关键数据和结论?”,
“作者提出了哪些建议?”
]
for q in questions:
result = qa_system.ask(q)
print(f”Q: {q}\nA: {result[\”result\”]}\n”)
“`
## 运行结果
跑上面的代码,会得到类似这样的输出:
“`
Q: 文档的核心观点是什么?
A: 根据文档内容,本文主要讨论了机器学习在自然语言处理领域的应用。
文档指出Transformer架构的提出标志着NLP进入了一个新的发展阶段…
Q: 有哪些关键数据和结论?
A: 文档中提到了以下关键数据:
1. 模型参数量从2018年的1亿增长到2023年的万亿级别
2. 训练数据规模扩大了1000倍
3. 在多项基准测试上的性能提升了30%以上…
Q: 作者提出了哪些建议?
A: 文档作者提出了以下建议:
1. 在实际应用中应考虑模型的效率和成本的平衡
2. 建议采用RAG架构来增强模型的可靠性
3. 强调了对模型输出进行验证的重要性…
“`
检索系统会从原始文档里找到最相关的段落作为参考,保证回答的准确性。
## 总结
本文详细介绍了怎么用 LangChain 搭一个 PDF 文档问答系统。核心就这几步:加载 PDF、文档分块、创建向量存储、用 RAG 架构做问答。一个能”读懂”文档内容的智能问答系统就搭好了。
这个方案的几个好处:语义检索比关键词搜索强得多;RAG 架构让 LLM 基于真实文档内容回答,避免了瞎扯;分块加检索机制保证了长文档也能处理;参考来源功能让用户能验证答案靠不靠谱。
实际用的时候,你可以换不同的嵌入模型、向量数据库或者 LLM 来调优性能和成本。中文文档的话,建议用支持中文的嵌入模型,比如中文版 BGE 或者 Jina AI 的嵌入模型。