☰
Agent知识库搭建实战:RAG流水线从文档解析到混合检索的完整指南
2026/10/7 13:25:58 网站建设 项目流程

1. 为什么你的 Agent 总是“答非所问”

做智能体开发的人,十有八九都经历过这个场景:你花了一下午把系统提示词打磨得滴水不漏,工具调用链路也跑通了,结果用户问一句“我们产品的退货政策是什么”,Agent 张口就来一段听起来特别合理、但完全是编造的答案。你检查日志发现,它压根没去查你准备好的那份 PDF 文档,而是直接靠模型内部的知识“硬答”了。

这个问题的根源,不在于模型不够聪明,而在于你喂给它的资料,它“查不动”。

我见过太多团队在这一步翻车。他们把几十份 Word、PDF、Excel 往某个目录里一扔,接个向量库就以为知识库建好了。结果上线之后,检索命中率惨不忍睹,用户问东它答西,最后只能靠不断加提示词“请务必基于以下资料回答”来硬撑。这种做法在 demo 阶段能糊弄过去,一旦资料量上去、问题变复杂,立刻原形毕露。

这篇内容要解决的,就是这件事:怎么把散落在各个角落的资料,整理成 AI 真正能查、查得准、查得全的知识库。我会从知识库的几种形态讲起,拆解 RAG 知识库的完整流水线,讲清楚文档解析、切分、向量化、检索、重排这几个环节里各自藏着什么坑,最后给出一套可以直接照着搭的实操方案。

适合谁看?如果你正在用 Dify、Coze 这类平台搭智能体,或者用 Python 自己写 Agent 框架,只要涉及到“让 AI 基于我的资料回答问题”,这篇内容都能帮你少走至少两周弯路。不需要你有很深的算法背景,但需要你对 Agent 的基本概念有了解——知道什么是工具调用、什么是上下文窗口就够了。

先说一个我踩过的坑。早期我做知识库,觉得“切分”这件事随便按字数切就行了,500 字一段,简单粗暴。结果有一次用户问“合同里关于违约金的计算方式”,检索出来的片段前半段在讲签约流程,后半段才提到违约金,模型拿到这个片段,回答得含含糊糊。后来我把切分策略改成按语义段落切,并且给每个片段加上所属章节的标题作为上下文,命中率直接上了一个台阶。这个细节后面会详细讲。

2. 知识库不是一种东西:三种形态先分清楚

很多人一提到“知识库”就默认是 RAG,其实在 Agent 的语境下,知识库至少有三种截然不同的形态,它们解决的问题、适用的场景、搭建的成本都不一样。选错了形态,后面怎么调都是事倍功半。

2.1 RAG 知识库:最通用,也最容易被低估

RAG(检索增强生成)是当前 Agent 知识库的主流方案。它的核心逻辑是:把资料切成片段,转成向量存起来,用户提问时先检索出最相关的几个片段,塞进模型的上下文里,让模型基于这些片段回答。

它的优势在于不需要训练模型,资料更新只需要重新入库,成本低、迭代快。适合的场景是:产品文档、客服问答、内部规章制度、技术手册这类“事实型”知识。

但 RAG 有个容易被忽视的短板:它擅长回答“资料里写了什么”,不擅长做多跳推理。比如你问“A 产品的保修期和 B 产品的保修期哪个长”,如果这两个信息分别在两份文档里,RAG 可能只能检索到其中一份,回答就会残缺。这种场景需要配合 Agent 的多轮检索能力,或者换用其他形态。

2.2 结构化知识库:当你的资料本身就有结构

如果你的资料天然是结构化的——比如数据库表、Excel 表格、JSON 配置——那硬把它塞进 RAG 反而是浪费。结构化知识库的做法是:让 Agent 通过工具调用去查询这些结构化数据源。

举个例子,你有一个产品价格表,包含型号、价格、库存三个字段。用户问“XX 型号现在多少钱”,Agent 不需要去检索文档片段,直接调用一个查询工具,执行SELECT price FROM products WHERE model = 'XX',拿到精确结果。这种方式的准确率是 100%,因为它是精确查询,不是模糊匹配。

结构化知识库的关键在于工具设计。你要把查询能力封装成 Agent 能理解的工具描述,让它知道什么时候该用这个工具、参数怎么填。这部分后面会展开。

2.3 图谱知识库:处理复杂关系的利器

知识图谱(KG)知识库适合处理实体之间关系复杂的场景。比如你要做一个医疗领域的 Agent,需要理解“某种症状可能对应哪些疾病,这些疾病又对应哪些药物,药物之间有什么相互作用”。这种多层关系,用 RAG 的片段检索很难覆盖,但用图谱就能很自然地表达。

图谱知识库的搭建成本明显高于前两者,需要做实体抽取、关系抽取、图谱构建。除非你的业务确实需要处理复杂关系,否则不建议一上来就上图谱。我见过一些团队为了“技术先进”硬上图谱,结果维护成本高得离谱,效果还不如老老实实做 RAG。

下面这张表可以帮你快速判断该选哪种形态:

形态适用场景搭建成本准确率特点更新难度
RAG 知识库文档问答、客服、手册中依赖检索质量低,重新入库即可
结构化知识库数据库查询、表格数据低精确低,改数据源即可
图谱知识库复杂关系推理、多跳问答高关系推理强高,需维护图谱

实际项目中,这三种形态往往是混用的。一个成熟的 Agent 可能同时挂着一个 RAG 知识库处理文档问答,一个结构化查询工具处理数据查询,再加一个图谱处理关系推理。关键是根据问题类型路由到合适的知识源,而不是指望一种形态包打天下。

3. RAG 知识库流水线:从原始文档到可检索片段

既然 RAG 是最通用的方案,接下来重点拆解它的完整流水线。一条标准的 RAG 流水线包含五个环节:文档采集、文档解析、文本切分、向量化、索引存储。每个环节都有坑,我逐个说。

3.1 文档采集:先把资料收拢到一处

这一步听起来简单,实际最容易被低估。资料散落在飞书、Notion、本地文件夹、微信公众号收藏、邮件附件里,格式五花八门。你需要先做一件事:把所有资料统一到一个可处理的目录下,并且记录每份资料的来源和更新时间。

为什么要记录来源和更新时间?因为当用户问“最新的政策是什么”时,检索环节需要能按时间过滤。如果资料没有元数据,Agent 就分不清哪份是旧版哪份是新版,可能把三年前的作废文件当成现行规定回答。

采集环节的实操建议:

  • 本地文件统一转成 Markdown 或纯文本,PDF 和 Word 优先转 Markdown,保留标题层级
  • 每份文档在文件名或元数据里标注来源、版本、生效日期
  • 对于网页内容,用阅读模式提取正文,去掉导航栏和广告
  • 图片类资料单独处理,后面会讲

注意:不要跳过采集环节的清洗。我见过有人直接把带页眉页脚的 PDF 扔进去,结果每个片段里都混着“第 3 页 共 20 页”这种噪音,检索时这些噪音会干扰向量匹配。

3.2 文档解析:PDF 是最大的敌人

文档解析的目标是把各种格式的文件转成干净的文本。这里最大的坑是 PDF。PDF 本质上是排版格式,不是内容格式,它不告诉你哪段是标题、哪段是正文、哪段是表格。扫描版 PDF 更是直接给你一堆图片。

解析 PDF 的常见方案:

  • 文本型 PDF:用 PyMuPDF 或 pdfplumber 提取文本,保留段落结构
  • 扫描型 PDF:需要 OCR,推荐 PaddleOCR 或云服务 OCR
  • 表格:用 camelot 或 tabula 单独提取,转成 Markdown 表格
  • 多栏排版:需要按栏切分,否则会把左右两栏的文字混在一起

我个人的经验是,PDF 解析出来的文本,一定要人工抽查几份。重点看三件事:标题层级有没有保留、表格有没有错乱、页眉页脚有没有去掉。这三件事没做好,后面切分和检索都会受影响。

对于 Word 文档,python-docx 可以提取段落和样式,标题样式能帮你识别章节结构。对于 Markdown,直接按标题层级切分即可,这是最省心的格式。

3.3 文本切分:决定检索质量的关键一步

切分是 RAG 流水线里最需要动脑子的环节。切得太粗,一个片段里混着多个主题,检索时匹配不精准;切得太细,一个完整的意思被拆散,模型拿到片段也拼不出完整答案。

常见的切分策略有三种:

固定长度切分:按字符数或 token 数切,比如每 500 字一段,段间重叠 50 字。优点是简单,缺点是经常在句子中间切断,语义不完整。

递归字符切分:按分隔符优先级切,先按段落切,段落太长再按句子切,句子还长再按字符切。这是 LangChain 的默认策略,比固定长度好很多。

语义切分:用嵌入模型计算相邻句子的相似度,相似度低的地方作为切分点。效果最好,但计算成本高。

我的建议是:优先按文档结构切分。如果文档有标题层级,就按标题切,每个小节作为一个片段,片段开头带上所属章节的标题路径。比如一个片段开头是“产品手册 > 第三章 售后服务 > 3.2 退货政策”,这样即使片段本身没提到“退货”两个字,检索时也能通过标题路径匹配到。

切分参数怎么定?没有万能值,但有个经验法则:片段长度控制在 200 到 500 字之间,重叠 10% 到 15%。太短信息不足,太长噪音太多。具体数值要根据你的资料特点调,问答类资料可以短一些,叙述类资料可以长一些。

实操心得:切分完之后,随机抽 20 个片段读一遍。如果发现有片段读起来莫名其妙、不知道在讲什么,说明切分策略有问题。这个检查花不了十分钟,但能帮你提前发现大问题。

3.4 向量化:选对嵌入模型比调参重要

向量化是把文本片段转成向量,存进向量数据库。这一步的核心决策是选哪个嵌入模型。模型选错了,后面检索怎么调都救不回来。

选嵌入模型的几个考量:

  • 语言支持:中文资料必须选中文效果好的模型,不能直接用英文模型
  • 维度:维度越高表达能力越强,但存储和计算成本也越高,常见的是 768 维和 1024 维
  • 上下文长度:模型能处理的最大 token 数,要大于你的片段长度
  • 成本:本地模型免费但需要 GPU,API 模型按量付费但省事

中文场景下,BGE 系列和 M3E 系列是常用的开源选择,效果稳定。如果预算允许,也可以用商业 API 的嵌入模型,省去部署麻烦。

这里有个容易忽略的点:查询和文档要用同一个嵌入模型。有人文档用模型 A 嵌入,查询用模型 B 嵌入,结果向量空间不一致,检索出来的东西完全不相干。这个错误听起来低级,但实际项目中真的有人犯。

3.5 索引存储:向量库怎么选

向量数据库负责存储向量并支持相似度检索。常见的选择有:

  • Chroma:轻量,适合本地开发和中小规模,上手快
  • Milvus:功能全,适合大规模生产环境,部署复杂一些
  • Qdrant:性能好,过滤能力强,API 设计友好
  • pgvector:如果你已经在用 PostgreSQL,直接加个扩展就行,省去额外运维

选哪个?如果是个人项目或小团队,Chroma 或 pgvector 足够。如果是企业级应用,数据量上百万条,考虑 Milvus 或 Qdrant。不要一上来就追求“生产级”,先用简单的跑通流程,遇到瓶颈再换。

索引存储时,除了向量本身,还要存元数据:来源文档、章节路径、更新时间、文档类型。这些元数据在检索时可以用来过滤,比如“只检索最近三个月更新的文档”或者“只检索产品手册类文档”。

4. 检索与重排:让 Agent 查到真正相关的内容

知识库建好了,接下来是检索。检索环节决定了 Agent 能不能拿到对的资料。很多人以为向量检索就是“算个相似度取 Top K”,实际上这里面有不少讲究。

4.1 向量检索的局限与补救

纯向量检索有个问题:它擅长匹配语义相似,但不擅长匹配精确关键词。比如用户问“错误码 E5021 是什么意思”,向量检索可能返回一堆讲“错误处理”的片段,但就是没返回包含“E5021”的那一条,因为“E5021”这个字符串在向量空间里没有特别的语义。

补救方案是混合检索:同时做向量检索和关键词检索(BM25),把两边的结果融合。向量检索负责语义匹配,关键词检索负责精确匹配,两者互补。融合算法常用 RRF(倒数排名融合),简单有效。

混合检索的配置要点:

  • 向量检索和关键词检索各取 Top 20
  • 用 RRF 融合后取 Top 10
  • 如果某类查询明显偏向关键词匹配,可以调高关键词检索的权重

4.2 重排:用更贵的模型做精排

检索出来的 Top 10 片段,顺序未必对。向量相似度高不代表真的相关,这时候需要一个重排模型(Reranker)来做精排。重排模型比嵌入模型更贵、更慢,但只对少量候选做计算,成本可控。

重排模型的工作方式是:把查询和每个候选片段拼在一起,输入模型,输出一个相关性分数,按分数重新排序。常用的重排模型有 BGE-Reranker 系列,中文效果不错。

加了重排之后,通常取 Top 3 到 Top 5 个片段塞进上下文就够了。片段太多反而会稀释关键信息,还可能超出上下文窗口。

4.3 上下文组装:怎么把片段喂给模型

检索出片段之后,不能直接一股脑塞给模型,需要组装成模型能理解的格式。我的做法是:

以下是与问题相关的资料片段: [片段 1] 来源:产品手册 > 第三章 售后服务 > 3.2 退货政策 内容:... [片段 2] 来源:客服 FAQ > 退换货 内容:... 请基于以上资料回答问题。如果资料中没有相关信息,请明确说明“资料中未提及”,不要编造。

这个格式做了三件事:给每个片段标注来源,方便模型引用;明确指示基于资料回答;给出“不知道”的退路,减少幻觉。

注意:提示词里一定要给模型“拒答”的选项。如果不给,模型在检索不到相关内容时,会倾向于用内部知识编一个答案。明确告诉它“资料中没有就说没有”,能大幅降低幻觉率。

5. 实操:用 Python 搭一条最小可用的 RAG 流水线

前面讲了原理,这一节给一套可以直接跑的代码。我用 Python 写,依赖尽量少,方便你理解每一步在做什么。生产环境可以用 LangChain 或 LlamaIndex 封装好的组件,但建议先手写一遍,知道每个环节的输入输出是什么。

5.1 环境准备与依赖安装

pip install pymupdf sentence-transformers chromadb rank-bm25 jieba

这里用到的组件:

  • pymupdf:解析 PDF
  • sentence-transformers:加载嵌入模型和重排模型
  • chromadb:向量存储
  • rank-bm25+jieba:关键词检索

嵌入模型我选BAAI/bge-small-zh-v1.5,中文效果好,模型小,CPU 也能跑。重排模型选BAAI/bge-reranker-base。

5.2 文档解析与切分代码

import fitz # pymupdf import re def parse_pdf(path): doc = fitz.open(path) text = "" for page in doc: text += page.get_text() return text def split_by_structure(text, max_len=500, overlap=50): # 按段落切分,段落过长再按句子切 paragraphs = re.split(r'\n\s*\n', text) chunks = [] for para in paragraphs: para = para.strip() if not para: continue if len(para) <= max_len: chunks.append(para) else: # 按句子切分 sentences = re.split(r'(?<=[。!?])', para) current = "" for sent in sentences: if len(current) + len(sent) <= max_len: current += sent else: if current: chunks.append(current) current = sent if current: chunks.append(current) # 加重叠 final_chunks = [] for i, chunk in enumerate(chunks): if i > 0: chunk = chunks[i-1][-overlap:] + chunk final_chunks.append(chunk) return final_chunks

这段代码的逻辑是:先按空行切段落,段落太长再按句号切,最后给每个片段加上前一个片段的尾部作为重叠。重叠的作用是防止关键信息正好落在切分点上被切断。

5.3 向量化与入库

from sentence_transformers import SentenceTransformer import chromadb model = SentenceTransformer('BAAI/bge-small-zh-v1.5') client = chromadb.Client() collection = client.create_collection("knowledge_base") def index_documents(chunks, source): embeddings = model.encode(chunks, normalize_embeddings=True) ids = [f"{source}_{i}" for i in range(len(chunks))] metadatas = [{"source": source, "chunk_index": i} for i in range(len(chunks))] collection.add( embeddings=embeddings.tolist(), documents=chunks, ids=ids, metadatas=metadatas )

注意normalize_embeddings=True,这会把向量归一化,之后用余弦相似度检索时可以直接用点积,计算更快。

5.4 混合检索与重排

from rank_bm25 import BM25Okapi import jieba from sentence_transformers import CrossEncoder reranker = CrossEncoder('BAAI/bge-reranker-base') def hybrid_search(query, chunks, top_k=5): # 向量检索 query_emb = model.encode([query], normalize_embeddings=True) vec_results = collection.query(query_embeddings=query_emb.tolist(), n_results=20) vec_docs = vec_results['documents'][0] # 关键词检索 tokenized = [list(jieba.cut(c)) for c in chunks] bm25 = BM25Okapi(tokenized) bm25_scores = bm25.get_scores(list(jieba.cut(query))) bm25_top = [chunks[i] for i in bm25_scores.argsort()[-20:][::-1]] # RRF 融合 rrf_scores = {} for rank, doc in enumerate(vec_docs): rrf_scores[doc] = rrf_scores.get(doc, 0) + 1 / (60 + rank) for rank, doc in enumerate(bm25_top): rrf_scores[doc] = rrf_scores.get(doc, 0) + 1 / (60 + rank) fused = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)[:10] # 重排 pairs = [[query, doc] for doc, _ in fused] rerank_scores = reranker.predict(pairs) reranked = sorted(zip([d for d, _ in fused], rerank_scores), key=lambda x: x[1], reverse=True) return [doc for doc, _ in reranked[:top_k]]

这段代码做了三件事:向量检索取 20 条,关键词检索取 20 条,用 RRF 融合后取 10 条,再用重排模型精排取 5 条。RRF 公式里的 60 是经验值,作用是平滑排名差异,不需要改。

5.5 组装上下文并调用模型

def build_prompt(query, retrieved_chunks): context = "\n\n".join([ f"[片段 {i+1}]\n{chunk}" for i, chunk in enumerate(retrieved_chunks) ]) prompt = f"""以下是与问题相关的资料片段: {context} 请基于以上资料回答问题。如果资料中没有相关信息,请明确说明"资料中未提及",不要编造。 问题:{query} """ return prompt

到这里,一条最小可用的 RAG 流水线就搭好了。你可以把build_prompt的输出接到任何大模型的 API 上,就能得到一个基于自己资料回答问题的 Agent。

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

实际跑起来之后,你会遇到各种问题。这一节整理我踩过的坑和排查思路,做成速查表。

问题现象可能原因排查方法解决方向
检索不到相关内容切分太粗或太细抽查片段质量调整切分策略和长度
检索到相关内容但答非所问重排缺失或失效检查重排模型是否加载加装重排模型
回答编造资料中没有的内容提示词没给拒答选项检查提示词明确指示“不知道就说不知道”
中文检索效果差嵌入模型选错换中文模型测试用 BGE 或 M3E 系列
精确关键词查不到纯向量检索的局限测试关键词查询加 BM25 混合检索
新旧版本资料混答元数据缺失检查是否记录时间加时间过滤
表格内容检索混乱表格未单独处理检查表格解析表格转 Markdown 单独入库

除了表格里的问题,还有几个经验性的避坑点:

不要一次性把所有资料都入库。先拿一小部分资料跑通流程,验证检索效果,再逐步扩大。我见过有人一上来就入库几万份文档,结果检索效果差,排查起来无从下手。

定期评估检索质量。建一个测试集,包含 50 到 100 个典型问题,每个问题标注正确答案所在的文档。每次调整切分或检索策略后,跑一遍测试集,看命中率变化。没有评估,调优就是盲人摸象。

关注上下文窗口。检索出来的片段加上提示词,总长度不能超过模型的上下文窗口。如果超了,要么减少片段数量,要么换更大窗口的模型。我一般控制在窗口的 70% 以内,留出余量给模型生成回答。

图片和表格单独处理。RAG 知识库默认处理纯文本,图片里的信息检索不到。如果资料里有大量图表,需要先用多模态模型把图片转成文字描述,再入库。表格同理,转成 Markdown 表格后单独作为一个片段。

7. 知识库的持续维护:上线只是开始

知识库不是建完就一劳永逸的。资料会更新,业务会变化,用户的问题分布也会变。一个健康的知识库需要持续维护。

维护的核心工作是监控和迭代。监控什么?监控检索命中率和用户反馈。如果发现某类问题经常检索不到,说明资料缺失或者切分策略不适用,需要针对性补充和调整。

迭代的节奏建议是:每周看一次检索日志,找出低分查询;每月做一次全量评估,跑测试集看整体指标;每季度做一次资料盘点,清理过期内容,补充新资料。

还有一个容易被忽略的点:知识库的版本管理。当资料更新时,不要直接覆盖旧版本,而是保留历史版本并标注生效时间。这样当用户问“去年的政策是什么”时,Agent 还能检索到旧版本。实现方式是在元数据里加valid_from和valid_to字段,检索时按时间过滤。

最后分享一个我在实际项目中总结的小技巧:给每个片段生成一个“假设性问题”。具体做法是,用模型为每个片段生成 2 到 3 个它可能回答的问题,把这些问题和片段一起入库。检索时,用户的问题会先和这些假设性问题匹配,命中后再返回对应片段。这个技巧对提升召回率有明显效果,尤其是当用户提问方式和资料表述方式差异较大时。代价是入库时多一步模型调用,但相比检索效果的提升,这个成本值得花。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询