从零搭建智能体知识库:RAG架构、分块策略与检索重排实战
1. 知识库到底在解决什么问题1.1 从“资料囤积”到“AI 可检索”的鸿沟我见过太多人做智能体项目时卡在同一个地方模型接好了提示词也调了工具调用也配了但一问稍微具体点的问题智能体就开始胡说八道。原因很简单——它不知道你的业务细节。你手头有一堆产品文档、客服话术、内部规范、历史工单但这些资料散落在飞书文档、Notion 页面、本地 Word、PDF 附件、甚至微信收藏里。智能体看不到它们自然答不准。知识库要解决的核心问题就一句话把非结构化的、散落各处的资料变成智能体在推理时能够实时检索到的结构化知识片段。注意这里的关键词是“实时检索”不是“训练进模型”。很多人第一反应是“我把资料喂给模型微调不就行了”这条路不是不能走但成本高、更新慢、容易灾难性遗忘而且你每次改一版产品手册都得重新训一遍完全不现实。RAGRetrieval-Augmented Generation检索增强生成是目前最主流的解法。它的逻辑很朴素用户提问 → 系统去知识库里找最相关的几段内容 → 把这几段内容和问题一起塞给模型 → 模型基于这些内容生成回答。整个过程模型不需要“记住”你的资料它只需要“读懂”当前检索到的片段。这样做的好处是资料更新即时生效你改完文档重新索引一下就行模型侧完全不用动。1.2 哪些场景必须上知识库不是所有智能体都需要知识库。如果你做的是一个通用闲聊机器人或者一个纯靠提示词就能搞定的简单任务比如“把这段话翻译成英文”那确实没必要。但以下场景没有知识库基本做不出可用产品企业客服智能体用户问“你们企业版和个人版的发票政策有什么区别”模型不可能凭空知道必须从产品文档和财务规范里检索。内部运维助手工程师问“上次那个数据库连接池爆满是怎么处理的”需要从历史工单和运维手册里找答案。法律/专利辅助工具需要从法条库、案例库、专利文档中检索相关条款和先例。个人知识管理你读了三百篇文章想让 AI 帮你回忆“那篇讲 MCP 协议和传统 API 区别的文章里提到了什么”没有知识库就只能靠记忆。销售支持智能体销售问“客户说竞品价格比我们低 20%我该怎么回应”需要从竞品分析文档和成功案例库里检索话术。这些场景的共同点是答案存在于特定资料中且资料会更新模型本身不具备这些知识。知识库就是连接“死资料”和“活智能体”的那座桥。1.3 知识库不是“向量数据库”的同义词很多人一提到知识库就想到向量数据库觉得搭个 Milvus 或者 Chroma 就完事了。这是一个典型的认知偏差。向量数据库只是知识库的一个存储组件完整的知识库流水线至少包含五个环节数据接入从各种来源本地文件、在线文档、数据库、API把原始资料拉进来。数据清洗与预处理去掉页眉页脚、乱码、重复内容把 PDF 里的表格还原成可读文本。分块与向量化把长文档切成合适大小的片段用嵌入模型转成向量。索引与存储把向量和原始文本一起存进数据库建立快速检索结构。检索与重排用户提问时先粗筛出候选片段再用重排模型精排把最相关的几条送给大模型。这五个环节里分块策略和检索重排是最容易翻车的地方后面会详细展开。向量数据库只是第 4 步的一个工具把它当成知识库的全部就像把冰箱当成整个厨房。2. 核心组件选型与架构设计2.1 嵌入模型知识库的“翻译官”嵌入模型的作用是把文本转成向量让语义相近的内容在向量空间里距离更近。选嵌入模型时很多人只看排行榜但实际落地要考虑三个维度第一语言支持。如果你的资料以中文为主必须选中文语义理解好的模型。有些英文榜单上排名很高的模型中文表现可能一塌糊涂。我实测下来BGE 系列如 bge-large-zh-v1.5和 M3E 系列在中文场景下表现稳定而且可以本地部署不依赖外部 API。第二维度与性能的平衡。向量维度越高表达能力越强但存储和检索成本也越高。768 维和 1024 维在实际检索效果上差距没有想象中那么大但存储成本差 30% 以上。如果资料量在百万级以下1024 维完全够用如果上千万级建议考虑 768 维甚至更低配合量化技术。第三是否支持指令微调。有些嵌入模型支持在检索时加指令前缀比如“为这个句子生成表示以用于检索相关文章”这能显著提升检索准确率。BGE 系列就支持这种用法实测在技术文档场景下召回率能提升 5 到 8 个百分点。注意嵌入模型一旦选定整个知识库的向量空间就固定了。如果中途换模型所有资料必须重新向量化。所以选型时宁可多花两天做对比测试也不要上线后才发现效果不行。2.2 分块策略切得好比选得好更重要分块是知识库流水线里最被低估的环节。我见过太多项目嵌入模型用的是顶配向量数据库也是企业级但检索效果就是不行最后发现是分块切得稀碎——一段完整的话被切成三截每截都缺主语向量化之后语义完全走样。分块的核心矛盾是块太小语义不完整块太大噪声太多。一个 200 字的块可能只讲了半个概念检索出来模型看不懂一个 2000 字的块可能包含五六个不同主题向量被平均之后哪个主题都不像。我的经验值是中文技术文档块大小控制在 300 到 500 字之间重叠 50 到 80 字。这个范围是经过多次实测的——小于 300 字很多段落被拦腰截断大于 500 字检索精度明显下降。重叠是为了防止关键信息刚好落在切割边界上被丢掉。但固定大小分块只是保底方案。更好的做法是按语义边界分块优先在段落结束、标题切换、列表项之间切实在找不到自然边界再按字数硬切。LangChain 的 RecursiveCharacterTextSplitter 就是干这个的它会依次尝试用双换行、单换行、句号、逗号来切尽量保持语义完整。还有一种进阶策略是父子块存的时候用小块200 字做向量检索但返回给模型的是它所属的大块1000 字。这样检索精度高模型拿到的上下文也完整。Dify 和 RAGFlow 都支持这种模式实测在问答场景下效果提升明显。2.3 向量数据库别为了“先进”而过度设计向量数据库选型是另一个容易过度设计的地方。我见过一个日活不到一千的内部工具非要上分布式 Milvus 集群结果运维成本比开发成本还高。选型要匹配实际数据量和并发量数据库适用场景优点缺点Chroma原型验证、小规模零配置、Python 原生不适合生产、并发差FAISS离线检索、嵌入式性能极强、无依赖不支持增删改、无持久化Milvus中大规模生产功能全、生态好运维复杂、资源占用高Qdrant中小规模生产轻量、Rust 性能好社区相对小pgvector已有 PostgreSQL和业务数据同库大规模性能一般Elasticsearch已有 ES 集群混合检索方便向量功能较新我的建议是原型阶段用 Chroma 或 FAISS生产环境如果数据量在百万级以下且已有 PostgreSQL直接用 pgvector如果数据量更大或需要独立扩展选 Qdrant 或 Milvus。不要一上来就追求“企业级”先把流程跑通再说。2.4 检索策略从“能查到”到“查得准”检索环节决定了知识库的上限。最基础的向量相似度检索余弦相似度或内积只能解决“语义相近”的问题但实际场景中往往需要更复杂的策略混合检索是把向量检索和关键词检索BM25的结果融合。向量检索擅长语义匹配比如用户问“怎么退款”能匹配到“退货流程”的文档关键词检索擅长精确匹配比如用户问“错误码 E5021”能精确找到包含这个码的文档。两者结合召回率通常能提升 10 到 15 个百分点。重排是在粗筛之后加一个精排模型。向量检索返回的 Top 20 里真正相关的可能只有 5 条而且排序不一定准。重排模型如 BGE-Reranker会逐条计算问题和文档的相关性分数重新排序。这一步计算量不大只处理几十条但效果提升非常明显实测能把 Top 3 准确率从 60% 拉到 85% 以上。查询改写是另一个实用技巧。用户的问题往往口语化、有歧义直接拿去检索效果不好。可以先用一个小模型把问题改写成更适合检索的形式或者生成多个相关查询分别检索再合并结果。比如用户问“那个新功能怎么用”可以改写成“新功能 使用说明 操作步骤”检索命中率会高很多。3. 从零搭建知识库的完整实操3.1 环境准备与依赖安装我以 Python 技术栈为例走一遍完整流程。这套方案在本地和服务器上都能跑依赖都是开源的不需要任何特殊网络环境。# 创建虚拟环境 python -m venv kb-env source kb-env/bin/activate # Windows 用 kb-env\Scripts\activate # 安装核心依赖 pip install langchain langchain-community chromadb sentence-transformers pip install pypdf unstructured markdown beautifulsoup4 pip install rank-bm25 jieba这里解释一下每个包的作用langchain提供文档加载和分块的抽象chromadb是向量存储sentence-transformers用来加载嵌入模型pypdf和unstructured处理 PDF 和各类文档rank-bm25和jieba用于关键词检索和中文分词。嵌入模型我选BAAI/bge-large-zh-v1.5这是目前中文开源嵌入模型里综合表现最稳的之一。首次运行会自动下载模型文件大约 1.3GB之后缓存在本地。from sentence_transformers import SentenceTransformer # 加载嵌入模型首次会自动下载 model SentenceTransformer(BAAI/bge-large-zh-v1.5) # 测试一下 sentences [知识库怎么搭建, 如何构建智能体的检索系统] embeddings model.encode(sentences, normalize_embeddingsTrue) print(embeddings.shape) # 应该是 (2, 1024)注意normalize_embeddingsTrue很重要。归一化之后余弦相似度等价于内积检索时计算更快而且不同长度的文本向量可比性更好。3.2 文档加载与清洗假设你有一批 PDF 和 Markdown 文档放在./docs目录下。加载和清洗的代码如下from langchain_community.document_loaders import PyPDFLoader, UnstructuredMarkdownLoader from langchain.text_splitter import RecursiveCharacterTextSplitter import os import re def load_documents(doc_dir): documents [] for filename in os.listdir(doc_dir): filepath os.path.join(doc_dir, filename) if filename.endswith(.pdf): loader PyPDFLoader(filepath) documents.extend(loader.load()) elif filename.endswith(.md): loader UnstructuredMarkdownLoader(filepath) documents.extend(loader.load()) return documents def clean_text(text): # 去掉多余空白 text re.sub(r\s, , text) # 去掉页眉页脚常见模式如纯数字行 text re.sub(r\n\d\n, \n, text) # 去掉乱码字符 text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f], , text) return text.strip() documents load_documents(./docs) for doc in documents: doc.page_content clean_text(doc.page_content)清洗这一步看起来简单但实际效果差异很大。我处理过一批从某系统导出的 PDF每页顶部都有“内部资料 请勿外传”的水印文字如果不清理每个块里都混着这句话向量化之后所有块的相似度都被拉高了检索精度直接崩掉。所以清洗不是可选项是必选项。3.3 分块与向量化入库分块用递归字符分割器优先按语义边界切text_splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap60, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) chunks text_splitter.split_documents(documents) print(f原始文档 {len(documents)} 页切分成 {len(chunks)} 个块)注意separators的顺序先尝试双换行段落边界再单换行再中文句号、感叹号、问号、分号、逗号最后才是空格和空字符。这样能最大程度保证块内语义完整。接下来向量化并存入 Chromafrom langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings embedding_function HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5, model_kwargs{device: cpu}, # 有 GPU 就改成 cuda encode_kwargs{normalize_embeddings: True} ) vectorstore Chroma.from_documents( documentschunks, embeddingembedding_function, persist_directory./chroma_db, collection_namemy_knowledge_base ) vectorstore.persist() print(入库完成)如果资料量很大比如超过 10 万块建议分批入库每批 5000 块避免内存爆掉。另外device参数根据实际情况选CPU 也能跑只是慢一些10 万块大概需要 20 到 30 分钟。3.4 检索与重排的代码实现基础检索很简单query 智能体怎么接入知识库 results vectorstore.similarity_search_with_score(query, k10) for doc, score in results: print(f分数: {score:.4f} | 内容: {doc.page_content[:100]}...)但基础检索的排序往往不够准。加上重排from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-large) def retrieve_with_rerank(query, vectorstore, top_k20, final_k5): # 粗筛 candidates vectorstore.similarity_search(query, ktop_k) # 精排 pairs [[query, doc.page_content] for doc in candidates] scores reranker.predict(pairs) # 按分数排序 ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [doc for doc, score in ranked[:final_k]]重排模型比嵌入模型大推理慢一些但只处理 20 条候选耗时通常在 200 毫秒以内完全可以接受。实测下来加了重排之后Top 3 里包含正确答案的比例从 62% 提升到了 87%这个投入非常值得。3.5 混合检索的融合策略混合检索需要同时跑向量检索和 BM25然后融合结果。融合算法常用 RRFReciprocal Rank Fusion倒数排名融合from rank_bm25 import BM25Okapi import jieba import numpy as np # 构建 BM25 索引 tokenized_corpus [list(jieba.cut(chunk.page_content)) for chunk in chunks] bm25 BM25Okapi(tokenized_corpus) def hybrid_retrieve(query, vectorstore, bm25, chunks, top_k10): # 向量检索 vector_results vectorstore.similarity_search(query, ktop_k) vector_ids [chunk.metadata.get(id) for chunk in vector_results] # BM25 检索 tokenized_query list(jieba.cut(query)) bm25_scores bm25.get_scores(tokenized_query) bm25_top_indices np.argsort(bm25_scores)[::-1][:top_k] # RRF 融合 rrf_scores {} for rank, doc in enumerate(vector_results): doc_id doc.metadata.get(id, str(hash(doc.page_content))) rrf_scores[doc_id] rrf_scores.get(doc_id, 0) 1 / (60 rank 1) for rank, idx in enumerate(bm25_top_indices): doc_id chunks[idx].metadata.get(id, str(hash(chunks[idx].page_content))) rrf_scores[doc_id] rrf_scores.get(doc_id, 0) 1 / (60 rank 1) # 按融合分数排序返回 sorted_ids sorted(rrf_scores.items(), keylambda x: x[1], reverseTrue) # 这里需要根据 id 找回原始文档实际实现时建议用字典映射 return sorted_ids[:top_k]RRF 里的常数 60 是经验值来自原论文实际用的时候不用改。这个融合策略的好处是不需要调权重向量检索和关键词检索的分数尺度不同直接加权平均很难调RRF 只看排名不看绝对分数鲁棒性更好。4. 常见问题与排查技巧实录4.1 检索结果不相关怎么办这是最高频的问题。排查顺序如下第一步检查分块质量。把检索到的块打印出来看是不是被切得支离破碎。如果是调整chunk_size和chunk_overlap或者换用语义分块。第二步检查嵌入模型是否匹配语言。如果资料是中文但用了英文嵌入模型效果肯定差。换成 BGE 中文系列试试。第三步检查查询是否需要改写。用户口语化的问题直接检索效果往往不好。加一个查询改写步骤用一个小模型把问题改写成更正式的检索语句。第四步加混合检索和重排。如果前三步都做了还不行基本就是检索策略太单一上混合检索和重排。我遇到过一个典型案例用户问“报销流程是什么”知识库里有完整的报销制度文档但检索出来的全是“流程”相关的其他文档。原因是“报销”这个词在文档里出现频率不高向量检索被“流程”带偏了。后来加了 BM25 混合检索“报销”作为关键词被精确匹配到问题就解决了。4.2 知识库更新后检索不到新内容这是增量更新的问题。向量数据库支持增删改但很多人入库时用了from_documents一次性全量写入后续更新不知道怎么处理。正确做法是用add_documents增量添加用delete删除旧版本# 增量添加新文档 new_chunks text_splitter.split_documents(new_documents) vectorstore.add_documents(new_chunks) # 删除旧版本文档按 metadata 过滤 vectorstore.delete(where{source: old_doc.pdf})但要注意Chroma 的删除是按 ID 或 metadata 过滤的如果你的块没有唯一 ID删除会很麻烦。所以入库时一定要给每个块生成唯一 ID并把源文件路径、版本号写进 metadata。这个习惯能省掉后面无数麻烦。4.3 中文分块把句子切断了中文没有空格按字符数硬切很容易把句子拦腰截断。RecursiveCharacterTextSplitter 的中文分隔符配置很关键。我试过只用[\n\n, \n, , ]结果中文句子被切得乱七八糟。加上中文标点之后好很多separators[\n\n, \n, 。, , , , , , ]但即便如此还是会有切断的情况。更稳妥的做法是用专门的中文分块器比如按句号、问号、感叹号、分号、逗号逐级切分保证每个块至少包含一个完整句子。如果对精度要求极高可以用 NLP 工具做句法分析按从句边界切但计算成本会高不少。4.4 常见问题速查表问题现象可能原因排查方法解决方案检索结果完全不相关嵌入模型语言不匹配检查模型名称是否含 zh换中文嵌入模型检索结果相关性差分块太大或太小打印块内容检查调整 chunk_size 到 300-500新文档检索不到未增量入库检查数据库文档数用 add_documents 增量添加相同问题每次答案不同检索结果不稳定检查是否有重排加固定重排模型检索速度慢向量维度太高或数据量大测单次检索耗时降维或加索引关键词搜不到纯向量检索不匹配关键词测试 BM25 单独检索上混合检索PDF 内容乱码编码或字体问题打印原始文本换 PDF 解析库或手动清洗表格内容丢失PDF 表格解析失败检查解析后文本用 unstructured 的表格模式4.5 几个踩过的坑坑一metadata 里塞了太多东西。我一开始把整个文档的元信息都塞进每个块的 metadata结果存储膨胀了三倍检索也变慢。后来只保留source、page、chunk_id三个字段清爽很多。坑二忘了归一化。嵌入向量不归一化的话余弦相似度和内积不等价Chroma 默认用 L2 距离结果排序会乱。一定要在encode_kwargs里加normalize_embeddingsTrue。坑三重排模型和嵌入模型不匹配。重排模型是在特定嵌入模型的基础上训练的如果混用不同系列的模型效果可能反而变差。建议嵌入和重排用同一系列比如都用 BGE。坑四忽略了查询长度。用户的问题如果特别长比如贴了一大段报错日志直接向量化效果很差。可以先做查询摘要提取关键信息再检索。坑五没有做检索结果去重。如果知识库里有多个版本文档检索可能返回同一内容的不同版本浪费上下文窗口。可以在检索后按内容相似度去重或者入库时就做好版本管理。5. 知识库与智能体的对接方式5.1 通过 MCP 协议标准化接入MCPModel Context Protocol是当前智能体工具调用领域的热门协议它定义了一套标准接口让智能体能够以统一的方式访问外部资源。把知识库封装成 MCP Server 之后任何支持 MCP 的智能体框架都能直接调用不需要为每个框架单独写适配代码。一个最简单的知识库 MCP Server 大概长这样from mcp.server import Server from mcp.types import Tool, TextContent server Server(knowledge-base) server.list_tools() async def list_tools(): return [ Tool( namesearch_knowledge, description搜索知识库返回最相关的文档片段, inputSchema{ type: object, properties: { query: {type: string, description: 搜索查询} }, required: [query] } ) ] server.call_tool() async def call_tool(name: str, arguments: dict): if name search_knowledge: query arguments[query] results retrieve_with_rerank(query, vectorstore) return [TextContent(typetext, text\n\n.join([doc.page_content for doc in results]))]这样封装之后智能体只需要知道“有一个叫 search_knowledge 的工具可以查知识库”具体底层用的是 Chroma 还是 Milvus、嵌入模型是什么智能体完全不用关心。这就是 MCP 的价值——把知识库的实现细节和智能体的调用逻辑解耦。5.2 直接集成到智能体框架如果不走 MCP也可以直接在智能体框架里集成。以 LangChain 为例from langchain.agents import Tool, AgentExecutor, create_react_agent from langchain_openai import ChatOpenAI retriever_tool Tool( nameknowledge_search, funclambda q: \n\n.join([doc.page_content for doc in retrieve_with_rerank(q, vectorstore)]), description当需要查询内部资料时使用此工具输入是搜索查询 ) llm ChatOpenAI(modelgpt-4, temperature0) agent create_react_agent(llm, [retriever_tool], prompt) agent_executor AgentExecutor(agentagent, tools[retriever_tool])这种方式更直接但和框架绑定较深。如果以后换框架需要重写适配层。MCP 的好处就是换框架不用改知识库侧代码。5.3 检索时机与上下文注入策略知识库检索不是每次对话都要触发。如果用户只是说“你好”没必要去查知识库。常见的触发策略有三种第一种工具调用模式。把知识库封装成一个工具让模型自己决定什么时候调用。这适合问题类型多样的场景但模型可能会漏调或误调。第二种前置检索模式。每次用户提问都先检索一遍把结果作为上下文注入提示词。这适合问答类智能体保证每次都有资料参考但会增加延迟和 token 消耗。第三种混合模式。先用一个轻量分类器判断问题是否需要查知识库需要则检索不需要则直接走通用对话。这是实际落地中最常用的方案兼顾效果和成本。上下文注入时要注意 token 预算。如果检索返回 5 个块每个块 400 字加起来 2000 字再加上系统提示词和对话历史很容易超过模型上下文窗口。建议对检索结果做截断或摘要只保留最核心的部分。6. 效果评估与持续优化6.1 怎么判断知识库好不好用知识库上线之后不能凭感觉说“好像还行”要有量化指标。最核心的两个指标是召回率在所有相关问题中知识库能检索到正确答案的比例。测试方法是准备一批问题每个问题标注好正确答案所在的文档然后看检索 Top K 里是否包含该文档。召回率低于 80% 就说明检索环节有问题。准确率检索返回的结果中真正相关的比例。如果召回率高但准确率低说明返回了太多无关内容会干扰模型生成。准确率低于 60% 就需要加严重排或调整分块。这两个指标需要人工标注测试集前期投入大概半天到一天但后续每次调整都能快速验证效果非常值得。6.2 持续优化的三个方向方向一补充资料。如果发现某些问题检索不到先看知识库里有没有对应资料。没有就补有但检索不到就调检索策略。方向二优化分块。如果检索到的块内容不完整调整分块参数。如果块内主题太杂尝试更小的块或语义分块。方向三迭代重排。如果 Top 10 里有正确答案但排不到前面说明重排模型不够好可以换更大的重排模型或者用业务数据微调重排模型。我个人的经验是一个知识库从能用变成好用通常需要两到三轮迭代。第一轮解决“能不能查到”第二轮解决“查得准不准”第三轮解决“快不快”。不要指望一次配置就完美留出迭代时间。6.3 一个容易被忽略的点知识库的“保鲜”知识库和代码一样会腐烂。产品文档更新了、流程改了、新政策发布了如果知识库不同步智能体就会给出过时甚至错误的答案。我见过最严重的情况是客服智能体还在引用两年前已经废止的退款政策导致用户投诉。解决办法是建立知识库更新流程文档变更时自动触发重新索引或者至少每周做一次全量同步。如果用的是在线文档如飞书、Notion可以配置 webhook文档一改就自动推送更新。这个流程不建立知识库的长期价值会大打折扣。我个人在实际操作中的体会是知识库项目最难的不是技术选型而是持续维护。技术方案再先进如果没人负责更新资料、没人看检索日志、没人迭代优化三个月后就会变成一个没人用的摆设。所以在启动项目之前先想清楚谁来维护、多久 review 一次、更新流程是什么。这些问题比选哪个向量数据库重要得多。