Python极简RAG知识库实战:从文档切分到向量检索的完整链路

发布时间:2026/10/1 23:54:46
Python极简RAG知识库实战:从文档切分到向量检索的完整链路 简介这份资源是一套面向人工智能、深度学习方向学生与爱好者的Python极简RAG知识库系统完整项目可作为毕业设计、课程设计或实战练手参考帮助理解检索增强生成在问答场景中的落地方式。压缩包共48个文件约152KB以31个Python源码为主体辅以yaml、yml配置、Dockerfile、Makefile、toml、sh脚本及README文档覆盖应用入口、文档加载与切分、向量嵌入、重排、检索服务、Elasticsearch接入、Web接口与测试用例等模块目录划分清晰便于按功能阅读与二次开发。项目将数据预处理、检索机制、知识库构建、容器化部署与文档规范整合在一起读者可据此掌握RAG系统的整体架构与关键实现思路并借助测试与配置脚本快速运行和调试。目前已有61人学习适合希望从零搭建极简知识库问答系统的开发者参考。1. 一个能跑起来的 Python 极简 RAG 知识库到底长什么样很多人第一次接触 RAG是从「把 PDF 丢进去就能问答」这个场景开始的。真动手才发现光是把文档切块、向量化、存进向量库、再拼 prompt 这一套流程串起来就够折腾一整天。这份Python极简RAG知识库系统.zip解决的就是这个断层它把 RAG 检索增强生成的核心链路压缩成一个能直接跑的最小闭环不依赖重型框架纯 Python 实现文档加载、文本切分、向量检索和答案生成。适合两类人一是做毕业设计或课程设计、需要一份结构清晰可讲解的 RAG 项目源码二是想搞懂 RAG 检索到底怎么落地、不想一上来就被 LangChain 抽象层绕晕的开发者。它不追求生产级性能但每个环节都看得见、改得动这是它最大的价值。2. 拆开压缩包先看什么目录结构与模块职责2.1 典型文件布局与各模块干什么拿到一个 RAG 项目压缩包别急着pip install先把目录树看清楚。极简 RAG 系统的文件布局通常长这样不同版本可能略有出入但核心模块跑不掉rag_kb/ ├── main.py # 入口串起加载→切分→检索→生成 ├── config.py # 路径、模型名、chunk_size 等参数集中管理 ├── loader.py # 文档加载支持 txt / md / pdf ├── splitter.py # 文本切分按字符或段落 ├── embedder.py # 向量化调 embedding 模型或本地模型 ├── vector_store.py # 向量存储与相似度检索 ├── generator.py # 拼 prompt 调 LLM 出答案 ├── requirements.txt # 依赖清单 └── data/ # 放你的知识库文档这个分法不是随便切的。loader和splitter分开是因为文档格式解析和文本切分是两个独立的坑PDF 解析出来的文本可能带乱码或多余换行切分策略又直接影响检索命中率。embedder和vector_store分开是为了让你能单独替换 embedding 模型而不动存储逻辑。常见做法是把config.py里的参数抽出来改 chunk_size、top_k 这些值时不用翻遍所有文件。2.2 先跑通再改最小启动路径在动任何代码之前先确认环境能跑。Python 版本建议 3.9 以上3.11 也稳。依赖安装这一步最容易翻车因为向量库和 embedding 库经常有版本冲突# 创建虚拟环境别在系统 Python 里直接装 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖建议先看 requirements.txt 里锁的版本 pip install -r requirements.txt # 如果 requirements 没锁版本导致冲突按这个顺序手动装 pip install numpy pip install sentence-transformers # 本地 embedding不依赖外部 API pip install faiss-cpu # 向量检索CPU 版够用装完之后先别改代码直接跑main.py看它能不能用data/里的示例文档完成一次问答。这一步的目的是确认「环境→加载→检索→生成」整条链路是通的。如果报ModuleNotFoundError大概率是虚拟环境没激活如果报向量维度不匹配那是 embedding 模型和索引维度对不上后面避坑章节会细说。提示如果你的项目用的是在线 embedding API 而不是本地模型先确认 API key 配好了否则第一步就卡住。2.3 参数集中在哪改config 里的关键项极简项目通常把可调参数放在config.py或main.py顶部。你需要认识这几个参数作用典型值改了会怎样chunk_size每块文本字符数300~500太小检索碎片化太大噪声多chunk_overlap相邻块重叠字符数50~100防止句子被切断丢上下文top_k检索返回块数3~5太多塞爆 prompt太少漏信息embedding_model向量化模型名本地或在线换模型必须重建索引llm_model生成模型名按需影响答案质量和速度这些值没有万能解。文档是技术手册chunk_size 可以小一点保精度文档是叙述性长文chunk_size 大一点保上下文完整。我一般会先用默认值跑一遍看检索出来的块是不是完整句子再微调。3. RAG 检索链路从文档切分到向量召回3.1 文档加载与文本切分chunk 策略决定检索上限RAG 的效果上限在切分这一步就定了一大半。加载器把 PDF、Markdown、txt 读成纯文本后切分器负责把它切成检索单元。极简项目里常见两种切法按固定字符数切和按段落/标题切。# splitter.py 典型实现 def split_by_chars(text, chunk_size400, overlap80): 按固定字符数切分带重叠防止语义断裂 chunks [] start 0 while start len(text): end start chunk_size chunk text[start:end] chunks.append(chunk.strip()) # 每次前进 chunk_size - overlap保证相邻块有重叠 start chunk_size - overlap return [c for c in chunks if c] # 过滤空块逻辑说明chunk_size控制单块信息量overlap保证跨块的句子不被腰斩。参数怎么定如果文档句子平均 50 字chunk_size 设 400 大约容纳 8 句overlap 设 80 约 1.5 句能覆盖大部分跨句语义。切完之后建议打印前几块看看如果发现块首块尾都是半截话说明 overlap 不够或者切分点没对齐标点。按段落切更适合结构化文档def split_by_paragraph(text, max_chars500): 按空行分段超长段落再二次切分 paragraphs [p.strip() for p in text.split(\n\n) if p.strip()] chunks [] for p in paragraphs: if len(p) max_chars: chunks.append(p) else: # 超长段落按句号再切 sentences p.replace(。, 。\n).split(\n) buf for s in sentences: if len(buf) len(s) max_chars: buf s else: chunks.append(buf) buf s if buf: chunks.append(buf) return chunks这种切法保留了段落完整性适合 FAQ、技术文档这类本身就有清晰段落结构的语料。代价是块长度不均匀检索时可能长短块混排。3.2 向量化与索引构建embedding 怎么选、索引怎么存切好的块要转成向量才能做相似度检索。极简项目一般用sentence-transformers加载本地模型不依赖外部接口离线也能跑# embedder.py from sentence_transformers import SentenceTransformer class Embedder: def __init__(self, model_nameall-MiniLM-L6-v2): # 这个模型轻量384 维CPU 跑得动 self.model SentenceTransformer(model_name) def encode(self, texts): # normalize 后余弦相似度等价于点积检索更快 return self.model.encode(texts, normalize_embeddingsTrue)all-MiniLM-L6-v2是极简项目的常客体积小、速度快、384 维对中文支持一般但英文和混合语料够用。如果你的知识库是纯中文换成paraphrase-multilingual-MiniLM-L12-v2或BAAI/bge-small-zh效果会明显好一截。换模型必须重建索引因为维度变了旧索引直接废掉。索引构建和检索用 FAISS 最省事# vector_store.py import faiss import numpy as np class VectorStore: def __init__(self, dim): # IndexFlatIP 做内积检索配合归一化向量等价余弦相似度 self.index faiss.IndexFlatIP(dim) self.chunks [] # 存原始文本检索后按索引取回 def add(self, vectors, chunks): self.index.add(np.array(vectors).astype(float32)) self.chunks.extend(chunks) def search(self, query_vector, top_k3): scores, indices self.index.search( np.array([query_vector]).astype(float32), top_k ) # 返回 (文本, 相似度分数) 列表 return [(self.chunks[i], scores[0][j]) for j, i in enumerate(indices[0]) if i ! -1]逻辑说明IndexFlatIP是暴力检索数据量小的时候精度最高几万块以内延迟可以接受。normalize_embeddingsTrue让内积等于余弦相似度省去额外归一化步骤。top_k控制召回数量一般 3 到 5 够用。检索返回的分数能帮你判断检索质量如果最高分只有 0.3 左右说明知识库里可能根本没有相关内容这时候硬让 LLM 回答就是逼它编。3.3 拼 prompt 与生成上下文怎么塞、幻觉怎么压检索回来的块要拼进 prompt 交给 LLM。这一步的写法直接决定答案质量# generator.py def build_prompt(query, retrieved_chunks): context \n\n---\n\n.join( f[片段{i1}]\n{chunk} for i, (chunk, _) in enumerate(retrieved_chunks) ) prompt f基于以下资料回答问题。如果资料中没有相关信息直接说资料中未提及不要编造。 资料 {context} 问题{query} 回答 return prompt关键在「资料中未提及」这句约束。没有它LLM 遇到检索不到的内容会一本正经地胡说这是 RAG 最常见的翻车点。加上这句之后模型在无依据时倾向于拒答幻觉率明显下降。另外把每个片段编号方便你调试时对照是哪个块贡献了答案。调用 LLM 的部分极简项目可能直接调在线 API也可能接本地模型。不管哪种temperature建议设低一点0.1~0.3RAG 要的是忠实于资料不是发挥创意。4. 避坑与排查RAG 跑不通时先查这几处4.1 检索结果全是无关内容现象问一个问题返回的块跟问题八竿子打不着LLM 基于这些块给出的答案自然也是错的。原因通常有三个一是 embedding 模型跟语料语言不匹配比如中文语料用了纯英文模型二是 chunk_size 太大一个块里混了好几个主题向量被平均掉了三是查询本身太短或太口语跟文档用词差异大。解决先换中文 embedding 模型试再把 chunk_size 降到 300 左右重建索引如果还不行在查询前做一次改写把口语问题转成文档里可能出现的表述。我一般会打印检索分数分数普遍低于 0.4 就说明匹配有问题别急着怪 LLM。4.2 换了 embedding 模型后报维度错误现象AssertionError: Dimension mismatch或者 FAISS 直接崩。原因旧索引是用 384 维模型建的新模型是 768 维维度对不上。解决换 embedding 模型必须删掉旧索引文件重新构建。如果索引是持久化到磁盘的找到对应的.index或.faiss文件删掉重跑构建流程。别想着兼容维度不同没法混。4.3 PDF 加载出来全是乱码或空文本现象loader读 PDF 后打印文本发现是空白或者一堆乱码符号。原因PDF 分两种文本型和扫描型。扫描型 PDF 本质是图片普通文本提取库读不出来有些文本型 PDF 用了特殊编码提取也会乱。解决扫描型 PDF 需要先做 OCR极简项目一般不带 OCR 模块你得自己加一步图片转文字。文本型乱码可以换解析库试试pdfplumber和PyPDF2对不同 PDF 的兼容性不一样一个读不出来换另一个。最稳的办法是先把 PDF 转成 txt 再喂给系统。4.4 答案里出现了资料中没有的内容现象明明知识库里没有相关信息LLM 还是给了一个看起来很合理的答案。原因prompt 里没有明确的拒答约束或者top_k太大把低相关度的块也塞进去了模型从噪声里「推理」出了不存在的信息。解决prompt 里加死约束明确要求无依据时拒答把top_k降到 3在检索后加一个分数阈值过滤低于阈值的块直接丢掉不传给 LLM。阈值设多少看你的 embedding 模型一般 0.3 到 0.5 之间试。4.5 依赖装完 import 就报错现象pip install显示成功但import faiss或import sentence_transformers报错。原因版本冲突是重灾区。sentence-transformers依赖torchfaiss-cpu依赖numpy这几个库的版本互相牵制。另外 Windows 上 FAISS 的安装跟 Linux 不一样有时候需要 conda 装。解决先看报错信息里是哪个库的哪个版本冲突用pip install 库名版本号锁版本。Windows 用户如果pip install faiss-cpu失败试试conda install -c conda-forge faiss-cpu。实在搞不定就新建一个干净虚拟环境按numpy → torch → sentence-transformers → faiss-cpu的顺序装。5. 把极简 RAG 用出效果三个能立刻上手的调优动作第一个动作是给检索加分数阈值过滤。极简项目默认把top_k个块全塞给 LLM但低分块就是噪声。在search返回后加一层过滤def filter_by_score(results, threshold0.35): 丢掉相似度低于阈值的块减少噪声干扰 filtered [(chunk, score) for chunk, score in results if score threshold] # 全被过滤掉说明知识库没有相关内容返回空让上层拒答 return filtered if filtered else []阈值不是拍脑袋定的。拿一批你知道答案的问题跑一遍看正确块的分数分布取一个能滤掉大部分无关块又不误杀正确块的值。我一般从 0.35 起步根据实际命中情况上下调。第二个动作是给 chunk 加上来源标记。检索回来的块如果不知道来自哪个文件、哪一段调试和溯源都很痛苦。在切分时给每块打上元数据def split_with_metadata(text, source, chunk_size400, overlap80): 切分同时记录来源方便检索后溯源 chunks [] start 0 idx 0 while start len(text): chunk text[start:start chunk_size].strip() if chunk: chunks.append({ text: chunk, source: source, # 文件名 index: idx # 块序号 }) idx 1 start chunk_size - overlap return chunks这样检索结果里能直接看到「这段来自产品手册.pdf第 12 块」答案的可信度判断和问题定位都快很多。做毕业设计答辩时这个溯源信息也是加分项。第三个动作是准备一组固定测试问题来验证改动。每次调完 chunk_size 或换完模型别凭感觉说「好像好点了」用同一组问题跑一遍对比检索命中率和答案正确率。测试集不用大10 到 20 个覆盖你知识库主要主题的问题就够。我习惯把问题和期望答案写成一个 json跑完自动对比这样每次调参都有依据不会越调越玄学。注意调参要一次只改一个变量。同时改 chunk_size 和 embedding 模型效果变好了你也不知道是哪个起了作用。从那以后我每次拿到一个新的 RAG 项目都强制先跑通默认配置、打印检索分数、用固定问题验证一遍再动手改任何参数。这套流程帮我省掉了大量「改了不知道有没有用」的时间。希望帮到你。本文还有配套的精品资源点击获取