1. 为什么知识获取管道是 AI Agent 落地的第一道坎做 AI Agent 开发的人绕不开一个尴尬的现实模型本身很聪明但它不知道你公司内部的报销制度、不知道你手上这个项目的接口文档、更不知道你昨天刚更新的产品报价单。你问它一个稍微偏门的问题它要么一本正经地胡说八道要么礼貌地告诉你“我无法获取实时信息”。这不是模型不行而是它的知识边界被训练数据锁死了。知识获取管道要解决的就是这件事——在模型和外部知识之间架一条可控、可维护、可扩展的通道。而RAGRetrieval-Augmented Generation检索增强生成目前是这条通道里最成熟、落地成本最低的方案。它的核心逻辑说白了就一句话先去知识库里把相关资料捞出来再把资料塞进模型的上下文让模型基于资料回答。听起来简单但真正做过 RAG 项目的人都知道从 demo 到生产之间隔着一堆坑。这篇内容适合谁看如果你正在从 0 到 1 搭建 AI Agent或者已经用 LangChain、Spring AI、LangChain4j 跑通了最基础的问答但发现效果不稳定、召回不准、答非所问那这篇就是写给你的。我会把 RAG 的知识获取管道拆开从稠密嵌入和稀疏嵌入这两个最底层的概念讲起一路讲到分块策略、检索融合、重排序以及我在实际项目里踩过的那些坑。不堆概念只讲能直接抄作业的东西。先说清楚一个定位问题。RAG 不是一个模型也不是一个框架它是一种架构模式。你可以用 Python 的 LangChain 实现也可以用 Java 的 Spring AI 实现甚至手写几百行代码也能跑。框架只是帮你省事真正决定效果的是管道里每一个环节的设计决策。所以别一上来就纠结用哪个框架先把管道想明白。2. RAG 知识获取管道的整体设计与核心思路2.1 一条完整的知识获取管道长什么样很多人对 RAG 的理解停留在“向量数据库 相似度检索”这其实只覆盖了管道的一半。一条能上生产的知识获取管道至少包含以下几个阶段数据接入把散落在 PDF、Word、网页、数据库、Confluence、飞书文档里的知识统一收集起来。清洗与预处理去掉页眉页脚、乱码、重复段落把表格和图片里的信息尽量结构化。分块把长文档切成适合检索和塞进上下文的小片段。嵌入把每个文本块转成向量存进向量库。索引除了向量索引通常还要建关键词索引为后面的混合检索做准备。检索用户提问时把问题也转成向量去库里找最相关的片段。重排序对初步召回的片段做精排把真正有用的排到前面。上下文组装把选中的片段拼成提示词连同用户问题一起送给模型。这条管道里分块和检索是两个最容易翻车的地方。分块切得不好检索再准也捞不到完整信息检索策略单一遇到专有名词或缩写就抓瞎。后面我会重点展开这两块。2.2 为什么是 RAG而不是微调经常有人问我直接把知识喂给模型微调不行吗行但要看场景。微调适合教模型一种风格或一种能力比如让模型学会用你公司的口吻写邮件。但如果你要的是频繁更新的、事实性的知识微调就是灾难——每次知识更新都要重新训练成本高、周期长而且模型很容易在微调后出现灾难性遗忘。RAG 的优势在于知识外置。知识库更新了重新嵌入一下就行模型完全不用动。而且 RAG 的回答可以附带引用来源用户能追溯这在企业场景里是刚需。所以我的经验是风格和能力用微调事实和知识用 RAG两者不冲突可以叠加使用。2.3 稠密嵌入与稀疏嵌入两种检索哲学这是理解 RAG 检索质量的关键也是很多教程一笔带过的地方。我用一个生活化的类比来解释。想象你在图书馆找书。稠密嵌入Dense Embedding像是请了一位理解力很强的图书管理员你描述“我想找一本讲小团队如何做敏捷开发的书”他能理解你的意图给你找来《Scrum 实战》和《精益创业》哪怕书名里没有“敏捷”两个字。稠密嵌入把文本映射成一个几百到几千维的稠密向量每一维都参与表达语义擅长捕捉语义相似性。稀疏嵌入Sparse Embedding则像是图书馆的关键词索引卡片。你搜“敏捷开发”它就把书名或目录里包含这几个字的书全找出来。稀疏向量的维度等于词表大小绝大多数维度是 0只有出现的词对应的维度有值。它擅长精确匹配尤其是专有名词、产品型号、人名这类稠密嵌入容易模糊掉的东西。对比维度稠密嵌入稀疏嵌入向量形态低维稠密如 768/1024/1536 维高维稀疏维度等于词表大小擅长场景语义相似、同义改写、意图理解精确关键词、专有名词、缩写典型算法BGE、M3E、text-embedding-3BM25、SPLADE弱点对罕见词、型号不敏感无法理解同义表达检索速度依赖 ANN 索引快倒排索引极快实际项目里单一使用任何一种都会出问题。只用稠密嵌入用户搜“X200 接口报错”模型可能给你召回一堆讲接口设计的通用文档只用稀疏嵌入用户换个说法“那个设备连不上”你就什么都搜不到。所以现在主流的做法是混合检索Hybrid Search两路并行召回再用融合算法合并结果。2.4 方案选型背后的取舍逻辑在动手之前有几个选型决策会直接影响后续的开发和维护成本我按优先级排一下。第一向量库选型。个人练手和小规模项目FAISS 或 Chroma 足够本地跑、零依赖。企业级项目要考虑并发、权限、持久化Milvus、Qdrant、Weaviate 更合适。如果你已经在用 Spring 生态Spring AI 对多种向量库都有适配切换成本不高。我的建议是先用最简单的跑通流程等遇到性能瓶颈再换别一上来就上重型组件。第二嵌入模型选型。中文场景下BGE 系列和 M3E 系列是性价比很高的选择可以本地部署不依赖外部接口。如果追求效果且预算充足商用嵌入接口也可以。要注意的是嵌入模型一旦选定知识库里的向量就绑定了这个模型换模型意味着全量重新嵌入所以选型时要考虑长期可用性。第三分块策略。这是最容易被忽视但影响最大的环节。固定长度切分简单粗暴但会把一句话拦腰截断按语义切分效果好但实现复杂。我的经验是按文档结构切分为主固定长度兜底具体后面细讲。3. 核心细节解析与实操要点3.1 分块RAG 效果的地基分块Chunking决定了检索的最小单元。切得太大一个块里混了好几个主题检索时噪声大切得太小信息不完整模型拿到半句话也没法回答。我见过太多项目效果差最后排查下来就是分块没做好。固定长度分块是最简单的做法比如每 500 个字符切一块块之间留 50 个字符的重叠。重叠是为了防止关键信息正好落在切割点上被切断。这种做法实现快但缺点明显它不理解文档结构可能把标题和正文分开把表格切得七零八落。按结构分块是我更推荐的方式。Markdown 按标题层级切HTML 按标签切PDF 先做版面分析再按段落切。这样每个块天然是一个语义完整的单元。对于技术文档我通常按二级标题切如果某个二级标题下的内容超过 800 字再按段落细分。语义分块是更进阶的做法用嵌入模型计算相邻句子的相似度在相似度骤降的地方切分。效果最好但计算成本高适合对质量要求极高的场景。注意分块大小没有万能值。技术文档、法律合同、客服问答最佳块大小完全不同。我的做法是先跑一批测试问题观察召回片段的完整性再反过来调整块大小而不是拍脑袋定一个数。还有一个细节给每个块加上元数据。来源文件、章节标题、页码、更新时间这些信息在检索时可以参与过滤在回答时可以用于引用。别小看这个用户看到“根据《XX制度》第3章”和看到一段没有出处的文字信任度完全不一样。3.2 嵌入的实操细节与常见误区嵌入这一步代码通常就几行但坑不少。第一个坑是文本预处理。嵌入模型对输入文本的长度有限制超出部分会被截断。如果你直接把一个 2000 字的块丢进去模型可能只看了前 512 个 token后面的信息全丢了。所以分块大小要和嵌入模型的最大输入长度匹配一般留出安全余量。第二个坑是查询和文档的嵌入方式不一致。有些嵌入模型对查询和文档使用不同的前缀或指令比如 BGE 系列建议查询加“为这个句子生成表示以用于检索相关文章”这样的指令。如果你查询和文档用同样的方式嵌入效果会打折扣。这个细节很多教程不讲但实测差异明显。第三个坑是归一化。稠密向量在做余弦相似度计算前通常要归一化有些向量库会自动做有些不会。如果不归一化相似度计算就会出错。用框架的时候要确认这一点手写代码的时候别忘了。# 以 BGE 为例的嵌入调用示意伪代码具体 API 以官方为准 from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5) # 文档嵌入直接编码 doc_embeddings model.encode(documents, normalize_embeddingsTrue) # 查询嵌入加指令前缀 query 为这个句子生成表示以用于检索相关文章 user_question query_embedding model.encode([query], normalize_embeddingsTrue)3.3 混合检索稠密与稀疏的融合策略混合检索的核心问题是两路召回的结果怎么合并常见的有两种做法。加权求和把稠密相似度和稀疏相似度分别归一化后按权重相加。比如final_score 0.7 * dense_score 0.3 * sparse_score。权重需要根据你的数据特点调语义类问题多就调高稠密权重专有名词多就调高稀疏权重。倒数排名融合RRFReciprocal Rank Fusion不看具体分数只看排名。每个文档在每路召回里的排名取倒数相加排名越靠前得分越高。RRF 的好处是不用关心两路分数的量纲差异鲁棒性强是我更常用的方案。# RRF 融合的简化实现 def rrf_fusion(dense_results, sparse_results, k60): scores {} for rank, doc_id in enumerate(dense_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(sparse_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)这里的k是一个平滑参数通常取 60作用是降低排名靠前文档的分数优势让融合更平滑。这个值不用太纠结60 是论文里的经验值实测差异不大。3.4 重排序把真正有用的顶上来初步召回通常取 top 20 到 top 50但真正能塞进上下文的可能只有 3 到 5 个。这时候就需要重排序Rerank来精排。重排序模型Cross-Encoder和嵌入模型Bi-Encoder的区别在于嵌入模型是查询和文档分别编码再算相似度快但精度有限重排序模型是把查询和文档拼在一起送进模型直接输出相关性分数慢但准得多。所以典型流程是嵌入召回一大批重排序精选一小批。重排序模型同样有中文可选项BGE Reranker 系列用得比较多。要注意的是重排序会增加延迟如果对响应时间敏感可以只对 top 10 做重排或者用更轻量的模型。实操心得重排序带来的效果提升在大多数项目里比换一个更好的嵌入模型更明显。如果你的 RAG 效果不理想先别急着换嵌入模型加一个重排序试试往往有惊喜。4. 实操过程与核心环节实现4.1 从零搭建一个最小可用的知识获取管道我以一个“企业内部制度问答”的场景为例走一遍完整流程。假设你有一批 Markdown 格式的制度文档要做一个能回答员工问题的 Agent。第一步加载文档。遍历目录读取所有 Markdown 文件保留文件路径作为元数据。import os from pathlib import Path def load_documents(root_dir): docs [] for path in Path(root_dir).rglob(*.md): with open(path, r, encodingutf-8) as f: content f.read() docs.append({ content: content, source: str(path), filename: os.path.basename(path) }) return docs第二步按标题结构分块。用正则识别 Markdown 标题按二级标题切分超长的再按段落切。import re def split_by_heading(text, max_len800, overlap100): # 按二级标题切分 sections re.split(r\n(?## ), text) chunks [] for section in sections: if len(section) max_len: chunks.append(section) else: # 超长段落再按长度切保留重叠 start 0 while start len(section): end start max_len chunks.append(section[start:end]) start end - overlap return chunks第三步嵌入并入库。这里用 Chroma 做演示它本地跑、零配置适合快速验证。import chromadb from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5) client chromadb.PersistentClient(path./chroma_db) collection client.get_or_create_collection(policy) def index_chunks(chunks, source): for i, chunk in enumerate(chunks): embedding model.encode(chunk, normalize_embeddingsTrue).tolist() collection.add( ids[f{source}_{i}], embeddings[embedding], documents[chunk], metadatas[{source: source, chunk_index: i}] )第四步检索。用户提问时先嵌入查询再去库里找最相似的片段。def retrieve(query, top_k5): query_embedding model.encode( 为这个句子生成表示以用于检索相关文章 query, normalize_embeddingsTrue ).tolist() results collection.query( query_embeddings[query_embedding], n_resultstop_k ) return results[documents][0], results[metadatas][0]第五步组装上下文并生成回答。把召回的片段拼成提示词送给大模型。def build_prompt(query, contexts): context_text \n\n---\n\n.join(contexts) return f基于以下资料回答问题。如果资料中没有相关信息请明确说明。 资料 {context_text} 问题{query} 回答这套流程跑通之后你就有了一个最基础的 RAG。但别急着上线接下来才是真正花时间的地方——调优。4.2 参数计算与选择过程分块大小怎么定我的方法是反推。假设你的嵌入模型最大输入是 512 token中文大约 1 token 对应 1.5 到 2 个汉字那么单块控制在 300 到 400 汉字比较安全。再考虑检索时通常要召回 3 到 5 块塞进上下文如果模型上下文窗口是 8K token那么 5 块加起来不能超过 4K token留一半给问题和回答。这样算下来每块 400 到 600 汉字是一个合理的起点。top_k 取多少初步召回取 20 到 30重排序后取 3 到 5。取太少容易漏掉关键信息取太多会引入噪声而且上下文太长模型反而抓不住重点。这个值要在测试集上跑看召回率和准确率的平衡点。重叠长度取多少一般是块大小的 10% 到 20%。太小起不到防切断的作用太大则冗余严重、存储和检索成本上升。我通常取 15%。4.3 混合检索的落地实现在上面最小管道的基础上加一路 BM25 稀疏检索。Python 里可以用rank_bm25库轻量好用。from rank_bm25 import BM25Okapi import jieba # 构建 BM25 索引 tokenized_chunks [list(jieba.cut(chunk)) for chunk in all_chunks] bm25 BM25Okapi(tokenized_chunks) def sparse_retrieve(query, top_k20): tokenized_query list(jieba.cut(query)) scores bm25.get_scores(tokenized_query) top_indices sorted(range(len(scores)), keylambda i: scores[i], reverseTrue)[:top_k] return [all_chunks[i] for i in top_indices]然后把稠密和稀疏的结果用 RRF 融合再送重排序。这套组合拳下来召回质量会有肉眼可见的提升尤其是那些包含专有名词的问题。注意中文分词对 BM25 效果影响很大。jieba 默认词典对通用场景够用但如果你有大量行业术语建议加载自定义词典否则术语会被切碎稀疏检索直接失效。5. 常见问题与排查技巧实录5.1 检索不准的排查思路检索不准是 RAG 最高频的问题但原因可能出在管道的好几个环节。我整理了一个排查顺序从后往前查效率最高。现象可能原因排查方法解决方向召回片段完全不相关嵌入模型不适配手动算几个查询和文档的相似度换中文优化模型召回片段相关但不完整分块切断了信息检查召回片段的边界调整块大小和重叠专有名词搜不到只用稠密检索测试关键词检索加稀疏检索做混合相关片段排名靠后缺少精排看 top 20 里有没有正确答案加重排序模型回答答非所问上下文组装有问题检查提示词和片段拼接优化提示词模板我遇到过一个典型案例用户问“年假怎么算”系统总是召回“请假流程”而不是“年假制度”。排查下来发现分块时“年假制度”那一节被切成了三块而“请假流程”因为内容短没被切完整地作为一个块被召回了。块大小不一致导致短块在相似度计算时占了便宜。解决办法是统一分块粒度并且给每个块加上章节标题作为上下文让“年假制度”这个标题信息进入嵌入。5.2 上下文超长的处理召回片段太多、太长塞不进模型上下文这是很常见的问题。我的处理策略是分级截断先按重排序分数排序从高到低往上下文里塞塞到接近上限就停。如果单个片段太长就只取片段中与查询最相关的部分而不是整段塞进去。还有一种情况是多个片段内容重复。比如同一份制度在多个文件里都有检索时会召回好几份几乎一样的。这时候要做去重可以用简单的文本相似度也可以用嵌入相似度把重复的合并掉节省上下文空间。5.3 知识更新的处理知识库不是一成不变的。文档更新了对应的向量也要更新。最简单的做法是全量重建每次更新都重新嵌入所有文档。小规模知识库这么做没问题但文档一多就受不了。更优雅的做法是增量更新给每个块记录来源文件的哈希值文件变了才重新嵌入这个文件对应的块。删除文档时按来源元数据批量删除对应的向量。Chroma、Milvus 这些向量库都支持按元数据过滤删除用起来很方便。实操心得一定要给向量库的元数据里加上updated_at字段。有一次线上知识库更新后效果变差排查半天才发现是旧向量没删干净新旧混在一起导致检索混乱。有了时间戳至少能快速定位是哪批数据的问题。5.4 效果评估怎么做没有评估就没有优化。RAG 的评估至少要覆盖两个维度检索质量和生成质量。检索质量看召回率相关文档有没有被召回和准确率召回的文档有多少是相关的。做法是准备一批测试问题人工标注每个问题对应的正确文档然后跑检索看命中情况。生成质量看回答是否忠实于召回内容、是否回答了问题。这个可以人工评估也可以用大模型做自动评估让模型判断回答和参考资料是否一致。业界有一些现成的评估框架思路都是类似的。我的建议是先建一个 50 到 100 条的小测试集覆盖典型问题和边界情况每次调整管道参数都跑一遍看指标变化。没有测试集调优就是盲人摸象。6. 从基础 RAG 到 Agentic RAG 的演进方向基础 RAG 跑通之后你会发现它有几个天然局限。比如用户问一个需要多步推理的问题单次检索根本搞不定比如一个问题需要查多个知识源基础 RAG 只会一股脑召回然后让模型自己拼。这时候就该考虑Agentic RAG了。Agentic RAG 的核心思路是把检索变成 Agent 的一个工具而不是固定流程。Agent 可以自己决定要不要检索、检索几次、用什么查询检索、检索结果够不够、要不要换个角度再查。这就把 RAG 从“一次性管道”变成了“可迭代的推理过程”。实现上你可以用 Agent 框架把检索工具注册进去让模型自己规划。比如用户问“我们公司和竞品的年假制度有什么区别”Agent 可以先检索本公司制度再检索竞品资料然后对比总结。这种多跳检索是基础 RAG 做不到的。另一个方向是GraphRAG把知识图谱和向量检索结合。对于实体关系复杂、需要多跳推理的场景图谱能提供向量检索给不了的结构化信息。不过 GraphRAG 的构建和维护成本高不少适合知识结构稳定、关系密集的领域比如医疗、法律、金融。普通的企业文档问答混合检索加 Agentic 编排已经够用了。我个人在实际项目里的体会是别一上来就追求最先进的架构。基础 RAG 加混合检索加重排序把这三个环节做扎实能解决 80% 的问题。剩下的 20% 再考虑用 Agentic 编排或图谱来补。很多团队效果不好不是架构不够先进而是分块、嵌入、检索这些基础环节没做对。把地基打牢上层怎么搭都稳。