DeepSeek + Faiss搭建本地知识库检索

# ChatPDF-Faiss智能文档问答


## 概述

 
ChatPDF-Faiss是一个基于大语言模型的PDF智能问答系统,能够对PDF文档内容进行语义理解和问答。系统使用FAISS向量数据库技术实现高效的相似度检索,并结合大语言模型提供准确的问答服务。

 
## 核心功能

 

### 1. PDF文本提取与页码关联

- 从PDF中提取文本内容

- 记录每段文本对应的原始页码信息

- 便于回溯答案的具体出处

 

### 2. 文本分块与向量化处理

- 使用RecursiveCharacterTextSplitter将长文本分割成适合向量化的小块

- 通过DashScope的文本嵌入模型将文本块转化为语义向量

- 构建FAISS向量索引,实现高效的相似度检索

 

### 3. 知识库持久化存储

- 将向量数据库保存到本地磁盘

- 存储文本块与对应页码的映射关系

- 支持后续快速加载,无需重复处理文档

 

### 4. 语义检索与问答

- 基于用户查询进行语义相似度匹配

- 检索与问题最相关的文本块

- 使用大语言模型(通义千问/DeepSeek)生成准确的答案

- 提供答案来源页码,增强可信度与可追溯性

 

## 技术栈

-**PDF处理**:PyPDF2

-**文本分割**:LangChain RecursiveCharacterTextSplitter

-**向量嵌入**:DashScopeEmbeddings (阿里云通义千亿级开源向量模型)

-**向量数据库**:FAISS (Facebook AI Similarity Search)

-**大语言模型**:通义/DeepSeek系列模型

-**问答链**:LangChain QA Chain

 

## 工作流程

 

1.**文档处理阶段**:

   - 提取PDF文本内容与页码信息

   - 文本分割为适当大小的块

   - 向量化处理并构建FAISS索引

   - 存储向量数据库和页码映射信息

 

2.**问答查询阶段**:

   - 接收用户问题

   - 问题向量化并在FAISS中检索相似文本块

   - 将相关文本块和问题一起传入大语言模型

   - 生成答案并提供来源页码

 

## 系统优势

 

-**高性能检索**:FAISS向量库支持亿级向量的快速相似度搜索

-**精准溯源**:回答问题时提供具体页码来源

-**灵活扩展**:支持不同的嵌入模型和大语言模型

-**本地存储**:向量数据库可持久化存储,支持增量更新

 

## 使用方法

 

1. 设置环境变量`DASHSCOPE_API_KEY`

2. 准备PDF文件,命名为"file.pdf"放在当前目录

3. 运行脚本,系统会自动处理文档并创建向量数据库

4. 设置查询问题,获取智能回答及来源页码 

 

 

  1 from PyPDF2 import PdfReader
  2 from langchain.chains.question_answering import load_qa_chain
  3 from langchain_community.callbacks.manager import get_openai_callback
  4 from langchain.text_splitter import RecursiveCharacterTextSplitter
  5 from langchain_community.embeddings import DashScopeEmbeddings
  6 from langchain_community.vectorstores import FAISS
  7 from typing import List, Tuple
  8 import os
  9 import pickle
 10 
 11 DASHSCOPE_API_KEY = os.getenv('DASHSCOPE_API_KEY')
 12 if not DASHSCOPE_API_KEY:
 13     raise ValueError("请设置环境变量 DASHSCOPE_API_KEY")
 14 
 15 def extract_text_with_page_numbers(pdf) -> Tuple[str, List[int]]:
 16     """
 17     从PDF中提取文本并记录每行文本对应的页码
 18     
 19     参数:
 20         pdf: PDF文件对象
 21     
 22     返回:
 23         text: 提取的文本内容
 24         page_numbers: 每行文本对应的页码列表
 25     """
 26     text = ""
 27     page_numbers = []
 28 
 29     for page_number, page in enumerate(pdf.pages, start=1):
 30         extracted_text = page.extract_text()
 31         if extracted_text:
 32             text += extracted_text
 33             page_numbers.extend([page_number] * len(extracted_text.split("\n")))
 34 
 35     return text, page_numbers
 36 
 37 def process_text_with_splitter(text: str, page_numbers: List[int], save_path: str = None) -> FAISS:
 38     """
 39     处理文本并创建向量存储
 40     
 41     参数:
 42         text: 提取的文本内容
 43         page_numbers: 每行文本对应的页码列表
 44         save_path: 可选,保存向量数据库的路径
 45     
 46     返回:
 47         knowledgeBase: 基于FAISS的向量存储对象
 48     """
 49     # 创建文本分割器,用于将长文本分割成小块
 50     text_splitter = RecursiveCharacterTextSplitter(
 51         separators=["\n\n", "\n", ".", " ", ""],
 52         chunk_size=1000,
 53         chunk_overlap=200,
 54         length_function=len,
 55     )
 56 
 57     # 分割文本
 58     chunks = text_splitter.split_text(text)
 59     print(f"文本被分割成 {len(chunks)} 个块。")
 60         
 61     # 创建嵌入模型
 62     embeddings = DashScopeEmbeddings(
 63         model="text-embedding-v1",
 64         dashscope_api_key=DASHSCOPE_API_KEY,
 65     )
 66     
 67     # 从文本块创建知识库
 68     knowledgeBase = FAISS.from_texts(chunks, embeddings)
 69     print("已从文本块创建知识库。")
 70     
 71     # 改进:存储每个文本块对应的页码信息
 72     # 创建原始文本的行列表和对应的页码列表
 73     lines = text.split("\n")
 74     
 75     # 为每个chunk找到最匹配的页码
 76     page_info = {}
 77     for chunk in chunks:
 78         # 查找chunk在原始文本中的开始位置
 79         start_idx = text.find(chunk[:100])  # 使用chunk的前100个字符作为定位点
 80         if start_idx == -1:
 81             # 如果找不到精确匹配,则使用模糊匹配
 82             for i, line in enumerate(lines):
 83                 if chunk.startswith(line[:min(50, len(line))]):
 84                     start_idx = i
 85                     break
 86             
 87             # 如果仍然找不到,尝试另一种匹配方式
 88             if start_idx == -1:
 89                 for i, line in enumerate(lines):
 90                     if line and line in chunk:
 91                         start_idx = text.find(line)
 92                         break
 93         
 94         # 如果找到了起始位置,确定对应的页码
 95         if start_idx != -1:
 96             # 计算这个位置对应原文中的哪一行
 97             line_count = text[:start_idx].count("\n")
 98             # 确保不超出页码列表长度
 99             if line_count < len(page_numbers):
100                 page_info[chunk] = page_numbers[line_count]
101             else:
102                 # 如果超出范围,使用最后一个页码
103                 page_info[chunk] = page_numbers[-1] if page_numbers else 1
104         else:
105             # 如果无法匹配,使用默认页码-1(这里应该根据实际情况设置一个合理的默认值)
106             page_info[chunk] = -1
107     
108     knowledgeBase.page_info = page_info
109     
110     # 如果提供了保存路径,则保存向量数据库和页码信息
111     if save_path:
112         # 确保目录存在
113         os.makedirs(save_path, exist_ok=True)
114         
115         # 保存FAISS向量数据库
116         knowledgeBase.save_local(save_path)
117         print(f"向量数据库已保存到: {save_path}")
118         
119         # 保存页码信息到同一目录
120         with open(os.path.join(save_path, "page_info.pkl"), "wb") as f:
121             pickle.dump(page_info, f)
122         print(f"页码信息已保存到: {os.path.join(save_path, 'page_info.pkl')}")
123 
124     return knowledgeBase
125 
126 def load_knowledge_base(load_path: str, embeddings = None) -> FAISS:
127     """
128     从磁盘加载向量数据库和页码信息
129     
130     参数:
131         load_path: 向量数据库的保存路径
132         embeddings: 可选,嵌入模型。如果为None,将创建一个新的DashScopeEmbeddings实例
133     
134     返回:
135         knowledgeBase: 加载的FAISS向量数据库对象
136     """
137     # 如果没有提供嵌入模型,则创建一个新的
138     if embeddings is None:
139         embeddings = DashScopeEmbeddings(
140             model="text-embedding-v1",
141             dashscope_api_key=DASHSCOPE_API_KEY,
142         )
143     
144     # 加载FAISS向量数据库,添加allow_dangerous_deserialization=True参数以允许反序列化
145     knowledgeBase = FAISS.load_local(load_path, embeddings, allow_dangerous_deserialization=True)
146     print(f"向量数据库已从 {load_path} 加载。")
147     
148     # 加载页码信息
149     page_info_path = os.path.join(load_path, "page_info.pkl")
150     if os.path.exists(page_info_path):
151         with open(page_info_path, "rb") as f:
152             page_info = pickle.load(f)
153         knowledgeBase.page_info = page_info
154         print("页码信息已加载。")
155     else:
156         print("警告: 未找到页码信息文件。")
157     
158     return knowledgeBase
159 
160 def answer_question(knowledgeBase, llm, query, k=2):
161     """
162     回答用户问题
163     
164     参数:
165         knowledgeBase: FAISS向量数据库
166         llm: 大语言模型
167         query: 用户问题
168         k: 检索的文档数量
169     
170     返回:
171         answer: 回答内容
172         sources: 来源页码列表
173     """
174     # 执行相似度搜索,找到与查询相关的文档
175     docs = knowledgeBase.similarity_search(query, k=k)
176     
177     # 加载问答链
178     chain = load_qa_chain(llm, chain_type="stuff")
179     
180     # 准备输入数据
181     input_data = {"input_documents": docs, "question": query}
182     
183     # 使用回调函数跟踪API调用成本
184     with get_openai_callback() as cost:
185         # 执行问答链
186         response = chain.invoke(input=input_data)
187         print(f"查询已处理。成本: {cost}")
188     
189     # 记录唯一的页码
190     unique_pages = set()
191     
192     # 收集每个文档块的来源页码
193     for doc in docs:
194         text_content = getattr(doc, "page_content", "")
195         source_page = knowledgeBase.page_info.get(
196             text_content.strip(), "未知"
197         )
198         
199         if source_page not in unique_pages:
200             unique_pages.add(source_page)
201     
202     return response["output_text"], list(unique_pages)
203 
204 def initialize_system():
205     """初始化系统,加载或创建知识库"""
206     save_dir = "./vector_db"
207     
208     # 检查是否已经存在向量数据库
209     if os.path.exists(save_dir) and os.path.isdir(save_dir):
210         print("检测到已有向量数据库,正在加载...")
211         try:
212             # 创建嵌入模型
213             embeddings = DashScopeEmbeddings(
214                 model="text-embedding-v1",
215                 dashscope_api_key=DASHSCOPE_API_KEY,
216             )
217             # 从磁盘加载向量数据库
218             knowledgeBase = load_knowledge_base(save_dir, embeddings)
219             print("成功加载已有知识库!")
220         except Exception as e:
221             print(f"加载失败: {e}")
222             print("将创建新的知识库...")
223             knowledgeBase = create_new_knowledge_base(save_dir)
224     else:
225         print("未检测到向量数据库,将创建新的知识库...")
226         knowledgeBase = create_new_knowledge_base(save_dir)
227     
228     # 初始化大语言模型
229     from langchain_community.llms import Tongyi
230     llm = Tongyi(model_name="deepseek-v3", dashscope_api_key=DASHSCOPE_API_KEY)
231     
232     return knowledgeBase, llm
233 
234 def create_new_knowledge_base(save_dir):
235     """创建新的知识库"""
236     # 检查PDF文件是否存在
237     pdf_path = './file.pdf'
238     if not os.path.exists(pdf_path):
239         raise FileNotFoundError(f"未找到PDF文件: {pdf_path},请确保文件存在!")
240     
241     # 读取PDF文件
242     pdf_reader = PdfReader(pdf_path)
243     
244     # 提取文本和页码信息
245     text, page_numbers = extract_text_with_page_numbers(pdf_reader)
246     print(f"提取的文本长度: {len(text)} 个字符。")
247     
248     # 处理文本并创建知识库,同时保存到磁盘
249     knowledgeBase = process_text_with_splitter(text, page_numbers, save_path=save_dir)
250     return knowledgeBase
251 
252 def chat_interface():
253     """交互式聊天界面"""
254     print("\n" + "="*50)
255     print("欢迎使用 ChatPDF-Faiss 智能文档问答系统")
256     print("="*50)
257     
258     # 初始化系统
259     try:
260         knowledgeBase, llm = initialize_system()
261     except Exception as e:
262         print(f"系统初始化失败: {e}")
263         return
264     
265     print("\n系统已准备就绪!您可以开始提问了。")
266     print("输入 'exit' 或 'quit' 退出对话。\n")
267     
268     # 开始对话循环
269     while True:
270         # 获取用户输入
271         query = input("\n请输入您的问题: ")
272         
273         # 检查是否退出
274         if query.lower() in ['exit', 'quit', '退出', '结束']:
275             print("感谢使用,再见!")
276             break
277         
278         # 如果输入为空,继续循环
279         if not query.strip():
280             continue
281         
282         try:
283             # 回答问题
284             answer, sources = answer_question(knowledgeBase, llm, query)
285             
286             # 打印回答
287             print("\n回答:")
288             print(answer)
289             
290             # 打印来源
291             if sources:
292                 print("\n来源页码:", end=" ")
293                 print(", ".join(str(page) for page in sources if page != -1))
294         except Exception as e:
295             print(f"\n处理问题时出错: {e}")
296             print("请尝试重新提问或检查系统配置。")
297 
298 if __name__ == "__main__":
299     chat_interface()

 

posted @ 2025-06-24 15:58  周捷Jay  阅读(249)  评论(0)    收藏  举报