ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零手搓RAG全流程:建库、检索、生成与Agentic RAG实战

从零手搓RAG全流程:建库、检索、生成与Agentic RAG实战 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 里的页眉页脚、表格错位、换行符乱入这些噪声在切分之后会变成一个个语义残缺的片段向量化出来的结果自然也是歪的。我的处理顺序通常是这样的先做格式归一化把 PDF、Word、Markdown 统一转成纯文本或结构化 Markdown。PDF 我一般用pymupdf或pdfplumber表格多的文档优先用后者它对表格的还原更靠谱。去掉重复噪声页眉页脚、页码、水印文字这些在每一页都出现的内容要批量剔除。我的做法是统计高频重复行出现次数超过总页数 60% 的短行直接删掉。修复断行中文文档里经常出现一句话被硬换行切断的情况用正则把“非标点结尾的换行”合并掉能显著提升切分质量。提示清洗这一步不要追求一步到位先做能明显提升效果的去页眉页脚、修断行剩下的边角问题等检索效果评估出来再针对性处理。清洗完之后我建议保留一份带元数据的结构化文档比如每个段落记录它来自哪个文件、哪一页、哪个章节标题。这些元数据在后面做引用溯源和过滤检索时会非常有用千万别图省事只留纯文本。2.2 切分策略固定长度、递归、语义切分怎么选切分是建库里最容易被低估的环节。切得太碎语义不完整切得太大检索精度下降还浪费 token。我实测下来递归字符切分RecursiveCharacterTextSplitter在大多数场景下是性价比最高的选择它按段落、句子、词的优先级递归尝试尽量在语义边界处断开。关于参数我的一般起点是参数推荐值说明chunk_size500-800 字符中文场景偏小英文可到 1000chunk_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( documentschunks, embeddingembedding_model, clientclient, collection_namemy_knowledge_base )Qdrant 我一般用 Docker 起一个服务然后通过 LangChain 的QdrantVectorStore接入它最大的好处是支持带条件的向量检索比如“只在某个部门的文档里搜”这在企业场景里非常实用。3. 检索阶段让“捞得准”成为常态3.1 向量检索的先天缺陷与混合检索的必要性纯向量检索有个绕不开的问题它对精确匹配不敏感。比如你问“XX-2000 型号的参数”向量检索可能给你返回一堆“XX 系列产品”的泛泛描述因为它在语义上觉得这些很像但那个精确的型号词被淹没了。解决办法就是混合检索Hybrid Search向量检索负责语义召回关键词检索BM25负责精确匹配两路结果融合。LangChain 里的EnsembleRetriever就是干这个的它用 RRFReciprocal 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( retrieverensemble_retriever, llmllm )多查询的代价是检索次数翻倍延迟会上升。我的经验是在离线评估阶段用多查询找出最优的查询模式线上则根据问题复杂度动态决定是否启用别一股脑全开。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_nameBAAI/bge-reranker-base) compressor CrossEncoderReranker(modelmodel, top_n5) rerank_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverensemble_retriever )提示Rerank 模型和 Embedding 模型最好来自同一系列它们的语义空间更匹配实测效果通常更稳。3.4 检索参数调优k 值、阈值与元数据过滤检索阶段有几个参数需要反复调召回数量 k太小会漏太大会引入噪声。我一般召回 20-30 个候选重排后取 3-5 个。相似度阈值低于阈值的片段直接丢弃避免“矮子里拔将军”。但阈值设太高会导致召回不足需要根据实际分布来定。元数据过滤如果知识库有明确的分类检索时加上过滤条件能大幅提升精度。比如只搜“技术文档”类别或者只搜某个时间之后的文档。我踩过的一个坑是相似度阈值的绝对值在不同 Embedding 模型之间不可比。换模型之后原来 0.7 的阈值可能就完全不适用了必须重新校准。所以我在代码里会把阈值做成配置项方便切换模型时快速调整。4. 生成阶段上下文组装与 Prompt 设计4.1 上下文组装的顺序与去重检索回来的片段怎么拼进 Prompt也是有讲究的。我的原则是按相关性排序重排后的顺序就是相关性顺序最相关的放最前面。去重内容高度重叠的片段只保留一个避免浪费 token。加来源标记每个片段前面加上来源信息文件名、页码方便模型引用也方便用户溯源。控制总长度根据模型的上下文窗口留出足够空间给回答别把窗口塞满。一个典型的组装格式长这样【资料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, nameknowledge_search, description搜索内部知识库输入自然语言问题返回相关文档片段 ) tools [retriever_tool] agent create_react_agent(llm, tools)这样 Agent 就能根据问题复杂度自己决定要不要检索、检索几次。对于简单问题它可能一次检索就够对于复杂问题它可以先检索、再基于结果追问、再检索。5.2 多轮检索与自我反思Agentic RAG 真正强大的地方在于自我反思。Agent 可以在检索之后评估“这些资料够不够回答问题”如果不够就换个查询再检索。这个循环能显著提升复杂问题的回答质量。我实现过一个简单的反思循环Agent 收到问题生成检索查询。检索并评估结果相关性。如果相关性不足改写查询重新检索最多 3 轮。资料充足后生成答案。这个循环的代价是延迟增加所以我会设置最大轮次上限避免 Agent 陷入无限检索。实测下来大部分问题 1-2 轮就能解决只有少数复杂问题需要 3 轮。5.3 Agentic RAG 的代价与适用边界Agentic RAG 不是银弹。它的延迟和成本都比固定流程高因为多了 LLM 的决策调用。对于简单的事实性问题固定流程反而更快更稳。我的判断标准是如果问题需要多步推理、或者需要综合多个来源的信息就用 Agentic RAG如果是单点事实查询固定流程就够了。实际系统里我通常会做路由让简单问题走快路径复杂问题走 Agent 路径。6. 评估与迭代没有度量就没有优化6.1 检索质量评估命中率与召回率RAG 的评估要分检索和生成两层来看。检索层我主要看两个指标命中率Hit RateTop-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 看起来是“检索生成”两件事但真正决定效果的是中间那一堆看似琐碎的工程细节。把每个环节的“为什么”想清楚比抄一套能跑的代码重要得多。
返回列表