AI Agent 知识获取管道:从 RAG 原理到工程化落地全解析

发布时间:2026/11/4 5:07:44
AI Agent 知识获取管道:从 RAG 原理到工程化落地全解析

先直接说结论:AI Agent 要靠谱,光有大模型不够,必须给它接一条能持续、稳定、精准获取知识的管道。这个系列前几篇聊了 Agent 的编排、工具调用和记忆机制,这一篇要补上最关键的一块拼图——RAG。RAG 不是新概念,但放在 Agent 的语境下重新审视,它的作用远不止“给模型补知识”那么简单。它是 Agent 感知外部世界的通道,是决定 Agent 是“看起来聪明”还是“真能解决问题”的分水岭。

这篇文章不打算绕弯子,我会从 RAG 的基本原理讲到一套可落地的知识获取管道搭建方案,再分享我在实际项目中踩过的坑。无论你是刚开始接触 Agent 开发,还是已经跑通了一个简单的 RAG Demo,这篇文章都能帮你把“知识获取”这块从零散的技巧整理成体系。

1. 为什么 AI Agent 必须先解决知识获取问题

很多人在做 Agent 的时候,第一反应是“把 LLM 接上工具,就能干活了”。其实不然。Agent 的核心能力是“在复杂环境中自主决策并执行”。但决策的依据从哪里来?如果只靠模型参数里固化的知识,Agent 就是一个“闭着眼睛开车”的司机。训练数据有截止时间,模型记不住你公司内部的制度、你的私有知识库、实时的业务数据。这时候,RAG 就派上用场了。

1.1 没有知识管道的 Agent 是什么状态

没有 RAG 的 Agent,经常会犯三个毛病。

第一个是“一本正经地胡说八道”。比如你问它“我们公司报销流程是什么”,它可能依据网上看到的某个大厂制度编造一套,语气自信,实则错误。第二个是“上下文窗口撑爆”。为了让它记住所有背景知识,有人直接把几百页文档塞进 System Prompt,结果 token 费用暴涨,模型注意力被稀释,最后什么都没记住。第三个是“无法追溯依据”。即使它答对了,你也不知道它是蒙对的还是真查了资料,这在企业场景里是致命的——领导要的不是一个黑盒,而是有依据的答案。

所以,知识获取管道要解决的核心问题有三个:时效性、准确性、可追溯性。RAG 恰好都能覆盖:从外部知识源检索出相关内容,再提供给模型,模型基于检索内容生成答案。这样一来,答案有出处,知识可更新,幻觉也会明显减少。

1.2 RAG 在 Agent 架构中的位置

在 Agent 的流程里,RAG 不是独立存在的模块,它要跟 Planner、Action、Memory 这几个部分深度联动。你可以把 Agent 理解成一个“智能体”,RAG 是它的“记忆读取系统”。当 Agent 接收到用户任务时,它先判断自己缺不缺知识。缺,就触发知识获取管道:查询向量库,召回候选片段,重排序,然后拼接成上下文交给模型。

这里有个常见的认知误区:觉得 RAG 就是“构建知识库+向量检索”,只要跑通一次就够了。但在 Agent 场景里,知识获取是一个动态过程。用户问题可能很模糊,需要先改写再检索;也可能一次检索不够,需要多轮迭代。这也是后面要讲的 Agentic RAG 的雏形。所以,RAG 做得好不好,直接决定了 Agent 的智力上限。

1.3 什么场景必须用 RAG

如果你做的 Agent 只是聊天陪聊、写写诗,那 RAG 确实没那么重要。但只要你涉及下面任意一种场景,就必须认真设计知识获取管道:

  • 企业知识库问答:员工手册、规章制度、产品文档、技术文档。
  • 客服助手:售后FAQ、退换货政策、产品使用疑难。
  • 数据洞察分析:需要结合实时业务数据,回答“为什么某指标下降了”。
  • 长尾、小语种或垂直领域问答:医疗、法律、工业、教育等领域知识专业性强,通用模型根本不熟。
  • 需要引用出处的场景:合规、审计、法律等,答案必须能追到原文。

这些场景的共同点是:知识量大、变动频繁、错误代价高。只靠模型记忆根本扛不住,必须引入外部检索。

2. RAG 核心原理拆解:索引、检索、生成三大环节

RAG 全称 Retrieval-Augmented Generation,检索增强生成。名字已经把核心说得明明白白:先检索,再生成。但实际落地时,封装好的框架让你一行代码就能调用,很多人反而不知道底层发生了什么。我建议每个做 Agent 的人都亲手拆一遍原理,否则出了问题根本定位不到原因。

2.1 索引阶段:把文档变成“模型能查的东西”

索引阶段的目标是:把原始文档(PDF、Word、Markdown、HTML)转换成适合检索的向量数据,并存储到向量数据库中。这个过程又分四步。

第一步,文档加载与解析。不同格式的文件解析方式千差万别。PDF 可能是文本型,可能是扫描件,可能需要 OCR;PPT 和 Excel 的表格信息容易丢失。这一步的要领是“保住语义完整性”,不能把表格拆得乱七八糟。

第二步,文本切分(Chunking)。这是 RAG 里最考验功力的环节。切得太粗,检索回的片段包含大量无关信息,噪音大;切得太细,语义不完整,容易丢关键上下文。我见过不少人直接按字符长度 256 或 512 硬切,结果一句话被拦腰截断,检索命中率一塌糊涂。好的做法是按自然段落切,再结合标题结构合并,让每个 chunk 是一个相对独立且完整的语义单元。

第三步,嵌入向量化(Embedding)。把文本转成向量,用 embedding 模型。市面上有很多选择,后面详述。这一步的关键是向量要能真实反映语义相似度。比如“苹果”和“水果店”应该近一些,和“iPhone”也要有合理的距离。

第四步,向量入库。把向量和原文、元数据一起存进向量数据库。元数据很重要,比如来源、标题、时间、权限标签,后续检索可以靠元数据做条件过滤。

2.2 检索阶段:从“大海捞针”到“精准定位”

用户提出一个问题,系统要做两件事:把问题转成向量,然后在向量库里找最相似的 chunk。

常用的是余弦相似度或向量内积。向量维度一般是 768、1024 或 1536。检索到的 top-k 个片段作为候选上下文,传给生成模型。这里有一个核心矛盾:返回太少,知识覆盖不全;返回太多,上下文噪音增加、token 成本飙升、模型“看不过来”。所以,检索阶段不是简单地“返回 top-k”,还需要重排序、过滤、压缩等策略。

2.3 生成阶段:让模型在“材料”上作答

生成阶段把检索到的片段和用户的原始问题一起打包成 Prompt,交给 LLM。Prompt 里要明确告诉模型:“请根据提供的资料回答问题,不要使用资料外的知识;如果资料中找不到答案,请直接说不知道。”

这里有一个经常被忽略的细节:检索到的材料顺序会影响答案质量。最相关的放前面,还是放后面,模型关注度不一样。有些框架支持对结果按相关性重新排序,影响很大。另外,还需要控制材料总量,防止超出模型上下文窗口。

2.4 为什么说 RAG 是“管道”而不是“功能”

我们把上面三个阶段串起来看:加载→切分→编码→存储→查询→检索→重排→生成。每一个环节都有参数需要调优,每一个环节出问题都会影响最终答案。所以 RAG 不是“调一个库函数”,而是一条流水线。这条流水线,就是我们说的“知识获取管道”。

管道化思维非常关键。建一条管道不是一次性工作,而是持续运营的过程。文档更新了,要增量索引;新业务出现了,要调整切分策略;用户反馈差,要调检索参数。这个体会我放到后面经验部分再展开。

3. 实操:从零搭建一个最小的 RAG 知识管道

这一节直接上干货。我们用 Python 语言、LangChain 框架和 Chroma 向量数据库,从零搭一套可运行的最小 RAG 管道。这套东西麻雀虽小五脏俱全,你跑通了之后,再往里面加各种高级特性都容易。

3.1 环境准备与依赖安装

建议用 Python 3.10 以上版本,新开一个虚拟环境,避免依赖冲突。

python -m venv rag_env source rag_env/bin/activate # Windows 下用 rag_env\Scripts\activate pip install langchain langchain-community langchain-openai chromadb pypdf

这里说明一下选型逻辑。LangChain 是生态最成熟的 RAG 编排框架,封装好了加载、切分、向量化、检索、生成的全流程。Chroma 是轻量级向量数据库,纯本地运行,不需要单独开服务,适合学习和原型验证。如果你要上生产,可能换成 Milvus、Qdrant 或 ES 8.x 的向量检索能力,但流程逻辑是完全一致的。

3.2 准备一份测试文档

我们自己造一份简单的文档,命名为employee_manual.md,内容写两三段公司制度。真实项目里可能是一堆 PDF,但第一版先用 Markdown 调试逻辑。

# 员工报销制度 第一条:员工报销需在费用发生后的一个月内提交申请。 第二条:报销金额超过一万元,需要总经理审批。 第三条:交通费报销需提供发票,发票抬头为公司全称。 ...

这种结构化文本切分很简单,正好用来理解 chunk 的含义。

3.3 实现代码:加载、切分、入库

下面的代码是整套管道的索引部分。

from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载文档 loader = TextLoader("employee_manual.md") documents = loader.load() # 2. 切分文本 text_splitter = RecursiveCharacterTextSplitter( chunk_size=200, chunk_overlap=50, separators=["\n\n", "\n", "。", ";", ",", " ", ""], ) chunks = text_splitter.split_documents(documents) print(f"切分得到 {len(chunks)} 个chunk") # 3. 向量化并入库 embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )

这段代码做了三件事,对应前面讲的索引阶段。留意chunk_size=200和chunk_overlap=50这两个参数,第一个是每个 chunk 的字符数上限,第二个是相邻 chunk 之间的重叠字符数。重叠的目的是避免一个完整语义被硬切断后,关键信息恰好落在边界上。比如“报销需在一个月内”这半句在 chunk A,后半句“提交申请”在 chunk B,检索时可能只召回 A 或 B,导致上下文不完整。有了 overlap,至少有一个 chunk 包含完整语义。

不要全程抄这个参数,chunk_size 和 chunk_overlap 必须根据你文档的类型调整。后文会专门讲怎么调。

3.4 实现代码:检索问答

索引建好之后,写一个问答函数。

from langchain_openai import ChatOpenAI from langchain.chains import RetrievalQA # 加载已有向量库 vectorstore = Chroma( persist_directory="./chroma_db", embedding_function=OpenAIEmbeddings(model="text-embedding-3-small") ) retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4} ) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=True, ) question = "报销金额超过一万元需要谁审批?" result = qa_chain.invoke({"query": question}) print(result["result"]) print("依据来源:", result["source_documents"])

chain_type="stuff"的意思是直接把检索到的所有文档堆积进 Prompt。这是最简单的模式,适合答案依赖单一上下文的情况。如果检索回来的多个 chunks 之间有矛盾,或者涉及多跳推理,stuff 就不行了,要换成 map-reduce 或 refine 模式,后面讨论。

search_kwargs={"k": 4}指召回 4 个 chunk。这个值可以调。太小容易漏,太大容易干扰模型注意力。我建议先默认 4,再根据效果调整。

3.5 第一个可改进的进阶点:带元数据的检索

真实项目里,知识库可能包含多个文档,不同文档可能属于不同部门、不同版本。我们希望检索时只查特定范围。这时要给 chunk 附上元数据,并在检索时用filter过滤。

在加载文档时指定元数据:

loader = TextLoader("employee_manual.md") documents = loader.load() for doc in documents: doc.metadata = {"source": "employee_manual", "type": "制度", "version": "v2.0"}

检索时过滤:

retriever = vectorstore.as_retriever( search_kwargs={ "k": 4, "filter": {"type": "制度"} } )

这样既加速检索,又防止跨域信息污染。这一点在维护大型知识库时特别有用。

4. 让 RAG 真正“可靠”:高级检索策略与调优方法

跑通上面这套基础管道,只是完成了“能用”。接下来聊怎么把它做得“好用、可靠”。这部分是很多教程不会细讲的实战调优,也是决定 RAG 项目成败的关键。

4.1 切分策略:没有最优,只有最合适

文本切分是最容易被低估的环节。先说结论:永远不要用固定字符数硬切。

我推荐的默认切分器是RecursiveCharacterTextSplitter,它会按你传入的分隔符列表,从高到低优先级尝试切分。先按段落\n\n,再按句号、分号、逗号,最后按空格。这样能最大限度保持语义边界。代码示例中的separators列表就是我调得比较顺的一套。

切分参数怎么定?给你一个经验公式做参考:

  • 如果你的文档是 FAQ/条款式,一条问答或一条条款作为一个 chunk,长度不限。
  • 如果是长文章,先按章节标题切,再根据 chunk_size 继续细化,chunk_size 建议在 300~800 之间,chunk_overlap 设为 chunk_size 的 10%~20%。
  • 如果是表格,尽量整行或整表作为一个 chunk,不要拆散单元格。

调完之后,强烈建议做一个“可视化检查”:把切出来的 chunks 打印出来,人眼扫一遍,重点看语义是否完整、有没有“断章取义”的怪片段。这一步没有必要省,坚持做几次,你对参数手感的把握会完全不一样。

4.2 嵌入模型选型:决定“语义理解”的底座

Embedding 模型直接决定了两个文本“像不像”。同一个知识库,用不同的 embedding 模型,同样的查询,召回结果可能天差地别。

主流的方案有:

模型特点适用场景
OpenAI text-embedding-3-small性能均衡,API 调用简单通用快速原型
OpenAI text-embedding-3-large维度更高,精度更好对质量要求高,预算充足
BGE 系列(中文)中文语义效果好,开源可本地部署中文知识库,数据合规要求
M3E 系列针对中文优化,轻量中文场景,资源受限
Cohere embed 系列多语言,配合 Rerank 效果好多语言场景

选型原则有三条:第一,尽量和您的 LLM 是同一生态,避免术语漂移;第二,中文场景强烈建议先测中文专用模型,别默认用英文模型;第三,考虑私有化部署的可能,如果数据不能出内网,选开源的本地模型。

我踩过的坑是:一开始图省事用了英文的小 embedding 模型处理中文财务文档,检索命中率惨不忍睹,后来换了中文专用模型,同一套参数的 hit rate 直接提升 20 个百分点以上。所以模型选型千万不要想当然。

4.3 检索质量的“隐藏炸弹”:HyDE 与 查询改写

用户提问往往口语化、省略化。比如用户问“流程怎么走”,如果你想检索“报销流程”相关的文档,直接拿这句话去向量搜索,效果通常很差。因为 query 和文档的表述方式不同。

两个常用解法:

查询改写(Query Rewriting):先用 LLM 把用户问题改写成适合检索的表述,再调用向量检索。比如将“流程怎么走”改写成“报销申请流程是什么”或“费用报销的具体步骤”。改写之后,query 与文档的语义距离更近。

HyDE(Hypothetical Document Embeddings):先让 LLM 根据问题生成一个“假设答案”,再用这个假设答案的向量去检索。因为假设答案更像目标文档的表述,检索效果往往出奇地好。但也会引入生成错误的风险,需要结合实际情况判断使用。

4.4 重排序:别让“最相关”被淹没

向量检索返回的 top-k 只是初筛。初筛结果里,前几名可能没有真正想要的,排在后面的反而更相关。尤其在知识密集场景,只用相似度分数排序,很容易被语义相近但并非关键答案的段落占据前排。

所以需要Reranker。Reranker 是一个独立的排序模型,它会把 query 和每个候选文档拼接起来,计算一个更精细的相关性分数,然后重新排序。市面上常用的有bge-reranker、Cohere Rerank等。

接入方式很简单,LangChain 里提供了ContextualCompressionRetriever:

from langchain.retrievers import ContextualCompressionRetriever from langchain_community.document_compressors import LLMChainExtractor from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain_community.retrievers import BM25Retriever from langchain.retrievers import EnsembleRetriever

等一等,这里先把基础补全。如果你用了HuggingFaceBgeEmbeddings,需要安装sentence-transformers包。我建议先跑通更简单的基于 API 的 reranker,避免本地依赖问题。

4.5 混合检索:向量 + 关键词的黄金组合

向量检索擅长语义,但关键词检索擅长精确匹配。比如员工手册里的“总经理”和“一万元”,这两个词如果出现在向量结果里,可能因为语义接近而排到前面,但在关键词检索里,能让包含“总经理”的段落尽量暴露出来。

混合检索就是两条腿走路:同时用向量检索和 BM25(稀疏检索)召回,然后合并去重,再做重排序。LangChain 提供了EnsembleRetriever,可以组合两个甚至多个 retriever:

from langchain.retrievers import EnsembleRetriever vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 6}) bm25_retriever = BM25Retriever.from_documents(chunks, k=6) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.3, 0.7] )

我这里给出的权重是常用经验值,具体该偏向量还是偏关键词,取决于你的文档风格。如果是工单、故障报错类,关键词权重可以升高;如果是长文、概念解释类,向量权重应更高。

混合检索最大的收益是:降低了因语义偏移导致的召回失败概率。比如你文档里写的“审批”,用户问的是“签字”,向量检索能匹配上;但文档里写了“一万元整”而用户输入“10000”,关键词检索就能帮你兜底命中。双通道互相补位,健壮性大幅提升。

4.6 上下文压缩:防止“冗余材料”干扰生成

哪怕我们召回 top-k 都成功了,每个 chunk 内部也可能包含大量无关内容。直接全塞给 LLM,模型容易被干扰。解决办法是上下文压缩,即用 LLM 对每个 chunk 做一次摘要,提取与 query 相关的句子。

LLMChainExtractor就是一个压缩器,它会遍历候选文档,用 LLM 剔除无关信息,保留相关片段。缺点是多了一次 LLM 调用,qps 会下降。更轻量的做法是直接截取 chunk 中与 query 相似度最高的句子,用句子级向量匹配。看你的性能要求。

4.7 从 RAG 到 Agentic RAG

现在回应标题里的“AI Agent”。基础的 RAG 管道是“一次检索,一次生成”。但真实 Agent 面对复杂问题时,往往需要改写、追问、多跳检索。于是出现了Agentic RAG——把 RAG 融入 Agent 的决策循环里。

典型的 agentic RAG 流程是这样的:

  1. Agent 收到“为什么上月离职率突然升高”这样的问题。
  2. Agent 拆解问题,拆成“离职率数据”“离职访谈示例”“近月政策变化”三个子查询。
  3. Agent 分别调用不同的知识库检索。
  4. 如果第一轮检索结果不足以支撑回答,Agent 会继续发起新查询,或者要求用户补充信息。
  5. 最终汇总所有检索结果,生成带依据的答案。

这种模式下,RAG 不再是固定管道,而是 Agent 手里的工具。检索与否、检索几次、查哪个库,都由 Agent 根据上下文动态决策。这也是从“demo”走向“产品”的必经之路。

我建议你在把基础管道跑得很稳之后再考虑 agentic RAG。很多人一上来就做复杂编排,结果基础召回质量一塌糊涂,Agent 越聪明,答得越离谱。地基不牢,上层越华丽越危险。

5. 常见问题与排查技巧实录

这一节是一线调试的“血泪合集”。我用一个速查表列出最常踩的坑和对应的排查方向,下面的段落再逐个演示排查思路。

现象最可能的原因排查方向
检索命中率低,答案明明在库中却查不到切分不当、embedding 模型不匹配、query 表述偏差检查 chunk 语义完整性;换中文 embedding;做查询改写
答案“编造”了库中没有的内容检索到的材料相关度低,或 LLM 不遵守指令增加 reranker;加强 Prompt 约束;降低 temperature
答案依赖多段资料,上下文不足top-k 太小;没有做多跳检索调大 k 值;引入 agentic RAG 多次检索
响应太慢重排序和压缩占用大量 LLM 调用换成高效 reranker;减少压缩;缓存
更新知识后,答案还是旧的索引未更新;chunk 被缓存命中增量索引;清理缓存;设定元数据版本
相同问题,答案不稳定LLM 采样随机性;检索结果顺序变化temperature 调为 0;固定检索排序

5.1 案例一:召回结果永远“差点意思”

以前我做一个制造业知识库,文档里有非常明确的参数表,比如“最高耐温 200℃”。用户问“这种材料的耐受温度是多少”,向量检索返回的前几条全是关于材料成分的内容,关于温度的段落排到第 6 名,而 k 设置的是 4,所以答案就是“无法从资料中找到”。

排查步骤:首先把 k 值加大到 10,发现其实能召回包含温度的段落。说明embedding认为材料成分和查询更相似,而温度描述被忽略了。然后我切分方式做了调整:把每个技术参数从段落中单独提取成“参数名:数值”这种小 chunk。结果同一个 query,温度段落直接排到第一。所以,召回率低不一定都是 embedding 的问题,很可能是 chunk 切得太粗,关键信息被淹没了。

5.2 案例二:LLM 不按检索内容回答

有时候库里有答案,检索也准确,但模型还是自己发挥。这种情况十有八九是 Prompt 约束不够。只写“根据资料回答”是不够的,要写“如果资料中有答案,必须原文引用;如果没有,回复‘未找到相关信息’,不要猜测。”

另外,temperature也很关键。做知识问答,我推荐直接设到 0。虽然少了一点灵活性,但答案稳定性和可解释性远比“文采”重要。

5.3 案例三:多文档关系复杂,单次检索搞不定

“统计一下各部门今年报销总额的中位数”——这已经不是单条检索能解决的问题,它需要先查报销记录,再查部门对照表,最后还要做数值计算。传统 RAG 一次检索搞不定。我的实践是拆成两步:第一步用 RAG 检索到相关数据表和字段说明,第二步让 Agent 生成查询代码去执行计算。这其实就是 text-to-sql 和 RAG 的结合。

这种场景再做 agentic RAG 就更有价值了。Agent 的 planner 会生成一个“查询计划”,每个计划步骤对应一次检索或者工具调用。本质上是把复杂的“知识杂交”问题,变成多个简单问题的组合。

5.4 案例四:知识库数据源多且杂

一个真实项目里,知识库可能来自很多地方:飞书文档、Confluence、数据库导出、本地 Word 文件。每一种格式都要用不同的 loader 和解析策略。我建议在管道层做一个统一的“文档标准化处理器”,把所有输入都转换成标准化的 Markdown 或 JSON 格式,再进入切分逻辑。这样后续维护管道的人不会被各种格式的特殊性逼疯。

另外,要做数据血缘追踪。每个 chunk 必须保留原始来源、文件版本、入库时间。这样当用户对答案提出质疑时,你可以顺着元数据找到原始文档,这是企业场景的底线要求。

6. 从“跑通”到“能用”:知识获取管道的工程化建议

这一部分聊点偏工程的思路,不涉及具体代码,但对你在真实环境中的落地非常有帮助。

6.1 管道的质量评测:没有指标就没有优化

不要凭感觉判断 RAG 好不好。我强烈建议建立一个评测集,至少包含 100 组(问题,标准答案,参考文档)三元组。跑一轮管道,用hit_rate(召回里是否包含正确答案)和answer_relevance(答案是否回答到点上)作为指标。

平时改动一个参数,比如换了切分策略、升级了 embedding 模型,都在这套评测集上跑一遍对比。没有评测集的调优就是在黑夜里开车。评测集要维护,随着业务发展不断补充新问题。做 Agent 开发的人,迟早要开始造自己的评测轮子。

6.2 索引更新:增量、定时、事件驱动

知识库不是静态的,经常有新文档、文档修改、删除。增量索引方案有很多种,核心是做好“变更追踪”。简单场景每天定时重建一次索引就行;复杂场景通过事件监听,文档一变更就触发对应 chunk 的重新切分和入库。

这里提醒一句:向量数据库里的旧向量要同步删除或标记为过期,否则检索时会把已废弃的文档捞出来,造成“知识打架”。

6.3 安全和权限:知识获取管道的“守门员”

在企业里,不是所有人都能查所有文档。RAG 检索之前,必须先做权限过滤。两个常见方案:一是预先按权限分库,不同用户查不同的库;二是在 chunk 元数据里打上权限标签,检索时用 filter 过滤。第二种更灵活,但需要在检索层做一次强制校验,不能依赖用户传入。

别忘了脱敏处理。PDF 里可能包含身份证号、薪资信息,这些内容在写入索引前就要做识别和脱敏,否则一旦检索引擎被外部访问,后果不堪设想。

6.4 响应延迟:管道的性能预算

我们经常说 RAG 延迟的大头不在向量检索,而在于:文档解析、embedding 计算、rerank、LLM 生成。按我的经验,一个普通 RAG 查询的延迟分布常常是:

  • 检索阶段(向量 + 关键词):20~80ms
  • 重排序:50~200ms(取决于候选数)
  • LLM 生成:800ms~几秒

如果你的 Agent 需要多轮 RAG,那么每一轮都要乘以这个数。优化方向有三个:减少检索轮次、使用更快的压缩模型、对常用查询结果做缓存。缓存特别适合“相同问题反复询问”的场景,比如企业制度问答。把问题和答案缓存起来,命中缓存直接返回,性能能提升一个数量级。

6.5 成本控制:不是“模型越多越好”

每引入一个 LLM 调用,就多一笔费用和延迟。有的管道里用了查询改写、rerank、上下文压缩、最终生成,总共四次模型调用。我的建议是:能不用 LLM 的环节就尽量不用。比如查询改写可以先用规则处理,把常见泛问映射到标准检索词;rerank 用专门的小模型而非大模型;上下文压缩用要谨慎,它往往是最不值得的那次调用。

成本和质量要平衡,少做一次高级处理,可能质量下降 2%,但成本下降 30%。做产品不是拿满分,而是在限定资源下找到足够的可用性。

7. 一些亲测有效的心得

文章快收尾了,按惯例分享几个我在 RAG 项目里亲测有效的操作习惯。

第一,先做“最小闭环”再优化。不要一上来就引入混合检索、rerank、agentic RAG。先把最简单的“切分→embedding→检索→生成”跑通,让用户看到完整流程。然后根据真实案例,找出最痛的问题,一项项加特性。很多团队做 RAG 失败是因为在烂地基上盖了太多楼。

第二,把“检索调试”单独拿出来做。很多问题其实在生成之前就已经注定了。所以我经常脱离开 Agent,单独写一个脚本:输入 query,打印出召回的 top10 chunk 和分数。用肉眼分析,哪些是相关的,哪些是噪音,为什么相关的不在前面。这比看最终答案去猜原因高效得多。

第三,注意 prompt 里检索材料的“引用痕迹”。为了可追溯性,最好让 LLM 在生成时标注出每个结论引用了哪份来源片段。这在代码上很好实现,给每个 chunk 编号,提示词里要求模型按编号引用。用户能回溯原文,模型也会更严谨,幻觉明显减少。

第四,用“会话内记忆”缓解重复检索。Agent 对话中,用户经常连续问同一个主题。比如先问“报销流程”,再问“那发票要求呢?”第二次问题如果从头检索,可能丢掉前文语境。好在 Agent 框架里本身就带记忆,可以将历史上下文压缩后作为查询辅助,或者把上一轮检索过的 chunk 作为候选反复使用,省一次检索。

第五,别把 RAG 当成万能药。有些问题本身不适合检索,比如“解释一下相对论”这种概念性问题,模型自己就很擅长,检索反而引入不必要的干扰;有些问题需要实时数据,你应该接 API 而不是查文档。知识获取管道只是知识获取的手段之一,它在正确的位置上最好使,但不要不加区分地什么都往里塞。

写到这里,RAG 基础这条线的核心已经聊得差不多了。接下来这个系列我会继续深入:Agentic RAG 的具体实现、如何在 Multi-Agent 框架中分配知识检索角色、RAG 评测的最优实践、以及混合记忆机制的架构。如果你正在做 Agent 应用,或者在知识密集场景里构建助手,这一篇欢迎收藏反复读。有任何不清楚的地方,评论区里说你的具体场景和遇到的问题,我会挑典型详细拆解。