ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python RAG源码拆解:23文件实现检索增强生成最佳实践

Python RAG源码拆解:23文件实现检索增强生成最佳实践

简介:这份源码资源面向希望深入理解大模型检索增强生成(RAG)技术的Python开发者与算法学习者,提供一套可运行的最佳实践工程范例,帮助解决从检索到生成链路搭建、配置管理与项目组织等实际问题。资源包共22个文件、约527KB,以7个XML配置、5个Python源码、3个文本文件、2个PNG示意图、2个Git忽略文件及1个Markdown文档、1个开源许可文件为主,涵盖环境参数配置、核心算法实现、说明文档与版本控制等模块,目录结构清晰,便于按功能定位学习。目前已有946人学习下载,适合具备一定Python与机器学习基础、希望动手复现RAG流程的读者。通过研读源码,可掌握检索器与生成器的接口设计、提示词构造、查询处理及数据组织方式,并借鉴配置分离与文档规范等工程实践,为搭建自己的检索增强生成应用提供可复用的参考模板。

1. 从一份 23 文件的 Python RAG 源码包说起:它到底能跑出什么

如果你最近在折腾大模型应用,大概率绕不开 RAG(检索增强生成)这个词。但真正落到代码层面,很多人卡在同一个地方:网上讲 RAG 原理的文章一抓一大把,能直接跑起来、结构清晰、还带最佳实践命名的完整源码包却不多。这份「基于 Python 的大模型 RAG 检索增强生成技术最佳实践设计源码」就是冲着这个缺口来的——23 个文件,5 个 Python 源文件,把检索、查询、提示词、主流程拆得明明白白。

它解决的不是「RAG 是什么」的问题,而是「一个能改、能调、能对照自己项目做减法」的工程骨架问题。适合两类人:一是刚入门想找一个不臃肿的 RAG 起点,二是已经用上 RAG 框架但被抽象层挡住、想看清底层调用链的从业者。下面我按实际拆包顺序,把这份源码从结构到跑通、从参数到踩坑讲一遍。

2. 拆开源码包:5 个 Python 文件各自扛什么活

2.1 目录结构与文件职责对照

拿到压缩包解压后,根目录下最显眼的是main.py,旁边是custom文件夹和data文件夹。custom里放着四个核心模块:retriever.py、query.py、prompt.py、glmfz.py。这个命名方式很直白——retriever 管检索,query 管查询处理,prompt 管提示词模板,glmfz 看名字像是「过滤/分治」类的辅助逻辑(具体以源码为准,这里不臆测功能)。

文件/目录类型在 RAG 流程中的角色
main.pyPython主入口,串联检索与生成
custom/retriever.pyPython检索器实现,负责向量召回
custom/query.pyPython查询构造与预处理
custom/prompt.pyPython提示词模板管理
custom/glmfz.pyPython辅助逻辑模块
data/testdata.txt文本示例知识库数据
requirements.txt文本依赖清单
readme.txt文本项目说明
.idea/XML 配置IDE 项目配置,7 个 XML 文件
LICENSE许可开源协议

这个结构的好处是没有过度分层。很多 RAG 项目一上来就是chains/、embeddings/、vectorstore/七八层目录,改一个参数要跳五个文件。这里四个模块加一个入口,读代码的认知负担低很多。

2.2 依赖安装与环境准备

先看requirements.txt。虽然具体版本号以文件内容为准,但一个典型 RAG 项目的依赖通常包括向量库客户端、嵌入模型 SDK、大模型调用库。安装前建议单独建虚拟环境,避免和系统 Python 打架:

# 创建虚拟环境,Python 3.8+ 均可 python -m venv rag_env # 激活环境(Linux/macOS) source rag_env/bin/activate # 激活环境(Windows) rag_env\Scripts\activate # 安装依赖 pip install -r requirements.txt

这里有个细节:如果requirements.txt里没有锁死版本号,pip 会拉最新版,而大模型相关的库版本迭代很快,接口签名说变就变。我一般会先pip install -r requirements.txt跑一遍,如果报错再逐个降级。更稳妥的做法是先pip freeze > locked.txt把当前能跑通的版本记下来。

提示:Python 3.8 和 3.10 在部分异步库上行为不同,如果遇到asyncio相关报错,优先检查 Python 版本是否和依赖要求匹配。

2.3 数据准备与知识库文件格式

data/testdata.txt是示例数据。RAG 的检索质量,七成取决于知识库切分。这个项目用的是纯文本文件,说明它的定位是「最小可运行示例」,不是生产级文档解析管线。实际使用时你需要把testdata.txt替换成自己的语料。

常见做法是把长文档按固定字数切块,比如每 500 字一块,块之间留 50 字重叠。重叠的目的是防止一个完整语义被切断后检索不到。如果你直接拿一整篇长文丢进去不做切分,检索器返回的片段会又长又杂,生成阶段的大模型反而被噪声干扰。

# 典型的文本切分逻辑(示意,具体实现看 retriever.py) def split_text(text, chunk_size=500, overlap=50): chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append(text[start:end]) start = end - overlap # 回退 overlap 个字符,保证语义连续 return chunks

chunk_size和overlap这两个参数没有万能值。中文场景下 300~500 字比较常见,英文可以放到 800~1000 字符。overlap 一般取 chunk_size 的 10%~20%。调完之后要看检索返回的片段是否语义完整,如果经常返回半句话,就把 overlap 加大。

3. 跑通主流程:从 main.py 到检索生成闭环

3.1 main.py 的调用链拆解

main.py是整个项目的入口。一个典型的 RAG 主流程会做这几件事:加载知识库文件、初始化检索器、接收用户查询、检索相关片段、拼接提示词、调用大模型生成回答。这个项目的main.py大概率也是这个顺序,只是具体实现藏在custom模块里。

读main.py的时候重点关注两个点:一是检索器和生成器的初始化参数从哪里来(硬编码还是配置文件),二是查询和检索之间的数据流是怎么传递的。如果初始化参数是硬编码的,你换模型或换向量库时就要改源码;如果是读配置,改配置文件就行。

# main.py 的典型结构(示意) from custom.retriever import Retriever from custom.query import QueryProcessor from custom.prompt import PromptTemplate # 初始化 retriever = Retriever(data_path="data/testdata.txt") query_processor = QueryProcessor() prompt_template = PromptTemplate() # 主循环 while True: user_input = input("请输入问题:") if user_input == "exit": break # 查询预处理 processed_query = query_processor.process(user_input) # 检索相关片段 contexts = retriever.retrieve(processed_query, top_k=3) # 拼接提示词 prompt = prompt_template.build(query=user_input, contexts=contexts) # 调用大模型生成 answer = call_llm(prompt) print(answer)

top_k=3是检索返回的片段数量。这个值太小会漏掉关键信息,太大会把无关内容塞进提示词、稀释有效信息。一般从 3 开始调,看生成质量再增减。如果知识库很大、问题很具体,top_k 可以到 5~8;如果知识库本身就小,3 足够了。

3.2 retriever.py 的检索逻辑与向量召回

retriever.py是 RAG 的检索核心。它的工作是把知识库文本转成向量存起来,查询时把用户问题也转成向量,然后算相似度、返回最接近的片段。这个项目如果用的是轻量方案,可能是内存向量存储加余弦相似度;如果用上了向量数据库,那retriever.py里会有客户端初始化代码。

# 检索器核心逻辑(示意) import numpy as np class Retriever: def __init__(self, data_path): self.chunks = load_and_split(data_path) self.embeddings = [embed(chunk) for chunk in self.chunks] def retrieve(self, query, top_k=3): query_vec = embed(query) # 计算余弦相似度 scores = [] for i, emb in enumerate(self.embeddings): sim = np.dot(query_vec, emb) / (np.linalg.norm(query_vec) * np.linalg.norm(emb)) scores.append((i, sim)) # 按相似度降序取 top_k scores.sort(key=lambda x: x[1], reverse=True) return [self.chunks[i] for i, _ in scores[:top_k]]

这里的关键参数是嵌入模型的选择。不同嵌入模型对中文的语义捕捉能力差异很大,换模型后检索效果可能天差地别。如果你发现检索结果总是答非所问,先别怀疑检索算法,换一个更适合中文的嵌入模型试试。

3.3 prompt.py 的模板设计与上下文注入

prompt.py管的是提示词模板。RAG 的提示词和普通对话提示词不一样,它需要把检索到的上下文片段注入进去,同时约束大模型「只根据给定上下文回答」。一个常见的模板结构是这样的:

# prompt.py 的模板构建(示意) class PromptTemplate: def build(self, query, contexts): context_text = "\n---\n".join(contexts) return f"""请根据以下参考资料回答问题。如果参考资料中没有相关信息,请直接说"资料中未提及"。 参考资料: {context_text} 问题:{query} 回答:"""

这个模板里有两个设计点值得注意。第一是「资料中未提及」的兜底指令,没有这句话,大模型很容易在检索不到相关内容时自己编答案。第二是上下文之间的分隔符,用---或空行隔开,帮助模型区分不同片段。如果你发现模型把多个片段的内容混在一起回答,可以试试给每个片段加编号。

4. 避坑与排查:RAG 跑不通时先看这五条

4.1 检索结果全是无关片段

现象:提问后返回的上下文和问题完全不搭边,生成答案也是胡言乱语。

原因:最常见的是嵌入模型和知识库语言不匹配。比如用英文嵌入模型处理中文语料,向量空间对不上,相似度计算就失效了。其次是文本切分太粗,一个 chunk 里混了好几个主题,检索时匹配到了但内容不聚焦。

解决:先换一个中文嵌入模型重新生成向量。然后把 chunk_size 调小,比如从 500 降到 300,看检索质量是否改善。如果知识库本身主题就很杂,考虑按主题分文件存储,检索时先做一次粗筛。

4.2 大模型不按上下文回答

现象:检索到的片段明明有答案,但大模型回答时忽略上下文,用自己的知识瞎编。

原因:提示词约束不够强。很多模板只写了「请参考以下资料」,没有明确说「只根据资料回答」。另外,如果上下文片段太长,模型可能把注意力放在开头和结尾,中间的关键信息被忽略。

解决:在提示词里加硬约束,比如「你的回答必须完全基于以下资料,不得使用资料之外的知识」。同时把最相关的片段放在上下文的最前面或最后面,利用模型的注意力偏好。如果还是不行,把 top_k 降到 2~3,减少干扰。

4.3 依赖安装报错、版本冲突

现象:pip install -r requirements.txt时报编译错误或版本冲突。

原因:大模型相关的库依赖链很长,不同库对同一个底层包要求的版本可能冲突。另外,部分库需要 C++ 编译环境,Windows 上容易缺 Visual C++ Build Tools。

解决:先单独装核心库,比如先装向量库客户端,再装嵌入模型 SDK,最后装大模型调用库,每装一个跑一次 import 测试。Windows 上如果报编译错误,装一下 Visual C++ Build Tools 再重试。实在搞不定就用 conda 建环境,conda 的依赖解析比 pip 强一些。

4.4 知识库文件读取乱码

现象:testdata.txt读进来是乱码,检索结果全是问号或方块。

原因:文件编码和 Python 默认读取编码不一致。Windows 上很多文本文件是 GBK 编码,而 Python 3 默认用 UTF-8 读。

解决:在读取文件时显式指定编码,比如open(path, encoding='utf-8')。如果文件本身是 GBK,就改成encoding='gbk'。更彻底的办法是用编辑器把文件转成 UTF-8 再存一次。

4.5 生成速度慢到无法接受

现象:每次提问要等十几秒甚至更久才出结果。

原因:如果每次查询都重新计算知识库所有片段的向量,那纯属浪费——知识库向量应该预先算好缓存起来。另外,如果嵌入模型跑在 CPU 上且模型较大,单次嵌入计算就很慢。

解决:把知识库向量预计算并持久化到本地文件或向量库,查询时只算用户问题的向量。如果嵌入模型太大,换一个小模型,检索质量下降通常有限但速度提升明显。生成阶段如果用的是大参数模型,考虑换小参数版本或加流式输出,至少让用户看到进度。

5. 进阶技巧:把这份源码改成你自己的 RAG 骨架

这份源码最大的价值不是直接拿去用,而是当骨架改。我一般会做三件事:换检索器、换提示词、加缓存。

换检索器是指把retriever.py里的内存向量存储替换成你实际用的向量数据库。接口不用变,还是retrieve(query, top_k),但内部实现换成数据库的相似度查询。这样上层main.py完全不用动。

换提示词是指根据你的业务场景重写prompt.py。比如客服场景要加「语气友好」的约束,法律场景要加「引用具体条款」的要求。模板改完之后,拿一组标准问题跑回归测试,看生成质量是否稳定。

加缓存是指把「问题 → 检索结果」的映射缓存起来。很多用户会重复问相似的问题,每次都重新检索是浪费。用一个简单的字典或 LRU 缓存就能省掉大量重复计算。

# 带缓存的检索调用(示意) from functools import lru_cache @lru_cache(maxsize=128) def cached_retrieve(query, top_k=3): return retriever.retrieve(query, top_k)

lru_cache的maxsize控制缓存条目数,128 对一般场景够用。注意缓存 key 要包含top_k,否则改了 top_k 会拿到旧结果。

验证改造是否成功,我习惯用三个指标:检索命中率(返回的片段里有没有正确答案)、生成忠实度(答案是否只基于上下文)、响应时间。前两个靠人工抽检,第三个直接计时。每次改完参数都跑一遍这三个指标,避免改了一个坏了另一个。

从那以后我每次拿到新的 RAG 源码包,都先跑通最小闭环,再动任何一个参数之前把基线指标记下来。没有基线,调参就是玄学。希望这份拆解能帮你少走点弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进