1. 为什么知识获取管道是 AI Agent 的分水岭
做 AI Agent 开发的人,绕不开一个尴尬的现实:模型本身很聪明,但它不知道你公司上个月刚改的报销制度,不知道你私有代码库里那个函数为什么被废弃,更不知道你手里那份 PDF 合同里到底写了什么。大模型的参数里装的是训练截止那一刻的公共知识,而真实业务里 80% 的问题,答案都躺在模型没见过的文档里。
这就是知识获取管道存在的意义。所谓管道,不是简单地把文档丢给模型,而是一条从原始资料到最终答案的完整链路:文档怎么进来、怎么切、怎么变成向量、怎么存、怎么在用户提问时把最相关的片段捞出来、怎么塞进上下文让模型生成靠谱的回答。这一整套东西,行业里有个大家耳熟能详的名字——RAG,检索增强生成。
我在过去一年多里,从零搭过好几个 RAG 系统,有给内部知识库用的,有给客服机器人用的,也有给代码助手用的。踩过的坑能写一本书:切分策略选错导致答案断章取义、嵌入模型选错导致中文检索一塌糊涂、召回率看着挺高但答非所问、上线后发现用户问法千奇百怪完全超出预期。所以这一篇,我想把 RAG 的基础知识获取管道从头到尾拆一遍,讲清楚每个环节为什么这么设计、有哪些坑、怎么调。适合正在搭 AI Agent 的开发者、想给自己的产品加知识库能力的工程师,以及刚接触 RAG 想搞明白它到底怎么回事的朋友。
RAG 这个词现在被用得很泛,有人把它当成一个库,有人把它当成一个流程,还有人把它当成一个产品。我的理解是:RAG 是一种架构模式,核心思想是让模型在生成答案之前,先去外部知识源里检索相关信息,把检索结果作为上下文一起喂给模型。这样模型就不需要把知识记在参数里,而是每次现查现用。好处很直接——知识可以随时更新,不用重新训练;答案有出处,可以追溯;成本比微调低得多。坏处也很直接——检索质量直接决定回答质量,检索没做好,后面全白搭。
2. RAG 知识获取管道的整体设计与思路拆解
2.1 管道的五个核心环节
一条完整的知识获取管道,我习惯把它拆成五个环节,每个环节都有明确的输入和输出,出了问题也方便定位。
第一个环节是文档接入。这一步要解决的是“知识从哪来”的问题。来源可能是 PDF、Word、Markdown、网页、数据库记录、API 返回的 JSON,甚至是聊天记录。不同来源的解析难度天差地别,PDF 里的表格和扫描件是老大难,网页里的导航栏和广告是噪音,数据库记录倒是干净但需要拼装成自然语言。
第二个环节是文本切分。整篇文档太长,不可能一股脑塞进模型上下文,所以要切成小块。切分不是随便按字数砍,切得好不好直接影响检索效果。切太碎,语义不完整;切太大,噪音多还浪费上下文。
第三个环节是向量化。把切好的文本块通过嵌入模型转成向量,也就是一串浮点数。语义相近的文本,向量距离也近。这一步是 RAG 能“理解”语义而不是只做关键词匹配的关键。
第四个环节是存储与索引。向量要存到向量数据库里,同时还要存原文、元数据(来源、时间、章节等),方便检索时过滤和展示出处。
第五个环节是检索与重排。用户提问时,把问题也向量化,去向量库里找最相似的若干块,然后可能再用重排模型精排一遍,最后把 top-k 个块拼进提示词。
这五个环节环环相扣。我见过太多人只关注向量数据库选哪个,却忽略了切分策略和重排,结果系统上线后效果惨淡。实际上,切分和重排对最终效果的影响,往往比换一个更贵的嵌入模型还大。
2.2 为什么是 RAG 而不是微调
经常有人问:我直接把知识微调进模型不行吗?行,但要看场景。
微调适合的是“让模型学会一种风格或一种能力”,比如让模型学会用你公司的口吻写邮件,或者学会某种特定的输出格式。微调不适合“让模型记住大量事实性知识”,原因有三:一是知识更新频繁时,每次更新都要重新微调,成本高周期长;二是微调后的模型容易产生幻觉,因为它把知识揉进了参数里,你没法追溯它到底从哪学的;三是微调需要大量高质量问答对,标注成本不低。
RAG 的优势正好互补:知识更新只需更新文档库,不用动模型;答案可以附上出处,用户能自己判断可信度;不需要标注问答对,有文档就行。所以我的经验是,事实性知识用 RAG,风格和能力用微调,两者可以叠加使用。
2.3 稠密嵌入与稀疏嵌入的分工
热词里提到了稠密嵌入和稀疏嵌入,这两个概念必须讲清楚,因为它们是检索的两条腿。
稠密嵌入就是刚才说的向量化,把文本变成几百到几千维的稠密向量,每一维都有值。它的强项是语义匹配——用户问“怎么报销”,文档里写“费用申请流程”,字面不一样但语义相近,稠密嵌入能匹配上。
稀疏嵌入则是传统的关键词匹配的升级版,比如 BM25、SPLADE。它的向量维度等于词表大小,大部分是零,只有出现的词才有值。它的强项是精确匹配——用户问“报销标准是多少”,文档里有“报销标准”这个词,稀疏嵌入能精准命中。
两者各有盲区。稠密嵌入对专有名词、型号、代码标识符不敏感,用户问“ERR-4032 怎么解决”,它可能给你返回一堆泛泛的错误处理文档。稀疏嵌入对同义改写不敏感,用户问“怎么请款”,文档写“报销流程”,它可能匹配不上。
所以现在主流的做法是混合检索:两路同时召回,再融合排序。融合算法常用 RRF(倒数排名融合),简单说就是把两路的排名做加权倒数求和,谁在两路里都靠前,谁就排最前。这个方案我在多个项目里实测,比单用稠密嵌入的召回率高 15% 到 30%,尤其是面对包含大量专有名词的企业知识库时,提升非常明显。
3. 核心细节解析与实操要点
3.1 文档解析:脏活累活但决定上限
文档解析是整条管道最不起眼但最影响上限的环节。垃圾进,垃圾出,这句话在 RAG 里体现得淋漓尽致。
PDF 解析是重灾区。普通文本型 PDF 用 PyMuPDF 或 pdfplumber 就能搞定,但遇到扫描件就得上 OCR。表格更麻烦,很多解析库会把表格拆成一行行乱码。我的做法是:能用结构化格式(Markdown、HTML、Word)就优先用,实在只有 PDF 就先判断是文本型还是扫描型,文本型用 pdfplumber 提取并保留段落结构,扫描型走 OCR 但要做好后处理。
网页解析要注意去噪。导航栏、页脚、广告、评论区都是噪音,直接抓正文可以用 readability 类的库,或者用大模型做一次内容提取。我试过用模型把网页 HTML 转成干净的 Markdown,效果比规则提取好很多,成本也可控。
注意:解析阶段一定要保留元数据,比如来源文件名、页码、章节标题、更新时间。这些信息在检索时可以用来过滤,在展示时可以用来标注出处,后期想补非常麻烦。
3.2 文本切分:不是切得越小越好
切分策略我踩过的坑最多。早期我按固定 500 字切,结果一句话被拦腰截断,检索出来的片段读起来莫名其妙。后来改成按段落切,又遇到超长段落塞不进上下文的问题。
现在我的做法是递归字符切分 + 语义边界优先。具体来说,优先按标题、段落、句子这些自然边界切,如果单个段落还是太长,再按字符数硬切,但保留一定的重叠(overlap)。重叠的作用是防止关键信息正好落在切分点上被割裂,一般设成块大小的 10% 到 20%。
块大小怎么定?没有万能答案,但有个经验值:中文场景下,300 到 800 字是比较舒服的区间。太小了语义不完整,太大了噪音多。我一般会先按 500 字切,然后用一批真实问题测召回,看命中的块是不是刚好包含答案,再微调。
还有一种进阶做法是父子块:检索时用小块(精准匹配),返回时用大块(上下文完整)。比如把文档切成 200 字的小块用于检索,但每个小块关联一个 1000 字的父块,命中后返回父块给模型。这样既保证了检索精度,又保证了上下文完整。这个方案在长文档场景下效果很好,代价是存储翻倍。
3.3 嵌入模型选型:中文场景别乱用
嵌入模型的选择直接决定检索质量。英文场景下,OpenAI 的 text-embedding-3 系列、Cohere 的 embed 系列都是成熟选择。中文场景要特别注意,很多英文模型在中文上表现平平。
我实测下来,中文场景比较靠谱的有 BGE 系列(智源开源)、M3E、GTE 等。BGE-large-zh 在中文语义相似度任务上表现稳定,而且可以本地部署,数据不出内网。如果追求更强效果且能接受调用外部服务,也可以考虑一些商业嵌入 API,但要注意数据合规。
选型时不要只看榜单,一定要用你自己的数据测。方法很简单:准备 50 到 100 个真实问题,每个问题标注哪些文档块是相关的,然后算召回率(相关块有多少被召回)和 MRR(相关块排在第几位)。不同模型在你的数据上可能排名完全不一样。
提示:嵌入模型的维度不是越高越好。高维度检索慢、存储大,但效果未必线性提升。1024 维在多数场景下已经够用,除非你的知识库特别大且语义特别细。
3.4 向量数据库:别为了时髦上重型方案
向量数据库这两年卷得厉害,Milvus、Qdrant、Weaviate、Chroma、pgvector 一大堆。我的建议是:按数据量和团队情况选,别为了时髦上重型方案。
数据量在十万块以内,pgvector 就够了,直接挂在现有的 PostgreSQL 上,运维成本几乎为零。数据量到百万级,Qdrant 或 Milvus 更合适,它们对大规模检索做了专门优化。如果只是本地实验或小项目,Chroma 开箱即用,几行代码就能跑起来。
选型时要关注的指标:检索延迟、召回率、过滤能力(能不能按元数据过滤)、是否支持混合检索、运维复杂度。我见过团队为了“技术先进”上了 Milvus 集群,结果数据量才几万块,运维成本比收益还高,得不偿失。
3.5 检索与重排:top-k 不是越大越好
检索阶段,用户问题向量化后去库里找最相似的 top-k 个块。k 取多少?常见是 5 到 20。k 太小可能漏掉答案,k 太大噪音多还浪费上下文。
但光靠向量相似度召回,质量往往不够。这时候重排就派上用场了。重排模型(比如 BGE-reranker)会把问题和每个候选块一起输入,输出一个相关性分数,比单纯的向量距离更准。流程是:先用向量检索召回 50 个候选,再用重排模型精排出 top-5 给模型。这个两阶段方案我在多个项目里用,效果比单阶段好一大截。
重排的代价是延迟增加,因为要对每个候选块跑一次模型。50 个候选的重排延迟通常在几百毫秒,可以接受。如果对延迟极敏感,可以减少候选数或者用更轻量的重排模型。
4. 实操过程与核心环节实现
4.1 从零搭一条最小可用管道
下面我用 Python 演示一条最小可用的 RAG 知识获取管道。技术栈选 LangChain + pgvector + BGE 嵌入,这套组合本地可跑、成本低、中文友好。
先装依赖:
pip install langchain langchain-community langchain-postgres pgvector psycopg2-binary sentence-transformers pymupdf文档解析和切分:
from langchain_community.document_loaders import PyMuPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 加载 PDF loader = PyMuPDFLoader("knowledge.pdf") docs = loader.load() # 递归切分,中文场景用中文分隔符 splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) chunks = splitter.split_documents(docs) print(f"切出 {len(chunks)} 个块")这里 separators 的顺序很关键,优先按段落切,再按句子切,最后才按字符切。中文的句号、问号、分号都要列进去,否则会按英文标点切,效果差很多。
嵌入和存储:
from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain_postgres import PGVector # BGE 中文嵌入模型 embedding = HuggingFaceBgeEmbeddings( model_name="BAAI/bge-large-zh-v1.5", model_kwargs={"device": "cuda"}, encode_kwargs={"normalize_embeddings": True} ) # 存入 pgvector vectorstore = PGVector.from_documents( documents=chunks, embedding=embedding, collection_name="my_knowledge", connection="postgresql+psycopg2://user:pass@localhost:5432/ragdb" )注意normalize_embeddings=True,BGE 模型要求向量归一化,否则相似度计算会偏。
检索:
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) results = retriever.invoke("报销流程是什么") for r in results: print(r.page_content[:100])这就是最小可用版本。但我要提醒一句:这个版本能跑通,但离生产可用还有距离,缺了混合检索、重排、元数据过滤、出处标注。
4.2 加上混合检索和重排
混合检索需要同时维护稠密和稀疏两路。稀疏检索可以用 BM25,LangChain 里有 BM25Retriever。
from langchain_community.retrievers import BM25Retriever from langchain.retrievers import EnsembleRetriever # 稀疏检索 bm25 = BM25Retriever.from_documents(chunks) bm25.k = 5 # 稠密检索 dense = vectorstore.as_retriever(search_kwargs={"k": 5}) # 融合 ensemble = EnsembleRetriever( retrievers=[bm25, dense], weights=[0.4, 0.6] )权重怎么定?如果知识库专有名词多,稀疏权重可以高一点;如果用户问法口语化、同义改写多,稠密权重高一点。我一般从 0.4/0.6 起步,用测试集调。
重排:
from langchain.retrievers import ContextualCompressionRetriever from langchain_community.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-large") compressor = CrossEncoderReranker(model=model, top_n=5) rerank_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble )这样检索流程就是:BM25 和稠密各召回 5 个,融合后得到候选集,重排模型精排出 top-5。实测这套组合比单路稠密检索的答案准确率高不少。
4.3 拼装提示词与生成
检索到相关块后,要拼进提示词。这里有个细节:一定要在提示词里明确告诉模型“只根据提供的资料回答,资料里没有就说不知道”。否则模型会用自己的参数知识瞎编。
def build_prompt(question, contexts): context_text = "\n\n".join([ f"[来源 {i+1}] {c.page_content}" for i, c in enumerate(contexts) ]) return f"""你是一个知识库助手。请严格根据下面的资料回答问题。 如果资料中没有相关信息,直接回答"资料中未找到相关内容",不要编造。 资料: {context_text} 问题:{question} 回答:"""生成部分可以接任意大模型,本地部署的 Qwen、ChatGLM,或者调用商业 API 都行。关键是提示词要约束住模型,让它别自由发挥。
4.4 参数计算与选择过程
块大小和重叠怎么定?我一般这样算:假设嵌入模型的最大输入是 512 token,中文大约 1 token 对应 1.5 到 2 个字,那单块最多 700 到 1000 字。但实际不用顶满,留出余量给重叠和特殊字符,500 字比较稳妥。
top-k 怎么定?看上下文窗口。假设模型上下文 8k token,提示词模板占 500,问题占 100,留给资料的有 7000 左右。每块 500 字约 350 token,那最多能塞 20 块。但塞太多噪音大,我一般给 5 到 8 块,剩下的空间留给多轮对话历史。
重排候选数怎么定?候选越多重排越准但越慢。50 是个平衡点,延迟可接受,召回也够。如果延迟敏感,降到 20。
5. 常见问题与排查技巧实录
5.1 召回率低怎么办
召回率低是最常见的问题,表现为答案明明在库里但检索不出来。排查顺序:
先看切分。是不是关键信息被切断了?把命中的块和原文对比,看答案是否完整。如果是,调整切分策略,加大重叠或改用父子块。
再看嵌入模型。用一批问题测,看相关块排在第几。如果相关块根本没进 top-50,说明嵌入模型不适合你的数据,换模型或加稀疏检索。
最后看查询改写。用户问法可能和文档表述差太远。可以加一步查询改写,用模型把用户问题改写成多个相关查询,分别检索再合并。这个技巧在多义词和口语化提问场景下很有效。
5.2 答案答非所问
检索到了相关块,但模型回答跑偏。原因通常是:提示词没约束好,或者检索块里噪音太多。
先检查提示词,确保明确要求“只根据资料回答”。再检查检索块,是不是混进了不相关的块。如果是,提高重排的 top_n 门槛,或者加元数据过滤,比如只检索某个部门的文档。
还有一种情况是块太大,一个块里既有答案又有大量无关内容,模型被带偏。这时候改用小块检索、大块返回的父子块方案。
5.3 专有名词检索不到
企业知识库里全是产品型号、内部术语、代码标识符,稠密嵌入对这些不敏感。解决方案就是加稀疏检索,BM25 对精确匹配很在行。如果还不行,可以在切分时把专有名词单独提取出来做关键词索引,检索时优先匹配。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 答案在库里但检索不到 | 切分切断、嵌入不匹配、查询改写缺失 | 对比命中块与原文、测嵌入召回率 | 调整切分、加稀疏检索、查询改写 |
| 检索到了但答非所问 | 提示词约束弱、噪音块混入 | 检查提示词、看检索块相关性 | 强化提示词、加重排、元数据过滤 |
| 专有名词匹配差 | 稠密嵌入对精确匹配弱 | 测 BM25 单独召回 | 混合检索、关键词索引 |
| 响应太慢 | 重排候选多、块太大、模型慢 | 看各阶段耗时 | 减候选、减块大小、换轻量模型 |
| 答案过时 | 文档库没更新 | 检查文档同步机制 | 加定时同步、版本管理 |
5.5 几个我踩过的坑
第一个坑是忽略元数据。早期我没存来源信息,用户问“这个答案哪来的”我答不上来,后来补元数据补得很痛苦。现在我在切分阶段就把来源、页码、章节都带上。
第二个坑是盲目追求大模型。以为换个更强的生成模型就能解决所有问题,结果发现检索不行,再强的模型也白搭。RAG 的瓶颈通常在检索,不在生成。
第三个坑是不做评测就上线。凭感觉调参数,上线后被真实问题打脸。后来我养成了习惯,每次改动都用固定测试集跑一遍,看召回率和准确率的变化。
第四个坑是块大小一刀切。不同文档类型适合不同块大小,技术文档适合小一点,叙述性文档适合大一点。现在我会按文档类型分别配置切分参数。
6. 知识获取管道的进阶方向
基础管道跑通后,可以往几个方向进阶。
GraphRAG是这两年的热点,核心思想是把知识图谱和 RAG 结合。传统 RAG 只能检索孤立文本块,GraphRAG 能沿着实体关系检索,适合需要多跳推理的问题。比如“A 产品的负责人之前在哪家公司”这种问题,传统 RAG 很难答,GraphRAG 可以顺着关系链找到。代价是构建图谱成本高,适合知识结构清晰的场景。
Agentic RAG是另一个方向,让 Agent 自己决定什么时候检索、检索什么、要不要多轮检索。传统 RAG 是固定流程,Agentic RAG 把检索当成一个工具,Agent 根据问题复杂度自主调用。这个方案灵活但复杂,适合问题类型多样的场景。
多模态 RAG处理图片、表格、图表。很多企业文档里关键信息在图表里,纯文本 RAG 会漏掉。方案是用多模态嵌入模型或者先用模型把图表转成文字描述再入库。
本体 RAG则是引入本体论,给知识加上形式化的语义约束,让检索更精准。这个偏学术,工程落地还不多,但值得关注。
我的建议是:先把基础管道做扎实,把召回率和准确率调到满意,再考虑进阶。基础没打好就上 GraphRAG,往往是花了大成本效果还不如优化好的基础版。
7. 关于评测的一点个人经验
最后聊聊评测,因为这是很多人忽略但极其重要的一环。没有评测,你根本不知道改动是变好还是变坏。
我的做法是建一个黄金测试集:收集 100 到 200 个真实用户问题,每个问题人工标注哪些文档块是相关的、标准答案是什么。然后每次改动都跑一遍,看三个指标:召回率(相关块被召回的比例)、MRR(相关块的平均排名倒数)、答案准确率(生成答案和标准答案的匹配度)。
召回率和 MRR 衡量检索质量,答案准确率衡量端到端效果。如果召回率高但准确率低,问题在生成或提示词;如果召回率低,问题在检索。
测试集要定期更新,因为用户问法会变,知识库也会变。我一般每季度补充一批新问题,淘汰过时的。
这套评测机制搭起来要花点时间,但一旦搭好,后续所有优化都有了方向,不用再凭感觉瞎调。我在实际项目里最大的体会就是:RAG 不是搭出来就完事的,它是一个需要持续迭代的系统,而评测就是迭代的眼睛。