☰
从零手搓RAG全流程:建库、检索、生成与Agentic RAG实战
2026/9/29 18:34:23 网站建设 项目流程

1. 为什么我要从零手搓一套 RAG 流程

1.1 从“玩具 Demo”到“能用的知识库”之间差了什么

刚接触 Agent 开发那会儿,我对 RAG 的理解停留在“把文档切一切、丢进向量库、检索出来拼进 Prompt”这三板斧。跑通一个 Hello World 级别的问答 Demo 确实只要半小时,但当我真正想拿它去处理一份两百多页的产品手册时,问题就全冒出来了:检索出来的片段答非所问、同一个问题换个问法就找不到答案、模型明明拿到了正确上下文却还是胡编。这些坑逼着我回头把 RAG 的每一个环节拆开重新理解,也就是这篇笔记想聊的东西——从建库、检索到生成,一条完整链路上到底有哪些决策点,每个决策点背后的取舍逻辑是什么。

RAG 这个词现在被用得很泛,但它的核心其实很朴素:大模型的知识是冻结在训练数据里的,你没法指望它知道你公司上周刚更新的内部文档。RAG 做的事情就是在模型回答之前,先从你自己的知识库里把相关资料捞出来,塞进上下文里让它“开卷考试”。听起来简单,但“捞得准”和“塞得好”这两件事,恰恰是最考验工程功力的地方。

这套流程适合谁?如果你已经会用 LangChain 跑通基础的对话,但发现自己的知识库问答效果不稳定,或者你正准备给团队搭一个内部文档助手,那这篇内容应该能帮你少走一些弯路。我会尽量把每个环节的“为什么”讲清楚,而不是只丢一段能跑的代码。

1.2 整体链路的四个阶段与核心关键词

把 RAG 拆开看,它其实是一条流水线,我习惯把它分成四个阶段:

  • 建库阶段:文档加载、清洗、切分、向量化、入库
  • 检索阶段:查询改写、向量检索、关键词检索、重排序
  • 生成阶段:上下文组装、Prompt 设计、答案生成与引用
  • 评估阶段:命中率、忠实度、召回率的量化与迭代

这四个阶段里,Embedding 模型决定了“语义能不能被正确表达”,向量数据库决定了“能不能快速找到”,LangChain这类框架决定了“这些环节能不能被优雅地串起来”。而 Agent 的加入,则是让整个流程从“一次性检索”变成“可以多轮思考、按需检索”的智能体行为,这也是最近 agentic rag 这个概念火起来的原因。

我下面会按这条链路一步步展开,中间会穿插我自己踩过的坑和实测下来比较稳的参数选择。

2. 建库阶段:文档切分与向量化的那些门道

2.1 文档加载与清洗:脏数据是检索不准的元凶

很多人一上来就急着调切分参数,但我要说一句可能不太中听的话:大部分检索不准的问题,根源在文档本身太脏。PDF 里的页眉页脚、表格错位、换行符乱入,这些噪声在切分之后会变成一个个语义残缺的片段,向量化出来的结果自然也是歪的。

我的处理顺序通常是这样的:

  1. 先做格式归一化:把 PDF、Word、Markdown 统一转成纯文本或结构化 Markdown。PDF 我一般用pymupdf或pdfplumber,表格多的文档优先用后者,它对表格的还原更靠谱。
  2. 去掉重复噪声:页眉页脚、页码、水印文字,这些在每一页都出现的内容要批量剔除。我的做法是统计高频重复行,出现次数超过总页数 60% 的短行直接删掉。
  3. 修复断行:中文文档里经常出现一句话被硬换行切断的情况,用正则把“非标点结尾的换行”合并掉,能显著提升切分质量。

提示:清洗这一步不要追求一步到位,先做能明显提升效果的(去页眉页脚、修断行),剩下的边角问题等检索效果评估出来再针对性处理。

清洗完之后,我建议保留一份带元数据的结构化文档,比如每个段落记录它来自哪个文件、哪一页、哪个章节标题。这些元数据在后面做引用溯源和过滤检索时会非常有用,千万别图省事只留纯文本。

2.2 切分策略:固定长度、递归、语义切分怎么选

切分是建库里最容易被低估的环节。切得太碎,语义不完整;切得太大,检索精度下降还浪费 token。我实测下来,递归字符切分(RecursiveCharacterTextSplitter)在大多数场景下是性价比最高的选择,它按段落、句子、词的优先级递归尝试,尽量在语义边界处断开。

关于参数,我的一般起点是:

参数推荐值说明
chunk_size500-800 字符中文场景偏小,英文可到 1000
chunk_overlapchunk_size 的 10%-15%保证跨块语义连续
separators["\n\n", "\n", "。", "!", "?", ";", " "]中文标点要加进去

这里有个细节值得展开:overlap 不是越大越好。我一开始把 overlap 设成 30%,结果检索时经常返回一堆高度重复的片段,反而挤占了有效上下文。10%-15% 是个比较舒服的区间,既能保证边界语义不断裂,又不会引入太多冗余。

如果你的文档结构非常清晰(比如技术手册有明确的章节层级),那可以试试按标题层级切分,用 Markdown 的#层级或者文档的标题样式来划分 chunk。这种方式切出来的片段语义完整性最好,但前提是你的文档本身结构规范。

至于最近常被提到的语义切分(用 Embedding 相似度判断句子边界),我的看法是:效果确实好,但计算成本高,而且对短文档收益不明显。我一般只在处理长篇文章、法律合同这类语义密度高的文档时才会用。

2.3 Embedding 模型选型:别只看排行榜

Embedding 模型是把文本变成向量的“翻译官”,它直接决定了语义检索的上限。现在各种 embedding 模型排行榜满天飞,但我想提醒一句:排行榜上的分数是在特定数据集上跑出来的,和你的实际场景可能差很远。

选型时我会重点看这几个维度:

  • 语言支持:中文场景一定要选中文语料训练充分的模型,很多英文榜单第一的模型中文表现其实一般。
  • 维度与成本:维度越高表达能力越强,但存储和检索成本也越高。768 维和 1024 维在实际效果上的差距,往往没有成本差距那么明显。
  • 最大输入长度:要和你 chunk_size 匹配,别切了 800 字符结果模型只支持 512。
  • 是否支持本地部署:涉及内部文档时,本地部署的模型在数据安全上更让人放心。

我自己的实践是,先用一个中等规模的模型快速跑通全流程,等评估体系建起来之后,再拿几个候选模型做 A/B 对比。没有评估的选型都是拍脑袋,这句话在建库阶段尤其成立。

注意:同一个知识库里的所有向量必须用同一个 Embedding 模型生成,中途换模型会导致新旧向量不在同一语义空间,检索结果会彻底乱掉。如果非要换,就得全量重建。

2.4 向量数据库选型:Milvus、Chroma、Qdrant 怎么挑

向量数据库这块,热词里提到的 Milvus、Chroma、Qdrant 我都用过,它们各有各的适用场景。我整理了一张对比表,方便你按自己的情况对号入座:

数据库定位优势适合场景
Chroma轻量嵌入式零配置、和 LangChain 集成极简本地开发、Demo、小规模知识库
Qdrant中量级服务过滤检索强、Rust 性能好、API 友好中小规模生产、需要元数据过滤
Milvus分布式重型水平扩展、支持十亿级向量大规模生产、企业级知识库

我的建议很直接:开发阶段用 Chroma,快速验证想法;要上生产且数据量在百万级以内,Qdrant 是很好的平衡点;只有当你真的面对亿级向量和分布式需求时,才值得上 Milvus。很多人一上来就搭 Milvus 集群,结果发现运维成本远超收益,这就本末倒置了。

Chroma 的用法简单到几乎不需要配置:

import chromadb from langchain_chroma import Chroma client = chromadb.PersistentClient(path="./chroma_db") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, client=client, collection_name="my_knowledge_base" )

Qdrant 我一般用 Docker 起一个服务,然后通过 LangChain 的QdrantVectorStore接入,它最大的好处是支持带条件的向量检索,比如“只在某个部门的文档里搜”,这在企业场景里非常实用。

3. 检索阶段:让“捞得准”成为常态

3.1 向量检索的先天缺陷与混合检索的必要性

纯向量检索有个绕不开的问题:它对精确匹配不敏感。比如你问“XX-2000 型号的参数”,向量检索可能给你返回一堆“XX 系列产品”的泛泛描述,因为它在语义上觉得这些很像,但那个精确的型号词被淹没了。

解决办法就是混合检索(Hybrid Search):向量检索负责语义召回,关键词检索(BM25)负责精确匹配,两路结果融合。LangChain 里的EnsembleRetriever就是干这个的,它用 RRF(Reciprocal Rank Fusion)算法把两路结果合并。

from langchain.retrievers import EnsembleRetriever, BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 5 vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.4, 0.6] )

这里有个热词里提到的点值得说:RRF 的去重逻辑存在缺陷。RRF 是按排名融合的,如果两路检索返回了内容高度相似但 chunk_id 不同的片段,它不会去重,结果就是上下文里塞了好几段几乎一样的内容。我的处理办法是在融合之后加一层基于文本相似度的去重,把相似度超过阈值的片段只保留排名最高的那个。

3.2 查询改写:用户的问题往往不是好的检索词

用户问“这个功能怎么用”,直接拿这句话去检索,效果通常很差,因为它太笼统了。查询改写(Query Rewriting)就是让模型先把用户问题改写成更适合检索的形式。

我常用的几种改写策略:

  • 扩展:把“这个功能”补全成具体的功能名,需要结合对话历史。
  • 多查询生成:让模型生成 3-5 个不同角度的查询,分别检索后合并结果,能显著提升召回率。
  • HyDE:让模型先“假装”生成一个答案,再用这个答案去检索。这招对专业术语多的场景特别有效,因为生成的答案里会包含更多领域词汇。
from langchain.retrievers.multi_query import MultiQueryRetriever multi_query_retriever = MultiQueryRetriever.from_llm( retriever=ensemble_retriever, llm=llm )

多查询的代价是检索次数翻倍,延迟会上升。我的经验是在离线评估阶段用多查询找出最优的查询模式,线上则根据问题复杂度动态决定是否启用,别一股脑全开。

3.3 重排序:把最相关的片段顶到最前面

检索回来的 Top-K 片段,顺序其实没那么可靠。重排序(Rerank)用一个更精细的模型(通常是 Cross-Encoder)对候选片段重新打分,把真正相关的排到前面。这一步对最终生成质量的影响,往往比换 Embedding 模型还大。

流程是这样的:先用向量检索召回 20-30 个候选,再用 Rerank 模型精排出 Top 5 送给生成模型。这样既保证了召回率,又保证了精度。

from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") compressor = CrossEncoderReranker(model=model, top_n=5) rerank_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble_retriever )

提示:Rerank 模型和 Embedding 模型最好来自同一系列,它们的语义空间更匹配,实测效果通常更稳。

3.4 检索参数调优:k 值、阈值与元数据过滤

检索阶段有几个参数需要反复调:

  • 召回数量 k:太小会漏,太大会引入噪声。我一般召回 20-30 个候选,重排后取 3-5 个。
  • 相似度阈值:低于阈值的片段直接丢弃,避免“矮子里拔将军”。但阈值设太高会导致召回不足,需要根据实际分布来定。
  • 元数据过滤:如果知识库有明确的分类,检索时加上过滤条件能大幅提升精度。比如只搜“技术文档”类别,或者只搜某个时间之后的文档。

我踩过的一个坑是:相似度阈值的绝对值在不同 Embedding 模型之间不可比。换模型之后,原来 0.7 的阈值可能就完全不适用了,必须重新校准。所以我在代码里会把阈值做成配置项,方便切换模型时快速调整。

4. 生成阶段:上下文组装与 Prompt 设计

4.1 上下文组装的顺序与去重

检索回来的片段怎么拼进 Prompt,也是有讲究的。我的原则是:

  1. 按相关性排序:重排后的顺序就是相关性顺序,最相关的放最前面。
  2. 去重:内容高度重叠的片段只保留一个,避免浪费 token。
  3. 加来源标记:每个片段前面加上来源信息(文件名、页码),方便模型引用,也方便用户溯源。
  4. 控制总长度:根据模型的上下文窗口留出足够空间给回答,别把窗口塞满。

一个典型的组装格式长这样:

【资料1】来源:产品手册.pdf 第12页 (片段内容) 【资料2】来源:FAQ.md (片段内容) 请基于以上资料回答问题,如果资料中没有相关信息,请明确说明。

4.2 Prompt 设计:如何让模型“忠于原文”

RAG 生成阶段最大的挑战是模型不忠于检索到的内容,要么自己发挥,要么把不相关的片段硬凑成答案。Prompt 设计要解决的就是这个问题。

我常用的约束性 Prompt 结构:

  • 角色设定:明确告诉模型它是基于给定资料回答的助手。
  • 资料边界:强调“只使用提供的资料”,资料里没有就说不知道。
  • 引用要求:要求模型在回答中标注信息来源,这能倒逼它更认真地对待资料。
  • 格式约束:规定输出格式,避免长篇大论。
prompt_template = """你是一个严谨的知识库助手。请严格基于以下资料回答问题。 资料: {context} 问题:{question} 要求: 1. 只使用资料中的信息,不要编造。 2. 如果资料中没有答案,直接说"根据现有资料无法回答"。 3. 回答时标注引用的资料来源。 """

实测下来,“资料里没有就说不知道”这条约束能显著降低幻觉率,但也会让一些本可以推断的问题被拒答。这个平衡点需要根据你的场景来调,客服场景可能更倾向于保守,而研究辅助场景可以放宽一些。

4.3 引用溯源:让答案可验证

引用溯源不只是为了好看,它是建立用户信任的关键。当用户能看到答案来自哪份文档的哪一页时,他们才会真正把系统当工具用。

实现上,我在组装上下文时给每个片段打上唯一 ID,然后在 Prompt 里要求模型引用这些 ID。生成之后再根据 ID 反查来源信息,渲染成可点击的引用链接。LangChain 的create_retrieval_chain配合自定义的输出解析器就能做到。

注意:模型引用 ID 的准确率不是 100%,有时候会张冠李戴。我的做法是在后处理阶段做一次校验,如果引用的 ID 不在提供的片段里,就标记为“引用存疑”,提示用户注意。

5. Agentic RAG:让检索变成智能体的决策

5.1 从固定流程到动态决策

传统的 RAG 是一条固定流水线:检索一次,生成一次。但真实问题往往需要多轮检索:先查概念,再根据结果查细节,最后综合回答。这就是 agentic rag 的核心思想——把检索变成 Agent 可以自主调用的工具,由它决定什么时候检索、检索什么、检索几次。

用 LangGraph 或 LangChain 的 Agent 框架,可以把检索器包装成一个 tool,然后让 Agent 自主规划:

from langchain.tools.retriever import create_retriever_tool retriever_tool = create_retriever_tool( rerank_retriever, name="knowledge_search", description="搜索内部知识库,输入自然语言问题,返回相关文档片段" ) tools = [retriever_tool] agent = create_react_agent(llm, tools)

这样 Agent 就能根据问题复杂度,自己决定要不要检索、检索几次。对于简单问题,它可能一次检索就够;对于复杂问题,它可以先检索、再基于结果追问、再检索。

5.2 多轮检索与自我反思

Agentic RAG 真正强大的地方在于自我反思。Agent 可以在检索之后评估“这些资料够不够回答问题”,如果不够就换个查询再检索。这个循环能显著提升复杂问题的回答质量。

我实现过一个简单的反思循环:

  1. Agent 收到问题,生成检索查询。
  2. 检索并评估结果相关性。
  3. 如果相关性不足,改写查询重新检索(最多 3 轮)。
  4. 资料充足后生成答案。

这个循环的代价是延迟增加,所以我会设置最大轮次上限,避免 Agent 陷入无限检索。实测下来,大部分问题 1-2 轮就能解决,只有少数复杂问题需要 3 轮。

5.3 Agentic RAG 的代价与适用边界

Agentic RAG 不是银弹。它的延迟和成本都比固定流程高,因为多了 LLM 的决策调用。对于简单的事实性问题,固定流程反而更快更稳。

我的判断标准是:如果问题需要多步推理、或者需要综合多个来源的信息,就用 Agentic RAG;如果是单点事实查询,固定流程就够了。实际系统里我通常会做路由,让简单问题走快路径,复杂问题走 Agent 路径。

6. 评估与迭代:没有度量就没有优化

6.1 检索质量评估:命中率与召回率

RAG 的评估要分检索和生成两层来看。检索层我主要看两个指标:

  • 命中率(Hit Rate):Top-K 结果里是否包含正确答案所在的片段。
  • 召回率(Recall):所有相关片段中被检索出来的比例。

评估集的构建是个体力活,但值得投入。我的做法是从真实用户问题里采样,人工标注每个问题对应的正确片段,然后跑评估脚本自动计算指标。没有评估集,所有的调优都是盲调。

6.2 生成质量评估:忠实度与相关性

生成层我关注:

  • 忠实度(Faithfulness):答案是否完全基于检索到的资料,有没有编造。
  • 相关性(Answer Relevancy):答案是否切题。
  • 上下文精度:检索到的片段里有多少是真正被用到的。

这些指标可以用 RAGAS 这类框架自动评估,也可以用 LLM 做裁判。我一般先用 LLM 裁判快速跑一轮,对可疑样本再人工复核。

6.3 常见问题速查表

问题现象可能原因排查方向
检索结果答非所问Embedding 模型不匹配 / 切分太碎换模型、调 chunk_size
精确词查不到纯向量检索的缺陷加 BM25 混合检索
答案编造Prompt 约束不足 / 上下文噪声大加强 Prompt、加 Rerank
相似片段重复RRF 去重缺陷加文本相似度去重
复杂问题答不全单轮检索不够上 Agentic RAG 多轮检索
延迟太高多查询 + 多轮检索叠加做问题路由,简单问题走快路径

6.4 迭代节奏与避坑心得

最后分享几条我踩坑换来的经验:

第一,先建评估再调优。我早期调参全靠感觉,改完这个坏那个,后来老老实实建了评估集,才发现很多“优化”其实是负优化。

第二,别一次改多个变量。切分、Embedding、检索参数、Prompt 是相互影响的,一次只改一个,才能知道到底是哪个起了作用。

第三,小步快跑,别追求一步到位。RAG 系统没有“调好”的那一天,它是随着知识库和用户问题不断演进的。先把主流程跑通,再针对最痛的问题逐个击破。

第四,日志要记全。每次检索的查询、返回的片段、最终的答案,都要落库。出问题时这些日志就是你的排查依据,没有日志的线上系统就是个黑盒。

这套流程我从头到尾跑了好几遍,每次重建知识库都会有新的体会。RAG 看起来是“检索+生成”两件事,但真正决定效果的,是中间那一堆看似琐碎的工程细节。把每个环节的“为什么”想清楚,比抄一套能跑的代码重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询