说真的看到这个标题自己都想笑——“重生”俩字怎么看都像网文套路。可这次我确实是抱着“重开一局”的心态来学检索增强生成RAG的。以前总觉得 RAG 不就是“搜一堆资料塞给大模型”嘛直到自己动手搭的时候才发现里面全是细节文本切多大、向量怎么算、召回准不准、答案是不是瞎编每一环都能把你坑得怀疑人生。这篇学习日志就是我从零开始、在“双体”这个学习小组里把 RAG 完整走一遍的记录。我会把整个链路拆开讲从文档处理讲到向量检索从提示词设计讲到效果调优中间穿插我实际跑过的实验数据、踩过的坑以及一些常规教程里不会写的小技巧。适合刚接触 RAG 的初学者也适合已经跑通 demo 但想系统性提升召回准确率的朋友。先交代一下背景我这边的技术栈偏向 Python全程用开源组件不依赖任何收费平台照着做就能复现。下面进入正题。1. 为什么是 RAG从“背题”到“开卷考试”1.1 大模型的先天短板先说一个最核心的认知大模型本质上是一个“背题选手”。你问它什么它都在调用训练时见过的参数化记忆。这种记忆有两个硬伤——时效性和覆盖范围。时效性不用多说训练数据有截止日期之后发生的事情它一概不知。覆盖范围更麻烦即便是通用领域它也倾向于输出“平均答案”因为训练时见过大量相似表述生成时会往统计上最稳妥的方向走。一旦你问的是小众领域、内部知识、刚发布的动态它就开始“一本正经地胡说八道”也就是行话说的幻觉Hallucination。我刚开始学的时候犯过一个典型错误拿一个通用模型直接问内部产品文档里的问题结果它回答得特别流畅细节全错。那一刻我才意识到RAG 要解决的根本不是“让模型更聪明”而是“给模型开卷考试的机会”。这个概念想通了后面所有技术细节都有了明确的指向。1.2 RAG 的本质检索 生成的双体结构RAG 的全称是 Retrieval-Augmented Generation检索增强生成。名字里已经把答案说了一半先检索Retrieval再增强生成Augmented Generation。理解它最简单的方式是把它看成“双体协作”一个体负责从外部知识库里捞证据另一个体负责基于证据写答案。第一个体通常是 embedding 模型加向量数据库第二个体是大语言模型两者通过一个精心设计的提示词拼接在一起。我刚入坑时觉得这不就是“复制粘贴”嘛。但真正上手才知道两个体之间如何衔接、证据如何筛选、答案如何约束每一环都决定最终效果。这也是我后来在“双体”小组里反复念叨的一句话RAG 是检索系统和生成系统的乘法任何一个项趋近于零整体都是零。检索做得再好生成端不按证据说话答案照样跑偏生成模型再强召回一堆无关内容它也只会把错误包装得更流畅。1.3 什么时候才该上 RAG这部分算是我踩过最贵的坑。RAG 不是银弹不是所有场景都需要。我总结下来适合上 RAG 的场景有三类知识时效性强比如产品文档、新闻资讯、内部制度领域知识密集比如医疗、法律、金融的专业问答需要追溯来源回答要能“找到出处”。反过来如果模型本身已经能答得很好或者你只是需要创意生成那 RAG 反而会拖慢速度、增加复杂度属于典型的“杀鸡用牛刀”。还有一个最容易被忽略的判断标准你的知识库质量是否可控。如果源头文档本身就是垃圾RAG 只会让垃圾显得更权威。我见过有人拿着一堆残缺的扫描件就想做智能客服结果模型每句话都振振有词但没有一句能对上事实。这种场景下正确的顺序是先做数据治理再谈 RAG 架构。2. 学习路线与整体设计2.1 从零开始的五个阶段我给自己定的学习路线分成五个阶段每个阶段目标明确不贪多。认知阶段先把 RAG 的链路结构搞清楚跑通最小 demo建立“完整感”数据阶段专注文档加载、清洗、切分这是最枯燥但回报最高的一环检索阶段选型 embedding 与向量库做一系列召回实验慢慢建立“什么会影响召回”的直觉生成阶段设计提示词模板约束输出格式让答案既不跑偏也不僵硬最后是调优阶段混合检索、重排序、评估指标一个一个上。说实话前两个阶段我花了太多时间在“完美主义”上总想把数据处理做到极致才开始下一步。后来发现这是错的——RAG 是一个系统工程你没法把单点做到完美再推进必须先跑通全链路再回头逐个优化。这个“先整体后局部”的思路是我这个月最大的收获之一。每当你觉得某个环节不够好先记下来继续往前走等整个链路通了再回头收拾它。2.2 双体视角算法侧与工程侧必须并行“双体”在我这里有两层意思。表面上是项目组的名称更深一层我把它理解成 RAG 学习必须分成的两个体算法体与工程体。算法体关心的是embedding 选哪家、切分多长合适、重排序模型怎么选、prompt 怎么写。工程体关心的是文档量大了怎么增量更新、并发请求怎么处理、向量库挂了怎么恢复、检索延迟怎么压下来。我见过太多人只盯着算法侧的指标优化结果线上召回率漂亮得不行接口却 3 秒才返回一次。也见过反过来的情况工程做得四平八稳但检索结果一出错就全盘崩溃。真正能落地的 RAG 项目两边必须同时有人盯着。对个人学习者来说这意味着你至少要在这两个维度上都建立认知框架哪怕不亲手写工程代码也要知道高并发、增量更新这些词对应的技术选型是什么。2.3 工具选型思路先开源后商用工具选型这件事我坚持一个原则能开源的坚决不开源能本地跑的不上云。原因很简单学习阶段你永远不知道下一个实验会怎么折腾云服务的每一分钱都可能在为错误买单。我最终的方案换过三轮第一轮是纯手写脚本第二轮上了 LangChain第三轮才固定到一套相对稳定的组合上。我的主力工具清单是这样的语言与框架部分Python 打底LangChain 负责流程编排后来用 LlamaIndex 做了对比实验向量库先用轻量的 Chroma 跑通再上 Milvus 做压力测试Embedding 用 bge-small-zh 起步后来对比过 bge-large生成模型用的是本地部署的开源模型以 Qwen 系列为主评估工具用 RAGAS 框架配合手工构造的测试集。这里要多说一句不要迷信框架。LangChain 确实封装得很好但也正因为封装得好很多人根本不知道底层发生了什么。我建议至少手写一遍向量化、检索、拼接的完整流程再回到框架里做工程化。手写过一遍之后你再去看任何框架的源码都会觉得通透很多。3. RAG 核心链路逐段拆解3.1 文档加载与清洗RAG 的第一步不是向量化而是把文档变成干净、结构化的文本。这一步没做好后面全白搭。我处理的文档类型大概有四类PDF、Word、Markdown、网页抓取。PDF 最坑很多是扫描件没有文本层需要 OCR即便有文本层表格、页眉页脚也会被粗暴地压成乱序文本。我的经验是优先找源头格式比如 Markdown 或 WordPDF 作为兜底方案。清洗环节最容易被忽视的是“噪声”。我踩过的坑包括网页里的导航栏、页脚版权声明被当成正文PDF 的换行被无脑保留导致句子断成两半表格数据被拍扁成纯文本后完全丢失语义。这些噪声直接污染向量化结果检索召回的内容经常前言不搭后语。后来我建了一个清洗清单按文档类型各自过一遍去重、去空行、合并断裂句子、表格转结构化文本、移除固定页眉页脚。看似琐碎但它决定了知识库的“地基”稳不稳。3.2 文本切分策略切多大、怎么切切分Chunking是 RAG 里最“玄学”的部分。切大了一个 chunk 里塞太多主题向量表达模糊检索精度下降切小了语义被截断上下文缺失生成端拿不到完整信息。我的实测经验中文场景下 200 到 500 个字符是一个比较稳的区间。但这只是起点真正的关键在“怎么切”而不是“切多大”。三种切分方式的对比方式原理优点缺点适用场景固定长度按字符数硬切简单可控容易切断语义快速验证递归分隔按标点、标题逐级切分保留自然边界依赖文档结构大多数场景语义切分按语义完整段切分语义保真度高计算成本高高质量知识库递归分隔是我用得最多的核心思路是先按段落级别切段落太长再按句号、逗号逐级下沉直到满足长度约束。后来我加了“重叠窗口”机制相邻 chunk 之间重叠 10% 到 15% 的文本避免关键信息正好落在切缝里被丢掉。这个细节让我的召回率涨了大概 5 个百分点。原理很简单自然语言的信息边界经常在句子中间重叠窗口相当于给切缝加了一层缓冲垫。3.3 Embedding 模型选择Embedding 模型的作用是把文本变成一串向量让语义相近的文本在向量空间里“距离近”。选型时我重点看三个维度语言支持、向量维度、中文语义能力。在中文场景下我强烈建议优先考虑专门针对中文优化的模型。用通用英文模型处理中文最直观的感受就是“召回的东西好像相关又好像不太相关”——那是向量空间里的语义分布压根没对齐。我实测 bge 系列在中文检索上明显优于通用模型小模型在资源受限的环境里也能跑得动。另一个容易被忽略的点向量维度不是越高越好。高维度确实能表达更精细的语义但存储和计算成本成倍上升。选型的核心原则是“够用就好”先跑通再评估要不要升级。我当时对比过一个小维度和一个大维度的模型召回率差距不到 3 个百分点但入库时间和查询延迟差了将近一倍。在资源有限的学习阶段这个账一定要算清楚。3.4 向量检索与重排序向量检索的基本流程是把用户查询也转成向量然后在向量库里做近似最近邻搜索ANN找到最相似的 top-k 个 chunk。这里有个新手最容易忽略的陷阱向量相似度不等于相关性。向量空间里距离近只能说明“表达形式接近”不能保证“语义内容正确”。比如用户问“退款政策是什么”库里有一条 chunk 说“退货流程包括以下步骤”两者向量相似度可能很高但它未必是用户要的答案。解决这个问题有两个手段。第一是混合检索把向量检索和关键词检索BM25结合取两种方式的结果做融合。向量检索擅长语义泛化BM25 擅长精确命中专有名词和编号两者互补性很强。第二是重排序用一个专门的 rerank 模型对召回结果做精排它会根据查询与文档的细粒度语义关联重新打分。重排序是我做过性价比最高的优化没有之一效果提升肉眼可见。这部分后面实操章节会详细展开。3.5 提示词组织与生成检索回来的证据最终要喂给大模型做生成。这一步的核心是提示词设计。我总结了三个要点。第一明确角色与任务告诉模型“你是一个基于给定资料回答问题的小助手”而不是让它自由发挥。第二明确证据边界在提示词里写清楚“只能依据参考资料回答如果资料中没有相关信息请直接说明不知道”这一句能极大减少幻觉。第三结构化输出让模型按固定格式输出比如“答案… 参考来源[编号]”方便后续做溯源和评估。我还做过一个实验把检索回来的 chunk 按照“和查询的相似度从高到低”排列喂给模型与乱序喂给模型相比前者的准确率稳定高出 3% 到 5%。模型的注意力天然更关注开头和结尾把最重要的证据放前面效果立竿见影。这听起来像小聪明但背后是注意力机制的分布规律值得记下来。4. 实操搭建一个最小可用 RAG 系统4.1 环境准备先交代环境Python 3.108GB 内存的本地机器没有 GPU。这个配置下我依然跑通了整套流程只是 embedding 推理稍慢完全可接受。依赖安装方面我主要用了这几个库pip install langchain chromadb sentence-transformers这里有个坑sentence-transformers 会自动下载模型很多人的网络环境经常卡在 huggingface 下载上。我后来直接手动下载模型文件放到本地目录再用本地路径加载省去无数等待时间。具体方法是先通过镜像站把模型仓库整个拉下来然后在代码里SentenceTransformer(./models/bge-small-zh-v1.5)直接指向本地文件夹。4.2 数据准备与向量化我用了一份虚构的产品问答文档作为实验数据大概五十多条 QA 对。先加载文档并切分from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter loader TextLoader(qa_data.txt, encodingutf-8) documents loader.load() text_splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[\n\n, \n, 。, , , , , ] ) chunks text_splitter.split_documents(documents)这个separators的顺序很关键它定义了递归切分的优先级先按段落切再按句子切再按逗号切。我最早的版本没有写中文标点导致切分结果里经常出现半句话后来调试时补上了。你可以把它理解成一套从粗到细的筛子每层筛不过就换更细的网眼。4.3 向量化与入库切分完成之后用 embedding 模型把每个 chunk 转成向量存进向量库。我用 Chroma 做持久化存储from sentence_transformers import SentenceTransformer import chromadb model SentenceTransformer(./models/bge-small-zh-v1.5) client chromadb.PersistentClient(path./chroma_db) collection client.get_or_create_collection(qa_kb) for i, chunk in enumerate(chunks): embedding model.encode(chunk.page_content).tolist() collection.add( ids[fchunk_{i}], embeddings[embedding], documents[chunk.page_content], metadatas[{source: chunk.metadata.get(source, )}] )代码很短但我要提醒三个要点。第一每次实验最好新建 collection避免脏数据残留影响判断。第二元数据metadata一定要保存来源信息后续做溯源和调试都靠它。第三我这里故意用了一个简单的循环没有做批量向量化数据量小的时候更直观方便你理解每一步在干什么。等数据量上来再换批量接口逻辑是一样的。4.4 检索与生成检索部分的核心就一句话把用户查询转成向量去向量库里查最近的 chunk。collection client.get_collection(qa_kb) def search(query, top_k5): query_embedding model.encode(query).tolist() results collection.query( query_embeddings[query_embedding], n_resultstop_k ) return results[documents][0]检索结果出来之后拼进提示词传递给生成模型。我用的是本地部署的开源模型通过兼容 OpenAI 的接口调用from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keynot-needed) def rag_answer(question): docs search(question, top_k5) context \n\n.join(docs) prompt f你是客服问答助手。请严格依据以下参考资料回答问题。 如果资料中没有相关信息请直接说“根据现有资料无法回答”。 参考资料 {context} 问题{question} 请回答 response client.chat.completions.create( modelqwen2.5-7b-instruct, messages[{role: user, content: prompt}], temperature0.3 ) return response.choices[0].message.contenttemperature 设为 0.3 是一个平衡点太低答案会显得机械太高容易跑偏。对问答场景来说宁可保守一点。我试过 0.7答案确实更“生动”但错误率明显上升也试过 0稳定是稳定就是偶尔答非所问时你想摔键盘。4.5 效果评估别靠感觉跑通之后最重要的一件事是建测试集。我从知识库里抽了 30 个典型问题人工标注标准答案构成一个 mini 评测集。然后逐题调用 rag_answer判分标准分三档完全正确、部分正确、错误。这个粗糙的评测给我提供了第一份基线数据完全正确率只有 60% 左右。这个数字一点都不好看但它是所有后续优化的锚点。没有这个锚点后面每一步优化都会变成“感觉好点了”的玄学。我特别强调这一点是因为大多数新手跑通 demo 之后就觉得自己“会了”实际上连系统现在处于什么水平都不知道。测试集不用大三五十条足够关键是覆盖不同的问题类型简单事实型、复杂推理型、跨 chunk 拼接型、知识库外问题型。5. 调优实战把准确率从 60% 拉到 90%5.1 第一轮切分粒度调整基线测出来之后我先动了切分参数。chunk_size 从 300 分别试了 200、400、500overlap 从 50 试到 80。实验结果很有意思不是越大越好也不是越小越好。chunk_sizeoverlap准确率2003067%3005063%4006068%5008065%400 加 60 的组合最好但幅度有限。这说明纯调切分参数只能解决一部分问题——那些答案跨 chunk 的问题本质上要靠检索融合来解决。切分参数更像是把“基础分”稳住真正拉开差距的是后面的检索策略。5.2 第二轮混合检索我引入了 BM25 关键词检索和向量检索结果做加权融合。思路很简单向量检索擅长语义匹配BM25 擅长精确匹配两者互补。具体实现时先对全部 chunk 建立 BM25 索引然后分别计算向量检索得分和 BM25 得分归一化后加权求和from rank_bm25 import BM25Okapi tokenized_corpus [list(chunk) for chunk in corpus] bm25 BM25Okapi(tokenized_corpus) def hybrid_search(query, top_k5, alpha0.5): vector_scores vector_search_scores(query, top_k10) bm25_scores bm25.get_scores(list(query)) # 两边归一化后按 alpha 加权融合 fused alpha * normalize(vector_scores) (1 - alpha) * normalize(bm25_scores) return top_k_indices(fused)融合权重 alpha 我试了 0.3、0.5、0.7最后 0.5 左右最稳。这一轮准确率从 68% 提到了 76%。特别适合处理那些包含产品专有名词、型号编号的问题——向量检索往往把这些关键 token 的语义淹没了BM25 能把它们精确捞出来。理论依据其实也简单向量检索是语义匹配的“大网”BM25 是精确匹配的“钓鱼竿”两者取交集召回的就都是有效信息。5.3 第三轮重排序这是效果最猛的一轮。我在召回阶段多取了一些候选比如 top-k20然后用跨编码器cross-encoder模型对每个候选重新打分。跨编码器的原理是让查询和文档同时过编码器做全量交互精度比双塔式的高很多代价是速度慢所以只对 top-20 重排不直接全库跑。from sentence_transformers import CrossEncoder reranker CrossEncoder(./models/bge-reranker-base) pairs [[query, doc] for doc in candidates] scores reranker.predict(pairs) top_indices scores.argsort()[-5:][::-1]这一轮准确率直接冲到 87%是三轮优化里提升最大的一次。我猜原因在于向量检索阶段用的是双塔结构查询和文档各自编码、最后算距离交互不充分而重排序模型让两个文本充分交叉能捕捉到“看似不相关实则关键”的语义关联。这里还要强调一个实操细节重排序之后一定要把重排后的顺序传给生成端因为模型对证据顺序敏感。我在这上面吃过亏——重排归重排拼接提示词时还是按旧顺序结果效果一点没变白忙活。5.4 第四轮提示词与生成参数最后一轮动的是生成端。我把提示词写得更严格增加了“引用来源编号”的指令同时把 temperature 从 0.3 调到 0.2并开启 top_p0.8 的采样约束。准确率最终稳定在 90% 左右。这个成绩算不上顶尖但已经足够应付大部分业务场景。更重要的是我明白了每一点优化的原理和代价后面遇到新场景我能直接判断该动哪里而不是盲目乱试。需要特别说明的是这四轮优化是层层叠加的每一轮的结果都建立在上一轮的基础上。我把每次实验的参数和结果都记在了一张表里这让我能清楚地看到每一步的边际收益。实验记录这个习惯是我能持续把指标往上推的重要原因。6. 常见问题与排查实录6.1 检索不到相关内容现象答案经常出现“根据现有资料无法回答”但知识库里明明有答案。排查思路第一步直接调检索接口看 top-5 的 chunk 到底是什么。如果召回结果与问题毫不相关问题出在检索端如果召回结果相关但回答不上来问题出在生成端。检索端常见原因有三个切分粒度太大导致 chunk 主题混杂embedding 模型与场景不匹配查询的表达方式与知识库差距太大。我遇到过最典型的情况是“口语化提问 vs 书面化知识库”用户问“钱怎么退”库里写的是“退款流程与账期说明”。这时把查询改写一下或者先用模型生成若干查询变体再做检索效果立竿见影。这一步业内叫查询扩写Query Expansion实现不复杂但收益非常直接。6.2 检索到了但回答错误现象召回结果完全正确模型却答非所问。八成是提示词的问题。检查两件事一是提示词里是否明确约束了“只依据资料回答”二是上下文是否因为过长被模型截断。我踩过一个具体的坑5 个 chunk 加起来超过了模型的上下文窗口后面的 chunk 根本没被模型看到答案自然缺关键信息。解决办法是控制 chunk 数量、压缩内容或按相关性截断。还有一个很容易被忽略的点知识库里的证据可能自相矛盾。比如 A 文档说退款需要七天B 文档说退款需要三天模型会随机选中一个看起来就像“回答错误”。这种情况不是 RAG 的 bug而是数据问题。我的处理方式是把冲突检测加入数据治理流程至少在高频问题上保持知识库一致性。6.3 向量库占用过大现象数据量没多大向量库文件却出奇地大。先看存储的字段。Chroma 默认会把原始文档也存进去如果你只需要向量可以考虑去掉 documents 字段只存向量和元数据。其次检查是否有重复添加——我调试过程中多次运行入库脚本同一个 chunk 被插入了好几遍数据直接翻倍。这个问题在早期的多次实验场景里太常见了记得给 collection 做去重或直接重建。还有一个小细节每次用同一批数据反复实验时可以给 collection 加版本号比如 qa_kb_v2、qa_kb_v3避免混在一起。我后来养成了习惯每次改动数据管线就新建一个 collection把旧的保留作对比省了不少排查时间。6.4 响应慢到无法接受现象接口平均响应 5 秒以上。从三个环节拆时间检索、重排、生成。检索慢通常是向量库性能问题可以上索引加速或者把向量库从本地文件换成服务化部署重排慢就减小候选集比如从 top-20 降到 top-10生成慢大概率是模型问题考虑换更小的模型或做量化。我最后的方案是把模型从 7B 换成量化版生成速度提升一倍准确率只掉了 1 个百分点非常划算。另外检索和生成这两个环节天然可以并行优化——检索结果先出来生成端还在排队时检索端已经可以预加载下一轮的候选。但这些属于工程优化细节学习阶段不用太早介入。先把准确率做上去再回头压延迟顺序不能反过来。7. 学习日志小结与我的几点体会7.1 先跑通再优化别困在完美主义里写到这儿这次“双体 RAG”的学习日志也接近尾声了。回头看最大的体会不是哪条技术路线有多牛而是“先跑通、再优化”这句话的分量。我见过太多人包括我自己在切分参数上纠结了一周却不舍得花半天建评测集。评测集才是 RAG 项目的方向盘没有它你根本不知道自己在往哪开。先有一个能跑但效果一般的系统比一个永远在准备中的系统有价值得多。7.2 每次只改一个变量第二个体会是每次实验只改动一个变量。我要求自己每次只调整一个参数记录结果然后回滚。看起来慢但每一份实验数据都是可信的。你如果同时改五个参数效果涨了都不知道是哪个起的作用效果跌了更是一头雾水。我这张表上记了二十多组实验每一组对应一个参数的变动后来排查问题时全靠它定位。7.3 数据质量决定上限第三个体会有点反直觉RAG 的瓶颈往往不在模型而在数据。知识库的质量、切分的合理性、检索的融合策略这些“外围工作”对最终效果的影响远超模型本身的选择。所以如果你问我新手最值得花时间的地方我会说把文档处理好把评测集建好这两件事做到位RAG 就成功了一大半。模型可以换、向量库可以换但知识库的质量问题没有任何技术手段能兜底。最后分享一个小技巧做评测集时故意加入一些知识库里没有的问题。很多人只测“库里有什么”不测“库里没什么”结果系统在未知问题上胡编乱造也没被发现。加入未知问题后你才能知道提示词里的“无法回答”约束到底生不生效。RAG 这条路还很长向量检索之外还有图检索、重排模型还有更好的选择、生成端还能做更精细的控制但核心技术框架就是这些。先把这套链路吃透后面怎么扩展都不会跑偏。希望这篇日志能给你省下我踩过的那些坑动手试试吧。