← 返回博客

本文属于主题:企业知识库怎么建:从分块、检索到评测

给检索系统造一把尺子

琅嬛检索评测实录:一条零召回的死通道、一个被证伪的调优假设,和一套让每个架构决策都有代价标签的评测体系。

检索评测,是给检索系统的质量造一把尺子:固定数据集、固定指标、同指纹可复现,让每个架构决策都有代价标签。这篇记录给琅嬛零人工标注地造出这把尺子的完整过程,以及它量出了什么。

换上真实模型跑出第一份基线的时候,我发现系统里有一条通道是死的。

FTS(全文检索)通道对中文疑问句的召回率是 0——不是低,是零。而系统对此毫无表现:向量检索足够强,混合检索的指标与纯向量那一格完全相同。「向量 + 关键词混合检索」在架构图里存在,在指标上不存在。

这件事能被发现,只因为两天前我做了一件朴素的事:给检索系统造一把尺子。这篇文章记录这把尺子怎么造、量出了什么,以及哪些经验值得沉淀。

所有测试都是绿的,但没人能回答「检索好不好」

琅嬛 是我在做的知识服务,为 Agent 提供可核对、可授权的检索能力。它的检索链路是大多数 RAG 系统的标准形态:文档解析 → 分块(父子 chunk)→ 向量与 FTS 双路召回 → RRF 融合(把两路结果按各自排名合并成一个榜单)→ 可选 rerank(用一个重排模型给合并后的候选重新排序)。

在 v1.1.0 之前,琅嬛的测试体系——单元、集成、E2E——全部是正确性测试。它们回答「功能对不对」:API 是否返回 200,字段是否齐全,流程是否不崩。

它们不回答「检索好不好」。分块合同从 v1 演进到 v3,RRF 融合、rerank,每个架构决策都有理由,但没有一个被实测验证过。「混合优于单路」是行业共识,不是本系统的结论。同一组查询换一组分块参数,结果是变好还是变坏?只能靠人工观感:拿几个问题试试,感觉还行。

「感觉还行」是检索系统的默认状态。它的问题不在于一定错,而在于无法积累:下一次改动之后,你不知道自己是在进步还是在退步。架构决策没有代价标签,每个「优化」都可能是负优化。

尺子的约束:零人工标注,且要能回归

造尺子的第一个硬约束:不做任何人工标注。标注是最贵的环节,也是大多数自建评测死掉的地方。解法是用带人工标注的公开数据集——我选了 MIRACL-zh:Wikipedia 中文语料,段落级人工标注的相关性,Apache-2.0 许可。固定种子确定性采样 200 条真实搜索 query,语料和标注的哈希写进 manifest,任何人可复现同一份数据。

第二个设计决策是双轨道,因为「检索好不好」其实是两个问题:

  • 段落检索轨:5,298 份单段落文档。隔离分块变量,专量 Embedding、FTS、RRF 融合本身的水平;
  • 长文档轨:709 篇全文文章。量「分块 → 父子 chunk → 检索」的全链路水平。

第三个决策影响最深远:命中判定不绑定 chunk ID。判断检索结果是否命中标准答案,用的是文本重叠率而非 ID 相等——chunk ID 随分块版本变化,绑死了标注,每次分块演进标注就全部作废;文本重叠判定让同一份数据在 chunker v3、v4、v5 下持续可用。评测体系首先要活得比被测系统的一个版本更久,否则每次架构演进都要重付评测成本。

此外是通道矩阵:同一份数据上分别跑纯向量、纯 FTS、混合、混合加 rerank 四格。这是后来能发现死通道的前提——只看混合一路的总分,你永远不知道增量来自哪里。

最后是可复现性。每份报告携带完整指纹:数据集哈希、分块版本与参数、Embedding 模型信息、rerank 模型、代码版本。琅嬛的 RRF 融合是确定性的,同指纹重跑,指标逐位一致——实测验证过。这条性质把评测从「一组数字」变成「一把尺子」:指标变了,必然是代码或配置变了,差异可归因。没有确定性,跑分只是噪音。

指标本身刻意朴素:recall@10、MRR@10、nDCG@10——衡量「找到没有、排得如何」的三个经典检索指标——全部基于确定性标注计算,无 LLM 参与、无 LLM 裁判。第一版尺子不需要聪明,需要可信。

尺子还没量到语义,就开始回本了

第一轮跑分用的是 mock embedding:一个本地服务,同文本永远返回同一向量,没有任何语义。它的唯一目的是验证评测装置本身无偏——mock 的指标应当与随机基线吻合,实测确实吻合。

结果这轮连语义都没有的冒烟,顺手抓出两个产品级 bug:standalone 模式创建知识库必 500(SQLite 外键约束缺延迟检查),以及向量检索直接报错(vec 扩展没链进发布二进制)。两个都在 v1.1.1 修复。

这是造尺子的第一课:尺子一旦造出来,回本的方式往往不是你当初预想的那个。

第一份真实基线:三个发现

换上真实模型(本地 Ollama 跑 bge-m3,云端对照 Qwen3-Embedding-0.6B)跑完整 200 条 query,三个发现同时出现。

发现一:FTS 通道对中文疑问句零召回。 机理不复杂:分词器把「埃及有哪些民族?」切成 [埃及, 有, 哪些, 民族, ?],FTS 按全词 AND 检索,「哪些」和「?」在正文里永远不会齐备,于是一条都查不出来。关键是系统对此毫无报警——向量一路足够强,混合的结果和纯向量每一位都相同。你以为有两路保险,其实只有一路,而且你不知道。

发现二:瓶颈不在模型。 本地 bge-m3 与云端 Qwen3 的差距在 1 个百分点上下。没有这份数据,最自然的动作是换更强的模型——更贵、更慢,且动不了大局。有了这份数据,优化方向转向一个便宜得多的修复。

发现三:长文档全链路有真实损耗。 同一批 query,段落轨 98% 的召回,到长文档轨掉到 79%,衰减约 19 个百分点。分块管线第一次有了自己的账。

修复之后,架构假设第一次闭环

修复针对发现一:查询侧过滤停用词。把标点、单字虚词、疑问填充词从 FTS 查询串里滤掉,词表刻意保守——宁漏勿错,误删实义词的代价远大于漏删虚词。同一份实现覆盖 SQLite FTS5 和 PostgreSQL 两个方言。随后同指纹重跑。表格里三个指标的含义:recall@10——前 10 条结果覆盖标准答案的比例,衡量该找到的找到没有;MRR@10——标准答案出现得越靠前分越高(排第 1 位得 1 分,排第 5 位得 0.2 分,对所有查询取平均);nDCG@10——前 10 条结果的整体排序质量,正确的条目越靠前分越高。

通道(段落轨) recall@10 MRR@10 nDCG@10
纯向量 0.9799 0.9942 0.9771
纯 FTS 0.1314 0.1825 0.1429
混合(RRF) 0.9826 0.9967 0.9799
混合 + rerank 0.9778 0.9975 0.9778

FTS 从 0 复活到 0.13;混合第一次严格高于纯向量(0.9826 > 0.9799)。「混合检索优于任一单路」从行业共识变成了本系统的实测结论——这个假设从写进架构的那天起就没被验证过,而验证它只用了两天。

rerank 的贡献也在矩阵里现形:段落轨把 MRR 推到 0.9975(四格最优),长文档轨修复了 FTS 候选对头部排序的污染(MRR 0.8911 → 0.9143),成为长文档场景的最强组合(recall@10 0.7938,nDCG 0.7952)。

顺带一提量化后的图景:段落级中文检索 98% recall、命中几乎都在第一位,且 200 条 query 没有一条完全无果;长文档全链路约 80%,其中 99.5% 的 query 的目标文档其实已被召回——差的是文档内部的排序。这个「差的是排序」的判断,正是下一节的方法产出的。

阴性结果也是结果

长文档轨还差约 20 个百分点,最直觉的假设是分块参数:父块是不是切小了?我跑了两轮单变量实验,其余指纹全部相同:

配置(长文档轨) 纯向量 recall@10 结果
基线 4096/384 0.7918 —
父块 4096 → 8192 0.7732 负向,证伪假设
子块 384 → 256 0.7942 +0.4pp,噪声级

父块翻倍,召回反降近 2 个百分点——「父块切分是损耗主因」的假设被证伪。子块调小的收益是 0.5 个百分点量级,6 个未命中 query 在两组实验里一个都没变。

结论:分块参数不是有效杠杆,维持默认合同不动。 这个阴性结果避免了一次会波及所有存量知识库的合同变更。评测体系里,阴性结果和阳性修复同样是交付——它给「不做某事」提供了证据,而不是让「要不要调一调」永远停留在争论里。

别停在数字上

80% 的召回率不指导行动。真正有产出的一步,是把 6 个未命中 query 逐条解剖:重启实例、回放查询、把返回内容与标准答案逐段比对。

结果是:5 个属于「文章内章节竞争」——正确的文章召回了,但含答案的章节在文章内部排不过其它章节(查「意大利汽车品牌」,命中了意大利文章的出口贸易章节);多个未命中还叠加繁简混杂因素——语料是繁体、query 是简体,词法匹配和命中判定同时受损,繁简归一化由此成为有数据背书的下一个改进方向;只有 1 个是真正的向量语义盲区。

指标告诉你哪里不对;逐条归因才告诉你下一步做什么。少了这一步,跑分再准也只是记分牌,不是地图。沿着这条路往下走,我把同一套方法用到了无结构语料的分块问题上,做了一次判决性实验:《语义分块值不值得做?先量它的天花板》。

诚实的边界

最后是这套数字不能说什么。

绝对分数不可与公开排行榜对比。 本评测在约 5,300 段落的采样池上检索;MIRACL 官方榜单在 490 万段落的全池上计算,池子越大分数越低。直接拿 98% 去和榜单比,要么虚高要么挨骂。这把尺子的价值是同指纹的相对比较——同一把尺子量每一次改动,而不是刷绝对值。

FTS 对问句型 query 天然偏弱(0.12~0.13),这是词法 AND 检索的本性,不是缺陷。它的主场是关键词型 query:文件名、术语、编号。问句场景由向量加混合兜底。知道每个通道的适用域,和知道它的分数一样重要。

沉淀

回头看,这套体系两天建成,零人工标注,独立二进制不进主链路。它验证了两个架构假设、证伪了一个、逼出三个产品修复、把剩余差距归因到具体机理。但比这些产出更可复用的是几条经验:

正确性测试与质量评测回答不同的问题。 前者验证你造对了东西,后者验证你造的东西好用。检索系统长期只有前者,是因为后者贵——用公开标注数据集可以把它变得便宜。

冗余通道会静默死亡。 一个子系统可以完全失效而系统「正常」,因为另一路足够强,把它的死亡遮住了。只有把每路单独拎出来量,才知道融合的增量来自哪里、你以为的冗余是不是真的存在。

先测量,再调优。 瓶颈往往不在你最想换的那个模型上。测量会把优化顺序重排成数据驱动的——这次它把「换模型」排到了「过滤停用词」后面。

可复现性是评测的生命,不是锦上添花。 指纹加确定性,指标差异才可归因;命中判定不绑实现细节,标注才能跨版本存活。评测体系要活得比任何一版被测系统更久。

阴性结果也是交付。 证伪一个假设、确认「默认值不用动」,和修复一个 bug 同样值钱——它让「不做」有了证据。

别停在数字上,追问到机理。 指标定位问题,归因指导行动。没有归因的评测报告只是记分牌。

回到开头那条死掉的通道。它现在活着,词表保守地过滤着虚词,每一轮回归里单独占一格。而这把尺子留下来的东西比它的任何一次读数都重要:从造出它的那天起,琅嬛的每个架构决策都有了代价标签。

完整的数据、方法与推荐配置,记录在琅嬛仓库的检索评测报告里,随时欢迎拿着你自己的系统来比划。