1. 为什么知识获取管道是 AI Agent 的第一道生死线
做 AI Agent 的人迟早会撞上一堵墙:模型本身很聪明,但你问它公司内部的报销标准、上周刚更新的产品参数、某个客户的特殊约定,它要么一本正经地胡说,要么直接告诉你“我不知道”。这不是模型不行,而是它的知识被冻结在训练截止的那一天,而你的业务知识每天都在变。知识获取管道要解决的,就是让 Agent 在运行时能够“现查现用”,把外部知识实时喂进推理链路。RAG(检索增强生成)就是目前最主流、最工程化的一条管道方案。
我先把话说透:RAG 不是一个模型,也不是一个库,它是一套数据流架构。你给它一个用户问题,它去知识库里捞出最相关的若干片段,拼进提示词,再交给大模型生成答案。听起来简单,但真正落地时,90% 的效果问题都出在“捞得准不准”上,而不是“生成得好不好”。所以这一篇我不打算停留在“RAG 是什么”的概念层面,而是把知识获取管道从 0 到 1 拆开,讲清楚每个环节为什么这么设计、参数怎么定、坑在哪里。
这篇文章适合三类人:正在从 0 到 1 搭建 AI Agent 的开发者、手里有知识库但检索效果一直不理想的工程师、以及想搞清楚 RAG 和 Agent 到底怎么结合的技术负责人。读完你应该能自己搭出一条可用的知识获取管道,并且知道效果不好时该往哪个方向调。
2. RAG 管道的整体设计与方案选型
2.1 一条完整管道的五个阶段
很多人以为 RAG 就是“向量数据库 + 大模型”,实际上一条能用的管道至少包含五个阶段,缺一个都会出问题。
- 文档加载(Loading):把 PDF、Word、网页、数据库记录等原始资料读进来,转成纯文本。
- 切分(Chunking):把长文本切成适合检索和拼提示词的小块。
- 嵌入(Embedding):把每个文本块转成一个稠密向量,让语义相近的内容在向量空间里靠得近。
- 检索(Retrieval):用户提问时,把问题也转成向量,找出最相似的若干块。
- 生成(Generation):把检索到的块和问题一起塞给大模型,让它基于这些材料回答。
这五步里,加载和切分决定了知识的上限,嵌入和检索决定了能不能捞准,生成只负责把捞到的东西说人话。我见过太多团队把精力全砸在换模型上,结果切分策略一塌糊涂,换十个模型也救不回来。
2.2 为什么是稠密嵌入而不是关键词匹配
传统搜索用关键词倒排索引,你搜“报销标准”,它只找包含这几个字的文档。但用户可能问“出差吃饭能报多少”,字面完全不重合,关键词搜索直接歇菜。稠密嵌入(Dense Embedding)把文本映射成高维向量,语义相近的句子即使一个字都不重叠,向量距离也很近。这就是 RAG 能理解“意思”而不是“字面”的根本原因。
不过稠密嵌入也有短板:它对精确的专有名词、编号、代码符号不敏感。比如你问“错误码 E5021 怎么解决”,嵌入模型可能把语义相近但错误码不同的文档也捞出来。所以成熟的管道通常是混合检索:稠密向量负责语义召回,BM25 这类稀疏检索负责精确匹配,两路结果融合后再排序。这一点后面实操部分会展开。
2.3 方案选型的三个现实考量
选型时别一上来就追求最先进,先问自己三个问题。
第一,知识规模有多大。几千个文档块,用本地内存向量库(比如 FAISS)就够了,没必要上分布式。上百万块才需要考虑专门的向量数据库。
第二,更新频率如何。如果知识每天变,你得设计增量索引流程;如果一个月更新一次,全量重建反而更省事、更不容易出错。
第三,延迟要求多严。嵌入和检索都要耗时,如果 Agent 要求秒级响应,就得在召回数量、重排模型上做取舍。我一般建议先跑通全链路,再拿真实数据压测,别凭感觉优化。
3. 核心细节解析与实操要点
3.1 文档加载:脏数据是万恶之源
加载环节最大的坑是格式。PDF 里的表格、双栏排版、页眉页脚,直接抽取出来往往是一团乱麻。我踩过的坑是:一份产品手册用双栏排版,抽取后左右栏文字交错,切分出来的块语义完全错乱,检索自然一塌糊涂。
实操建议是:能用结构化源就别用 PDF。如果只有 PDF,优先选带版面分析能力的解析工具,把表格单独抽成结构化文本。加载完一定要人工抽查几段,确认文字顺序和表格内容没被破坏。这一步偷懒,后面全白干。
3.2 切分策略:块大小和重叠的取舍
切分是 RAG 里最被低估的环节。块太大,检索时噪声多,拼进提示词还挤占上下文;块太小,语义不完整,模型拿到的信息支离破碎。
我的经验值是这样的:中文技术文档,块大小 300 到 500 字,重叠 50 到 80 字。为什么要有重叠?因为一句话可能正好被切在边界上,重叠能保证关键信息至少完整出现在一个块里。为什么是 300 到 500?因为一个段落通常讲一个完整意思,这个长度既能装下一个完整论点,又不会混入太多无关内容。
更讲究的做法是按语义切分:先按标题层级切大块,再在块内按句子边界细分,保证每个块是语义自洽的。纯按固定字数硬切,遇到列表和代码块会切得莫名其妙。
3.3 嵌入模型:别只看排行榜
嵌入模型决定了语义空间的质量。选型时别只盯着公开榜单,要拿你自己的数据测。方法很简单:准备 20 到 50 个真实问题,每个问题标注它应该命中的文档块,然后看不同模型的前 5 召回率。榜单第一的模型在你的领域数据上未必最好。
还有一个常被忽略的点:查询和文档要用同一个嵌入模型。有人图省事,文档用一个模型,查询用另一个,向量空间都不一致,检索结果纯属随机。另外,有些模型区分“查询侧”和“文档侧”的嵌入方式,用错了效果会明显下降,接入前务必看清文档说明。
3.4 检索环节:召回数量不是越多越好
检索时你会设一个 top_k,就是取最相似的 k 个块。很多人觉得 k 越大越好,反正模型能自己挑。错。k 太大,无关内容会稀释有效信息,模型反而容易被带偏,而且提示词变长、成本上升、延迟增加。
我的做法是两段式:先用向量检索召回 top 20 到 30,再用一个重排模型(Reranker)精排,取前 3 到 5 个拼进提示词。重排模型比嵌入模型更重、更准,专门用来给候选块打分。这样既保证了召回广度,又保证了最终喂给模型的内容质量。实测下来,加了重排之后,答案准确率提升非常明显。
4. 从零搭建知识获取管道的完整实操
4.1 环境与依赖准备
我用 Python 生态来演示,这套组合最成熟、资料最多。核心依赖是文档加载、文本切分、向量库和嵌入模型四类。下面是一个典型的依赖清单,具体版本按你环境调整。
pip install langchain langchain-community pip install faiss-cpu pip install sentence-transformers pip install pypdf如果你打算用在线嵌入服务,把 sentence-transformers 换成对应厂商的 SDK 即可。本地跑嵌入模型的好处是数据不出内网、没有调用成本,缺点是首次加载模型慢、占内存。中小规模知识库我强烈建议本地嵌入。
4.2 加载与切分的代码实现
先加载文档,再切分。这里的关键是切分参数,我按前面说的经验值来设。
from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 加载 loader = PyPDFLoader("product_manual.pdf") docs = loader.load() # 切分:中文按字符切,块 400 字,重叠 60 字 splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=60, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = splitter.split_documents(docs) print(f"切分出 {len(chunks)} 个块")RecursiveCharacterTextSplitter会按分隔符优先级递归切分,优先在段落边界切,实在不行才在句子边界切,最后才硬切字符。这比固定字数切分合理得多。中文场景一定要把中文标点加进 separators,否则它会按空格切,中文没空格就退化成硬切。
4.3 构建向量索引
切分完就可以建索引了。用 FAISS 做本地向量库,配合本地嵌入模型。
from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings # 本地嵌入模型,首次运行会自动下载 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5" ) # 建索引并持久化 vectorstore = FAISS.from_documents(chunks, embeddings) vectorstore.save_local("faiss_index") print("索引已保存")bge-small-zh-v1.5是中文场景下性价比很高的嵌入模型,体积小、速度快、效果稳。如果你的知识库专业性强,可以换成更大的 bge-large 版本,代价是内存和耗时上升。建索引是一次性工作,但知识更新后要重建或增量更新,这个流程要提前设计好。
4.4 检索与生成的串联
索引建好后,检索就是加载索引、传入问题、取回相关块。
from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") vectorstore = FAISS.load_local( "faiss_index", embeddings, allow_dangerous_deserialization=True ) query = "出差住宿的报销上限是多少" results = vectorstore.similarity_search(query, k=5) for i, doc in enumerate(results): print(f"--- 块 {i+1} ---") print(doc.page_content[:200])拿到 results 后,把它们的内容拼成上下文,加上系统提示词,一起发给大模型。提示词里要明确要求“只根据以下材料回答,材料中没有的信息就说不知道”,这一句能大幅降低幻觉。拼提示词时给每个块加上来源标记,方便后续追溯和引用。
4.5 加入重排提升精度
前面提到两段式检索,重排的实现是在向量召回之后加一层精排。
from sentence_transformers import CrossEncoder reranker = CrossEncoder("BAAI/bge-reranker-base") # 先向量召回 20 个 candidates = vectorstore.similarity_search(query, k=20) # 用重排模型打分 pairs = [[query, doc.page_content] for doc in candidates] scores = reranker.predict(pairs) # 按分数排序取前 4 ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) top_docs = [doc for doc, _ in ranked[:4]]重排模型是交叉编码器,它把问题和文档拼在一起过一遍模型,比向量点积更能捕捉细粒度的相关性。代价是慢,所以只对少量候选做。这个“先粗召回再精排”的结构,是工业界 RAG 的标准做法,值得你直接抄。
5. 常见问题与排查技巧实录
5.1 检索不准的排查顺序
检索不准是最常见的问题,别急着换模型,按这个顺序查。
| 排查项 | 现象 | 处理方式 |
|---|---|---|
| 切分质量 | 块内容语义断裂 | 调整块大小和分隔符,人工抽查 |
| 嵌入模型 | 语义相近却召不回 | 换模型,用真实问题测召回率 |
| 查询改写 | 口语化问题召不回 | 加查询改写,把口语转成书面表达 |
| 召回数量 | 正确答案排在很后面 | 增大 top_k,加重排 |
| 混合检索 | 专有名词召不准 | 加入 BM25 稀疏检索融合 |
我遇到最多的是切分问题。有一次知识库里全是问答对,按固定字数切分把问题和答案切到了两个块里,检索到问题却拿不到答案。后来改成按问答对切分,问题立刻解决。所以切分策略一定要贴合你的数据形态,没有万能参数。
5.2 幻觉依然存在的处理
即使检索到了正确材料,模型有时还是会编。原因通常是提示词没约束好,或者检索到的材料里混了矛盾信息。处理办法:提示词里强制要求引用来源,材料里没有的就明确说不知道;检索结果做去重和冲突检测,同一问题有多个矛盾答案时,把冲突暴露给用户而不是让模型瞎选。
5.3 知识更新的增量方案
知识库不是建一次就完事。我的做法是给每个块打上来源和版本标记,更新时先按来源删除旧块,再插入新块,然后重建这部分索引。FAISS 支持增删,但大规模更新还是全量重建更稳。如果更新频繁,考虑换成支持增量写入的向量库。
5.4 性能与成本的平衡
嵌入和重排都吃算力。如果 Agent 并发高,嵌入服务要单独部署并做批处理;重排模型可以量化压缩,牺牲一点精度换速度。检索结果做缓存,相同或相似问题直接命中缓存,能省下大量重复计算。这些优化等链路跑通、有了真实流量再做,别过早优化。
6. 把 RAG 接进 Agent 的关键思路
RAG 单独跑通只是第一步,接进 Agent 才是它的价值所在。核心区别在于:普通 RAG 是“一问一检索一回答”,而 Agent 里的 RAG 是“Agent 自己决定什么时候检索、检索什么、要不要多轮检索”。这就是所谓的 Agentic RAG。
具体来说,Agent 会先判断这个问题需不需要查知识库。闲聊、常识问题直接答,专业问题才触发检索。检索一次不够,它可以改写查询再检一次,或者根据第一次结果决定下一步查什么。这种自主性让知识获取管道从“被动工具”变成“主动能力”。
实现上,你把检索封装成一个工具(Tool),注册给 Agent。Agent 的推理框架会在需要时调用它。工具的描述要写清楚“什么时候该用”,这直接影响 Agent 的调用准确率。我见过工具描述写得含糊,Agent 该查的时候不查、不该查的时候乱查,效果还不如固定流程。所以工具描述本身也是一门功夫,要写清楚适用场景和输入格式。
另外,多轮检索时要注意上下文管理。每次检索返回的块会累积,容易撑爆上下文窗口。我的做法是每轮检索后做一次压缩,只保留和当前子问题最相关的部分,把历史检索结果摘要化。这样既能多轮深挖,又不会让提示词无限膨胀。
7. 我在实际项目里踩过的几个坑
第一个坑是过度依赖单一嵌入模型。早期我所有场景都用同一个模型,结果在代码类知识库上表现很差,因为代码的语义和自然语言差别很大。后来针对不同知识类型选不同模型,效果才上来。所以别偷懒,按领域选模型。
第二个坑是忽略查询侧的处理。用户的问题往往很口语、很模糊,直接拿去检索效果打折。加一层查询改写,把“那个报销的事咋弄”改写成“差旅费报销流程和标准”,召回率立刻不一样。这一步成本很低,收益很高。
第三个坑是没有评估体系。凭感觉调参是灾难。一定要建一个小规模评测集,每次改动都跑一遍,用数据说话。哪怕只有 30 个问题,也比拍脑袋强。
第四个坑是把 RAG 当银弹。有些问题根本不适合 RAG,比如需要复杂推理、多跳关联的查询,单纯检索拼上下文解决不了。这时候要考虑 GraphRAG 或者让 Agent 做多步推理。认清 RAG 的边界,比盲目堆技术更重要。
知识获取管道这条线,说到底就是让 Agent 的“记忆”能够实时更新、精准调用。把加载、切分、嵌入、检索、生成这五步的细节抠到位,再把它作为工具接进 Agent 的推理循环,你就拥有了一条真正可用的知识获取能力。剩下的,就是在真实数据上不断迭代,让召回率和准确率一点点往上走。