ARTICLE DETAIL

资讯详情

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

AI Agent知识获取管道:RAG基础与混合检索实战

AI Agent知识获取管道:RAG基础与混合检索实战 1. 为什么知识获取管道是 AI Agent 的第一道生死线做 AI Agent 的人迟早会撞上一堵墙模型本身很聪明但你问它公司内部某个产品的退货政策它要么胡编要么说“我无法访问你的数据”。这不是模型不行而是它缺一条知识获取管道。RAGRetrieval-Augmented Generation检索增强生成就是目前工程上最成熟、性价比最高的那条管道。我做了几个 Agent 项目之后有个很深的体会Agent 的能力上限往往不取决于你选了哪个大模型而取决于你喂给它的知识有多准、多快、多干净。一个用中等模型但 RAG 管道打磨得很扎实的 Agent实际表现会碾压一个用顶配模型但知识管道稀烂的 Agent。原因很简单——模型负责推理和表达知识管道负责事实供给两者是乘法关系任何一边趋近于零整体就趋近于零。这篇是“走进 AI Agent”系列的第四篇专门讲 RAG 基础。我会把知识获取管道拆成几个关键环节文档怎么进来、怎么切、怎么变成向量、怎么存、怎么检索、怎么塞回给模型。每个环节我都会说清楚为什么这么设计以及我在实操中踩过的坑。适合正在从 0 到 1 搭建 AI Agent 的开发者也适合已经跑通了 demo 但发现“检索命中率上不去”的同行。先给一个整体认知RAG 的本质是用检索替代记忆。大模型的参数里存的是通用世界知识但存不下你私有的、实时的、细粒度的知识。与其花大价钱去微调不如在推理时临时把相关资料“查出来”塞进上下文。这个思路听起来简单但工程细节极多下面逐个拆。2. RAG 管道的整体设计与方案选型2.1 一条完整的知识获取管道长什么样我把 RAG 管道分成两个阶段离线索引阶段和在线检索阶段。离线阶段负责把原始文档变成可检索的向量库在线阶段负责根据用户问题把最相关的片段捞出来。离线索引的流程是文档加载 → 文本清洗 → 分块chunking→ 嵌入embedding→ 存入向量库。在线检索的流程是用户提问 → 问题嵌入 → 向量相似度检索 → 重排序可选→ 组装上下文 → 交给 LLM 生成。很多人一上来就纠结“用哪个向量库”其实这是最不该先纠结的问题。真正决定 RAG 效果的是分块策略和嵌入模型的选择向量库只是存储和检索的容器主流几个性能差距没有想象中那么大。我见过太多项目在向量库选型上花了两周结果分块用的是固定 1000 字符硬切检索命中率惨不忍睹。2.2 稠密嵌入与稀疏嵌入不是二选一而是搭配用热词里提到的稠密嵌入dense embedding和稀疏嵌入sparse embedding是理解 RAG 检索质量的关键。稠密嵌入把一段文本映射成一个几百到上千维的稠密向量每一维都是浮点数。它的优势是语义匹配——用户问“怎么退款”文档里写的是“如何申请退货”字面不一样但语义接近稠密向量能匹配上。代表模型有各种 text-embedding 系列。稀疏嵌入则是高维稀疏向量大部分维度是零只有出现的词对应的维度有值。它的优势是关键词精确匹配——用户搜“订单号 A12345”稀疏检索能精准命中稠密向量反而可能因为语义泛化而漏掉。BM25 就是经典的稀疏检索算法。我的实操结论是两者搭配用效果远好于单用任何一个。这就是所谓的混合检索hybrid search。做法是分别用稠密和稀疏各召回一批候选然后用 RRFReciprocal Rank Fusion倒数排名融合把两路结果合并。RRF 的公式很简单对每个文档把它在各路检索中的排名取倒数再求和score(d) Σ 1 / (k rank_i(d))其中 k 通常取 60rank_i(d) 是文档 d 在第 i 路检索中的排名。这个公式的好处是不需要归一化两路分数直接融合排名工程上非常省心。2.3 为什么我不建议一上来就上 GraphRAG热词里有 graphrag、ontology rag、rag graphrag llm wiki 本体 rag 这些词说明大家都在关注知识图谱增强的 RAG。我的建议是除非你的知识本身高度结构化、实体关系密集否则别一上来就上 GraphRAG。GraphRAG 的核心思路是把文档抽成实体和关系构建知识图谱检索时沿着图走。它在处理“A 和 B 是什么关系”“跨文档的多跳推理”这类问题上确实强。但代价是构建图谱需要额外的 LLM 抽取成本图谱维护复杂检索链路长调试困难。我做过一个对比同样一批文档朴素 RAG 从零到能用大概两天GraphRAG 从零到能用至少两周而且中间会遇到实体消歧、关系抽取错误等一堆问题。所以我的选型原则是先用朴素 RAG 跑通把分块和嵌入调好等遇到明确的多跳推理瓶颈再考虑引入图谱。不要为了技术先进性而牺牲工程可控性。3. 核心细节解析与实操要点3.1 文档加载与清洗脏数据是命中率的第一杀手文档加载看起来是最没技术含量的环节但它决定了后面所有环节的上限。我踩过最惨的坑是PDF 里的表格被解析成了一堆乱序文本导致检索出来的片段完全没法用。不同格式的文档要用不同的加载器。纯文本和 Markdown 直接读PDF 建议用能保留版面结构的解析器比如基于版面分析的方案而不是简单的文本抽取Word 和 HTML 要先把标签和样式剥掉扫描件必须先做 OCR。清洗环节我通常会做这几件事去掉页眉页脚、页码、水印文字这些内容会在每个 chunk 里重复出现严重干扰检索合并被硬换行打断的句子PDF 解析经常把一句话拆成好几行统一全角半角、去除多余空白把表格转成结构化的 Markdown 表格或键值对而不是让它散成一堆词注意清洗不要过度。我见过有人把标点符号全删了结果嵌入模型对语义边界的判断变差。清洗的目标是去噪不是把文本变成一坨没有结构的词。3.2 分块策略RAG 效果的分水岭分块是 RAG 里最需要花心思的地方。切得太大一个 chunk 里混了好几个主题检索出来噪声多切得太小一个完整的语义单元被拆散模型拿到手也拼不出完整答案。我常用的分块策略是递归字符分割 语义边界优先。具体做法是先按段落分段落太长再按句子分句子还长才按字符硬切。分隔符的优先级是\n\n→\n→。→→→→ 空格。这样能最大程度保证每个 chunk 是一个语义完整的单元。chunk 大小我一般从500 到 800 字符起步overlap重叠设成 chunk 大小的 10% 到 15%。overlap 的作用是防止关键信息正好卡在两个 chunk 的边界上被切断。比如一句话前半段在 chunk A 结尾后半段在 chunk B 开头有了 overlap两个 chunk 都能包含完整句子。但这里有个反直觉的点chunk 大小没有万能值要按文档类型调。技术文档、法律条文这种逻辑密集的chunk 可以小一点400 到 600 字符叙事性的、上下文依赖强的chunk 要大一点800 到 1200 字符。我的做法是先跑一批测试问题看命中率再微调。还有一种进阶做法是父子分块parent-child chunking索引时用小的子 chunk 做检索命中后返回它所属的大的父 chunk 给 LLM。这样检索精度高同时给模型的上下文又足够完整。这个技巧在长文档场景下特别好用。3.3 嵌入模型选型别只看排行榜嵌入模型决定了文本被映射到向量空间后的质量。选型时我会看三个维度语义区分度、维度成本、语言支持。语义区分度就是模型能不能把语义相近的文本映射到相近的位置。这个不能只看 MTEB 排行榜因为排行榜的测试集未必和你的领域匹配。我的做法是拿 20 到 30 个自己领域的真实问题配上正确答案所在的文档片段手动测一下检索命中率。这个自建测试集比任何排行榜都靠谱。维度成本方面向量维度越高存储和检索开销越大。768 维和 1536 维在效果上可能只差几个百分点但存储成本差一倍。如果数据量不大用高维没问题如果上百万文档就要权衡了。语言支持上如果你的文档是中英混合一定要选多语言模型否则中文检索会明显拉胯。我实测过纯英文模型处理中文文档检索命中率能掉一半以上。提示嵌入模型和 LLM 是两回事可以分开选。嵌入模型不需要生成能力所以可以选小而专的成本低、速度快。别用生成模型去做嵌入那是杀鸡用牛刀还慢。3.4 向量库选型够用就好向量库的选择我按数据规模分数据规模推荐方案理由万级以下内存向量库或本地文件零运维够快十万到百万级单机向量数据库支持持久化和索引优化百万级以上分布式向量库需要水平扩展和副本我特别想说的是别过早引入分布式向量库。很多项目文档才几千条就上了集群方案结果运维成本比开发成本还高。向量检索在百万级以下单机方案完全扛得住。等真的到了瓶颈再迁移迁移成本也没想象中那么高因为向量数据本身是标准格式。索引类型上HNSW分层可导航小世界图是目前最常用的近似最近邻索引查询快、召回率高代价是内存占用大。如果内存紧张可以用 IVF 系列索引用召回率换内存。这个取舍要看你的场景如果对召回率要求极高比如法律、医疗用 HNSW如果数据量巨大且能接受少量漏召回用 IVF。4. 实操过程与核心环节实现4.1 从零搭一条最小可用管道我用 Python 生态举例因为这是目前最成熟的。整体流程分五步。第一步加载文档。假设文档是 Markdown 格式直接读文件内容按标题层级做初步切分。import os def load_docs(root_dir): docs [] for dirpath, _, filenames in os.walk(root_dir): for fn in filenames: if fn.endswith(.md): path os.path.join(dirpath, fn) with open(path, r, encodingutf-8) as f: docs.append({path: path, text: f.read()}) return docs第二步分块。实现递归字符分割按分隔符优先级逐级降级。def split_text(text, chunk_size600, overlap80): separators [\n\n, \n, 。, , , , ] chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) if end len(text): best -1 for sep in separators: pos text.rfind(sep, start, end) if pos best: best pos if best start: end best 1 chunks.append(text[start:end]) start end - overlap if end - overlap start else end return chunks这段代码的逻辑是先按 chunk_size 划一个窗口然后在窗口内从后往前找优先级最高的分隔符把切点定在那里。这样能保证尽量在语义边界处切开。overlap 通过回退 start 实现。第三步嵌入。调用嵌入模型把每个 chunk 转成向量。这里要注意批量处理一次传一批文本比逐条传快得多。def embed_chunks(chunks, embed_fn, batch_size32): vectors [] for i in range(0, len(chunks), batch_size): batch chunks[i:ibatch_size] vectors.extend(embed_fn(batch)) return vectors第四步存入向量库。以本地方案为例把向量和原文、元数据一起存。import json def save_index(chunks, vectors, pathindex.json): data [{text: c, vector: v} for c, v in zip(chunks, vectors)] with open(path, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse)第五步检索。用户提问后把问题嵌入和库里所有向量算余弦相似度取 top-k。import numpy as np def cosine_sim(a, b): a, b np.array(a), np.array(b) return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) def retrieve(query_vec, index, top_k5): scored [(cosine_sim(query_vec, item[vector]), item[text]) for item in index] scored.sort(keylambda x: x[0], reverseTrue) return scored[:top_k]这套代码在几千条文档的规模下完全够用。数据量上去之后把最后一步换成向量库的检索接口即可前面的流程不变。4.2 混合检索的落地实现单用稠密检索遇到专有名词、编号、代码标识符容易漏。加上稀疏检索能补上这块。我用 BM25 做稀疏那一路。from rank_bm25 import BM25Okapi def build_bm25(chunks): tokenized [list(c) for c in chunks] # 中文按字切英文按词切需另处理 return BM25Okapi(tokenized) def hybrid_retrieve(query, query_vec, chunks, bm25, index, top_k5, k60): dense_ranked retrieve(query_vec, index, top_k20) dense_ids [chunks.index(t) for _, t in dense_ranked] bm25_scores bm25.get_scores(list(query)) sparse_ids sorted(range(len(chunks)), keylambda i: bm25_scores[i], reverseTrue)[:20] fused {} for rank, idx in enumerate(dense_ids): fused[idx] fused.get(idx, 0) 1 / (k rank) for rank, idx in enumerate(sparse_ids): fused[idx] fused.get(idx, 0) 1 / (k rank) ranked sorted(fused.items(), keylambda x: x[1], reverseTrue)[:top_k] return [(chunks[i], s) for i, s in ranked]这里中文的 BM25 分词是个坑。中文没有天然空格按字切虽然能用但效果一般更好的做法是接一个中文分词器。如果不想引入额外依赖按字切也能凑合因为 BM25 主要靠词频和逆文档频率单字也有区分度。4.3 上下文组装把检索结果喂给 LLM检索出 top-k 片段后不能直接一股脑塞给 LLM要组装成结构清晰的上下文。我的模板是这样的根据以下资料回答问题。如果资料中没有相关信息请明确说明不知道不要编造。 【资料 1】 {chunk_1} 【资料 2】 {chunk_2} 【问题】 {user_query}这个模板有两个关键点。一是明确告诉模型“不知道就说不知道”这能大幅降低幻觉。二是给每个片段编号方便模型引用也方便你调试时定位是哪个片段起了作用。top-k 取多少也有讲究。取太少可能漏掉关键信息取太多噪声增加还会挤占上下文窗口。我一般取 3 到 5 个如果 chunk 比较小可以取到 8 个。判断标准是把检索结果拼起来看总长度是否超过模型上下文窗口的 30%。超过就说明取多了。5. 常见问题与排查技巧实录5.1 检索命中率上不去先查这五个地方命中率是 RAG 的核心指标。我遇到命中率低的时候会按这个顺序排查排查项常见问题解决方向分块chunk 太大混主题太小断语义调整大小和 overlap试父子分块嵌入模型领域不匹配语言不支持换模型自建测试集验证清洗页眉页脚噪声表格乱序加强清洗表格结构化检索方式单路检索漏召上混合检索查询用户问题太短或太口语做查询改写或扩展我特别想强调查询改写这一项。用户问“那个退款的事咋弄”直接拿这句去检索效果往往很差。做法是用 LLM 先把问题改写成更规范的检索式比如“如何申请退款退款流程是什么”再去检索。这一步能明显提升命中率成本也不高。5.2 检索到了但答案不对问题出在重排序有时候检索确实把相关片段捞出来了但排在了后面top-k 截断时被切掉了。这时候要引入重排序rerank。重排序的思路是先用向量检索快速召回一批候选比如 20 个再用一个更精细的模型对这 20 个逐一打分重新排序取前几个。重排序模型通常比嵌入模型大但只对少量候选打分所以总开销可控。我用过的重排序方案里交叉编码器cross-encoder效果最好。它把问题和文档拼在一起输入模型直接输出相关性分数比向量点积精细得多。代价是慢所以只用在召回后的精排阶段。提示重排序是性价比很高的优化。如果你的 RAG 已经跑通但效果差口气加一层重排序往往能立竿见影比换嵌入模型省事。5.3 知识更新了但检索还是旧内容这是索引没更新的问题。RAG 的离线索引和在线检索是分离的文档更新后必须重新索引。我的做法是给每个文档记录一个内容哈希更新时只重新处理哈希变化的文档避免全量重建。如果是增量更新还要注意删除旧向量。我见过有人只加新向量不删旧的结果同一个文档出现两个版本检索时新旧混在一起模型都懵了。5.4 长文档检索效果差试试分层索引一份几百页的手册直接切块检索往往捞不到最相关的部分。这时候可以用分层索引先对文档做摘要用摘要建一层粗索引检索时先命中相关文档再在文档内部做细检索。这相当于先定位到章节再定位到段落符合人查资料的习惯。5.5 几个我踩过的坑第一个坑是过度依赖默认参数。很多框架的默认 chunk_size 是 1000overlap 是 200这个值对某些文档合适对另一些就是灾难。一定要按自己的数据调。第二个坑是忽略元数据。chunk 除了文本和向量还应该存来源、章节、时间等元数据。检索时可以按元数据过滤比如只搜某个产品线的文档。没有元数据后面想加过滤功能就得重建索引。第三个坑是不做评估。RAG 效果好不好不能靠感觉。我建议至少维护一个 30 到 50 条的问题集每次改动后跑一遍看命中率和答案质量的变化。没有评估优化就是盲人摸象。第四个坑是把 RAG 当万能药。有些问题根本不适合 RAG比如需要精确计算的、需要实时数据的、需要多步推理的。这些场景要么接工具调用要么走别的方案。RAG 擅长的是“从已有文档里找事实”别让它干它不擅长的活。6. 从基础 RAG 到 Agentic RAG 的演进方向把基础 RAG 跑通之后下一步自然是让它和 Agent 结合也就是热词里的 agentic rag。基础 RAG 是“一问一检索一答”的固定流程而 Agentic RAG 把检索变成了 Agent 的一个工具Agent 可以自己决定什么时候检索、检索几次、要不要换个查询再检索。这个演进的价值在于多跳推理。比如用户问“我们去年销量最好的产品的退货政策是什么”基础 RAG 一次检索很难同时命中“销量最好”和“退货政策”两个信息。Agentic RAG 可以分两步先检索出销量最好的产品是哪个再拿这个产品名去检索退货政策。实现上就是把检索封装成一个函数注册给 Agent 作为工具。Agent 的规划能力会决定它怎么调用。这里的关键是给检索工具写清楚描述告诉 Agent 这个工具能查什么、返回什么格式Agent 才能用对。另一个方向是查询分解。把复杂问题拆成几个子问题分别检索再综合。这个可以在检索前用 LLM 做也可以让 Agent 在推理过程中动态做。我个人在实际操作中的体会是基础 RAG 解决的是“知识从哪来”的问题Agentic RAG 解决的是“知识怎么用”的问题。前者是管道后者是调度。管道没修好调度再花哨也没用。所以我的建议始终是先把基础 RAG 的每个环节打磨扎实再往上叠 Agent 的能力。分块、嵌入、混合检索、重排序这几样调好了你的 Agent 就已经超过了市面上大部分 demo 级产品。最后分享一个小技巧调试 RAG 时把每次检索的 query、召回的 chunk、最终的答案都打日志存下来。跑一段时间后你会从这些日志里发现大量优化线索——哪些问题总是检索不到、哪些 chunk 总是被误召回、哪些查询改写效果差。这些真实数据比任何理论分析都有价值。
返回列表