简介这份资源是面向Python开发者与大模型应用学习者的RAG检索增强生成最佳实践源码聚焦提升大模型在检索与文本生成环节的协同能力适用于搜索引擎、智能问答、自动文稿撰写等场景适合具备一定Python与机器学习基础的中高级读者研读。压缩包共22个文件、约527KB以7个XML配置、5个Python源码文件为主另含3个txt数据与说明、2个gitignore、2张png示意图、1个iml工程文件、1份md文档及开源许可覆盖环境配置、核心算法、数据样例与项目说明等模块。目录中可见main.py入口、retriever与query等检索生成核心脚本、prompt提示词模块及data测试数据结构清晰便于按模块拆解学习。目前已有946人学习下载可帮助读者理解RAG系统的组织方式、检索与生成接口的衔接思路并作为二次开发与工程落地的参考模板。1. 从一份 Python RAG 源码说起为什么你跑通的 Demo 一上真实文档就翻车很多人第一次接触检索增强生成都是被一段几十行的 Python 脚本带进门的加载一个 PDF切一切塞进向量库接上大模型问一句答一句效果惊艳。可一旦把文档换成公司几百页的产品手册、合同模板、运维日志回答立刻开始胡言乱语——检索回来的片段答非所问模型一本正经地编造条款编号引用来源对不上原文。这不是模型不行而是这套「基于 Python 的大模型 RAG 检索增强生成」方案里真正决定成败的环节被 Demo 省略了。这份源码要解决的核心问题是把 RAG 从「能跑」推到「敢用」文档怎么切才不丢语义、向量检索为什么经常召回不准、检索结果怎么重排、上下文怎么塞进有限的窗口、答案怎么带可核验的出处。它适合已经用 Python 跑通过最简 RAG、但被真实数据打回原形的工程师也适合准备把 RAG 知识库落到内部系统、需要一套可复现工程骨架的人。下面我按自己踩过的顺序把这条链路拆开讲清楚每一步都给能直接抄的代码和参数。2. 文档解析与切分RAG 效果的地基在这里就决定了2.1 为什么切分策略比换模型更影响召回RAG 的检索质量本质取决于「切出来的块」和「用户问题」在语义空间里能不能对上。很多人一上来就调 embedding 模型、换更大的 LLM却忽略了切分才是第一道闸门。切得太碎一个完整条款被拆成三段检索命中其中一段也拼不出完整答案切得太粗一个块里混了五六个主题向量被平均成一个模糊的中心点跟任何具体问题都只是「有点像」。常见做法是按固定字符数切配一点重叠。但真实文档里标题、表格、代码块、列表的边界和纯文本完全不同。我一般会先做结构化解析把标题层级保留下来再按语义边界切最后才用长度兜底。这样切出来的块自带上下文比如「第 3 章 3.2 计费规则」检索时命中率会明显不一样。2.2 用 Python 做结构化切分的最小实现下面这段是解析加切分的骨架用递归字符切分器做兜底同时把标题路径拼进每个块的元数据。依赖langchain-text-splitters和pypdf这两个是当前 Python 生态里最省事的组合。from langchain_text_splitters import RecursiveCharacterTextSplitter from pypdf import PdfReader def load_pdf(path: str) - str: reader PdfReader(path) # 逐页抽取保留页边界方便后续定位出处 pages [] for i, page in enumerate(reader.pages): text page.extract_text() or pages.append(f[PAGE {i1}]\n{text}) return \n.join(pages) def split_docs(raw: str, chunk_size: int 500, overlap: int 80): splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, # 单块目标字符数 chunk_overlapoverlap, # 相邻块重叠防止语义被切断 separators[\n## , \n### , \n\n, \n, 。, ], length_functionlen, ) chunks splitter.split_text(raw) # 给每块补一个序号便于回溯和调试 return [{id: i, text: c} for i, c in enumerate(chunks)]逻辑上分两步先把 PDF 按页抽成带页码标记的纯文本页码标记会在后面变成引用出处再用递归切分器按「标题 → 段落 → 换行 → 句号」的优先级逐级尝试尽量在语义边界断开。chunk_size我一般从 500 起步中文技术文档可以到 800overlap取 chunk_size 的 10% 到 15%太小接不上太大检索会返回大量重复内容。separators的顺序很关键把 Markdown 标题放在最前面能优先在章节边界切。注意如果文档里有大量表格纯文本抽取会把表格拍平成一行行数字语义全丢。这类文档要么单独走表格解析要么在切分前把表格转成「字段: 值」的描述文本。2.3 切分参数怎么调三个可量化的判断标准调参不能靠感觉我一般看三个指标。第一是块长度分布如果大量块贴着 chunk_size 上限说明分隔符没起作用语义边界没被识别第二是检索命中率拿一批真实问题去测看正确答案所在块有没有进 top-k第三是答案完整度命中的块能不能独立支撑一个完整回答。参数起步值调整方向观察信号chunk_size500中文技术文档调到 800块内主题是否单一chunk_overlap80取 chunk_size 的 10%~15%相邻块是否语义断裂separators标题优先按文档实际结构排序块是否在章节边界切开如果命中率低但块长度正常问题往往不在切分而在 embedding 或检索环节别在这里反复折腾。3. 向量化与检索召回不准的锅多半在这里3.1 embedding 模型选型中文场景别照搬英文榜单embedding 模型决定了「问题」和「文档块」能不能在同一个语义空间里靠近。英文榜单上排名靠前的模型放到中文技术文档上经常水土不服因为训练语料分布不同。中文场景我一般优先选在中文检索任务上验证过的模型或者直接用支持多语言的通用模型先跑通再谈优化。选型时看三个点向量维度影响存储和检索速度、最大输入长度要能覆盖你的 chunk_size、是否支持查询和文档用不同前缀。有些模型对「查询」和「文档」需要加不同的指令前缀漏加会导致检索质量断崖式下跌这是很隐蔽的坑。3.2 用 Python 建索引并跑通一次检索下面用sentence-transformers做向量化用faiss做近邻检索这是本地跑 RAG 最轻量的组合不依赖任何外部服务。import numpy as np import faiss from sentence_transformers import SentenceTransformer # 换成你验证过的中文或多语言模型 model SentenceTransformer(BAAI/bge-small-zh-v1.5) def build_index(chunks): texts [c[text] for c in chunks] # normalize 后内积等价于余弦相似度 emb model.encode(texts, normalize_embeddingsTrue, batch_size32) emb np.asarray(emb, dtypefloat32) index faiss.IndexFlatIP(emb.shape[1]) # 内积索引 index.add(emb) return index, emb def search(query, index, chunks, top_k5): q model.encode([query], normalize_embeddingsTrue) q np.asarray(q, dtypefloat32) scores, ids index.search(q, top_k) return [(chunks[i][text], float(s)) for i, s in zip(ids[0], scores[0])]normalize_embeddingsTrue是关键归一化之后内积就等于余弦相似度省去额外计算。IndexFlatIP是精确检索数据量在十万块以内完全够用超过再考虑 IVF 或 HNSW 这类近似索引。batch_size按显存调32 是稳妥起点。检索返回的分数要留着后面重排和阈值过滤都要用。3.3 纯向量检索为什么会漏混合检索补上关键词这一路向量检索擅长语义相近但对精确关键词、编号、专有名词很弱。用户问「第 4.2 条违约金比例」向量可能召回一堆讲违约的段落却漏掉真正写着「4.2」的那一块。常见做法是加一路关键词检索BM25 或倒排把两路结果融合。融合最简单的是倒数排名融合RRF不依赖分数尺度只看得票排名工程上很稳。把向量检索和关键词检索各取 top-20用 RRF 合并成 top-10再进重排。这一步加上去编号类、术语类问题的召回会有肉眼可见的提升。提示融合前两路的候选数量要够各取 20 是经验值。取太少正确答案可能压根没进候选池后面重排再强也救不回来。4. 重排与上下文组装把对的块喂给大模型4.1 重排模型解决的是「排序」不是「召回」检索阶段追求的是「别漏」所以候选可以宽一点重排阶段追求的是「把最相关的顶上来」。向量相似度高不等于真的相关尤其是块比较长的时候一个块里只要有一句沾边整体向量就会被拉高。重排模型cross-encoder 类把问题和每个候选块拼在一起打分精度高但慢所以只对 top-20 到 top-50 的候选做不能全量跑。我一般把重排后的分数做一次归一化设一个阈值低于阈值的直接丢掉。这样能挡掉一批「看起来相关其实没用」的块减少喂给大模型的噪声。阈值没有通用值要在自己的测试集上标一批正负样本调出来。4.2 上下文组装的三个硬约束组装上下文不是把 top-k 拼起来就完事有三个约束必须处理。第一是总长度要留出空间给系统提示和模型输出别把窗口塞满导致截断第二是去重重叠切分会让相邻块内容高度重复重复喂进去既浪费窗口又干扰模型第三是顺序把最相关的块放在开头或结尾中间位置容易被模型忽略这是长上下文里常见的「中间遗忘」现象。def assemble_context(hits, max_chars3000): seen, picked, total set(), [], 0 for text, score in hits: # hits 已按重排分数降序 key text[:60] # 用前缀做粗去重 if key in seen: continue if total len(text) max_chars: break seen.add(key) picked.append(text) total len(text) # 最相关的放最前其余按原序避免中间遗忘 return \n\n---\n\n.join(picked)max_chars要按你实际用的模型窗口倒推留出至少 30% 给输出。去重用前缀做粗判足够精确去重成本高收益低。顺序上把最高分块放开头是因为多数模型对开头和结尾的注意力更强。4.3 提示词里必须写死的两条规则系统提示里有两句话我每次都会写死一是「只依据提供的上下文回答上下文没有的信息直接说不知道」二是「回答时标注引用的片段编号」。第一条压制幻觉第二条让答案可核验。别指望模型自觉不写清楚它就会拿预训练知识来补尤其在上下文没命中时编得最凶。5. 避坑与排查RAG 上线前必须过的五道坎5.1 检索回来一堆块答案却总是错的现象是 top-k 里明明有正确块模型还是答错。原因通常是上下文太长太杂正确块被淹没或者块里混了多个主题模型抓错重点。解决是先看重排分数把低分块砍掉再检查切分粒度把混主题的块拆细。别急着换大模型先确认喂进去的上下文是不是干净的。5.2 同一个问题两次回答不一样现象是相同问题重复问答案在几个版本间跳。原因是检索结果不稳定近似索引有随机性或模型温度太高。解决是把生成温度调到 0 或接近 0检索侧如果用了近似索引固定随机种子或改用精确检索。RAG 要的是可复现不是创意。5.3 编号、金额、日期总是对不上现象是模型把「第 3 条」说成「第 5 条」把金额抄错。原因是纯向量检索漏掉了精确匹配的块模型只能靠上下文里相近的内容猜。解决是加关键词检索那一路并在提示里要求「涉及编号和数字必须原样引用」。这类问题靠换模型解决不了是检索链路缺了一路。5.4 文档更新后答案还是旧的现象是知识库文件改了回答还是老内容。原因是索引没重建或者用了缓存没失效。解决是把「文档变更 → 重新切分 → 重新向量化 → 重建索引」做成一条固定流水线任何文档改动都触发全量或增量重建。别手动改手动一定会忘。5.5 中文文档检索效果明显差于英文现象是同样的流程英文文档召回准中文就拉胯。原因多半是 embedding 模型没选对或者切分时按英文习惯设了分隔符中文句子被切得七零八落。解决是换中文验证过的模型分隔符里加上中文句号和换行chunk_size 适当放大。中文信息密度高同样字数承载的语义更多块可以比英文大一些。6. 把 RAG 做成可评估、可迭代的工程我的收尾习惯跑通链路只是起点真正让 RAG 稳定的是评估闭环。我一般会维护一个小而精的测试集50 到 100 条真实问题每条标注正确答案所在的文档块和期望答案要点。每次改动切分、模型、重排参数都跑一遍这个集子看三个数召回率正确块进没进 top-k、命中率重排后正确块排第几、答案正确率人工或模型判分。没有这套东西调参就是玄学改完不知道是变好还是变坏。评估集不用大但要覆盖典型场景精确编号查询、跨段落综合、术语解释、无答案问题考幻觉。无答案问题特别重要专门测模型会不会硬编。我见过太多 RAG 系统在「文档里没有」的问题上翻车用户一问就露馅。迭代顺序我固定成先修切分再修检索再修重排最后才动提示词和模型。因为前面的环节错了后面怎么调都是白费。很多人一上来就换更大的模型结果发现是切分把答案切没了换模型也救不回来这是最常见的血泪经验。最后留一个我自己的习惯每次上线前随机抽 20 条真实问题把检索到的块和最终答案并排打出来人工过一遍。这一步能抓到自动指标抓不到的问题比如块内容对但表述绕、答案对但引用错。RAG 没有后悔药上线前多看一眼比上线后被用户追着问强得多。希望帮到你。本文还有配套的精品资源点击获取