我给自己定了一个学习周期,3月21日到4月7日,前后18天,主题就是RAG开发。在这段时间里,我主攻的是检索增强生成(Retrieval-Augmented Generation)这条技术链路,目标也从“能讲清楚RAG是什么”一步步推进到“能独立搭出一个带本地知识库的问答系统”。这篇内容算是对这段时间的完整整理,包括学习路线怎么排的、每个环节背后真正值得琢磨的技术点、踩过的问题,以及我现在回看会怎么优化这套方案。无论你刚接触RAG概念,还是已经跑过一些demo但被效果问题卡住,这篇应该都能找到对应参考。
我收到不少朋友的问法是:大模型不都已经很聪明了吗,为什么还要专门学RAG?这个疑问恰恰也是我最早入坑时的困惑。所以18天的安排里,我并没有一上来就刷代码,而是先把RAG到底在解决什么问题这件事理顺了。下面我就按我的学习路径,把这段时间的内容完整展开。
1. 从标题看这次学习计划的核心目标
1.1 为什么是RAG而不是微调
很多人在接触大模型开发时,首先会想到微调(Fine-tuning),甚至默认“效果不好就该微调”。但RAG和微调解决的其实是两个维度的问题。微调是修改模型自身的参数,让模型“记住”新的知识或说话风格;而RAG是在生成过程中额外拉取相关资料,让模型“查到”再“用到”。用一句话概括我后来的体会:微调改变的是模型的记忆,RAG改变的是模型的输入。
大部分业务场景里,真正需要的不是让模型记住某个知识点,而是让它在回答时能够引用最新的、私有的、领域特殊的资料。比如企业内部规章制度、某款设备的维修手册、某门课程的全部讲义,这些内容让模型用参数去背既不现实也没必要。RAG的优势在于知识即时更新:资料变了,换掉检索库就行,模型参数完全不动。
所以我在这次学习中给自己定的目标是:围绕RAG,把“资料准备 → 文档解析 → 分块切分 → 向量化 → 存储召回 → 重排 → 融合生成”这条链路亲手跑通,并能在遇到效果问题时,知道从哪个环节下手定位。
1.2 这次学习要交付什么
我给这次学习定了三个交付物:第一,一个跑在本地的最小RAG问答系统,能对一份真实文档进行问答;第二,一套针对检索效果和问答质量的评估方法;第三,一篇可以讲给别人听的技术复盘,也就是你正在看的这篇内容。这套交付物对应的是RAG开发中最核心的能力——不只是会调接口,而是能自己构建、评估、优化一条检索生成链路。
如果你也想复现,机器要求并不高,一张普通显卡甚至纯CPU都能跑基础版本,只是速度差一些。我这次用的是已有的大模型推理接口和开源嵌入模型,整体部署成本控制得很低。
2. 学习路线与时间排期:18天怎么拆
2.1 四个阶段的整体脉络
我把18天拆成了四个阶段,不是平均分配,而是权重明显偏向检索链路和工程实践。直接上表格:
| 阶段 | 时间范围 | 周期 | 核心主题 | 阶段产出 |
|---|---|---|---|---|
| 第一阶段 | 3.21 - 3.25 | 5天 | RAG原理与基础组件 | 技术选型清单、概念思维导图 |
| 第二阶段 | 3.26 - 3.29 | 4天 | 文档解析、分块与向量化 | 本地向量库、可检索的文档集 |
| 第三阶段 | 3.30 - 4.3 | 5天 | 检索链路与系统集成 | 完整可用的最小RAG系统 |
| 第四阶段 | 4.4 - 4.7 | 4天 | 评估、优化与复盘 | 评估报告、优化清单、总结文章 |
这个排期背后有两个考量。第一,RAG的上手门槛其实不在“能否调用大模型”,而在检索质量。很多人第一次跑通demo只用了半天,但真正把召回结果调好,花的时间远远更多。所以我给了“解析、分块、向量化”和“检索链路”两个阶段将近三分之二的时间。第二,我刻意把评估放到系统集成之后而不是最后,这样有充足的时间做多轮优化迭代,而不是临近截止才仓促看效果。
2.2 各阶段任务拆解
第一阶段我主要做了三件事:理解RAG的完整流程图、对比RAG与微调的适用边界、调研主流开源组件。学习资料我建议优先看原论文和成熟项目的架构文档,而不是只刷短视频科普,短视频能帮你建立直观印象,但细节很快就会忘干净。
第二阶段开始动手。先找了两份有代表性的资料:一份是PDF格式的技术文档,包含大量专业名词;另一份是Markdown格式的文本资料。两类文档代表了解析难度的高和低。第二阶段的重点是体验“文档变成向量”的完整过程,并初步建立评估意识——不是“能搜出来就行”,而是“该搜出来的能不能搜出来”。
第三阶段是真正的重头戏。我复用了开源的LangChain风格工具链和向量数据库,快速搭建出第一版系统,然后把精力花在调试检索逻辑上。这里有个重要的认识:RAG系统的瓶颈多半在“检”不在“答”。你喂给大模型的上下文正确,它很少会答错;上下文本身就是错的或缺少关键片段,它会用很流畅的语言编一个错误答案。因此第三阶段里,我的调试重心完全放在检索链路。
第四阶段是收尾,也是最出经验的一个阶段。我针对测试记录里暴露出的问题,逐一优化了分块大小、检索条数、重排策略和Prompt模板,最后沉淀出这套优化方法。之所以把复盘写进计划,是因为我发现如果学完不做整理,很多细节会在三天内全部遗忘。
2.3 每天的时间分配方案
我每天大概投入2到3小时,用的是“1小时概念理解 + 1小时编码实践 + 30分钟记录沉淀”的模式。记录沉淀是这次坚持下来最有用的动作,哪怕只写两百字都行,我会记三件事:今天做了什么、遇到什么问题、明天要验证什么。这样做的好处是,问题不会反复在同一个地方绊倒。
如果你时间更紧,我建议保住两个环节:动手搭建最小系统、做效果评估。哪怕只跑通一个简单的文档问答,你对RAG的理解深度也会远超只看文章的人。
3. RAG链路核心环节:每个“为什么”都值得抠一遍
3.1 文档加载与解析:格式适配是第一步
文档加载这一步看起来简单,实际上决定了后续分块和检索的质量。纯文本和Markdown直接读取就行,PDF就要单独处理:有的PDF是文本型,可以直接提取;有的是扫描件,需要OCR识别。我选的那份PDF里有一些表格和代码片段,直接解析会丢失结构信息,这直接影响了retrieval阶段的效果。后来我改用按版面解析的工具,保留标题层级和表格结构,召回效果明显上升。
这里的“为什么”值得多说一句:RAG后面所有环节都是跟“文本块”打交道,解析阶段丢掉的任何信息,后面都找不回来。很多初学者把精力全花在Embedding模型选型上,却忽略了解析环节,结果就是“垃圾进,垃圾出”。在RAG这条链路里,前面每个环节的效果会累积放大,所以前置环节的质量优先级高于后置环节。
3.2 分块策略:决定检索颗粒度
分块(Chunking)是整个RAG链路里最容易被低估的环节。分块太大,单个块包含的信息多但也很杂,向量化之后的语义不够聚焦,检索时容易召回一堆“沾边但不精确”的内容;分块太小,又可能导致上下文不完整,模型看到的内容缺少关键背景。这两者我都实际感受过,差别很明显。
常用的分块方式有按固定字符数切、按段落切、按语义切。固定字符数实现最简单,但容易把一个完整段落拦腰截断;按段落切对结构良好的文档友好;语义切分效果最好但有一定的计算开销。我这次的参数选择是这样的:中文资料按token估算,单块设置为300到500 token,块之间加了50 token的重叠(overlap)。overlap的作用很实际——当检索目标恰好横跨两个块的边界时,重叠区域能保证信息不至于被拦断。
用数字举个实际例子:一份约12000字的中文技术文档,估算下来约6000个token。按每块400 token、重叠50 token切分,大概会生成16到17个块。这个数量级对向量检索来说非常轻量,查询响应基本无感知。
3.3 向量化Embedding:选对模型比堆参数重要
向量化是把文本映射成稠密向量的过程,也是RAG实现“语义检索”的基础。我直接用了一个开源中文Embedding模型,维度是1024维。选型时对比过好几个模型,各模型在中文语义匹配上的表现差异不算小,建议优先看中文基准榜单上的表现,而不是只看参数量大小或某个开源社区项目的star数。
Embedding模型有个反直觉的特性:不能像大模型那样指望它“理解”复杂指令,它只需要判断两段文本语义相近。所以用于构建知识库的原文与查询时的问法如果差异很大,向量检索很容易失配。比如文档里写“设备发生周期性抖动”,用户问“机器总是一顿一顿的”,字面差异很大,糟糕的Embedding模型可能就匹配不到。针对这种情况的解法,我会在后面的优化部分展开。
还有一点必须注意:查询文本和知识库文本应该使用同一个Embedding模型。如果索引时用A模型,查询时不小心换成B模型,两者的向量空间完全不同,检索必然失败且难以排查。
3.4 向量数据库与检索:召回质量决定回答质量
向量数据库负责存储向量并提供相似度检索,常见的方案有Chroma、Milvus、Qdrant、pgvector等。我这次选择了Chroma,原因很简单:对单机小项目来说它足够轻量,无需额外部署服务,能直接嵌入Python进程使用。如果你的知识库规模到百万级向量,或者需要高并发,才需要考虑独立部署的向量数据库。
相似度检索的距离度量也有讲究。我测试下来的经验是:余弦相似度(Cosine)在处理文本向量时通常比欧氏距离(L2)更稳定,因为余弦关注的是方向而非模长。但在实际应用中,很多向量库默认用的就是L2,只要模型本身做过归一化处理,两者排名结果差别很有限。
单靠向量检索远远不够,我后续还加了混合检索。所谓混合检索,就是把向量检索和基于关键词的稀疏检索(BM25)结果合并。BM25擅长精确匹配专有名词、编号、型号,向量检索擅长语义相近的表达。RAG系统面对的查询往往混合了这两类需求,只用一个都容易漏召回。
3.5 重排Rerank:对召回的粗筛结果精排序
召回阶段的目标是“别漏”,此时宁肯多召回一些相关或不相关的内容;但真正送入大模型的上下文是有限的,所以需要重排阶段把最相关的几个片段放到最前面。我用了一段语料实测过:召回Top 5时,前两名非常相关,第三名开始明显跑偏。如果没有重排,大模型会被后面这些不相关内容干扰,回答中就可能出现信息混用。
重排的常见做法是引入一个专门的交叉编码器(Cross-Encoder)模型:它把查询和文档片段拼接后一起编码,能更精细地判断相关性,但计算成本也比双塔结构的Embedding模型高很多。所以标准的工程实践是:召回阶段用轻量快速的向量检索拉出Top 20或Top 50,再由重排模型精筛出Top 3或Top 4喂给大模型。这样兼顾了效率和精度。
3.6 生成融合:Prompt设计与上下文控制
最后一步是把检索到的相关内容注入Prompt,让模型基于这些资料进行回答。这里有个“度”的问题:注入的资料太少,模型缺少依据;注入太多又可能超长或引入噪声。实践中还需要在Prompt里明确约束“只能依据提供的资料回答,资料中没有的内容要说明不知道”,能把幻觉比例降下去不少。
更细一层的做法是给每条资料标注来源或序号,要求回答时注明“根据第几段资料”。这样既方便核对答案有没有跑偏,也方便用户追溯原始文档。这个细节我们实际使用时反馈很好,因为通常一个错误答案如果没有来源根本没法判断它是模型编的还是资料里本来就有的。
4. 从零搭建一个最小可用的RAG系统
4.1 技术选型参考
我这次用的是完全开源可本地部署的方案组合:一个中文Embedding模型做向量化,Chroma做向量存储和召回,LangChain做流程编排,大模型推理接口负责最终生成。如果你希望所有组件都跑在本地,也可以选择本地部署的生成模型,但会额外增加显存需求和推理延迟。
选型时我给了自己三条约束:不引入过于复杂的分布式组件、全程可本地运行、每个环节尽量可单独调试。这三点让后期的定位问题轻松很多——我可以先单独查Chroma里存了多少块,再单独测试某一个块能否被检索到,而不用每次都从整个链路里猜。
4.2 核心实现框架
下面这一段是经过精简的最小链路,保留了核心逻辑,你可以直接对照搭建自己的第一版系统:
# -*- coding: utf-8 -*- from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser # 1. 文档加载 loader = TextLoader("records.txt", encoding="utf-8") documents = loader.load() # 2. 分块:每块400字符,重叠50字符 text_splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", " "], ) chunks = text_splitter.split_documents(documents) print(f"共生成 {len(chunks)} 个文本块") # 3. 向量化与入库 embeddings_model = HuggingFaceEmbeddings(model_name="BAAI/bge-m3") vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings_model, persist_directory="./chroma_db", ) # 4. 检索:默认取Top 5 retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) # 5. 构建Prompt prompt_text = """ 基于以下资料回答问题。 资料: {context} 问题:{question} 回答要求: 1. 优先依据资料内容回答。 2. 如果资料中没有相关信息,请直接说明“资料中未找到相关信息”,不要编造。 3. 引用资料时标注来源序号。 """ prompt = ChatPromptTemplate.from_template(prompt_text) # 6. 组装RAG链路 def format_docs(docs): return "\n\n".join(f"[{i+1}] {doc.page_content}" for i, doc in enumerate(docs)) rag_chain = ( { "context": retriever | format_docs, "question": RunnablePassthrough(), } | prompt | chat_model # 你的大模型调用对象 | StrOutputParser() ) # 7. 发起一次问答 result = rag_chain.invoke("设备出现抖动现象,可能的原因有哪些?") print(result)这段代码走完一遍,RAG的最小闭环就成立了。实际使用中,你还需要把文档加载替换成你自己的资料路径,以及调整分块参数。这里我特别建议把文档内容打印出来看一眼,因为分隔符顺序直接影响切分效果。上面代码里我把中英文标点都加入了分隔符,这对中文文档非常关键——如果只用换行符分隔,中文段落很容易被生硬截断。
4.3 用一份样例数据验证链路是否健康
搭好系统之后,我先用一份自制的“单知识点”文档做了冒烟测试:文档只有三小段,分别写清A、B、C三个事实。我再分别用三种问法去提问:直接问原文句子、改写问法问同一事实、问一个文档中不存在的事实。
这三类问题其实在分别验证链路的不同能力:改写问法测试语义检索是否真的“理解”了意图;不存在事实的提问测试大模型是否会被诱导出幻觉。我的第一版系统在“改写问法”上的召回效果并不理想,后来增加了混合检索,把字面匹配和语义匹配两条路径的结果合并,问题就基本解决了。
5. 学习周期内实测的高频问题与排查套路
5.1 问题速查表
我把这段时间踩过的高频问题整理成了表格,对应原因和解法都是实际验证过的判断,不包含空洞的建议:
| 问题现象 | 最可能的原因 | 排查与解法 |
|---|---|---|
| 回答的内容明显来自错误段落 | 召回结果包含噪声,或重排未生效 | 打印检索返回的前5个片段,人工判断相关性 |
| 改写问法搜不到原文 | 纯向量检索对同义改写不敏感 | 增加BM25混合检索,或换更强的Embedding模型 |
| 分块后信息被截断 | chunk_size偏小,且分隔符未配置中文标点 | 调大块大小,调整分隔符优先级,加入重叠 |
| 对不存在的事实给出答案 | Prompt约束不足,或检索到强相关的误导片段 | 在Prompt里明确“未找到则声明不知道”;加强重排过滤 |
| 准确率低但检索出的片段看起来相关 | 关键细节在片段里只是顺带提及,模型未重点关注 | 提高相关片段权重,把k增高后人工观察更多召回结果 |
| 中文文档检索时英文标点导致切分混乱 | 分隔符只有"\n\n",中文段落被整块保留 | 在separators中加入中文句号、叹号、问号 |
5.2 三个影响最大的排查经验
第一个经验是“先看召回再看回答”。很多情况下,回答错得离谱,问题根本不出在模型,而在于检索回来的内容里压根没有正确答案。直接在代码里打印retriever的返回结果,一眼就能判断问题归属。这一步排查成本最低,判断效率最高。
第二个经验是“不要让k值一开始就太小”。第一版系统我把k设成了2,检索结果里只要有一个噪声片段,答案就会跑偏。后来把k调到5,配合重排精筛后,准确率提升非常明显。如果你的大模型上下文窗口足够,k值稍大通常不会让效果变差,尤其是你加了重排之后。
第三个经验是“Prompt约束能显著降低幻觉”。我最早用的Prompt只有短短一句“基于以下资料回答问题”,模型依然倾向凭空补充。后来加了“资料中未找到就直接说不知道”,幻觉比例下降得极为明显。这个调整在工程上几乎不花成本,却是我这次体验中性价比最高的一次优化。
5.3 一个容易被忽略的坑:重复段落
由于分块重叠的存在,同一句话可能出现在相邻的两个块里。检索Top 5时,这两个块会同时被召回,导致喂给模型的内容有大量重复,既浪费上下文窗口,又让模型更难抓重点。我的解法是在格式化上下文时做一次简单的去重判断,保留一个块即可。这个细节几乎不会出现在官方文档里,但实测能让回答质量提升脆响地上一档。
6. 关于这段学习周期的几句复盘与后续扩展
如果你也打算按18天的方式推进RAG开发,我个人的体会是:一定要给“检索质量”留足够时间,因为调好检索的收益远超换一个更强的大模型。我后面测试过把生成模型换成推理能力更强的版本,在检索质量不变的情况下,效果提升有限;但一旦把分块和重排调好,哪怕生成模型没那么强,回答也明显更稳。这个判断基本扭转了我的投入优先级。
另外,这套系统后续做扩展时可以顺着几个方向走:一是覆盖更多文档类型,比如PDF和扫描件,这涉及OCR和版面解析;二是把单轮问答升级成多轮对话,这时需要维护对话历史并把历史问题改写为独立的查询语句;三是加入更完整的评估体系,用一组带标准答案的测试集定期回归效果。这三个方向任意推进一个,都会推动整个系统向生产可用靠近。
最后说一个我在实际学习中最有用的小动作:每天学完顺手记录“今天有什么问题没解决”。到了第10天回头看第三天记录的问题,会发现大部分已经无意间解决了。这种记录式的复盘,比任何学习计划表都更能反映你的真实成长轨迹。RAG开发这条路不算难走,但很容易被表象迷惑,只要愿意把链路中的每一个环节都亲手验证一遍,效果就会扎实地体现出来。