尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

AI Agent知识获取管道:RAG混合检索与重排实战 1. 为什么知识获取管道是 AI Agent 的分水岭做 AI Agent 开发的人绕不开一个尴尬的现实模型本身很聪明但它对你私有的业务知识一无所知。你问它公司内部的报销流程它给你编一个看起来很像那么回事的答案你让它查某个产品的技术参数它张口就来一段似是而非的描述。这不是模型不行而是它的知识边界停在了训练数据截止的那一天而你的业务知识每天都在更新。知识获取管道要解决的就是这个问题。它的核心任务只有一句话在 Agent 需要知识的时候把正确、最新、可溯源的信息准确送到模型的上下文里。而RAGRetrieval-Augmented Generation检索增强生成就是目前最成熟、最工程化的一条实现路径。我见过太多团队在搭 Agent 时把 90% 的精力花在 Prompt 调优和工具编排上结果上线后用户一问具体业务问题就翻车。根因往往不在生成端而在检索端——知识根本没被正确地喂进去。所以我把 RAG 放在这个系列的第四篇因为它是 Agent 从能聊天走向能干活的关键基础设施。这篇文章适合三类人正在从 0 到 1 搭建 AI Agent 的开发者、需要给企业级应用接入私有知识库的工程师、以及想搞清楚 RAG 到底怎么回事的产品和技术负责人。我会从整体设计思路讲到稠密嵌入与稀疏嵌入的选型再到完整的实操流程和踩坑记录尽量把每个为什么都讲透。2. RAG 知识获取管道的整体设计与思路拆解2.1 一条完整的知识管道到底包含哪些环节很多人对 RAG 的理解停留在把文档切块、向量化、存进向量库、检索出来拼进 Prompt这四步。这个理解不算错但太粗了粗到落地时处处是坑。一条真正能扛住生产环境的知识获取管道至少包含六个环节数据接入从各种来源文件、数据库、API、网页把原始知识捞进来解析与清洗把 PDF、Word、HTML 这些格式拆成纯文本去掉页眉页脚、乱码、重复内容切分Chunking把长文本切成适合检索和嵌入的片段嵌入Embedding把文本片段转成向量这里就涉及稠密嵌入和稀疏嵌入的选择存储与索引把向量和原文存进向量数据库建立可快速检索的索引检索与重排根据用户 query 召回候选片段再用重排模型精排最后送给 LLM这六个环节里任何一个掉链子最终效果都会崩。我见过最典型的案例是切分策略没设计好一个完整的操作步骤被从中间切断检索时只召回了一半模型拿着半截信息生成答案用户照着做直接出错。2.2 为什么是 RAG 而不是微调这是每个团队都会问的问题。我的答案很直接绝大多数场景下RAG 的性价比远高于微调。微调的本质是把知识烧进模型参数里代价是每次知识更新都要重新训练成本高、周期长而且模型对训练时没见过的知识依然无能为力。RAG 的本质是把知识放在外部模型只负责阅读理解知识更新只需要更新向量库分钟级就能生效。打个比方微调像是把整本教材背下来的学生背完就固定了RAG 像是带着参考书进考场的学生随时能翻到最新版本。对于知识频繁变动、需要溯源、需要权限控制的业务场景RAG 几乎是唯一合理的选择。当然RAG 也不是万能的。它依赖检索质量如果知识本身结构混乱、表述模糊检索效果会很差。这时候就需要在数据治理上下功夫而不是指望换个更强的模型就能解决。2.3 稠密嵌入与稀疏嵌入两条腿走路才稳这是本篇最核心的技术点也是很多 RAG 项目效果上不去的根因。稠密嵌入Dense Embedding把一段文本映射成一个固定长度的高维向量比如 768 维或 1024 维。它的优势是能捕捉语义相似性——用户问怎么报销差旅费即使文档里写的是出差费用申请流程稠密向量也能把它们拉到相近的位置。缺点是它对精确的关键词匹配不敏感比如产品型号XR-2000A这种稠密向量可能召回一堆语义相近但型号不对的内容。稀疏嵌入Sparse Embedding则是传统的词频类表示比如 BM25。它把文本表示成一个稀疏向量维度等于词表大小大部分位置是 0只有出现的词才有值。它的优势是精确匹配强用户搜什么词就匹配什么词对专有名词、型号、代码标识符特别有效。缺点是无法理解语义报销和费用申请在它眼里是两个完全不同的词。我的经验是生产级 RAG 一定要用混合检索Hybrid Retrieval把稠密和稀疏的结果融合起来。纯稠密检索在语义泛化上强但会漏掉精确匹配纯稀疏检索精确但不懂语义。两者结合再配一个重排模型召回率和准确率都会有明显提升。维度稠密嵌入稀疏嵌入表示方式固定维度稠密向量词表维度稀疏向量语义理解强弱精确匹配弱强典型算法BGE、M3E、text-embeddingBM25、SPLADE适用场景语义问答、模糊查询型号检索、代码搜索存储开销中等较大词表维度检索速度快ANN 索引快倒排索引2.4 方案选型的几个关键决策点在动手之前有几个决策点必须先想清楚否则后面返工成本很高。第一向量库选什么。小规模验证阶段用 FAISS 或 Chroma 就够了轻量、零运维。生产环境如果数据量上百万级建议用 Milvus、Qdrant 或 Weaviate 这类专业向量库支持分布式、支持混合检索、支持元数据过滤。如果团队已经在用 Elasticsearch它的向量检索能力也够用能省一套运维。第二嵌入模型选什么。中文场景我一般推荐 BGE 系列或 M3E在中文语义任务上表现稳定。如果对多语言有要求可以看 multilingual 的模型。模型维度不是越高越好1024 维在大多数场景已经够用维度太高会拖慢检索速度、增加存储成本。第三要不要上重排。我的建议是只要召回数量超过 10 条就应该上重排。重排模型如 BGE-Reranker会对召回的候选做精细打分把真正相关的排到前面。实测下来加了重排之后Top-3 的命中率能提升 20% 以上。3. 核心细节解析与实操要点3.1 文档切分最容易被低估的环节切分策略直接决定了检索质量的上限。切得太粗一个 chunk 里混了好几个主题检索时噪声大切得太细一个完整语义被拆散模型拿到的是碎片。我常用的策略是递归字符切分 语义边界保护。具体做法是优先按段落切段落太长再按句子切句子还长才按字符硬切。同时设置一个重叠窗口overlap一般取 chunk 大小的 10% 到 20%保证跨 chunk 的语义连续性。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap64, separators[\n\n, \n, 。, , , , , ], length_functionlen, ) chunks splitter.split_text(document_text)这里有几个参数需要根据业务调。chunk_size我一般从 512 起步技术文档可以到 800对话记录可以降到 256。chunk_overlap不要超过 chunk_size 的 20%否则会有大量冗余。separators的顺序很重要中文场景一定要把中文标点加进去否则会按空格硬切把句子切得七零八落。注意切分时一定要保留元数据比如来源文件名、页码、章节标题。这些元数据在检索时可以用来做过滤在生成时可以用来做引用溯源。没有元数据的 RAG 系统用户问这个答案从哪来的你就答不上来。3.2 稠密嵌入的实操细节稠密嵌入的核心是选对模型和用好模型。模型选型上我建议先在自己的业务数据上做一个小规模评测别盲目追榜单。评测方法是准备 50 到 100 个真实 query人工标注每个 query 对应的正确文档然后看不同模型在这些 query 上的召回率。嵌入时有两个细节容易踩坑。一是归一化大多数嵌入模型输出的向量需要做 L2 归一化这样余弦相似度才能正确计算。二是批量处理嵌入是计算密集型操作一定要批量调用batch size 根据显存调整一般 32 到 128 之间。from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(BAAI/bge-large-zh-v1.5) def embed_texts(texts, batch_size64): embeddings model.encode( texts, batch_sizebatch_size, normalize_embeddingsTrue, show_progress_barTrue, ) return np.array(embeddings)normalize_embeddingsTrue这一步千万别省省了之后相似度计算会出问题而且这种问题很隐蔽往往要排查很久才发现。3.3 稀疏嵌入与 BM25 的落地稀疏嵌入这块BM25 是最经典也最实用的选择。它的核心思想是一个词在当前文档中出现次数越多、在所有文档中出现次数越少它的区分度就越高。BM25 有两个关键参数k1 和 b。k1 控制词频饱和一般取 1.2 到 2.0b 控制文档长度归一化一般取 0.75。这两个参数不用太纠结默认值在大多数场景都够用。from rank_bm25 import BM25Okapi import jieba # 中文需要先分词 tokenized_corpus [list(jieba.cut(doc)) for doc in documents] bm25 BM25Okapi(tokenized_corpus, k11.5, b0.75) query_tokens list(jieba.cut(query)) scores bm25.get_scores(query_tokens) top_indices np.argsort(scores)[::-1][:10]中文场景下分词是必须的jieba 是最常用的选择。如果你的领域有大量专有名词建议自定义词典否则分词会把知识图谱切成知识和图谱影响匹配效果。3.4 混合检索的融合策略稠密和稀疏的结果怎么融合有两种主流做法。一种是加权求和把两路归一化后的分数按权重相加。权重一般稠密给 0.6 到 0.7稀疏给 0.3 到 0.4。这种做法的优点是简单可控缺点是需要调权重。另一种是 RRFReciprocal Rank Fusion倒数排名融合不看分数只看排名把两路结果的排名做倒数加权。RRF 的好处是不用归一化、不用调权重鲁棒性更好我一般优先用这个。def rrf_fusion(dense_results, sparse_results, k60): scores {} for rank, doc_id in enumerate(dense_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(sparse_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)k 值一般取 60这是 RRF 论文里的推荐值实测下来也比较稳。3.5 重排把真正相关的顶上来召回阶段追求的是不漏重排阶段追求的是排准。重排模型通常是交叉编码器Cross-Encoder它把 query 和候选文档拼在一起输入模型直接输出相关性分数。这种方式比向量点积精确得多但计算量大所以只能用在召回后的少量候选上。from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-large) pairs [(query, doc) for doc in candidate_docs] scores reranker.predict(pairs) reranked sorted(zip(candidate_docs, scores), keylambda x: x[1], reverseTrue)重排的候选数量一般控制在 20 到 50 条太少会漏太多会慢。最终送给 LLM 的片段控制在 3 到 5 条太多会超出上下文窗口也会引入噪声。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把环境搭起来。我用的技术栈是 Python LangChain BGE 嵌入模型 Qdrant 向量库 BM25 稀疏检索。这套组合在中文场景下成熟稳定社区资料也多。pip install langchain langchain-community sentence-transformers pip install qdrant-client rank-bm25 jieba pypdfQdrant 我选择用 Docker 起一个本地实例方便调试。生产环境可以换成集群部署。docker run -d -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant4.2 数据接入与解析假设我们的知识源是一批 PDF 技术文档。解析 PDF 是个脏活pypdf 能处理大部分标准 PDF但扫描件需要 OCR复杂排版可能解析错乱。from pypdf import PdfReader def parse_pdf(file_path): reader PdfReader(file_path) pages [] for i, page in enumerate(reader.pages): text page.extract_text() if text.strip(): pages.append({text: text, page: i 1, source: file_path}) return pages解析完一定要做清洗去掉多余空行、合并断行、去掉页眉页脚。页眉页脚是 PDF 解析的重灾区每页都重复出现如果不清理会被切进每个 chunk严重干扰检索。4.3 切分与元数据绑定切分时把元数据一起带上这是后面做过滤和溯源的基础。def chunk_with_metadata(pages, splitter): chunks [] for page in pages: for i, chunk_text in enumerate(splitter.split_text(page[text])): chunks.append({ text: chunk_text, metadata: { source: page[source], page: page[page], chunk_index: i, } }) return chunks4.4 双路索引构建稠密索引和稀疏索引要分别建。稠密索引用 Qdrant 存向量稀疏索引可以用内存里的 BM25也可以存到 Elasticsearch。from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client QdrantClient(hostlocalhost, port6333) client.recreate_collection( collection_nameknowledge_base, vectors_configVectorParams(size1024, distanceDistance.COSINE), ) embeddings embed_texts([c[text] for c in chunks]) points [ PointStruct(idi, vectorembeddings[i].tolist(), payloadchunks[i]) for i in range(len(chunks)) ] client.upsert(collection_nameknowledge_base, pointspoints)注意size参数必须和嵌入模型的维度一致BGE-large-zh 是 1024 维写错了会直接报错。4.5 检索流程的完整实现把稠密检索、稀疏检索、融合、重排串起来就是完整的检索流程。def retrieve(query, top_k5): # 稠密检索 query_vec embed_texts([query])[0] dense_hits client.search( collection_nameknowledge_base, query_vectorquery_vec.tolist(), limit20, ) dense_ids [hit.id for hit in dense_hits] # 稀疏检索 query_tokens list(jieba.cut(query)) sparse_scores bm25.get_scores(query_tokens) sparse_ids np.argsort(sparse_scores)[::-1][:20].tolist() # RRF 融合 fused rrf_fusion(dense_ids, sparse_ids) candidate_ids [doc_id for doc_id, _ in fused[:20]] # 重排 candidates [chunks[i][text] for i in candidate_ids] pairs [(query, doc) for doc in candidates] rerank_scores reranker.predict(pairs) reranked sorted( zip(candidate_ids, rerank_scores), keylambda x: x[1], reverseTrue, ) return [chunks[i] for i, _ in reranked[:top_k]]4.6 接入 Agent 的生成环节检索出来的片段要拼进 Prompt这里有个技巧给每个片段编号并要求模型在回答时标注引用来源。这样既方便溯源也能抑制模型胡编。def build_prompt(query, retrieved_chunks): context \n\n.join([ f[{i1}] {c[text]}\n来源{c[metadata][source]} 第{c[metadata][page]}页 for i, c in enumerate(retrieved_chunks) ]) return f基于以下参考资料回答问题并在答案中标注引用编号。 如果参考资料中没有相关信息请明确说明资料中未找到。 参考资料 {context} 问题{query} 答案这个 Prompt 模板看起来简单但资料中未找到这句话非常关键。没有它模型在检索不到相关内容时会强行编造有了它模型会诚实地承认不知道。5. 常见问题与排查技巧实录5.1 检索命中率低的排查思路检索命中率低是最常见的问题排查要按环节来别一上来就换模型。现象可能原因排查方法解决方向语义相近但召回不到嵌入模型不适配领域人工评测嵌入模型换领域适配模型或微调精确词召回不到缺稀疏检索检查是否只有稠密路加 BM25 混合检索召回内容不完整切分过细检查 chunk 边界增大 chunk 或加 overlap召回噪声大切分过粗看 chunk 内容是否混杂减小 chunk 或语义切分相关文档排后面缺重排看 Top-10 里有没有加重排模型中文匹配差分词问题检查分词结果自定义词典我踩过最深的坑是分词。有一次做产品型号检索用户搜XR2000Ajieba 把它切成XR、2000、A结果匹配不到文档里的XR-2000A。后来加了自定义词典把产品型号整体作为一个词问题才解决。5.2 嵌入模型的显存与速度优化嵌入模型跑起来吃显存尤其是大模型。如果显存不够有几个办法一是换小模型BGE-base 比 BGE-large 小一半效果差距在多数场景可以接受二是用半精度推理model.half()能省一半显存三是用 ONNX 或 TensorRT 加速速度能提升 2 到 3 倍。批量大小也要调。batch size 太小GPU 利用率低太大显存爆掉。我一般从 32 开始试逐步往上加直到显存占用到 80% 左右。5.3 向量库的性能调优Qdrant 默认用 HNSW 索引这个索引有几个关键参数m控制每个节点的连接数ef_construct控制构建时的搜索范围。m 越大、ef_construct 越大索引质量越高但构建越慢、内存占用越大。我的经验值是m 取 16ef_construct 取 100在大多数场景是平衡点。如果数据量特别大千万级以上可以适当调大 m 到 32。检索时的ef参数控制搜索范围调大能提升召回但会变慢一般取 64 到 128。5.4 几个容易忽略的实操心得第一一定要做检索评测。别凭感觉判断效果准备一个评测集每次改动都跑一遍看召回率、MRR、NDCG 这些指标的变化。没有评测的优化都是瞎猜。第二元数据过滤能救命。如果知识库有权限区分检索时一定要带上权限过滤条件否则会召回用户无权查看的内容。这个在 Qdrant 里用 filter 实现别等到出事故才想起来。第三缓存高频 query。很多业务场景下用户问的问题高度重复。把高频 query 的检索结果缓存起来能大幅降低响应延迟和计算成本。第四监控检索质量。上线后要持续监控记录每次检索的 query、召回结果、用户反馈。发现某类 query 效果差就针对性优化。第五别迷信单一指标。召回率高不代表效果好如果召回的都是一堆边缘相关的内容反而会干扰模型。要综合看召回率和准确率必要时牺牲一点召回换准确。5.5 从基础 RAG 到 Agentic RAG 的演进方向基础 RAG 是一次检索、一次生成但真实场景往往需要多轮检索。比如用户问对比 A 产品和 B 产品的技术参数一次检索可能只召回 A 的信息需要 Agent 自己判断信息不足再发起一次针对 B 的检索。这就是Agentic RAG的思路——把检索变成 Agent 的一个工具由 Agent 决定什么时候检索、检索什么、检索几次。再往上是GraphRAG和本体 RAG把知识组织成图结构利用实体间的关系做多跳推理。这类方案适合知识关联复杂的场景比如医疗诊断、法律条文推理。但工程复杂度也高得多建议先把基础 RAG 做扎实再考虑往上走。我个人在实际操作中的体会是RAG 的效果 70% 取决于数据质量和切分策略20% 取决于检索策略只有 10% 取决于模型选型。很多团队本末倒置花大量时间对比模型却不肯在数据清洗和切分上多花一天。把数据治理做好用中等模型也能跑出很好的效果数据一团糟用最强的模型也救不回来。
返回列表