ARTICLE DETAIL

资讯详情

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

RAG检索增强生成实战:从本地知识库搭建到优化与Agent进阶

RAG检索增强生成实战:从本地知识库搭建到优化与Agent进阶 1. 先弄清楚 RAG 是什么再动手1.1 一句话解释和典型误区我最早接触 RAG 这个概念时其实是被各种术语绕晕的。检索增强生成拆开每个字都认识合在一起却不知道它到底解决什么问题。后来自己动手写了一套最小系统才真正理解RAG 核心就一件事——在让语言模型回答之前先从你自己的知识库里把相关资料找出来塞进上下文里再让模型基于这些资料作答。它解决的是大模型“知识停留在某个时间点、也不知道你内部资料”的问题。所以网上经常有人问“RAG 知识库能干什么”本质上问的就是能不能让模型基于我指定的文档回答问题并且回答的时候还能给出来源出处。答案是可以而且可以做得非常轻量。前提是你得知道它并不是把文档“灌进模型”也别指望一个接口调用完就能得到精准的语义检索。RAG 的难点从来不在生成而在检索。很多初学者会陷入几个典型误区。第一个误区是“只要接上向量数据库就是 RAG”结果文档切得乱七八糟、embedding 模型选得随意检索出来一堆无关段落大模型再怎么生成也是错的。第二个误区是“RAG 一定需要昂贵的算力”实际上最基础的一套系统CPU 就能跑中文场景下找一个不错的开源 embedding 模型几百 MB 内存就能用。第三个误区是“RAG 可以完全替代微调”其实两者是互补关系RAG 擅长可更新的知识微调擅长改变模型的风格和固化知识。1.2 适用场景、不适合场景以及我的技术选型思路拿我自己的经验来说RAG 最适合下面这几类场景企业内部文档问答比如制度、手册、产品说明书、合同条款个人知识库管理比如把笔记、微信收藏、PDF、网页链接统一检索客服机器人让模型基于商品资料和政策文件回答用户问题需要引用出处的写作场景比如研究报告、方案策划、法律检索辅助相反如果你需要的是模型“自由发挥”的闲聊、创意写作或者要处理的是实时金融行情这类强时间敏感数据RAG 并不是最优选择。前者不需要外部知识后者需要的是稳定可靠的 API 数据源而不是文档检索。技术选型上我坚持一个原则先从最小可运行的系统开始再逐步加重。很多教程一上来就给你 LangChain 复杂 Agent 多路召回 重排序新手根本分不清哪些环节是必须的哪些是锦上添花。我见过一些人照着教程把代码跑通却连 embedding 是什么都说不清。所以我这篇文章的思路是先给你一套可以直接抄作业的最小实现再在后面的章节里讲解优化点和瓶颈最后扩展到知识图谱、本体、Agent 这些进阶话题。我自己在实际项目中积累的经验是一套“够用”的 RAG 系统只需要四个部分文档加载和拆分、embedding 模型、向量存储与检索、大模型生成。后面我们会一个环节一个环节地做。2. 在本地搭一套最小可运行的环境2.1 安装依赖我默认你在 Mac 上操作其实这套流程在 Windows 和 Linux 上也是一样的。Mac 的独特优势是 Apple Silicon 的内存统一架构比较适合跑本地模型后面会详细说。先建一个干净的虚拟环境避免把系统 Python 搞乱。python -m venv .venv source .venv/bin/activate # Windows 是 .venv\Scripts\activate pip install chromadb sentence-transformers langchain-text-splitters这三个包的分工很明确sentence-transformers负责文本转向量是 RAG 最关键的形象化理解入口chromadb本地持久化向量数据库把向量存下来并用余弦相似度等算法做快速检索langchain-text-splitters文档拆块工具只单独用它的文本分割器而不是整个 LangChain 全家桶保持项目干净如果你只是想试验还可以只装sentence-transformers和numpy用一个 numpy 数组模拟存储但那样每次重启都要重新计算不适合真正的知识库。ChromaDB 的好处是轻量、数据落盘、API 简单十几万条的规模完全没问题。2.2 embedding 模型怎么选embedding 模型是 RAG 的“翻译官”。它把一段文字转换成一串数字这串数字就叫向量。向量在空间里的位置代表了语义两段话意思越接近它们在空间里的距离就越近。你可以把它理解成给每个句子发了一个坐标然后在坐标空间里找邻居。选 embedding 模型我有几条实战标准是否支持中文中英文混合效果如何是否需要 GPUCPU 上能否跑得动模型体积和向量维度语义相似度排行MTEB 之类的中文评测榜单是否开源、本地化部署是否方便我用得比较多的是BAAI/bge-large-zh-v1.5和BAAI/bge-small-zh-v1.5。前者维度 1024效果更好但模型更大后者只有 512 维度速度快适合在 CPU 上跑。它们对中文的支持在开源模型里是比较靠谱的而且 sentencetransformers 库一行代码就能加载。from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5)这里我提一个误区很多人觉得 embedding 模型越大越好其实对于几千条文档的初级项目大模型的收益并不明显反而把查询速度拖下来了。小模型如果检索效果不够更优先的优化方向是换分块策略和做混合检索而不是立刻换大模型。2.3 向量数据库要不要上重型方案初学者最容易纠结向量数据库选型。市面上有 ChromaDB、FAISS、Milvus、Qdrant、Weaviate 一大堆名字多得吓人。我的建议非常直接个人项目、中小规模文档、学习实践用 ChromaDB大批量知识库、微服务架构、需要水平扩展再考虑 Milvus 或 Qdrant只是玩一下不想持久化用 FAISS但它本质上是一个索引库而不是数据库ChromaDB 在本地运行时默认会把数据持久化到磁盘上的一个目录里比如./kb_data所有向量都存在这里。下一次重新启动程序可以直接加载这个目录不需要重新计算一遍 embedding对增量更新也很友好。从测试体验来说Mac 上用 ChromaDB 跑十万条向量的相似度检索返回毫秒级结果完全感知不到压力。3. 从零实现一个完整的 RAG 流水线3.1 文本加载与本地拆解工具不少热搜词都在问“有没有本地的 RAG 文本拆解工具”。其实拆解这个动作本身就是 RAG 流水线的第一步LangChain 的RecursiveCharacterTextSplitter足够处理绝大多数场景。先指定你的源文档路径读取文本然后把它拆成小块。块的“大小”一般是按字符数算中文场景下我习惯用 500 字符左右块与块之间重叠 50 字符。这样做的目的是如果一句话刚好横跨在两个块之间重叠部分可以保证关键词不会被“拦腰斩断”。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , ], length_functionlen, ) chunks text_splitter.split_text(my_text) print(len(chunks))separators这个参数是拆分优先级我习惯先按段落分再按句子分最后才是空格。这样分出来的块基本保持语义完整而不是简单粗暴地每 500 个字切一刀。除此之外格式复杂的 PDF、Word、Excel 也有专门的本地处理工具比如Unstructured可以解析扫描件外的绝大多数格式。这些工具的模型不参与向量计算只负责把文本“洗干净”我一般配合 Pandoc 和 PyMuPDF 做格式转换。3.2 向量入库与检索的细节文本拆完之后进入核心阶段把每个文本块分别拿到 embedding 模型里转成一个向量然后把向量连同原始文本、文档标题等元数据存进 ChromaDB。import chromadb client chromadb.PersistentClient(path./kb_data) collection client.get_or_create_collection( namemy_kb, metadata{hnsw:space: cosine} ) ids [fchunk-{i} for i in range(len(chunks))] embeddings model.encode(chunks, show_progress_barTrue).tolist() collection.add( idsids, embeddingsembeddings, documentschunks, metadatas[{source: manual.pdf, index: i} for i in range(len(chunks))], )这里要特别解释两个细节。第一metadata{hnsw:space: cosine}指的是向量相似度计算使用余弦距离。余弦相似度对文本长度不那么敏感因为它是计算方向而非长度非常适合判断语义相关性。第二model.encode(...).tolist()里面ChromaDB 需要的是 Python list而模型默认输出可能是 numpy 数组这个转换不能省。检索的时候同样把用户问题 encode 成向量然后交给 collection 查最相近的若干块。query 什么是检索增强生成 query_embedding model.encode([query]).tolist() results collection.query( query_embeddingsquery_embedding, n_results3, include[documents, metadatas, distances], ) for doc, meta, dist in zip( results[documents][0], results[metadatas][0], results[distances][0], ): print(f来源: {meta[source]} 距离: {dist:.4f}) print(doc)这段代码会输出三块距离最近的内容。注意distances是余弦距离数值越小代表越接近。在实际项目里我建议设置一个“最小相关阈值”比如距离大于 0.45 的块就不要送进大模型因为语义已经不够相关了。这个阈值需要根据你自己的文档情况试调。3.3 提示词模板和接入大模型生成检索结果不能直接抛给大模型得把它们组织成一个有条理的提示词。核心原则是把检索内容放在一个明确的上下文标签中并告诉模型“只能基于这些内容回答不要编造”。def build_prompt(query, contexts): context_str \n\n---\n\n.join( f[片段 {i1}]\n{ctx} for i, ctx in enumerate(contexts) ) prompt f你是知识库问答助手。请严格基于以下给定的资料片段回答问题。 如果资料中没有相关信息请直接说“根据现有资料无法回答”不要编造。 【参考资料开始】 {context_str} 【参考资料结束】 问题{query} 回答 return prompt生成阶段你可以接 API 型大模型也可以用 Ollama 跑本地模型。这里我强烈建议初学者先用 Ollama因为 Ollama 在 Mac 上跑qwen2.5:7b这类模型非常方便而且不会产生 API 费用。ollama pull qwen2.5:7b接上之后整个流程就完整了import requests import json def generate_with_ollama(prompt): resp requests.post( http://localhost:11434/api/generate, json{model: qwen2.5:7b, prompt: prompt, stream: False}, ) return resp.json()[response]从加载文档到最终输出答案这套最小实现已经可以在 Mac 本地全部跑通不需要任何公网 API。我建议你亲手跑一遍因为只有真正看到“文档被检索、被引用、模型基于它回答”的全过程你才能理解 RAG 到底在哪里起作用。4. 检索优化的关键点和 RAG 瓶颈排查4.1 分块参数不只是大小很多初学者把分块当成一个简单的字符切分实际上分块质量直接决定了检索质量。分块太大比如一次 2000 字向量表示的信息过于混杂查询时很难命中精确答案分块太小比如 100 字又会导致上下文不足模型拿到一堆碎片拼不出完整含义。我总结出来的经验是不同文档类型应该用不同策略规章制度、合同按条款和段落分尽量保留原结构产品说明、FAQ按问答对分一个问答一个块学术论文、长文500 字符加 50 重叠效果比较折中代码仓库按函数或类分普通文本分块器并不适用还有一种更高级的做法叫“父子分块”就是把文档先按大粒度分块用于向量检索检索到的大块再切分成更小的块给大模型。我实际测试下来这个方案在需要长上下文记忆的场景表现不错代价是索引和检索逻辑复杂度上了一个台阶。初学者先把格式拆干净、大小调对比什么都强。4.2 混合检索与重排序向量检索是语义搜索它能理解同义词。比如查“笔记本”能搜到包含“laptop”的内容。但纯向量检索也有盲点比如产品型号R4500X这种精确字符串语义模型几乎无法有效表示它更擅长的是语义相似而不是精确匹配。这时候就需要混合检索。把 BM25 这类传统全文检索和向量检索结合起来先各自召回一批候选再合并去重。BM25 是“精确词匹配”的老技术对型号、代码、人名这类信号非常敏感。# 示意理想情况下用 ElasticSearch 或 SQLite FTS5 做 BM25 # 这里用最简单的方式举一个合并逻辑 def hybrid_search(query, top_k5): dense_results vector_search(query, top_ktop_k * 2) bm25_results bm25_search(query, top_ktop_k * 2) combined {doc_id: score for doc_id, score in dense_results} for doc_id, score in bm25_results.items(): combined[doc_id] combined.get(doc_id, 0) score * 0.3 ranked sorted(combined.items(), keylambda x: x[1], reverseTrue) return ranked[:top_k]混合检索之后通常还会跟一个“重排序”环节。向量检索初次召回的候选可能有几十条把这几十条全部交给一个轻量级reranker模型重新打分只保留前 3 到 5 条给大模型可以显著提高答案质量。我推荐BAAI/bge-reranker-v2-m3它专门做查询与片段的匹配打分。4.3 哪些问题是 RAG 真正绕不过去的瓶颈这里回到热搜里的“rag 瓶颈”。我梳理了实际项目中最常见的四类问题并给出可操作的排查思路瓶颈类型表现排查思路与对策分块语义割裂检索到的片段看着相关但接不上上下文调整 separators保留段落结构改用父子分块精确词匹配失败型号、编号、人名搜不到混合 BM25 或全文检索长上下文信息丢失问题复杂模型被无关片段干扰重排序只留 top-3提示词中强制编号引用幻觉仍然存在模型回答内容不在检索片段里检查提示词是否明确“禁止编造”增加距离阈值过滤此外知识库更新也是个容易忽略的瓶颈。实际业务中文档经常变动如果删改文档后还沿用旧向量检索结果就会错乱。ChromaDB 的增删改其实很灵活但要写一套“增量索引”逻辑比如按来源文件的 hash 判断哪些文档需要重新切分和编码这套基建才是知识库长期可用的关键。5. 特殊场景图片知识库、结构化知识库与本体5.1 RAG 知识库能存储图片吗热搜里有“rag知识库能存储图片嘛”答案是可以而且有两条主流路径。第一条路径是“图片 OCR 转文本之后进向量库”。图片本身就是一个文档先通过 OCR 提取文字再按普通文本流程走。适用场景是截图、扫描件、含文字的流程图。第二条路径是“图片和文本统一向量空间”。专门有一个多模态 embedding 模型族叫 CLIP它能把图片和文字映射到同一个向量空间。当用户搜“会议室里一张白色桌子的照片”这个文本可以转化成向量在固定向量空间里和图片向量做相似度比对。使用起来也很直接from sentence_transformers import SentenceTransformer model SentenceTransformer(clip-ViT-B-32) img_emb model.encode(Image.open(/path/to/photo.jpg)) text_emb model.encode(会议室里一张白色的桌子)然后把图片向量同样存进 ChromaDB元数据里记录图片的路径。查询时用文本向量去检索就能拿到最接近的图片。这种方案的代价是模型体积比较大特征提取速度也慢一些但它确实实现了真正的“图片知识库”。5.2 RAG 知识库、结构知识库和知识图谱怎么区分这是一个非常好的热搜问题。很多人把“知识库”这个概念混在一起实际上三类东西的定位完全不同类型存储结构代表技术最适合的场景RAG 知识库非结构化文本块向量数据库文档问答、语义搜索、开放域问题结构化知识库表结构、字段SQL、Airtable、Excel订单查询、库存管理、人事档案知识图谱实体与关系Neo4j 等图数据库人际关系、供应链、复杂关联推理我在一个项目里同时遇到过这三种需求。用户先问“公司有哪些员工”这是结构化查询直接查 SQL 最稳又问“员工张三参与了哪些项目项目又依赖哪些同事”这是关系推理应该查知识图谱最后问“这些项目文档里的风险点有哪些”这才是 RAG 的活。很多初学者想用一个向量数据库解决所有问题这是不对的。RAG 擅长的是读文档、做语义联想而不是做精确计算和关系推理。正确做法是把三者打通先用 RAG 理解用户的复杂问题再根据实体抽取结果去查结构化数据或图谱最后把结果汇总给大模型生成自然语言答案。5.3 本体在 RAG 里起什么作用热搜里的“ontology rag”看起来偏学术但在实际项目中本体可以理解为“领域概念词典概念关系网”。比如在汽车维修领域“制动系统”包含“刹车片”“刹车盘”“制动液”“刹车片”又和“磨损”“更换周期”相关。把这些概念和关系整理成结构化定义就是一个小型本体。本体在 RAG 里最大的作用不是检索而是查询改写和路由。用户问“刹车不太灵敏怎么办”系统如果只做向量检索可能搜出来的全是“刹车片价格”因为没有把“不灵敏”和“磨损检查”联系起来。如果本体里定义了“不灵敏”是“制动异常”的子概念而“制动异常”关联“刹车片磨损检查”就可以先改写查询词再去做向量检索。这个思路不需要复杂的图数据库一个简单的概念映射词典加上规则引擎就能实现。实际落地时我建议先只做一层“同义词扩展”把用户问题里的术语替换成本体定义的规范术语再交给向量检索效果往往就有明显改善。6. 从 RAG 到智能体迭代检索与自我纠错6.1 为什么需要 Agent 化基础 RAG 有一个天然问题一次性检索就不回头了。如果第一轮检索到的片段不够好模型也硬着头皮回答或者干脆说“资料不足”。实际复杂问题往往是多跳的比如“A 方案里提到的那个客户他所在行业的政策风险是什么”这个问题需要先在文档里定位“A 方案里的客户”再关联“行业政策风险”两步检索才能完成。这时候把 RAG 做成一个可以反复迭代、判断是否需要重新检索的智能体效果会更稳定。6.2 一个轻量级的 Agent 化 RAG 实现我不建议初学者一上来就接 LangGraph、AutoGen 这类重框架先用一个 while 循环就能体会到 Agent 化的价值。核心逻辑是检索-生成-评判-决定是否重检索。def agentic_rag(question, llm, retriever, max_rounds3): conversation_history [] for turn in range(max_rounds): docs retriever(question) prompt build_agent_prompt(question, docs, conversation_history) answer llm(prompt) verdict judge(answer) # 简单规则是否包含“不确定”“无法回答”等词 if verdict: return answer, docs conversation_history.append((检索片段不充分需要继续, answer)) question rewrite_question(question, docs, answer) fallback llm(build_fallback_prompt(question)) return fallback, []代码里judge可以是一个很简单的规则判断比如回答里出现“根据现有资料无法回答”就触发第二轮检索也可以用一个小模型专门打分判断答案是否覆盖了问题。在这一过程中rewrite_question是关键函数它把上一轮检索到的信息整合进新的问题描述里比如原问题加上“结合刚才提到的方案文档背景”这样第二轮检索可以比第一轮更有针对性。我把这种模式称为“带着答案去问下一个问题”。6.3 多轮检索的坑和本地工具的收尾建议Agent 化 RAG 也有明显的坑。第一是成本放大每一轮都要做 embedding 和 LLM 生成轮次多了会非常慢第二是问题改写可能偏离原始意图多轮之后反而越跑越偏。我自己的经验是每个问题最多跑三轮第三轮仍然失败就直接兜底回答不要无限循环。最后回应一下“有没有本地的 RAG 文本拆解工具”。这类工具其实分两层一层是格式解析工具推荐Unstructured、PyMuPDF、markitdown负责把 PDF、Word、网页变成干净文本另一层是文本切分工具LangChain 里的RecursiveCharacterTextSplitter够用如果想要更多控制可以直接自己写正则按章节切。Mac 上这些工具全部能原生运行连 PDF 解析都不需要额外装大型软件整个流程完全可以离线完成。我个人的体会是RAG 的入门关键是“亲手跑通一次最小系统”然后在此基础上做一个优秀的“文档清理 分块工程师”再去谈重排序、知识图谱和 Agent。如果你把这一篇里的代码在自己的 Mac 上跑通一遍接下来再去看任何高级 RAG 框架都会觉得它们的每个抽象层其实都建立在今天这些基础操作之上。最后分享一个小技巧把你整个知识库中最难回答的 20 个问题先记录下来每次调整检索策略前后都跑一遍这 20 题用答案质量的变化而不是单个案例来评估优化效果这样你的系统才能持续稳定地变好。
返回列表