2025年7月15日,我花了一整天把RAG从"听说过"到"亲手跑通",这篇笔记就是当天学习wow-rag项目时整理出来的完整记录——从RAG到底解决了什么问题,到经典链路里每个环节的职责,再到本地部署时那些网上教程不会告诉你的参数细节,最后还聊了聊RAG知识库、RAG瓶颈这些概念背后真正的含义。无论是刚接触RAG检索增强的小白,还是已经跑过demo但总觉得效果不稳定的入门者,这篇笔记应该都能给你一些可参考的东西。
1. RAG到底是什么:先说清它解决的三个问题
很多人一上来就背定义:RAG是检索增强生成。但光背定义没用,你得先搞清楚它到底在解决什么。我当时学wow-rag项目时最大的感触是,标题里的名词背后其实是一串非常具体的问题。
1.1 大模型的两块短板:幻觉和知识过期
大模型本质上是"背课文长大的",它的所有知识都锁死在训练时见过的数据里。这带来两个非常现实的问题。
第一是幻觉。模型不知道答案时,它不会老实说"我不知道",而是会基于概率"编"一个看起来合理的答案。就像一个人考试时遇到不会的题,硬着头皮写了个答案,字迹工整、逻辑通顺,但内容全错。在企业场景里,这种幻觉是致命的——比如让模型回答公司报销流程,它可能一本正经地告诉你一个根本不存在的审批环节。
第二是知识过时。模型训练完成后就处于"封存"状态,2024年训练完的模型,不可能知道2025年的新政策。很多公司投入大量成本做微调(Fine-tuning),但微调有几个难题:数据准备周期长、每次知识更新都要重新训练、成本随模型规模剧增。而且你没法保证微调后模型依然稳定,很可能"学新忘旧"。
除了这两点,还有一个常被忽略的问题——知识割裂。公司内部的知识散落在各种文档、表格、聊天记录里,今天修订的SOP可能和新版产品手册相矛盾,而模型本身并不知道"哪个才是最新版本"。RAG知识库要解决的,其实正是这种"知识割裂":把散落的文档拉通成统一可检索的知识库。
1.2 RAG的核心思想:开卷考试
理解RAG最快的方式,是把它想成闭卷考试和开卷考试的区别。
传统方式(纯LLM生成)是闭卷考试。模型靠记忆答题,记不清就编,遇到没学过的直接交白卷。RAG是开卷考试。每次答题前,先给你一本指定教材(知识库),允许你翻书找到相关内容,再结合参考材料来作答。
具体拆开看,RAG的流程分三步:先做检索(Retrieval),把用户的问题拿去知识库里找最相关的段落;再做增强(Augmented),把找到的段落和原始问题组装成一个完整的提示词;最后生成(Generation),把组装好的提示词交给大模型,让它"基于给定材料回答问题"。
这个流程看起来简单,但它带来一个巨大的优势:知识可以随时更新。你不需要重新训练模型,只要往知识库里丢新文档,模型下一次回答时就能检索到最新内容。这也是为什么"RAG框架""RAG检索"这些词这几年在技术圈和业务圈同时火起来——它把大模型的"脑子"和企业自己的"资料库"解耦了。
1.3 wow-rag项目:一个把整条链路串起来的入门项目
我学习用的wow-rag项目,本质上就是这条标准流程的最小可用实现。它把文档处理、文本切分、向量化、向量存储、相似度检索、提示词组装、模型生成这七个环节全部串起来,跑通之后你就能直观看到"一条RAG管道"长什么样。
这类入门项目的价值在于:上手快、链路完整、出了问题好排查。调参时有太多失败方式,但你先得有一条"能跑通的基准线",才知道每一步调参到底在调什么。接下来我按这个思路,把整个RAG项目的技术链路逐层拆开。
2. 从wow-rag的实际结构看RAG完整链路:索引与检索生成
这一节是全文的核心。我把wow-rag项目的各个模块分别对应到RAG的标准流程里,这样你学完一个项目,就能举一反三看懂市面上其他RAG框架。
2.1 第一段管道:文档加载与切分
RAG的第一步,是把知识库里的文档(PDF、Word、Markdown、HTML等)加载进系统。wow-rag里用了一个统一的文档加载器,把各种格式统一转成纯文本。这一步看着不起眼,实际坑很多,比如PDF里表格会乱、扫描件需要OCR、Markdown里的代码块会被切碎。
加载之后是切分(Chunking)。这是决定RAG效果的第一关键环节。文档是不能直接整篇塞进提示词的——一个大模型上下文窗口有限,而且就算有足够空间,塞进无关内容反而会干扰回答。所以要先把长文档切成多个片段,检索时只取最相关的几个片段。
切分不是越短越好。片段太长,检索到的内容可能只有一小段相关,其余全是噪音;片段太短,又会丢失上下文,比如"该方案成本1800元"和"该方案"出现在两个相邻片段里,单看哪一段都信息不完整。wow-rag默认用的是按固定字符数切分(比如512字符)+ 重叠窗口(比如80字符)。重叠的意义在于:让相邻片段之间保留交叉信息,避免一句话刚好被拦腰截断。
2.2 第二段管道:向量化与向量存储
切出来的每个片段,接下来要被"翻译"成计算机能比较的格式。RAG里用的标准翻译工具是Embedding模型,也叫向量化模型。它把一段文字映射成一组浮点数向量(比如768维、1024维),核心特性是:语义相近的文本,向量距离也更近。
"苹果好吃"和"苹果营养丰富"在向量空间里距离很近,哪怕它们没有一个字是重复的;而"苹果好吃"和"汽车保养"距离就很远。这个语义表达的能力,是RAG检索能超越"关键词匹配"的根本原因。
向量化之后,这些向量连同原文一起存入向量数据库。常见的向量库有FAISS、Chroma、Milvus、Weaviate等。wow-rag默认用的是Chroma——一个轻量级本地向量库,对入门来说最友好,不需要部署单独的服务,一个文件夹就能存所有数据。
向量数据库除了能存向量,还实现了高效的相似度搜索。普通的数据库做不了"找最相似的向量"这种操作,向量库底层用了近似最近邻检索(ANN),能在海量向量里快速找到距离最近的TopK个。这里提醒一句:千万不要自己用Python逐条算余弦相似度,几百条数据还凑合,上万条就慢到怀疑人生,向量库存在的意义就是把这个检索做到毫秒级。
2.3 第三段管道:检索、组装与生成
说完索引侧,再来看查询侧。用户提一个问题,系统要做三件事。
第一步是查询向量化:把用户问题用同一个Embedding模型转成向量。注意这里必须和索引阶段用同一个模型,否则向量空间的坐标系不一致,检索结果会完全失真——这是新手最容易踩的坑之一。
第二步是相似度检索:在向量库中搜索与查询向量最相似的TopK个片段,K通常是3~5。wow-rag里还支持简单的元数据过滤,比如只检索某个指定目录下的文档。
第三步是提示词组装:把检索到的片段按一定格式拼接,连同原始问题,一起构造出一个完整的Prompt。
你是知识库助手。请基于以下资料回答问题,如果资料中没有相关信息,请明确说明"根据现有资料无法回答"。 参考资料: [1] 文档A,第3页:《公司报销流程》 员工报销需填写报销单,经部门负责人审批后,提交财务部审核... [2] 文档B,第5节:差旅费用标准 国内出差住宿标准为每晚400元,超过部分自理... 用户问题:公司差旅住宿的报销标准是多少?最后把Prompt交给大模型生成答案,再返回给用户。到这里,一条完整体验的RAG流程就走完了。
现在你再看任何RAG框架(Spring AI RAG、LangChain4j、LangChain等)的文档,骨架其实都是一样的——无非是加载器不同、切分策略不同、用的Embedding模型不同、向量库不同、编Prompt的模板不同。wow-rag的价值就是把这个通用骨架具象化了,让我从"知道概念"变成"看得见每一个环节的输入和输出"。
3. 本地部署实操:用ollama跑通一个最小RAG知识库
概念清楚了,接下来是动手。我是在一台没有NVIDIA显卡的普通笔记本上完成的全部实操,靠的是CPU推理和本地模型,所以下面这套流程对绝大多数零基础读者都是可复制的。整体选型是"ollama + 本地嵌入模型 + Chroma + 本地LLM",全程不需要联网调用任何付费API。
3.1 环境准备与工具选型
先说工具选型,这是很多教程讲不清楚的地方。整个RAG链路里有三个模型角色:Embedding模型(负责把文本转向量)、LLM对话模型(负责最终生成)、重排序模型(可选,负责对检索结果二次打分)。我用了以下组合:
- Embedding模型选nomic-embed-text(约500MB),支持1024维向量,中文效果尚可,资源占用极小。
- LLM对话模型选qwen2.5:7b(约4.7GB),中文能力在线,CPU推理速度虽慢但可接受。
- 向量数据库用Chroma,无需单独服务,直接本地文件存储。
- 文档处理用LangChain社区里现成的加载器,配合正则清理做文本清洗。
安装ollama之后,拉取模型只需两条命令:
# 拉取Embedding模型和对话模型 ollama pull nomic-embed-text ollama pull qwen2.5:7b这里我踩了一个比较影响体验的坑:初版我图省事,直接用了qwen2.5:7b自带的嵌入能力,结果检索效果惨不忍睹。原因是对话模型虽然能输出向量,但输出维度、语义质量和专门的Embedding模型差距很大,尤其是对短查询的匹配能力很差。Embedding模型和对话模型必须是分开的两个模型,这是新手最容易埋下的隐患。
3.2 文档加载与切分:参数到底怎么选
我准备了一份约20页的Markdown文档,内容是某个内部系统的操作手册。用LangChain的Markdown加载器读入后,开始切分。
切分我先后试了三组参数,记录如下:
| 切分策略 | chunk_size | chunk_overlap | 检索效果(目测) |
|---|---|---|---|
| 固定长度 | 200字符 | 40字符 | 答案碎片化严重,上下文断裂 |
| 固定长度 | 512字符 | 80字符 | 基本可用,偶尔信息不全 |
| 按标题/段落语义切分 | 动态 | 无 | 效果最好,但依赖文档结构规范 |
第三组方案其实依赖LangChain里的MarkdownHeaderTextSplitter——按文档的#、##、###标题级别自动分段,每个标题下的内容成为一个独立片段。对于有清晰章节结构的文档,这种方式远胜固定字符数切分,因为它天然保留了语义边界。如果你的文档结构不规范,那再退回到固定长度,并把重叠窗口设在chunk_size的10%~20%之间。
一个小技巧:切分完之后,先打印几个片段看看。如果片段里含有"……未完,接下一段"这种上下文断裂感强,或者出现了半个表格、半个代码块,就说明参数需要调整。这一步十分钟的检查能省掉后面一小时的排错。
3.3 向量化入库:你的第一个本地知识库
接下来把片段向量化并存入Chroma。代码大致是这个流程:
from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma embedding = OllamaEmbeddings(model="nomic-embed-text") vectorstore = Chroma.from_documents( documents=split_chunks, embedding=embedding, persist_directory="./chroma_db" )执行这个脚本后,本地会生成一个chroma_db目录,里面就是你的知识库。这里我犯过一个蠢错:每次启动脚本都执行Chroma.from_documents(),直接把旧库覆盖掉了,结果前一天导入的文档第二天全没了。
正确做法是先判断库是否存在,存在就加载,不存在才重建:
import os if os.path.exists("./chroma_db"): vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embedding) else: vectorstore = Chroma.from_documents(documents=split_chunks, embedding=embedding, persist_directory="./chroma_db")另外一个很多教程没提的问题:同一个知识库里只能用同一个Embedding模型。如果你今天用nomic-embed-text建库,明天换了个模型,检索结果会全部错乱,因为向量空间对不上。要么删库重建,要么一开始就固定好模型。
3.4 检索与生成联调:把RAG管道接起来
入库完成后,我写了一个简单的查询脚本,把之前的三个环节串起来:
question = "如何重置用户密码?" retrieved = vectorstore.similarity_search_with_score(question, k=3) context = "\n\n".join([doc.page_content for doc, score in retrieved]) prompt = f"""基于以下资料回答问题,资料不足就直说不知道: {context} 问题:{question} """ # 调用ollama本地模型生成第一次联调时,我发现一个问题:检索得分很差的片段居然也被塞进了上下文。后来把k=3调成k=5,答案质量反而下降了,因为多出来的两个片段里有一个是不相关的内容,模型的注意力被带偏。这说明不是检索得越多越好。对大部分场景,TopK取3~4是性价比最高的值;如果你的文档质量高且问题明确,K甚至可以再小一点。
联调跑通后,我顺手测了几个不同角度的问题,覆盖"完全在文档里""部分在文档里""完全不在文档里"三类情况。第三类情况最能检验Prompt写得好不好——如果Prompt没写"资料不足就直说",模型极有可能开始自由发挥编答案,这本质上还是幻觉。所以Prompt里那句"如果资料中没有相关信息,请明确说明无法回答"不是可有可无的装饰,是RAG系统的安全底线。
4. 实测中的三个坑:切分、命中率与上下文污染
RAG入门阶段真实遇到的坑,往往记录在案的人不多。我自己从下午踩到晚上,总结了三个典型问题,每一个都能显著影响最终效果。
4.1 切分不合理,召回率从根上就输了
先说我最初用200字符切分时踩的坑。当时我检索"快递费用谁负责",系统返回的片段里只有一句"快递费用由发起人承担",但上下文里明明还写着"如需加急,费用由申请人个人承担"——因为这句话被切到了下一个片段里,没被检索到。
这就是RAG圈子里常说的"召回率问题"。切分粒度直接决定了召回率的上限。片段切得太碎,一条完整信息被拆进两个片段,检索时只能召回一半;切得太粗,有效信息淹没在无关文字里,相似度被拉低,同样召不回来。
我后面改用语义切分(按标题),这个问题就大幅缓解了。建议你拿到一份新文档时,先人工扫一遍结构,如果章节标题清晰就优先用语义切分;没有结构的文本,至少把chunk_overlap设置在80~120字符。另外,如果你的检索经常漏内容,可以在入库时额外保存每个片段对应的源文档名和章节号,这样召回后即便内容不完整,也能根据元数据定位到原文去复核。
4.2 RAG命中率(Hit Rate)低,先从这三个点排查
命中率低是RAG最头疼的现象之一。你问它一个问题,它答非所问,或直接说不知道。我在排查时用的顺序基本是固定的:
先查检索是否命中,打印出TopK的片段,人工看看有没有相关内容。如果TopK片段里压根没有相关内容,说明问题出在Embedding模型或切分方式上,属于索引侧缺陷。这一步可以临时用关键词匹配做对比测试,确认到底是语义检索没生效还是知识库根本没收录。
再查检索到了但生成效果差,说明问题出在Prompt组装或模型能力上。此时把Prompt打印出来直接喂给模型,如果同样答错,就可以断定生成侧的问题。这里最常见的是"上下文放得太多""有效信息占比太低""片段之间风格冲突导致模型困惑"。
最后查知识库本身。确认文档真的入库了、没有被覆盖或漏发。我前面提过的
from_documents覆盖旧库的坑就属于这一类——知识库本身就已经是空的了,检索自然零命中。
另外可以聊一下 "Hit Rate" 这个指标的含义,它统计的是"有多少次提问能在检索结果里找到正确答案"。参考基准是:一个有合理切分和Embedding的RAG系统,Hit Rate通常在70%~90%。如果你的命中率远低于这个数,老老实实按上面的顺序排查,而不是去调K值碰运气。
4.3 上下文污染:检索到的不该全塞进Prompt
跑通之后我做了个粗鲁的实验:把TopK从3改成10,把检索到的所有片段一股脑塞给模型。结果答案质量直线下降,甚至出现了"编造文档里根本没有的细节"的情况。
这里涉及RAG里的"上下文污染"概念。大模型面对大量文本时,注意力是有限的。当你塞入10个片段,其中只有2个相关,另外8个就是噪音,噪音会在生成时干扰模型的判断,让它错误地"借用"了无关信息。
更隐蔽的是"片段间矛盾"。如果切分粒度不合理,检索到的片段可能来自同一流程的不同版本,一个片段说"先审批后报销",另一个片段说"先报销后审批",模型夹在中间无所适从。应对手段就是在User Query里明确"如果资料之间矛盾,以标注了最新日期或最高版本号的内容为准"。这个设计极其有效,本质上是把文档的"时效性判断"交给了模型。
所以,上下文组装的原则是"少而精",而不是"多而全"。宁可只给3个相关片段,也不要给10个混着噪音的片段。这也是为什么现在有专门的"上下文压缩""重排序(Reranker)"技术——先用向量检索粗筛,再用重排序模型精排,把最相关的片段筛出来再进Prompt。本地部署如果性能允许,强烈推荐加一个rerank环节,效果提升立竿见影。
5. RAG进阶方向:从朴素RAG到图谱RAG与Agentic RAG
跑通wow-rag只是起点。我翻热搜时看到"RAG瓶颈""ontologk RAG""GraphRAG""Agentic RAG"这些词,当时觉得是噱头,但深入了解后发现,它们其实是同一个问题的不同解法。
5.1 朴素RAG的瓶颈到底在哪
教科书式的朴素RAG(Naive RAG),核心问题有三个。
一是跨文档推理能力弱。我问"比较A方案和B方案的优缺点",朴素RAG检索到的片段可能只覆盖A方案,因为B方案的描述换了措辞和罐装名词,向量检索没能把它们关联起来。这种"知识割裂"问题在跨文档场景尤其严重。
二是多跳问题处理能力差。所谓多跳(Multi-hop),就是"要回答这个问题,需要先找到X,再通过X找到Y"。比如"负责A项目的那个人,他之前参与过的项目里,哪个是盈利最多的?"朴素RAG检索是单轮的,没法做到这种推理式查找。
三是不确定性管理和全局理解弱。朴素RAG擅长"定位一段信息",却很难回答"整个文档库的主题和骨架是什么"这类全局性问题。
这三个瓶颈,正好催生了两个重要的RAG演进方向。
5.2 图谱RAG与本体RAG:从向量距离走向"关系"
GraphRAG的思想是:不只把文档切成碎片存向量,而是从文档里抽取出实体(公司、人物、产品)和关系(A负责B、B属于C),构建出知识图谱。查询时先在图谱上定位相关实体,再顺着关系找到关联信息,之后再用来引导检索。
这么做的好处很直观:解决跨文档和多跳问题。因为图谱把"A方案"和"B方案"连到了同一个概念的上下位关系里,哪怕它们出现在完全不同的文档段落中,图谱检索也能把它们捞回来。
Ontology RAG比GraphRAG更进一步,它在图谱之上构建了一层"本体"——相当于定义了领域里的概念体系、类别层次和属性关系。比如医疗领域的本体里,"感冒"是"呼吸道疾病"的子类,"发热"是"症状"属性。有了本体,模型对知识的理解就不再是纯字符串或纯向量,而是有语义骨架的"概念网络"。这听着学术化,但实际效果是:它能更好地区分"含义相同但说法不同"的表达,减少检索偏差。
5.3 Agentic RAG:让检索变成自主决策
如果说GraphRAG是改进了"索引与检索"的底层结构,Agentic RAG则是改写了"利用检索结果"的方式。
朴素RAG是固定流程:用户问→检索→拼接→生成,一步到位,中间没有判断。Agentic RAG里则有一个"决策者"(Agent),它会根据问题自主规划行动——先检索一波,看结果够不够回答;不够,再换个关键词或换个数据源检索;多个数据源都要查时,还会并行检索再汇总。整个过程像是一个懂得检索技巧的人在工作,而不是一个只会翻同一本书的机械流程。
中文技术社区里也有人讨论 "RAG as a Service" 的趋势,也就是把RAG能力封装成标准的服务接口,让上层业务直接调用,不再每家自己搭管道。这个方向背后其实暗示着一个行业判断:RAG会像数据库一样,成为一种基础设施能力。我的看法是,作为学习者,先别急着追这些新名词,把朴素RAG每一个环节的"为什么"吃透,再往图谱RAG和Agentic RAG方向顺路延伸,会比较稳。
回到wow-rag这个项目本身,它最大的贡献不是功能有多强,而是把"RAG是什么"从概念变成了一条可以亲手跑通、逐步调优的管道。我最后想分享的一个建议是:跑通之后,一定要自己动手改一个地方,比如把固定长度切分改成语义切分,把TopK从3改成5,把Embedding模型换一个,然后记录前后差别。只要动手改过一次参数,你对RAG的理解就会比读十篇论文都深刻。