拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

AI Agent知识获取管道:RAG检索增强生成从0到1实战指南

AI Agent知识获取管道:RAG检索增强生成从0到1实战指南

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

做 AI Agent 的人迟早会撞上同一堵墙:模型本身很聪明,但你问它公司内部的报销标准、上周刚更新的产品参数、某个客户的特殊约定,它要么一本正经地胡说,要么干脆承认不知道。这不是模型不行,而是它的知识被冻结在训练截止那一刻,而真实业务的知识每天都在变。知识获取管道要解决的,就是让 Agent 在推理的那一刻,能动态地把外部知识"喂"进上下文。

这条管道在业内有个更常见的名字——RAG,检索增强生成。名字听着学术,本质特别朴素:先去知识库里把相关资料捞出来,再让模型基于这些资料回答。你可以把它理解成开卷考试,模型是考生,知识库是允许带进考场的那本书,检索器就是考生翻书的手。考生再聪明,如果翻错了页,照样答错。所以 RAG 系统的上限,往往不取决于模型多强,而取决于检索这一步做得多准。

我见过太多团队在 RAG 上翻车,翻车点几乎都不在生成环节,而在管道的前半段:文档切得稀碎、嵌入模型选错、检索只做单路召回、召回了也不重排。结果就是模型拿着半截上下文硬答,答得越流畅越危险。这篇就按"从 0 到 1 搭一条能用的知识获取管道"的思路,把 RAG 的基础拆开讲透,重点放在那些文档里不写、但实操中一定会踩的细节上。

适合谁看?如果你正在给 Agent 接知识库、正在选 RAG 框架、或者被"检索命中率上不去"折磨过,这篇能帮你把整条链路的每个环节想清楚。基础概念我会讲,但更值钱的是每个环节的取舍逻辑和踩坑经验。

2. 把 RAG 拆成四段管道,先看清每一段在干什么

2.1 离线索引:文档进来之前要过几道工序

RAG 的离线部分经常被低估,很多人以为"把文档丢进去建个向量库"就完事了。实际上从原始文档到可检索的索引,中间至少要过四道工序:解析、清洗、切分、嵌入。每一道都能决定最终检索质量的天花板。

解析这一步,PDF 是最大的坑。扫描版 PDF 得走 OCR,带复杂表格的 PDF 解析出来经常串行,双栏排版的论文解析后文字顺序全乱。我一般会先用解析工具把文档转成结构化文本,再人工抽查几份,确认表格和标题层级没丢。清洗则是去掉页眉页脚、页码、重复的水印文字,这些噪声如果混进嵌入,会严重稀释语义信号。

切分是最考验经验的一步。切太大,一个 chunk 里混了好几个主题,检索时语义被平均掉;切太小,一句话被拦腰截断,上下文丢失。我的默认策略是按语义边界切,而不是按固定字数切:优先在段落、标题、列表项这些自然边界断开,再控制单块长度。下面是一个常见的递归切分配置,用 LangChain 的文本切分器举例:

from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=512, # 目标块大小,按 token 或字符 chunk_overlap=64, # 相邻块重叠,防止语义断裂 separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""], length_function=len, ) chunks = splitter.split_text(raw_text)

chunk_overlap这个参数很多人随手设 0,结果关键句子正好卡在边界上,两边都只拿到半句。设成 chunk_size 的 10% 到 15% 是个稳妥起点。至于 chunk_size 到底设多少,没有万能值,得看你的文档类型:技术文档、法律条文这种信息密度高的,256 到 512 就够;叙述性的长文可以放到 800 到 1000。我的建议是先跑一批真实 query 做评测,再回头调这个值,别拍脑袋。

2.2 嵌入:稠密和稀疏不是二选一,而是互补

嵌入是把文本变成向量,让"语义相近"变成"向量距离近"。这里有个新手最容易混淆的点:稠密嵌入和稀疏嵌入是两套完全不同的表示方式,各有各的强项。

稠密嵌入(dense embedding)把一段文本压成一个几百到几千维的稠密向量,每一维都有值,捕捉的是整体语义。它的强项是"同义不同词"——你搜"怎么退款",它能召回写着"申请退货流程"的文档,因为语义接近。但它的弱项也很明显:对精确的专有名词、型号、编号不敏感。你搜"X200-Pro",它可能给你召回一堆"X200"和"Pro"相关的无关内容。

稀疏嵌入(sparse embedding)走的是另一条路,典型代表是 BM25 这类基于词频的表示。它把文本表示成高维稀疏向量,大部分维度是 0,只有出现的词才有值。它的强项恰恰是精确匹配:关键词、编号、专有名词一搜一个准。弱项是理解不了同义替换。

所以成熟的 RAG 系统几乎都做混合检索:稠密和稀疏两路并行召回,再融合结果。这不是为了炫技,而是因为真实 query 里既有"帮我找找关于数据安全的规定"这种语义型问题,也有"查一下工单号 INC-20240517 的处理记录"这种精确型问题,单靠一路必然瘸腿。融合时常用的做法是倒数排名融合(RRF),它不依赖两路分数的量纲,只看排名,鲁棒性好:

def rrf_fuse(dense_ids, sparse_ids, k=60): scores = {} for rank, doc_id in enumerate(dense_ids): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) for rank, doc_id in enumerate(sparse_ids): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) return sorted(scores.items(), key=lambda x: x[1], reverse=True)

k取 60 是文献里的经验值,作用是压低头部排名的绝对优势,让两路结果更均衡地融合。这个参数不用太纠结,60 附近都稳。

2.3 检索:召回只是开始,重排才是精度关键

很多人把"检索"等同于"向量相似度搜索",这是最大的认知误区。向量库返回的 top-k 只是粗召回,它的目标是"别漏",而不是"别错"。真正决定最终精度的,是召回之后的重排(rerank)。

粗召回阶段,我一般会把 top-k 设得偏大,比如 20 到 50,宁可多捞一些。然后用一个交叉编码器(cross-encoder)做重排。交叉编码器和双编码器的区别在于:双编码器是把 query 和文档分别编码再算相似度,快但粗;交叉编码器是把 query 和文档拼在一起送进模型,让模型直接判断相关性,慢但准。所以标准做法是双编码器粗召回、交叉编码器精排,兼顾速度和精度。

# 伪代码示意:粗召回 + 重排 candidates = vector_store.search(query, top_k=30) # 粗召回 reranked = reranker.rerank(query, candidates, top_n=5) # 精排取前5 context = "\n\n".join([c.text for c in reranked])

实测下来,加了重排这一步,最终喂给模型的上下文质量会有肉眼可见的提升。代价是延迟增加,重排 30 条大概多几十到几百毫秒,取决于模型大小。如果对延迟敏感,可以把重排模型换成轻量级的,或者只对 top-10 做重排。

2.4 生成:上下文怎么塞,比塞多少更重要

到了生成这一步,很多人以为把检索到的内容一股脑拼进 prompt 就行。其实上下文怎么组织,直接影响模型能不能用对信息。

我的经验是:给每个 chunk 带上来源标识和顺序,让模型知道这段话出自哪份文档、哪一节。这样模型在回答时能引用来源,也更容易判断信息之间的优先级。另外,上下文不是越多越好,塞太多无关内容反而会干扰模型。一般控制在 3 到 5 个高质量 chunk,比塞 10 个良莠不齐的效果好得多。

prompt 里还要明确告诉模型:如果检索到的内容不足以回答,就直说不知道,不要编。这句话能挡掉相当一部分幻觉。下面是一个我常用的生成 prompt 骨架:

你是知识库问答助手。请严格基于以下参考资料回答问题。 如果参考资料中没有相关信息,请直接回答"根据现有资料无法回答",不要编造。 参考资料: [1] 来源:员工手册.pdf 第3节 {chunk_1} [2] 来源:报销制度.docx 第2节 {chunk_2} 问题:{query}

3. 稠密与稀疏嵌入的选型:别被"向量数据库"四个字带偏

3.1 稠密嵌入模型怎么挑

选稠密嵌入模型,别只看排行榜上的分数。排行榜(比如 MTEB)测的多是通用语义任务,而你的业务可能是法律、医疗、工业这些垂直领域,通用模型未必吃得住。我的选型顺序是这样的:

先看语言支持。中文场景下,很多英文为主的模型直接掉档,得选中文语料训练充分的。再看维度。维度越高表达能力越强,但存储和计算成本也越高。768 维和 1024 维在多数业务上差距不大,1536 维以上就要掂量一下成本了。最后看最大输入长度。如果你的 chunk 设得比较长,模型的最大长度得跟得上,否则会被截断。

还有一个容易被忽略的点:嵌入模型和检索模型要配套。有些模型是专门为检索任务训练的(带 query/passage 前缀区分),用的时候必须按它的要求加前缀,否则效果打折。这个细节文档里经常一笔带过,但实操中不加前缀和加了前缀,召回率能差好几个点。

3.2 稀疏检索不是"过时技术"

有些团队一听稀疏检索就觉得是老古董,直接上纯向量方案,结果在精确匹配场景上栽跟头。稀疏检索(BM25 及其变体)在下面这些场景里依然是王者:

  • 查询里含专有名词、产品型号、错误码、工单号
  • 用户明确知道要找的关键词,只是不知道在哪份文档里
  • 需要可解释性,能说清楚"因为命中了这几个词"

而且现在的稀疏检索也在进化,出现了学习型稀疏表示(比如 SPLADE 这类),它用神经网络学出每个词的重要性权重,既有稀疏检索的精确性,又有一定的语义泛化能力。如果你的场景对精确匹配要求高,又想要一点语义能力,学习型稀疏是个不错的折中。

3.3 混合检索的权重怎么调

稠密和稀疏两路融合,权重不是固定的。我的做法是按 query 类型动态调:如果 query 里检测到大量数字、编号、英文专有名词,就提高稀疏路的权重;如果是自然语言长句,就偏向稠密路。简单点也可以先固定 0.5/0.5,跑评测集看效果再微调。

下面这张表是我在实际项目里总结的选型参考,不同场景侧重点不一样:

场景特征稠密权重稀疏权重说明
客服问答、语义搜索高低用户表达口语化,同义替换多
技术文档、错误码查询中高精确匹配需求强
法律、合规条文检索中中既要语义又要精确引用
产品型号、SKU 检索低高编号必须精确命中

这张表不是铁律,但能帮你快速定个起点。真正调参还是得靠你自己的评测集。

4. 从零跑通一条最小可用管道:代码和踩坑一起给

4.1 环境与依赖准备

先把最小依赖装齐。我用的是 Python 生态里比较通用的组合,向量库选 FAISS 做本地验证(生产可以换 Milvus、Qdrant 这类),嵌入模型走 sentence-transformers,稀疏检索用 rank_bm25:

pip install langchain sentence-transformers faiss-cpu rank_bm25 jieba

这里有个坑:中文稀疏检索需要分词,jieba是常用选择。如果你直接用空格分词,中文会被当成一整个字符串,BM25 基本失效。英文场景可以用 nltk 或直接空格切分,中文务必上分词器。

4.2 建索引:把文档变成可检索的两套表示

下面这段代码把同一批 chunk 同时建成稠密索引和稀疏索引,为后面的混合检索做准备:

import jieba from rank_bm25 import BM25Okapi from sentence_transformers import SentenceTransformer import faiss import numpy as np chunks = [...] # 前面切分好的文本块列表 # 稠密索引 embed_model = SentenceTransformer("your-chinese-embedding-model") dense_vecs = embed_model.encode(chunks, normalize_embeddings=True) index = faiss.IndexFlatIP(dense_vecs.shape[1]) # 内积,配合归一化即余弦 index.add(np.array(dense_vecs, dtype="float32")) # 稀疏索引 tokenized = [list(jieba.cut(c)) for c in chunks] bm25 = BM25Okapi(tokenized)

normalize_embeddings=True这一步别省,归一化之后内积就等于余弦相似度,省去额外计算。FAISS 用IndexFlatIP做精确检索,数据量大了再换 IVF 或 HNSW 这类近似索引。

4.3 查询:两路召回加融合

查询时两路并行,再用 RRF 融合:

def search(query, top_k=5): # 稠密召回 q_vec = embed_model.encode([query], normalize_embeddings=True) _, dense_idx = index.search(np.array(q_vec, dtype="float32"), 30) dense_ids = dense_idx[0].tolist() # 稀疏召回 q_tokens = list(jieba.cut(query)) sparse_scores = bm25.get_scores(q_tokens) sparse_ids = np.argsort(sparse_scores)[::-1][:30].tolist() # RRF 融合 fused = rrf_fuse(dense_ids, sparse_ids) return [chunks[i] for i, _ in fused[:top_k]]

跑通之后你会发现,纯稠密召回在精确查询上经常翻车,加上稀疏路之后明显改善。这就是混合检索的价值。

4.4 我踩过的三个坑

第一个坑是嵌入模型没加前缀。我用的模型要求 query 加query:、文档加passage:,一开始没加,召回率一直上不去,排查了半天才发现是前缀问题。换模型时一定要读清楚它的使用说明。

第二个坑是chunk 重叠设太小。有次 overlap 设成 0,结果一个关键表格被切成两半,检索时两半都召回了但都不完整,模型拼不出完整信息。后来 overlap 调到 chunk_size 的 15%,问题消失。

第三个坑是BM25 没做停用词处理。中文里"的""了""是"这些高频词会干扰 BM25 打分,加个停用词表过滤一下,稀疏召回质量会好不少。

5. 检索质量上不去时,按这个顺序排查

5.1 先确认是召回问题还是排序问题

检索效果差,第一步是定位问题出在哪一段。做法很简单:拿一批已知答案的 query,看正确答案所在的 chunk 有没有被召回进 top-30。如果没进,是召回问题;如果进了但没排进 top-5,是排序问题。这两个问题的解法完全不同,别一上来就换模型。

召回问题的常见原因:嵌入模型不适配领域、chunk 切分破坏了语义、query 和文档表述差异太大。排序问题的常见原因:没做重排、融合权重不合理、相似度度量选错。

5.2 用评测集说话,别凭感觉调

调 RAG 最忌讳凭感觉。你得有一批带标注的评测 query,至少几十条,覆盖不同类型的问法。每次改动(换模型、调 chunk、改权重)都跑一遍评测,看命中率和 MRR 这些指标的变化。没有评测集,你根本不知道改动是变好还是变坏。

我一般会记录每次实验的配置和指标,做成一张对照表。这样几轮下来,就能摸清哪些参数对你的数据真正重要。

5.3 常见问题与对策速查

现象可能原因对策
精确查询召回差只用了稠密检索加稀疏路做混合
语义查询召回差嵌入模型不适配换领域适配的嵌入模型
答案不完整chunk 太小或重叠不足调大 chunk 或增加 overlap
召回内容相关但答非所问缺少重排加交叉编码器重排
模型编造答案上下文不足或 prompt 没约束补检索 + 加"不知道"约束
延迟太高重排模型太重或 top-k 太大换轻量重排或减小 top-k

这张表基本覆盖了 RAG 初期 80% 的问题。遇到具体现象先对号入座,能省不少排查时间。

6. 让管道真正好用的几个进阶思路

6.1 查询改写:用户问得烂,你得帮他问好

真实用户的 query 往往很短、很口语,甚至带错别字。直接拿这种 query 去检索,效果自然差。查询改写就是在检索前先把 query 优化一遍,常见做法有:把口语 query 改写成更规范的表述、把多轮对话里的指代补全、把复杂问题拆成几个子查询分别检索再合并。

多轮对话场景下,指代消解特别重要。用户先说"这个产品的保修期多久",再问"那它的配件呢",第二句里的"它"如果不补全,检索器根本不知道在问什么。我一般会用一个小模型或规则先把 query 补全成独立完整的句子,再送进检索。

6.2 上下文压缩:只留有用的,别塞垃圾

召回的内容里经常有大量和问题无关的句子。上下文压缩就是在送进生成模型之前,把每个 chunk 里真正相关的部分抽出来,去掉冗余。这样既省 token,又能减少干扰。做法可以是让模型对每个 chunk 做抽取式摘要,也可以用一个小的相关性模型给句子打分,只保留高分句。

6.3 把 RAG 和 Agent 结合起来

基础的 RAG 是"一问一检索一答"的单轮管道。但 Agent 场景下,检索往往需要多轮:Agent 先检索一次,发现信息不够,再换个 query 检索,甚至根据中间结果决定去查哪个知识库。这就是所谓的 Agentic RAG,检索不再是固定流程,而是 Agent 的一个可调用工具。

这种模式下,检索工具的设计就很关键:要能接受结构化的查询参数(比如指定知识库、指定时间范围),要能返回带来源的结果,还要能处理"没查到"的情况让 Agent 决定下一步。把检索做成一个健壮的工具,比把它写死在流程里灵活得多。

6.4 知识更新:别让索引变成一潭死水

知识库是活的,文档会增删改。索引如果不能及时更新,Agent 就会拿着过期信息回答。我的做法是给文档加版本和更新时间戳,增量更新索引而不是全量重建。删除文档时,要确保对应的向量和稀疏索引条目一起删掉,否则会出现"召回了已删除内容"的诡异情况。

另外,定期用一批新 query 回归测试,看看知识更新后检索质量有没有退化。知识库越大,越需要这种例行体检。

7. 我在实际项目里最想强调的几件事

搭 RAG 这件事,工具和框架其实不是门槛,LangChain、LlamaIndex 这些框架已经把流程封装得很好了。真正的门槛在于对数据的理解和对评测的坚持。我见过太多团队花两周选框架,却花不到两天做评测集,结果调来调去都是盲调。

我的建议是:动手写第一行检索代码之前,先花时间把文档摸清楚,知道你的知识长什么样、用户会怎么问。然后老老实实建一个哪怕只有 50 条的评测集,把它当成你调参的指南针。有了它,你换模型、调 chunk、改权重才有依据。

还有一点,别追求一步到位。先用最小可用管道跑通,哪怕召回率只有 60%,先让业务用起来,收集真实 query,再针对性优化。RAG 是个迭代的活,不是一次性的工程。真实 query 分布会不断给你惊喜,也会不断暴露你没想到的边界情况。

最后分享一个我常用的自检习惯:每次上线新版本前,随机抽 20 条真实 query,人工看一遍检索结果和最终回答。机器指标再好看,人工抽查这一关过不了,就别急着上。这一步花不了多少时间,但能挡掉很多"指标漂亮、体验拉胯"的尴尬。

返回列表