LangChain RAG PDF 问答 Demo
一、任务目标
本示例实现一个基于 RAG(检索增强生成)的 PDF 问答应用:用户先导入一份 PDF,再针对文档内容提问,系统会先检索与问题相关的片段,再结合这些片段由大模型生成答案,而不是仅靠模型自身知识回答。
核心能力:文档入库 → 按问题检索相关段落 → 将检索结果与问题一起交给大模型 → 返回基于文档的答案。
二、技术栈与依赖
| 模块 | 作用 |
|---|---|
| Chroma | 向量数据库,存储文档块对应的向量,支持相似度检索 |
| ChatZhipuAI | 大模型接口(智谱),用于根据「上下文 + 问题」生成回答 |
| FastEmbedEmbeddings | 文本嵌入模型,将文本转为向量供检索 |
| PyPDFLoader | 加载 PDF,解析为文本 |
| RecursiveCharacterTextSplitter | 将长文本按块切分,便于检索与上下文限制 |
| PromptTemplate / StrOutputParser | 提示模板与输出解析,组成 LCEL 调用链 |
三、代码结构概览
- 类:
ChatPDF,封装「入库 + 检索 + 生成」全流程。 - 主要方法:
__init__:初始化大模型、文本分割器、RAG 提示模板。ingest(pdf_file_path):加载 PDF → 切块 → 写入向量库 → 构建检索器与调用链。ask(query):对当前已入库的 PDF 进行提问,走 RAG 链得到答案。clear():清空向量库、检索器与链,便于重新加载文档。
四、逻辑说明
4.1 初始化__init__
- 大模型:使用智谱
glm-4-flash。API Key 建议通过环境变量或配置文件传入,不要写死在代码中。 - 文本分割:块长 1024 字符,块间重叠 100 字符,在保留上下文与控制单块长度之间做平衡。
- 提示模板:采用「系统角色 + 用户问题 + 检索上下文」的结构,要求模型严格依据上下文作答、不知道则明确说明、回答简洁(如最多三句话)。
def__init__(self):# 大模型:api_key 建议用 os.environ.get("ZHIPU_API_KEY") 等从环境变量读取self.model=ChatZhipuAI(model="glm-4-flash",api_key=None)# 文本分割:块长 1024,重叠 100self.text_splitter=RecursiveCharacterTextSplitter(chunk_size=1024,chunk_overlap=100)# 提示模板:系统角色 + 用户问题 + 检索上下文self.prompt=PromptTemplate.from_template("""<|system|> 你是专业的问答助手,需严格根据提供的检索上下文回答问题,未知内容直接回复不知道,答案最多三句话且保持简洁。 <|user|> 问题:{question} 上下文:{context} <|assistant|> """)4.2 文档入库ingest(pdf_file_path)
- 用PyPDFLoader加载指定 PDF,得到页面级文档列表。
- 用RecursiveCharacterTextSplitter将文档切分为多个块(chunks)。
- 使用filter_complex_metadata过滤掉不利于向量化的复杂元数据。
- 用FastEmbedEmbeddings为每个块生成向量,并写入Chroma向量库。
- 基于向量库创建retriever:
search_type="similarity_score_threshold":按相似度阈值检索;k=3:最多取 3 个相关块;score_threshold=0.5:仅保留相似度 ≥ 0.5 的块。
- 构建RAG 链:输入为用户问题;
context由 retriever 根据问题检索得到;question原样传入;再经 prompt → 大模型 →StrOutputParser()得到最终字符串答案。
defingest(self,pdf_file_path:str):# 1. 加载 PDF,得到页面级文档列表docs=PyPDFLoader(file_path=pdf_file_path).load()# 2. 切分为多个块(chunks)chunks=self.text_splitter.split_documents(docs)# 3. 过滤复杂元数据chunks=filter_complex_metadata(chunks)# 4. 生成向量并写入 Chromavector_store=Chroma.from_documents(documents=chunks,embedding=FastEmbedEmbeddings())# 5. 创建 retriever:相似度阈值检索,k=3,score_threshold=0.5self.retriever=vector_store.as_retriever(search_type="similarity_score_threshold",search_kwargs={"k":3,"score_threshold":0.5},)# 6. 构建 RAG 链:context 来自 retriever,question 原样传入self.chain=({"context":self.retriever,"question":RunnablePassthrough()}|self.prompt|self.model|StrOutputParser())4.3 提问ask(query)
- 若尚未调用
ingest(即self.chain为空),则提示「请先添加 PDF 文件」。 - 否则对
query调用self.chain.invoke(query),链内部会先检索再生成,返回基于当前 PDF 的答案。
defask(self,query:str):ifnotself.chain:return"请先添加PDF文件"returnself.chain.invoke(query)4.4 清空clear()
- 将向量库、检索器、链引用置为
None,便于后续重新加载另一份 PDF 或重置状态。
defclear(self):self.vector_store=Noneself.retriever=Noneself.chain=None五、RAG 数据流简述
用户问题 query ↓ retriever.invoke(query) → 从 Chroma 中按相似度取 top-k 文档块(如 k=3) ↓ Prompt 填入:context = 检索到的文本,question = query ↓ 大模型(ChatZhipuAI)根据 context + question 生成回答 ↓ StrOutputParser() 得到字符串 → 返回给用户即:检索(Retrieval)→ 用检索结果增强提示(Augmented)→ 大模型生成(Generation),构成完整 RAG 流程。
六、使用方式示例
if__name__=="__main__":chatpdf=ChatPDF()# 先入库一份 PDF(路径按实际替换)chatpdf.ingest(r"你的PDF路径.pdf")# 针对该 PDF 内容提问answer=chatpdf.ask("请简要介绍这份文档的主要内容")print(answer)注意:运行前需配置大模型 API Key(如智谱开放平台申请),并通过环境变量或ChatZhipuAI(..., api_key=...)传入,避免在代码或文档中明文出现密钥。
七、小结
| 项目 | 说明 |
|---|---|
| 任务 | 基于 RAG 的 PDF 问答:先检索再生成,答案依托指定文档 |
| 关键技术 | 向量库(Chroma)+ 嵌入(FastEmbed)+ 检索器 + 大模型(智谱)+ LCEL 链 |
| 使用顺序 | 先ingest( pdf 路径 ),再ask( 问题 );可调用clear()后重新ingest其他 PDF |
完整代码
fromlangchain_community.vectorstoresimportChromafromlangchain_community.chat_modelsimportChatZhipuAIfromlangchain_community.embeddingsimportFastEmbedEmbeddingsfromlangchain_core.output_parsersimportStrOutputParserfromlangchain_community.document_loadersimportPyPDFLoaderfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain_core.runnablesimportRunnableLambda,RunnablePassthroughfromlangchain_core.promptsimportPromptTemplatefromlangchain_community.vectorstores.utilsimportfilter_complex_metadata# 可选:导入环境变量模块,推荐用环境变量存储密钥,更安全# import osclassChatPDF:vector_store=Noneretriever=Nonechain=Nonedef__init__(self):self.model=ChatZhipuAI(model="glm-4-flash",api_key="YOUR_ZHIPU_API_KEY_HERE")self.text_splitter=RecursiveCharacterTextSplitter(chunk_size=1024,chunk_overlap=100)self.prompt=PromptTemplate.from_template("""<|system|> 你是专业的问答助手,需严格根据提供的检索上下文回答问题,未知内容直接回复不知道,答案最多三句话且保持简洁。 <|user|> 问题:{question} 上下文:{context} <|assistant|> """)defingest(self,pdf_file_path:str):docs=PyPDFLoader(file_path=pdf_file_path).load()chunks=self.text_splitter.split_documents(docs)chunks=filter_complex_metadata(chunks)vector_store=Chroma.from_documents(documents=chunks,embedding=FastEmbedEmbeddings())self.retriever=vector_store.as_retriever(search_type="similarity_score_threshold",search_kwargs={"k":3,"score_threshold":0.5,},)self.chain=({"context":self.retriever,"question":RunnablePassthrough()}|self.prompt|self.model|StrOutputParser())defask(self,query:str):ifnotself.chain:return"请先添加PDF文件"returnself.chain.invoke(query)defclear(self):self.vector_store=Noneself.retriever=Noneself.chain=Noneif__name__=="__main__":chatpdf=ChatPDF()chatpdf.ingest(r"F:\Documents\项目大纲-AI就业班.pdf")answer=chatpdf.ask("请介绍一下这份AI就业班的项目大纲")print(answer)