ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent知识库搭建实战:RAG流水线从文档解析到混合检索的完整指南

Agent知识库搭建实战:RAG流水线从文档解析到混合检索的完整指南 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 数要大于你的片段长度成本本地模型免费但需要 GPUAPI 模型按量付费但省事中文场景下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解析 PDFsentence-transformers加载嵌入模型和重排模型chromadb向量存储rank-bm25jieba关键词检索嵌入模型我选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_len500, overlap50): # 按段落切分段落过长再按句子切 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_embeddingsTrue) ids [f{source}_{i} for i in range(len(chunks))] metadatas [{source: source, chunk_index: i} for i in range(len(chunks))] collection.add( embeddingsembeddings.tolist(), documentschunks, idsids, metadatasmetadatas )注意normalize_embeddingsTrue这会把向量归一化之后用余弦相似度检索时可以直接用点积计算更快。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_k5): # 向量检索 query_emb model.encode([query], normalize_embeddingsTrue) vec_results collection.query(query_embeddingsquery_emb.tolist(), n_results20) 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(), keylambda x: x[1], reverseTrue)[:10] # 重排 pairs [[query, doc] for doc, _ in fused] rerank_scores reranker.predict(pairs) reranked sorted(zip([d for d, _ in fused], rerank_scores), keylambda x: x[1], reverseTrue) 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[片段 {i1}]\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 个它可能回答的问题把这些问题和片段一起入库。检索时用户的问题会先和这些假设性问题匹配命中后再返回对应片段。这个技巧对提升召回率有明显效果尤其是当用户提问方式和资料表述方式差异较大时。代价是入库时多一步模型调用但相比检索效果的提升这个成本值得花。
返回列表