1. 从"能查"到"会想":增强版知识库到底增强了什么
很多人做 RAG 知识库,第一步就跑偏了。他们把文档切碎、灌进向量库、接一个大模型,然后兴冲冲地问一句"我们公司的报销标准是多少",结果模型答得驴唇不对马嘴——要么检索回来的片段跟问题八竿子打不着,要么检索对了但模型自己"脑补"出一堆原文没有的数字。这不是模型不行,是这套朴素 RAG 的链路本身太脆。
我做的这个"增强版智能知识库",核心目标就一句话:让知识库不只是"能查到",而是"会思考着查"。普通 RAG 是"问题 → 向量检索 → 拼上下文 → 生成",一条直线走到底,中间没有任何纠错和反思的机会。增强版在这条直线上插了好几个"检查站":查询改写、假设性文档生成、多路召回、重排序、答案自检。每一个检查站都在解决朴素 RAG 的一个具体病灶。
这篇文章适合谁看?如果你已经跑通过一个最基础的 RAG demo,但发现它在真实场景里"时灵时不灵",那这篇就是写给你的。如果你还没入门,也没关系,我会把每个环节为什么这么做讲透,你照着搭也能跑起来。关键词里的 Agent、LangChain、RAG、FAISS、HyDE 会贯穿全文,但我不会堆术语,而是告诉你每个东西在链路里到底站哪个位置、解决什么问题。
先说清楚一个认知:增强版知识库的本质,是把"检索"从一个静态动作,变成一个由 Agent 驱动的动态决策过程。普通 RAG 里,检索是一次性的;增强版里,Agent 会判断"这次检索结果够不够好""要不要换个问法再查一次""要不要先编一个假答案去钓真文档"。这个思路的转变,才是"增强"两个字的真正含义。
2. 朴素 RAG 的三个致命瓶颈,以及它们为什么必然出现
2.1 瓶颈一:用户的问题和文档的表述根本不在一个频道
这是最普遍、也最容易被忽视的问题。用户问"这个功能怎么退钱",而文档里写的是"退款流程说明"。字面上"退钱"和"退款"差一个字,向量化之后余弦相似度可能就掉了一大截。更极端的例子:用户问"我账号登不上了咋办",文档标题是"身份认证异常处理指南"——这俩在语义空间里离得老远,向量检索大概率召回一堆无关内容。
为什么会这样?因为嵌入模型是把文本映射到一个高维空间,它衡量的是"整体语义接近程度",而不是"意图匹配程度"。用户的口语化表达、省略、代词指代,都会让查询向量偏离文档向量。这不是换个更好的嵌入模型就能根治的,这是自然语言本身的歧义性决定的。
2.2 瓶颈二:单路召回的天花板很低
大部分入门教程只做一件事:把 query 向量化,去 FAISS 里找 Top-K。但单一检索策略有个硬伤——它只能捕捉一种"相似性"。向量检索擅长语义相近,但对精确的关键词、专有名词、编号(比如"型号 XR-200")反而不敏感。反过来,BM25 这类关键词检索对精确匹配很强,但完全不懂语义。
只走向量这一条路,就等于放弃了关键词匹配的能力。真实业务里,用户的问题往往同时包含"语义意图"和"精确实体",单路召回必然漏。
2.3 瓶颈三:检索回来的东西,模型不一定用得好
就算你召回对了文档,还有一个坑:上下文里塞了一堆片段,其中只有一两句是真正相关的,其余都是噪声。大模型在长上下文里会"分心",容易被无关内容带偏,甚至把不同文档里的数字张冠李戴。这就是所谓的"lost in the middle"现象——关键信息如果埋在上下文中间,模型很可能忽略它。
朴素 RAG 把召回结果一股脑塞给模型,不做筛选、不做排序、不做压缩,等于让模型在一堆草稿纸里找答案。增强版要做的,就是在把上下文交给模型之前,先把这堆草稿纸整理干净。
提示:这三个瓶颈不是独立的,它们会叠加放大。查询表述不准 → 召回质量差 → 上下文噪声多 → 生成答案错。所以增强也必须成体系地做,单点优化效果有限。
3. 查询改写与 HyDE:在检索之前先"骗"一次向量库
3.1 查询改写:把用户的话翻译成"文档听得懂的话"
增强链路的第一步,不是检索,而是改写查询。思路很直接:既然用户的问题和文档表述有鸿沟,那我先用大模型把用户问题"翻译"成更接近文档风格的多个版本。
具体做法是让模型基于原始问题生成 3 到 5 个改写版本,覆盖不同的表述角度。比如用户问"退钱怎么弄",模型可能生成:"退款申请流程是什么""如何发起退款""退款操作步骤"。这几个改写版本分别去检索,召回率会明显高于只用原始问题。
这里有个实操细节:改写不是让模型自由发挥,而是要给它约束。我通常会在 prompt 里明确要求"保持原意不变""使用正式书面表达""不要添加原文没有的信息"。否则模型容易改着改着就加戏,把"退款"改成"退货退款并补偿",意图就偏了。
3.2 HyDE:用一个"假答案"去钓"真文档"
HyDE(Hypothetical Document Embeddings)是我个人觉得最巧妙的一个技巧。它的逻辑有点反直觉:不要拿问题去检索,而是先让模型编一个假想的答案,再拿这个假答案去检索。
为什么这样有效?因为问题和答案在语义空间里的分布是不一样的。问题通常是疑问句式、口语化、信息密度低;而文档是陈述句式、信息密度高。用问题去检索文档,本质上是拿"疑问"去匹配"陈述",天然有偏差。但如果让模型先编一个"看起来像答案"的段落,这个假答案的文体、用词、信息密度就更接近真实文档,检索命中率自然更高。
举个例子。用户问"FAISS 支持哪些索引类型"。直接检索可能召回一堆介绍 FAISS 是什么的泛泛内容。但如果先让模型编一段假答案:"FAISS 支持 Flat、IVF、HNSW、PQ 等索引类型,其中 Flat 适合小规模精确检索,IVF 适合大规模近似检索……"——拿这段去检索,就很容易命中真正讲索引类型的文档。
HyDE 的代价是多一次模型调用,延迟会增加。所以我的经验是:对延迟敏感的场景,HyDE 只在对召回质量要求高的查询上启用,比如用户明确在问"具体怎么做""有哪些类型"这类需要精确信息的问题。闲聊式的查询就没必要上 HyDE。
3.3 改写和 HyDE 怎么配合
这两个不是二选一,而是可以叠加。我的默认配置是:先做查询改写生成多个版本,对其中信息密度最高的那个版本再跑一次 HyDE。这样既有表述多样性,又有文体对齐。实测下来,这套组合在专业文档库上的召回率比裸向量检索能高出不少,具体提升幅度取决于文档领域,越专业的领域提升越明显。
4. 多路召回 + FAISS 索引选型:别让单条腿走路
4.1 为什么必须做多路召回
前面说了,向量检索和关键词检索各有盲区。多路召回就是把这两条路都走一遍,然后把结果合并。向量路负责语义相近,关键词路(BM25 或类似方案)负责精确匹配。两路结果用 RRF(Reciprocal Rank Fusion,倒数排名融合)合并,比简单拼接效果好得多。
RRF 的逻辑很简单:不看每路的绝对分数(因为向量相似度和 BM25 分数根本不是一个量纲,没法直接比),只看排名。一个文档如果在两路里都排得靠前,那它大概率真的相关。公式是每个文档的得分等于它在各路中排名的倒数之和。这样既避免了分数归一化的麻烦,又能让"两路都认可"的文档浮上来。
4.2 FAISS 索引怎么选,别一上来就用最复杂的
FAISS 是 Facebook 开源的向量检索库,关键词里高频出现不是没道理的——它快、成熟、本地就能跑。但很多人一上来就纠结用哪种索引,其实选型逻辑很清晰:
| 索引类型 | 适用规模 | 特点 | 我的建议 |
|---|---|---|---|
| IndexFlatL2 / IP | 万级以下 | 精确检索,暴力计算 | 小知识库直接用,别折腾 |
| IndexIVFFlat | 十万到百万级 | 倒排+聚类,需训练 | 中等规模首选,调 nprobe |
| IndexHNSWFlat | 十万到百万级 | 图索引,查询快 | 对延迟敏感时用,内存占用高 |
| IndexIVFPQ | 百万级以上 | 乘积量化,压缩存储 | 内存吃紧时用,有精度损失 |
我的实操经验是:知识库文档在几万条以内,直接用 IndexFlatIP 配归一化向量,别想太多。这个规模下暴力检索的延迟完全可接受,而且结果是精确的,没有任何近似误差。等到数据量真的上去了,再考虑 IVF 或 HNSW。过早优化索引,只会给自己增加调参负担。
用 IVF 类索引时有个关键参数 nprobe,它决定查询时扫描多少个聚类簇。nprobe 越大越准但越慢,越小越快但可能漏。我一般从 nprobe=10 起步,根据召回测试结果往上调。这个参数没有万能值,必须拿你自己的数据测。
4.3 多路召回合并后的去重
多路召回有个副作用:同一个文档可能被两路都召回,合并后出现重复。必须做去重,否则上下文里全是重复内容,白白占用 token。去重的依据可以用文档 ID,也可以用内容哈希。我习惯用文档 ID + chunk 序号组合成唯一键,简单可靠。
5. 重排序:把真正相关的片段顶到最前面
5.1 召回和排序是两回事
很多人把召回和排序混为一谈,其实它们目标不同。召回阶段追求的是"别漏",宁可多召回一些,所以用相对宽松的策略;排序阶段追求的是"别错",要把最相关的顶到最前面。召回用双塔模型(query 和 doc 分别编码,快但粗),排序用交叉编码器(query 和 doc 拼一起编码,慢但准)。
交叉编码器为什么更准?因为它能让 query 和 doc 的每个词互相"看见",捕捉细粒度的交互信息。双塔模型里 query 和 doc 是各自独立编码的,交互只发生在最后的相似度计算,信息损失大。所以重排序这一步,是提升最终上下文质量的关键。
5.2 重排序的工程取舍
交叉编码器精度高但慢,如果对召回的几十个片段逐个跑,延迟会很难看。我的做法是:召回阶段多召回(比如 30 条),重排序阶段只对 Top 20 跑交叉编码器,最后取 Top 5 进上下文。这样在精度和延迟之间取得平衡。
重排序模型的选择上,中文场景我一般用 bge-reranker 系列,它对中文语义的把握比通用模型好。如果你的知识库是中英混合,选多语言版本。这里不展开具体模型名,因为模型迭代很快,关键是理解"重排序用交叉编码器"这个原则,具体用哪个可以按当期效果最好的来。
5.3 重排序之后还要做上下文压缩
重排序解决了"排序"问题,但没解决"冗余"问题。一个片段里可能只有一句话相关,其余都是废话。上下文压缩就是把这些废话去掉,只留关键句。做法可以是用模型抽取相关句,也可以按句子粒度重新打分筛选。
这一步对 token 成本敏感的场景特别值。我见过不少项目,上下文塞了 4000 token,其实真正有用的不到 500 token,剩下的全是噪声,既费钱又降低答案质量。
6. 用 LangChain 把链路串起来:Agent 在哪里介入
6.1 为什么用 LangChain 而不是裸写
LangChain 的价值不在于它帮你省了多少代码,而在于它把 RAG 的各个组件抽象成了标准接口。检索器、重排序器、提示模板、输出解析器,都有统一的调用方式。这意味着你可以像搭积木一样替换组件——今天用 FAISS,明天想换别的向量库,改一行配置就行。
但 LangChain 也有坑:它的抽象层有时候会掩盖细节,出问题时不好排查。我的建议是先用 LangChain 快速跑通,等链路稳定后,对性能瓶颈环节做针对性替换,而不是一上来就全部自己写。
6.2 Agent 在增强链路里的三个介入点
普通 RAG 是固定流水线,Agent 增强版的关键区别是:在几个决策点上让模型自己判断下一步做什么。
第一个介入点是查询分析。Agent 先判断用户问题属于哪一类:是事实查询、比较查询、还是需要多步推理的复杂问题。不同类型走不同的检索策略。事实查询直接检索,复杂问题可能需要拆成子问题分别检索再综合。
第二个介入点是检索质量评估。Agent 拿到召回结果后,判断"这些内容够不够回答问题"。如果不够,它可以决定换个查询重试,或者扩大召回范围。这就是所谓的"self-RAG"思路——让模型对自己的检索结果有反思能力。
第三个介入点是答案自检。生成答案后,Agent 检查答案是否完全基于检索到的内容,有没有编造。如果发现答案里有检索内容不支持的部分,就重新生成或标注不确定。
6.3 用 LangGraph 编排这些决策点
当决策点变多,普通的链式调用就不够用了,因为存在分支和循环(检索不够就重试,这是个循环)。这时候用 LangGraph 把流程建模成状态图就很自然:每个节点是一个处理步骤,边是转移条件,状态在节点间传递。
LangGraph 的好处是流程可视化、可调试。你能清楚看到一次查询走了哪条路径、在哪个节点重试了几次。这对排查"为什么这次答得不好"特别有用。不过要注意,循环必须有终止条件,否则 Agent 可能陷入无限重试。我一般设置最大重试次数为 2,超过就返回当前最好的结果并标注置信度低。
7. 实测中那些文档不会告诉你的坑
7.1 分块策略比你想的重要得多
几乎所有教程都会讲分块,但很少有人讲清楚分块策略对最终效果的影响有多大。我踩过的坑是:一开始用固定长度 512 字符切分,结果把表格切得七零八落,把"步骤 1"和"步骤 2"分到不同块里。检索时召回半个步骤,模型根本没法用。
后来我改成按语义边界切分:优先在段落、标题、列表项边界切,实在超长再按句子切。对表格和代码块,尽量整块保留不切。这个改动带来的效果提升,比换任何模型都明显。分块的本质是"保证每个块是一个语义完整的单元",长度只是次要约束。
7.2 嵌入模型的维度不是越高越好
很多人迷信高维嵌入,觉得 1536 维一定比 768 维好。实际上维度高意味着存储和计算成本高,而效果提升未必明显。更重要的是嵌入模型和你的文档领域是否匹配。一个在通用语料上训练的模型,用在法律或医疗文档上,效果可能还不如一个领域微调过的低维模型。
我的做法是:先用通用模型跑基线,如果效果不达标,再考虑领域适配。别一上来就追求最贵的模型。
7.3 元数据过滤能救命
纯向量检索有个问题:它不知道"时效性"。如果知识库里有新旧两版文档,向量检索可能把旧版召回上来。解决办法是给每个 chunk 打元数据标签(版本、日期、来源、权限),检索时先按元数据过滤再走向量。这一步在多版本、多来源的知识库里几乎是必须的。
7.4 评估集必须自己建
没有评估集,你根本不知道改动是变好还是变坏。我建议至少准备 50 到 100 个真实问题,每个问题标注正确答案和应该召回的文档。每次调整链路(换模型、改分块、调参数),都跑一遍评估集看指标。召回率、精确率、答案准确率,这几个指标要分开看,因为它们的优化方向可能冲突。
注意:评估集要用真实用户问题,不要自己编。自己编的问题往往表述规范、意图清晰,和真实场景差距很大,测出来的效果会虚高。
8. 并发与性能:Agent 链路怎么扛住真实流量
8.1 增强链路的延迟来自哪里
增强版比朴素 RAG 慢,这是必然的,因为多了改写、HyDE、重排序、自检这些步骤。延迟主要来自模型调用次数:查询改写一次、HyDE 一次、重排序一次、生成一次、自检一次,加起来可能五六次模型调用。每次几百毫秒,累积起来就很可观。
优化的核心思路是能并行的并行,能省的省。查询改写生成的多个版本,检索可以并行跑;多路召回的两路也可以并行。HyDE 和自检这种"锦上添花"的步骤,可以做成可开关的,对延迟敏感的场景关掉。
8.2 缓存是性价比最高的优化
知识库场景有个特点:很多问题是重复的或高度相似的。把"查询 → 最终答案"做缓存,命中时直接返回,能省掉整条链路的开销。缓存键可以用查询的语义哈希(把查询向量化后做近似匹配),这样表述不同但意思相同的问题也能命中。
向量检索本身也可以缓存。同一个查询的检索结果在知识库不变的情况下是稳定的,缓存起来能省掉检索和重排序的开销。
8.3 异步和批处理
模型调用是 IO 密集型操作,用异步能显著提升吞吐。Python 里用 asyncio 配合支持异步的模型客户端,能让多个请求的模型调用重叠起来。批处理则适合离线场景,比如批量重建索引、批量跑评估集。
这里有个容易忽略的点:FAISS 的检索本身是 CPU 密集的,且默认不是线程安全的。多线程并发检索时要注意加锁或每个线程用独立索引副本,否则可能出现难以复现的崩溃。
9. 知识库类型的选择:RAG、KG 还是结构化,别选错
热词里出现了"kg知识库、rag知识库和结构知识库区分以及应用场景",这个问题确实值得说清楚,因为选错类型会让整个项目事倍功半。
RAG 知识库适合非结构化文本:文档、手册、FAQ、聊天记录。它的优势是灵活,什么都能塞,不需要预先定义结构。劣势是检索精度依赖模型,且难以做复杂的逻辑推理(比如"找出所有满足条件 A 且不满足条件 B 的记录")。
知识图谱(KG)知识库适合实体关系密集的场景:人物关系、组织架构、产品依赖。它的优势是能做多跳推理和精确的关系查询。劣势是构建成本高,需要实体抽取和关系定义,且对非结构化文本的覆盖不如 RAG。
结构化知识库(数据库、表格)适合数值查询和聚合统计:财务报表、库存数据。它的优势是精确、可计算。劣势是只能回答结构化问题,对自然语言的理解能力弱。
实际项目里,这三者往往是组合使用的。我的经验是:以 RAG 为主干,对需要精确关系推理的部分接入 KG,对数值查询接入结构化数据源。Agent 的价值就体现在这里——它能根据问题类型,决定去哪个知识源找答案。这也是"增强版"相比单一 RAG 的重要升级。
10. 我在这套链路上踩过的几个真实坑
第一个坑是过度依赖 HyDE。有段时间我把 HyDE 设成默认开启,结果发现对简单事实查询反而变差了。原因是 HyDE 生成的假答案如果偏离太远,会把检索带偏。后来改成按查询类型条件启用,效果好多了。这告诉我:任何增强手段都有适用边界,无脑全开是偷懒。
第二个坑是重排序模型和嵌入模型不匹配。我一开始嵌入用 A 模型、重排序用 B 模型,结果重排序的效果很不稳定。后来统一到同一系列的模型,效果明显稳定。原因是不同模型的语义空间不一致,跨模型组合容易出问题。
第三个坑是忽略了 chunk 之间的上下文。有些答案需要跨多个 chunk 才能拼出来,但检索只召回了其中一个。解决办法是在分块时保留一定的重叠(overlap),或者在检索后把相邻 chunk 也带上。重叠比例我一般设 10% 到 20%,太多会冗余,太少会断片。
第四个坑是没有做查询意图分类就统一处理。所有查询走同一条链路,导致简单查询被复杂链路拖慢,复杂查询又得不到足够的处理。后来加了意图分类,简单查询走快速通道,复杂查询走完整链路,整体体验提升明显。
这套增强版知识库搭下来,我最大的体会是:RAG 的难点从来不在"能不能跑通",而在"跑通之后怎么让它稳定地好用"。每一个增强手段都是在跟自然语言的模糊性做斗争,没有一劳永逸的方案,只有不断根据真实数据调整。评估集、真实问题、持续迭代,这三样东西比任何花哨的技术都重要。