☰
AI Agent知识获取管道:稠密与稀疏嵌入的RAG检索实战
2026/9/29 18:40:05 网站建设 项目流程

1. 为什么知识获取管道是 AI Agent 落地的第一道坎

做 AI Agent 开发的人,绕不开一个尴尬的现实:模型本身很聪明,但它对你私有的业务知识一无所知。你问它公司内部的报销流程,它只能编;你问它某个产品的技术参数,它要么胡诌要么拒答。这不是模型能力不行,而是它的知识边界停在了训练数据截止的那一天。

知识获取管道要解决的就是这个问题。它的核心任务只有一句话:在 Agent 需要知识的时候,把正确的那一小块知识,准确、及时地送到模型面前。听起来简单,做起来全是细节。我见过太多团队,模型选得挺好,Agent 框架搭得也漂亮,结果卡在知识管道上——检索出来的内容驴唇不对马嘴,或者明明知识库里有答案却死活搜不到,最后整个项目不了了之。

RAG(Retrieval-Augmented Generation,检索增强生成)就是目前最主流的知识获取管道方案。它的思路很朴素:不指望模型记住所有东西,而是在生成回答之前,先去知识库里把相关资料捞出来,塞进上下文,让模型"看着材料答题"。这个思路从 2020 年提出到现在,已经演化出了朴素 RAG、高级 RAG、模块化 RAG、Agentic RAG 等多个阶段,但底层逻辑没变——检索的质量决定了生成的质量。

这篇是"走进 AI Agent"系列的第四篇,专门讲知识获取管道的基础部分。我会把 RAG 的核心链路拆开,重点讲清楚稠密嵌入和稀疏嵌入这两条技术路线的区别、各自的适用场景,以及在实际搭建中怎么选、怎么调、怎么避坑。不管你是刚接触 RAG 的新手,还是已经搭过几套知识库但效果不理想的老手,这篇内容应该都能给你一些可以直接抄作业的东西。

提示:这篇偏基础,重点在"理解原理"和"建立正确的技术直觉"。如果你已经能熟练搭建 RAG 管道,可以重点关注第 3 节和第 5 节的调优经验部分。

2. RAG 管道的完整链路拆解:从文档到答案经历了什么

很多人对 RAG 的理解停留在"把文档切一切、向量化、搜一搜"这个层面,但真正跑起来会发现,每一个环节都有大量决策点。我先把完整链路摊开,让你看清楚数据从原始文档到最终答案,中间到底经过了哪些加工站。

2.1 文档加载与预处理:脏数据是检索失败的头号元凶

知识获取管道的第一站是文档加载。你的知识可能散落在 PDF、Word、Markdown、HTML、数据库、甚至飞书/钉钉的聊天记录里。加载器的任务就是把这些异构数据统一成纯文本。

这一步看起来最没技术含量,但恰恰是坑最多的地方。我踩过的典型坑包括:

  • PDF 解析丢格式:很多 PDF 是扫描件或者排版复杂的报表,直接解析出来文字顺序全乱,表格变成一堆散落的数字。这种脏数据进了知识库,检索出来的内容根本没法用。
  • 页眉页脚污染:每页都重复的页眉页脚、页码、水印文字,如果不清理,会被切成独立的 chunk,白白占用检索名额。
  • 编码问题:中文文档经常遇到 GBK 和 UTF-8 混用,解析出来一堆乱码。

我的经验是,文档预处理阶段至少要花整个项目 30% 的精力。具体做法:PDF 优先用能保留版面结构的解析器(比如基于版面分析的方案),解析后做一轮正则清洗,把页眉页脚、连续空行、特殊符号干掉。对于表格密集的文档,考虑单独走表格提取通道,别指望通用解析器能处理好。

2.2 文本分块:chunk 切得好,检索成功一半

文本分块(Chunking)是 RAG 里最容易被低估的环节。chunk 太大,检索出来的内容包含太多无关信息,稀释了关键信号;chunk 太小,语义不完整,模型拿到手也拼不出完整答案。

常见的分块策略有这么几种:

策略做法适用场景缺点
固定长度按 token 数硬切,如每 512 token 一块快速原型容易切断句子和语义
递归分块按段落→句子→词的优先级递归切通用文档需要调参数
语义分块用嵌入相似度判断语义边界高质量要求计算成本高
结构化分块按标题层级、代码块等结构切技术文档、法律合同依赖文档结构

我实测下来,递归分块 + 适度重叠是性价比最高的方案。具体参数上,中文文档我一般用 chunk size 300-500 字,overlap 50-100 字。overlap 的作用是防止关键信息正好卡在切割边界上被切断,但也不能太大,否则检索结果里全是重复内容。

注意:chunk size 没有万能值。技术文档可以小一点(300 字左右),因为概念密集;叙述性文档可以大一点(500-800 字),因为需要上下文才能理解。一定要根据你的文档类型做实验。

2.3 嵌入与索引:把文字变成可以计算距离的向量

嵌入(Embedding)是把文本映射成高维向量的过程。这个向量有个关键性质:语义相近的文本,向量距离也相近。这样一来,"检索"就变成了"在向量空间里找最近的邻居",可以用数学方法高效完成。

嵌入模型的选择直接决定了检索的上限。目前主流的中文嵌入模型有 BGE 系列、M3E、GTE 等,英文有 OpenAI 的 text-embedding 系列、Cohere 等。选型时重点看三个指标:检索准确率(Recall/NDCG)、向量维度、推理速度。

索引阶段则是把生成的向量存进向量数据库,建立高效的相似度搜索结构。常见选择有 FAISS(本地、轻量)、Milvus(分布式、功能全)、Qdrant(Rust 写的、性能好)、Chroma(上手快、适合原型)。小规模知识库(几万条以内)用 FAISS 或 Chroma 完全够用,上百万条再考虑 Milvus 这类分布式方案。

2.4 检索与重排:先粗筛再精排的两段式打法

检索阶段,系统拿用户 query 的向量去向量库里找 top-K 个最相似的 chunk。但纯向量检索有个问题:它擅长捕捉语义相似,但对精确匹配(比如产品型号、专有名词)不敏感。

所以工业级 RAG 普遍采用两段式检索:第一阶段用向量检索(或向量+关键词混合检索)快速召回一批候选(比如 top-50),第二阶段用重排模型(Reranker)对候选做精细打分,选出最相关的 top-3 到 top-5 送给模型。

重排模型通常是交叉编码器(Cross-Encoder),它把 query 和每个候选 chunk 拼在一起过一遍模型,打分更准,但速度慢,所以只用在候选集上。这个"粗筛+精排"的组合,是我实测下来提升检索命中率最明显的手段之一。

2.5 生成与引用:让模型看着材料答题

最后一步是把检索到的 chunk 拼进 prompt,让模型基于这些材料生成答案。这里的关键是prompt 设计:要明确告诉模型"只根据提供的材料回答,材料里没有就说不知道",并且要求它标注引用来源。

引用来源这个功能看似小,实际价值极大。它让用户能验证答案的可靠性,也方便你排查是检索错了还是生成错了。我在项目里会强制要求模型输出[1][2]这样的引用标记,对应到具体的 chunk 来源。

3. 稠密嵌入与稀疏嵌入:两条检索路线的本质区别

理解了完整链路,现在聚焦到最核心的技术分歧点:稠密嵌入(Dense Embedding)和稀疏嵌入(Sparse Embedding)。这两个词你可能听过很多次,但它们的本质区别、各自的强项和短板,值得掰开揉碎讲清楚。

3.1 稠密嵌入:用语义空间捕捉"意思相近"

稠密嵌入的输出是一个固定长度、每个维度都是非零实数的向量,比如 768 维或 1024 维。它的核心能力是语义匹配。

举个例子,用户搜"怎么退钱",知识库里写的是"退款流程说明"。字面上几乎没有重叠词,但稠密嵌入能把它们映射到相近的向量位置,因为模型在训练时学到了"退钱"和"退款"是同一个意思。这就是稠密嵌入的杀手锏——它理解语义,不受字面表达限制。

稠密嵌入的典型代表是 BGE、M3E、OpenAI text-embedding 这些模型。它们的训练目标通常是让语义相近的文本对(比如问题和答案、同义句)在向量空间里靠近,让无关文本远离。

但稠密嵌入也有明显短板:

  • 对精确匹配不敏感:用户搜"X2000 型号的电池规格",如果知识库里有"X2000"这个型号,稠密嵌入可能因为整体语义偏向"电池规格"而漏掉精确的型号匹配。
  • 对罕见词、专有名词、数字不友好:这些词在训练数据里出现少,模型没学好它们的表示。
  • 可解释性差:你很难说清楚为什么这两个向量相似,因为每个维度都没有明确含义。

3.2 稀疏嵌入:用词频统计守住"字面精确"

稀疏嵌入的输出是一个维度等于词表大小、绝大多数维度为零的向量。每个非零维度对应一个词,值代表这个词的权重。最经典的稀疏表示就是BM25,它基于词频(TF)和逆文档频率(IDF)计算权重。

稀疏嵌入的核心能力是精确匹配。用户搜"X2000 电池",它会把 query 拆成词,然后找包含这些词的文档,并根据词的稀有程度加权——"X2000"这种罕见词权重高,"电池"这种常见词权重低。所以它能精准命中包含特定型号、专有名词的文档。

稀疏嵌入的短板同样明显:

  • 不理解语义:用户搜"退钱",文档写"退款",如果这两个词没有共同词根,BM25 完全匹配不上。
  • 词表固定:遇到词表外的新词(OOV)就无能为力。
  • 无法捕捉上下文:同一个词在不同语境下含义不同,稀疏表示区分不了。

3.3 一张表看清两者的取舍

维度稠密嵌入稀疏嵌入
向量形态固定维度,稠密实数词表维度,大量为零
核心能力语义匹配字面精确匹配
代表方案BGE、M3E、text-embeddingBM25、SPLADE
强项同义改写、模糊查询专有名词、型号、数字
弱项精确匹配、罕见词语义泛化、OOV
可解释性差好(能看出哪些词命中)
计算成本需要模型推理统计计算,轻量

看到这里你应该明白了:稠密和稀疏不是替代关系,而是互补关系。稠密管"意思对不对",稀疏管"词对不对"。真实场景里,用户的 query 往往两者都需要——既希望理解语义,又希望精确命中关键词。

3.4 混合检索:把两条路线的优势叠起来

既然互补,那就混合检索(Hybrid Retrieval)。做法是同时跑稠密检索和稀疏检索,各自召回一批候选,然后用融合算法把两路结果合并排序。

最常用的融合算法是RRF(Reciprocal Rank Fusion,倒数排名融合)。它的逻辑很简单:对每个文档,把它在两路结果里的排名取倒数再相加,得到最终分数。排名越靠前,贡献越大。RRF 的好处是不需要归一化两路分数(稠密是余弦相似度,稀疏是 BM25 分数,量纲完全不同),直接基于排名融合,鲁棒性好。

我实测下来,混合检索相比纯稠密检索,在包含大量专有名词的技术文档场景下,召回率能提升 15%-30%。这个提升幅度足以决定一个 RAG 项目是能用还是不能用。

提示:混合检索的权重需要调。有些框架允许你给稠密和稀疏分配不同权重,一般从 0.5:0.5 起步,然后根据你的评测集调整。技术文档可以适当提高稀疏权重,对话类场景可以提高稠密权重。

4. 从零搭一条最小可用的 RAG 管道

理论讲完了,现在动手。这一节我带你把一条最小可用的 RAG 管道搭起来,用 Python 生态里最成熟的组件。目标不是搭一个生产级系统,而是让你亲手跑通全链路,建立对每个环节的体感。

4.1 环境准备与依赖选型

我选的技术栈是:LangChain 做流程编排 + BGE 做稠密嵌入 + BM25 做稀疏检索 + FAISS 做向量索引 + 一个开源重排模型。这套组合全部可以本地跑,不依赖外部 API,适合学习和中小规模部署。

pip install langchain langchain-community faiss-cpu rank_bm25 pip install sentence-transformers FlagEmbedding

选型理由:LangChain 的抽象层次适中,既能快速搭原型,又不会把你锁死;BGE 系列在中文检索任务上长期霸榜,bge-large-zh-v1.5是我常用的默认选择;FAISS 是 Facebook 开源的向量检索库,CPU 版本就够用,零运维成本。

4.2 文档加载与递归分块实操

假设你有一批 Markdown 格式的技术文档,先加载再分块:

from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 加载文档 loader = DirectoryLoader('./docs', glob='**/*.md', loader_cls=TextLoader, loader_kwargs={'encoding': 'utf-8'}) docs = loader.load() # 递归分块 splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=80, separators=['\n\n', '\n', '。', '!', '?', ';', ',', ' ', ''] ) chunks = splitter.split_documents(docs) print(f'共生成 {len(chunks)} 个 chunk')

这里separators的顺序很关键。RecursiveCharacterTextSplitter 会优先按靠前的分隔符切,切出来的块还太大就换下一个分隔符继续切。我把中文标点加进去,就是为了尽量在句子边界切,避免把一句话拦腰截断。

4.3 稠密向量生成与 FAISS 索引构建

from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain_community.vectorstores import FAISS # 加载 BGE 嵌入模型 embed_model = HuggingFaceBgeEmbeddings( model_name='BAAI/bge-large-zh-v1.5', model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} ) # 构建 FAISS 索引 vectorstore = FAISS.from_documents(chunks, embed_model) vectorstore.save_local('./faiss_index')

normalize_embeddings=True这个参数别漏。它把向量归一化到单位长度,这样余弦相似度就等价于内积,计算更快,而且 BGE 模型本身推荐这样用。

4.4 BM25 稀疏检索器的搭建

from rank_bm25 import BM25Okapi import jieba # 中文分词 tokenized_corpus = [list(jieba.cut(chunk.page_content)) for chunk in chunks] bm25 = BM25Okapi(tokenized_corpus) def bm25_search(query, top_k=20): tokenized_query = list(jieba.cut(query)) scores = bm25.get_scores(tokenized_query) top_indices = scores.argsort()[::-1][:top_k] return [(chunks[i], scores[i]) for i in top_indices]

中文用 BM25 必须分词,jieba是最常用的选择。如果你的领域有大量专有名词,记得往 jieba 的自定义词典里加,否则这些词会被切碎,BM25 就匹配不上了。

4.5 用 RRF 融合两路检索结果

def rrf_fusion(dense_results, sparse_results, k=60): scores = {} for rank, (doc, _) in enumerate(dense_results): scores[doc.page_content] = scores.get(doc.page_content, 0) + 1 / (k + rank + 1) for rank, (doc, _) in enumerate(sparse_results): scores[doc.page_content] = scores.get(doc.page_content, 0) + 1 / (k + rank + 1) sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True) return sorted_docs

k=60是 RRF 论文里的推荐值,作用是平滑排名靠前文档的权重,避免某一路的 top-1 过度主导。这个值一般不用改。

4.6 接上重排模型做精排

from FlagEmbedding import FlagReranker reranker = FlagReranker('BAAI/bge-reranker-large', use_fp16=True) def rerank(query, candidates, top_k=5): pairs = [[query, doc] for doc, _ in candidates] scores = reranker.compute_score(pairs) ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return ranked[:top_k]

重排模型是整条管道里性价比最高的投入。它把候选集从几十个精筛到几个,送给模型的上下文质量直接上一个台阶。我做过对比,加了重排之后,最终答案的准确率能提升 20% 以上。

5. 检索效果调优:那些文档里不会写的实战经验

管道搭起来只是开始,真正决定成败的是调优。这一节我分享几个踩过坑才总结出来的经验,都是常规文档里不会写的。

5.1 命中率上不去的三个隐藏原因

第一个原因:query 和文档的"语言风格"不匹配。用户提问是口语化的短句,知识库文档是书面化的长句。这种风格差异会让嵌入模型算出的相似度偏低。解决办法是做query 改写——用一个小模型把用户的口语 query 改写成更接近文档风格的表述,再拿去检索。

第二个原因:嵌入模型和你的领域不匹配。通用嵌入模型在通用语料上训练,对你的垂直领域(比如医疗、法律、工业)可能水土不服。如果预算允许,用领域数据对嵌入模型做微调,效果提升非常明显。预算有限的话,至少试试在你的领域数据上评测几个不同的开源模型,选最适合的那个。

第三个原因:chunk 里混入了太多噪声。前面提过,页眉页脚、无关段落会稀释信号。我遇到过一个案例,知识库里每个 chunk 都带着一段公司简介的模板文字,导致检索时这段模板文字贡献了大量相似度,真正的答案反而被淹没。清理掉之后,命中率立刻回升。

5.2 重排模型不是万能药:什么时候它会帮倒忙

重排模型很强,但不是所有场景都适合。我遇到过两种情况,加了重排反而变差:

  • 候选集本身就没有正确答案:如果第一阶段召回率太低,top-50 里根本没有相关文档,重排再准也没用。这时候要解决的是召回问题,不是排序问题。
  • query 本身模糊:用户 query 太短太泛(比如就一个词),重排模型缺乏足够信号判断相关性,可能把原本靠前的正确结果排下去。

所以我的建议是:先保证召回率,再优化排序。评测时分开看召回率和最终准确率,定位问题到底出在哪一环。

5.3 评测集:没有它你就是在盲调

这是我最想强调的一点。没有评测集的调优都是玄学。你需要准备一批"query-正确答案"的配对,每次改动管道后跑一遍,看指标是涨是跌。

评测集不用很大,50-100 条就能反映趋势。构造方法:从真实用户提问里采样,人工标注每个 query 对应的正确 chunk。指标上,检索阶段看Recall@K和MRR,生成阶段看答案准确率和引用正确率。

我见过太多团队凭感觉调参,今天改改 chunk size,明天换换模型,最后自己也说不清哪个改动有效。有了评测集,每次改动都有数据支撑,调优效率能提升好几倍。

5.4 一个容易被忽略的细节:元数据过滤

真实场景里,用户的问题往往隐含了过滤条件。比如"2024 年的销售政策是什么",这里"2024 年"就是一个过滤条件。如果只靠向量检索,很可能把 2023 年的政策也召回来。

解决办法是给每个 chunk 打上元数据标签(年份、部门、文档类型等),检索时先按元数据过滤,再做向量搜索。这个技巧在多版本、多部门的文档库里特别有用,能大幅减少干扰项。

6. 从基础 RAG 到 Agentic RAG 的演进方向

基础 RAG 跑通之后,你会发现它有几个天花板:只能检索一次、不会判断检索结果够不够、不会根据问题类型切换检索策略。这些正是Agentic RAG要解决的问题。

Agentic RAG 的核心思路是把检索变成 Agent 的一个工具,让 Agent 自己决定什么时候检索、检索几次、用什么策略检索。比如面对一个复杂问题,Agent 可以先拆解成子问题,逐个检索,发现信息不足就换个 query 再搜,最后综合所有材料生成答案。

这个演进方向涉及的技术点包括:查询规划(Query Planning)、自适应检索(Adaptive Retrieval)、多跳检索(Multi-hop Retrieval)、自我反思(Self-Reflection)等。这些内容足够单独写一篇,这里先给你一个方向感——基础 RAG 是"一次检索一次生成",Agentic RAG 是"边想边搜边答"。

另外值得一提的是GraphRAG和本体 RAG(Ontology RAG)这两个方向。它们不满足于把文档切成孤立的 chunk,而是试图构建知识之间的关联图谱,让检索能沿着关系链走。对于需要多跳推理的场景(比如"和 A 产品共用同一个供应商的产品有哪些"),这类方案比纯向量检索有天然优势。不过它们的构建成本也高得多,适合知识结构稳定、关系复杂的领域。

我在实际项目里的体会是:别一上来就追求 Agentic RAG 或 GraphRAG。先把基础 RAG 的每个环节调扎实,把评测集建起来,把召回率和准确率做到一个可用的水平。基础打牢之后,再根据实际瓶颈决定往哪个方向演进。很多团队基础还没跑通就急着上高级方案,结果是在沙地上盖楼,越盖越不稳。

最后分享一个小技巧:如果你的知识库更新频繁,一定要把索引更新流程自动化。文档一变就重新嵌入、重建索引,否则知识库很快就会和实际文档脱节。我一般用文件哈希做增量更新,只重新处理变化的文档,能省下大量计算。

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

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

立即咨询