三个月前,我在 Github 上开源的一个 RAG 练手项目,目前已经有了 327 个 star,总共解决了 22 个 issues。结合过去几个月的项目实践,我重新对项目做了轻量化重构,降低资源消耗与部署门槛。
项目地址:https://github.com/weiwill88/Local_Pdf_Chat_RAG
麻雀虽小,五脏俱全。总体来说,这是一个轻量级但组件完整的本地化 RAG 智能问答平台。可以通过 Gradio Web UI 直观体验混合检索、重排序、递归查询及联网搜索等高级 RAG 策略,更能从源码层面学习和实践 RAG 的完整流程与优化技巧。BTW,也支持 API 的调用方式.
这篇试图说清楚,项目的各个核心组件构成,日志分段拆解含义,以及进阶和扩展方向参考,欢迎感兴趣的盆友基于此项目进行探索和贡献。
以下,enjoy:
1、项目定位
在接触如 Dify、RAGFlow 这类高度封装的 RAG 框架之前,复现和二开这个项目,可以:
熟悉 RAG 核心组件:实际体验文本加载、切分、向量化、向量存储与检索(本项目使用 FAISS)、LLM 集成等关键环节。
理解 RAG 基本流程:从底层脚本层面观察数据如何在 RAG 系统中流转和处理。
进行初步优化与测试:尝试调整参数、替换模型、优化提示词等,直观感受不同策略对结果的影响。
掌握这些基础后,能更有的放矢地使用高级 RAG 框架的 API 进行针对性调优或定制开发。
2、核心优化
这部分要介绍的项目轻量化改造,主要也是为了让初学者盆友更好的抓住 RAG 的核心脉络,避免过早陷入数据库管理和配置的细节中。
当理解了核心流程后,再过渡到如 ChromaDB 或其他生产级向量数据库,就能更好地理解这些数据库所解决的问题和提供的价值。
2.1旧版依赖问题
在上一个版本的 issues 中,有挺多用户反馈依赖安装时间过久,几个主要的“重量级”组件及其依赖项是导致安装时间较长的主要原因:
torch 和 transformers
这两个库是 sentence-transformers 的核心依赖。sentence-transformers 用于生成文本嵌入(向量化)以及进行结果重排序(通过交叉编码器)。torch 是一个庞大的深度学习框架,而 transformers 包含了许多预训练模型和工具。这些是现代 NLP 和 RAG 系统的基石,因此体积较大。
onnxruntime
这是 chromadb 的一个依赖。chromadb 在内部可能使用 ONNX Runtime 来执行其默认的嵌入模型或其他优化计算,即使项目代码中指定了使用 sentence-transformers 来生成嵌入。onnxruntime 本身是一个跨平台的机器学习模型执行引擎,体积也不小。
Langchain
虽然原项目目前主要使用它的文本分割器 (RecursiveCharacterTextSplitter),但完整安装 langchain 会引入不少间接依赖。
2.2轻量化改造
针对 Langchain 的优化
项目主要使用了 langchain 的文本分割功能,考虑到 langchain 已经将许多组件模块化,所以可以仅安装文本分割器模块,并相应修改代码中的导入语句。
针对向量数据库
ChromaDB 虽然功能较为全面,但在某些场景下,尤其是对于本地运行和初学者而言,其依赖和服务可能相对“重”一些,会涉及到更多的后台进程和磁盘空间占用。
FAISS-CPU 是一个专注于高效向量相似性搜索的 C++库,Python 绑定通常更为轻量,依赖更少,尤其是在 CPU 版本下,不需要额外的数据库服务运行,直接在内存中进行索引和搜索。这使得项目更容易在普通个人电脑上快速启动和运行。
注:针对嵌入和重排序模型 (sentence-transformers, torch, transformers),这部分是 RAG 系统效果的核心,轻量化难度较大,且容易牺牲模型性能,所以暂时不做处理。
3、核心组件拆解
项目虽然经过了轻量化改造,但依然包含了构建一个完整 RAG 系统的所有核心组件。学习和理解下述组件的运作方式,对于入门 RAG 技术很重要。
3.1文档加载与解析:
使用 pdfminer.six 从 PDF 文件中提取文本内容,理解如何从不同格式的非结构化数据源中提取原始文本,这是 RAG 流程的第一步。熟悉不同的解析库及其优缺点,能为后续处理多种数据源打下基础。
3.2文本切分:
使用 langchain_text_splitters (如 RecursiveCharacterTextSplitter) 将长文本分割成小的数据块 (chunks)。理解文本切分对于 RAG 的重要性。合适的切分策略能确保每个数据块既包含足够的上下文,又不超过后续处理(如向量化模型输入长度、LLM 上下文窗口)的限制。学习不同的切分方法(如按字符数、按句子、递归等)及其适用场景。
3.3文本向量化 :
使用 sentence-transformers 库加载预训练的句向量模型(如 moka-ai/m3e-base),将文本块转换为高维向量。这是 RAG 的核心之一。理解文本向量化的概念,即如何将语义信息编码为计算机可以理解和比较的数字表示。
熟悉不同的向量化模型及其特点(如多语言支持、特定领域优化、向量维度等),并了解如何选择合适的模型。
3.4向量存储与索引 :
使用 faiss-cpu 构建向量索引 (IndexFlatL2),并在内存中存储和管理这些向量及其与原始文本块的关联(通过我们自己维护的 faiss_contents_map, faiss_metadatas_map, faiss_id_order_for_index)。
理解向量数据库/搜索引擎的基本原理,即如何高效地存储大量向量,并根据查询向量快速找到最相似的 K 个向量。通过 FAISS,可以直观感受到索引构建、相似度计算(如 L2 距离)的过程。学习不同的索引策略对检索效率和精度的影响。
3.5检索:
语义检索:用户问题向量化后,在 FAISS 索引中执行 search 操作,获取最相似的文本块。
关键词检索:使用 rank_bm25 实现 BM25 算法,根据关键词匹配度进行检索。
混合检索:结合语义检索和 BM25 的结果,进行加权合并。
理解不同的检索策略。语义检索关注意义的相似性,关键词检索关注字面匹配。混合检索则试图结合两者优点,提高召回率和相关性。学习如何评估和调整不同检索策略的权重。
3.6上下文重排序:
使用 sentence-transformers 加载交叉编码器 (CrossEncoder) 模型,对初步检索到的上下文片段进行重新打分和排序,选出与问题最相关的片段。
理解在初步检索后,如何进一步优化上下文的相关性。交叉编码器通常比双编码器(用于向量化的模型)在相关性判断上更精确,但计算量也更大,因此常用于对少量候选结果的精排。
3.7提示工程与大语言模型交互 :
构建合适的提示 (Prompt),将用户问题和检索到的相关上下文整合后,提交给大语言模型 (LLM)。通过 requests 与本地 Ollama 服务或云端 SiliconFlow API 进行交互,获取 LLM 生成的答案。
递归检索:利用 LLM 分析当前结果,判断是否需要生成新的查询以进行更深入的探索。
理解 LLM 在 RAG 中的核心作用——基于提供的上下文生成答案。学习如何设计有效的提示词,以引导 LLM 更好地利用检索到的信息。体验与不同 LLM(本地/云端)集成的过程。递归检索则展示了更高级的 RAG 模式,即如何让 LLM 参与到信息检索的迭代优化中。
3.8用户界面:
使用 Gradio 构建交互式的 Web 界面。虽然不是 RAG 核心算法的一部分,但一个好的界面能极大地方便用户与 RAG 系统交互、测试和调试。学习 Gradio 这类工具可以快速搭建原型。
4、运行日志拆解
下文会清晰地追踪 RAG 系统处理问题的每一步,各位仔细阅读下,有助于更好理解各组件的功能和协同方式。
4.1应用启动与用户界面初始化 (Gradio)
复制(venv) PS D:\Projects\Ongoing\开源项目\local_pdf+Chat_rag> python rag_demo_pro.py Gradio version: 5.29.0 D:\Projects\Ongoing\开源项目\local_pdf+Chat_rag\rag_demo_pro.py:1703: UserWarning: You have not specified a value for the `type` parameter. Defaulting to the 'tuples' format for chatbot messages, but this is deprecated and will be removed in a future version of Gradio. Please set type='messages' instead, which uses openai-style dictionaries with 'role' and 'content' keys. chatbot = gr.Chatbot( INFO:httpx:HTTP Request: GET https://api.gradio.app/pkg-version "HTTP/1.1 200 OK" * Running on local URL: http://0.0.0.0:17995 INFO:httpx:HTTP Request: GET http://localhost:17995/gradio_api/startup-events "HTTP/1.1 200 OK" INFO:httpx:HTTP Request: HEAD http://localhost:17995/ "HTTP/1.1 200 OK" * To create a public link, set `share=True` in `launch()`.
python rag_demo_pro.py: 这是启动整个 RAG 应用的主命令。radio version: 5.29.0: 显示了 Gradio 库版本,它负责构建用户交互界面。
UserWarning... chatbot = gr.Chatbot(...): Gradio 提示其聊天机器人组件参数 type 的未来变更。INFO:httpx:HTTP Request...: Gradio 启动过程中的网络请求,如检查版本等。
Running on local URL: http://0.0.0.0:17995 : Gradio 服务成功启动,用户可通过此地址访问 Web UI。
4.2数据初始化/清理
复制INFO:root:成功清理历史FAISS数据和BM25索引
在处理新文档前或应用启动时,系统会清理旧的 FAISS 向量索引和 BM25 关键词索引,确保基于当前文档进行问答,避免数据混淆。
涉及组件:FAISS 索引管理、BM25 索引管理。
4.3文档处理、向量化与 FAISS 索引构建
复制Batches: 100%|████████████████████████████████████████████████████████████| 1/1 [00:02<00:00, 2.71s/it] INFO:root:FAISS索引构建完成,共索引 9 个文本块
此阶段包括了从 PDF 提取文本、将文本切分成小块(chunks)、然后使用 sentence-transformers 模型(如 moka-ai/m3e-base)将这些文本块批量转换为向量。Batches: 100%...: 显示文本块向量化的进度。
INFO:root:FAISS 索引构建完成...: 表明所有文本块的向量已成功存入 FAISS (IndexFlatL2) 索引。此处示例中,PDF 被处理成了 9 个文本块。
涉及组件:pdfminer.six (PDF 解析)、langchain_text_splitters (文本切分)、sentence-transformers (向量化)、faiss-cpu (向量索引)。
4.4BM25 关键词索引构建
复制Building prefix dict from the default dictionary ... DEBUG:jieba:Building prefix dict from the default dictionary ... Loading model from cache C:\Users\10440\AppData\Local\Temp\jieba.cache DEBUG:jieba:Loading model from cache C:\Users\10440\AppData\Local\Temp\jieba.cache Loading model cost 6.168 seconds. DEBUG:jieba:Loading model cost 6.168 seconds. Prefix dict has been built successfully. DEBUG:jieba:Prefix dict has been built successfully. INFO:root:BM25索引更新完成,共索引 9 个文档
系统为相同的文本块构建 BM25 关键词索引,以支持后续的混合检索。Building prefix dict...: jieba 分词库正在初始化并加载词典,这是处理中文文本进行 BM25 计算的前提。
INFO:root:BM25 索引更新完成...: 表明针对这 9 个文本块的 BM25 索引已创建。
涉及组件:rank_bm25 库、jieba 分词库。
4.5用户提问与递归检索启动 (第一轮)
复制INFO:root:递归检索迭代 1/3,当前查询: 发动机冒蓝烟的故障原因分析
用户通过 Gradio 界面输入问题“发动机冒蓝烟的故障原因分析”。系统启动递归检索流程,配置的最大迭代次数为 3,这是第一轮的开始。
涉及组件:Gradio UI、递归检索控制逻辑。
4.6联网搜索(可选,第一轮)
复制INFO:root:网络搜索返回 5 条结果,这些结果不会被添加到FAISS索引中。
如果启用了联网搜索,系统会使用 SerpAPI 根据当前查询从互联网获取实时信息。这些结果作为临时上下文,当前版本不存入 FAISS。
涉及组件:SerpAPI 集成、requests 库。
4.7查询向量化 (第一轮)
复制Batches: 100%|████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 3.53it/s]
用户的查询(或其变体)被送入 sentence-transformers 模型转换为查询向量,用于在 FAISS 中进行语义相似度搜索。
涉及组件:sentence-transformers 模型。
4.8检索结果重排序(第一轮)
复制Some weights of DistilBertForSequenceClassification were not initialized from the model checkpoint at sentence-transformers/distiluse-base-multilingual-cased-v2 and are newly initialized: ['classifier.bias', 'classifier.weight', 'pre_classifier.bias', 'pre_classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference. INFO:sentence_transformers.cross_encoder.CrossEncoder:Use pytorch device: cpu INFO:root:交叉编码器加载成功 Batches: 100%|████████████████████████████████████████████████████████████| 1/1 [00:07<00:00, 7.42s/it]
初步通过 FAISS 和 BM25 检索到的候选文本块,会由交叉编码器 (CrossEncoder) 进行更精确的相关性打分和重排序。
Some weights...: Hugging Face Transformers 库关于交叉编码器底层模型部分权重新初始化的提示。INFO:...Use pytorch device: cpu: 交叉编码器在 CPU 上运行。Batches: 100%...7.42s/it: 显示重排序过程及其耗时。
涉及组件:sentence-transformers (CrossEncoder 模型)。
4.9LLM 交互:判断是否需要递归查询及生成新查询 (第一轮后)
复制INFO:root:使用SiliconFlow API分析是否需要进一步查询 INFO:root:生成新查询: 新查询(如果需要): 1. 涡轮增压器故障是否会引起发动机冒蓝烟? 2. 曲轴箱通风系统(PCV阀)故障如何导致烧机油? 3. 气门油封老化与冒蓝烟的具体关联是什么? 4. 使用错误粘度的机油的烧机油风险有哪些?
系统将第一轮检索的上下文及原始问题提交给大语言模型 (LLM,此处为 SiliconFlow API)。LLM 分析后判断需要进一步探索,并生成了一系列更具体的新查询点,以指导下一轮检索。
涉及组件:LLM (SiliconFlow API/Ollama)、提示工程。
4.10递归检索 (第二轮)
复制INFO:root:递归检索迭代 2/3,当前查询: 新查询(如果需要): 1. 涡轮增压器故障是否会引起发动机冒蓝烟? 2. 曲轴箱通风系统(PCV阀)故障如何导致烧机油? 3. 气门油封老化与冒蓝烟的具体关联是什么? 4. 使用错误粘度的机油的烧机油风险有哪些? INFO:root:网络搜索返回 5 条结果,这些结果不会被添加到FAISS索引中。 Batches: 100%|█████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 15.57it/s] Batches: 100%|█████████████████████████████████████████████████████████████████████████████| 1/1 [00:06<00:00, 6.47s/it]
系统进入第二轮递归检索,使用 LLM 生成的新查询。重复进行联网搜索、查询向量化、混合检索(隐含)、重排序等步骤。
涉及组件:同第一轮的检索、向量化、重排序组件。
4.11LLM 交互:再次判断与生成新查询 (第二轮后)
复制INFO:root:使用SiliconFlow API分析是否需要进一步查询 INFO:root:生成新查询: 新查询: 1. 活塞环磨损或断裂如何导致发动机冒蓝烟? 2. 气门油封老化与烧机油的因果关系及检测方法 3. 气缸壁划伤是否会引起过量机油进入燃烧室? 4. 高粘度与低粘度机油选择错误对烧蓝烟现象的具体影响差异 5. 废气再循环系统(EGR)故障是否可能间接引发烧机油问题? 理由: - **角度扩展**:现有信息聚焦于PCV阀、涡轮增压器和基础油品问题(如低粘度),但未覆盖活塞环/气门油封等机械磨损核心因素。需补充机械结构失效的关联分析。 - **技术细化**:针对已知的“粘度过低”提示,需明确不同粘度机油的适用场景与异常消耗阈值(如高温剪切性能)。 - **系统关联性**:EGR系统虽不直接涉及润滑回路,但其堵塞可能导致异常燃烧压力变化间接加剧窜油现象。
第二轮检索后,再次调用 LLM。LLM 进一步分析并生成了更细化、更深入的新查询及理由,展示了其分析和引导能力。
涉及组件:LLM (SiliconFlow API/Ollama)、提示工程。
4.12递归检索 (第三轮 - 最后一轮)
复制INFO:root:递归检索迭代 3/3,当前查询: 新查询: 1. 活塞环磨损或断裂如何导致发动机冒蓝烟? 2. 气门油封老化与烧机油的因果关系及检测方法 3. 气缸壁划伤是否会引起过量机油进入燃烧室? 4. 高粘度与低粘度机油选择错误对烧蓝烟现象的具体影响差异 5. 废气再循环系统(EGR)故障是否可能间接引发烧机油问题? 理由: - **角度扩展**:现有信息聚焦于PCV阀、涡轮增压器和基础油品问题(如低粘度),但未覆盖活塞环/气门油封等机械磨损核心因素。需补充机械结构失效的关联分析。 - **技术细化**:针对已知的“粘度过低”提示,需明确不同粘度机油的适用场景与异常消耗阈值(如高温剪切性能)。 - **系统关联性**:EGR系统虽不直接涉及润滑回路,但其堵塞可能导致异常燃烧压力变化间接加剧窜油现象。 INFO:root:网络搜索返回 5 条结果,这些结果不会被添加到FAISS索引中。 Batches: 100%|█████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 11.48it/s] Batches: 100%|█████████████████████████████████████████████████████████████████████████████| 1/1 [00:04<00:00, 4.05s/it]
进入最后一轮递归检索,重复之前轮次的步骤。在此轮结束后,系统会将所有收集到的、经过筛选的上下文信息,与原始问题一起,提交给 LLM 以生成最终答案(此部分日志未完全显示)。
涉及组件:同前几轮的检索、向量化、重排序组件,以及最终的 LLM 答案生成。
5、进阶与扩展方向
项目作为一个入门级的 RAG 实现,为后续的迭代和功能扩展提供了良好的基础。以下是各位一些可以考虑的进阶方向:
5.1更精细化的文本切分策略
当前的RecursiveCharacterTextSplitter是通用策略。可以研究并实现基于语义的切分(如使用模型判断句子边界或主题连贯性)、或针对特定文档类型的结构化切分(如解析 Markdown 标题、表格等)。
5.2高级 FAISS 索引与管理
目前使用的是基础的IndexFlatL2。可以尝试更高级的 FAISS 索引类型,如IndexIVFPQ,以优化大规模数据下的检索速度和内存占用。同时,研究如何更优雅地支持对 FAISS 中向量的删除和更新(例如,使用IndexIDMap)。
5.3多元数据源接入
目前主要处理 PDF 和可选的网络搜索。可以扩展支持导入其他格式的本地文档(如.txt,.md,.docx),或者接入外部 API(如 Notion、Confluence 等知识库)。
5.4查询改写与意图识别:
在进行检索前,使用 LLM 对用户的原始查询进行改写(如纠错、同义词扩展、澄清模糊表述)或识别用户真实意图,可以提高检索的精准度。
5.5上下文管理与压缩
当检索到的相关片段过多,超出 LLM 的上下文窗口限制时,需要有效的上下文压缩策略(如筛选最重要片段、总结次要片段)来保证信息质量。
5.6更复杂的重排序模型/策略
除了当前的交叉编码器和基于 LLM 打分,可以尝试集成更先进的重排序模型,或实现多阶段重排序策略。
5.7答案生成效果评估与追溯:
引入简单的评估机制(如用户反馈、答案与来源的相似度计算)和更清晰的答案来源追溯展示,帮助分析和改进系统表现。
6、写在最后
Anyway,动手实践的手搓方式来理解底层机制,是为后续深入学习和使用更高级框架的重要铺垫。