同一份知识库,4 种切分 × 3 种召回,纯关键词比纯向量高 13 个点

这次跑了什么

31 篇中文技术笔记当语料,30 条有唯一答案的问题当尺子,四种切分乘三种召回,一共 24 组配置。全部离线,不需要 API Key。

结果里有三件事跟我跑之前的预期不一样。

观察 数字
纯关键词召回比纯向量更准 53% 对 40%
按句子切分把表格完整度打到 6%,命中率只掉 6 个点 67% 对 73%
纯关键词在跨块题上一个都没命中 0%

另外还踩了两个坑,都属于「看着在工作、其实没工作」那一种。

先看一组数字

同一批 563 个块、同一批 30 条问题,三种召回方式,上下文预算固定 1200 字。

召回方式 总体 字面 改写 结构 跨块 易混
纯关键词 53% 75% 38% 83% 0% 50%
混合(0.6 / 0.4) 73% 75% 75% 100% 25% 75%
纯向量 40% 50% 50% 33% 25% 25%

字面题和结构题,关键词单干就有 75% 和 83%,纯向量只有 50% 和 33%。改写题反过来,关键词 38%,向量 50%。跨块题关键词一个都没中,向量和混合各 25%,谁也没解决。

所以「上 RAG 就得配向量库」这个默认动作,在这批语料上是站不住的。混合召回的分工也说不上巧妙,关键词负责专有名词和代码块,向量负责同义改写。跨块两边都弱,这个问题后面单独说。

先得让「答对」能判

上面这些百分比能算出来,前提是每道题只有一个答案,而且脚本能判对错。

判据用的是「源文件 + 唯一片段」,不是块编号。块编号会随切分方式变,拿它当判据,换一个切分器三十条题就全废了,而这篇文章要做的恰好就是换切分器。

json 复制代码
{
  "id": "q-07",
  "type": "structure",
  "question": "坑-17 那次并发探针打印出来的 auditDup 是多少?",
  "expected": {
    "source": ".../坑-17-幂等先查后写在并发下全漏.md",
    "phrases": ["auditDup=0"]
  }
}

命中的定义很朴素。取回的前 k 个块里,只要有一块来源在允许集合内、正文含了答案片段中至少一条,就算中。

三十条题分了五类,每一类都在验一件特定的事。

类型 条数 要证明什么
字面 8 基准线。这一档都命中不了,说明索引坏了
改写 8 语义能力。问句不照抄原文的词
结构 6 主结论。答案只在代码块或表格里
跨块 4 答案要两块以上拼起来
易混 4 问句里的词在多处出现,看能否挑对文件

有一条规则必须写进用例。答案片段在它所属的那个文件里只能出现一次。出现零次,判据永远判不中;出现两次,取回另一个块也算命中,命中率虚高,而程序不报错。这条我做过反向验证,把片段换成出现两次的字符串,用例立刻变红,还点出是哪一条。

还有个麻烦是重复副本。已发布的文章照抄过笔记原文,同一个结论散在两到四个文件里。只认一个来源,命中率会虚低,所以判据里加了「等价来源」,允许一组文件都算中。易混类故意不加,那一类考的就是能不能挑对文件。

24 组配置铺开看

有三样东西固定住不动。本地 embedding 用 bge-small-zh,同一个块永远得到同一个向量;上下文预算 1200 字;三十条题不变。

预算是这里最要紧的约束。块越大,同样取 k 块塞进去的字越多,命中率自然越高。不限预算的话,最优解永远是「一块装整篇」,那是算术,不是结论。把 1200 字钉死,四种切分才站到同一杆秤上。

切分 块/重叠 块数 召回 命中率 literal paraphrase structure crossblock confusable 平均上下文 表格完整 代码完整
按字符 300/30 431 向量 33% 38% 25% 50% 50% 0% 1180 字 55% 55%
按字符 300/30 431 混合 60% 75% 50% 83% 50% 25% 1181 字 --- ---
按字符 300/30 431 父块 53% 75% 50% 50% 50% 25% 984 字 --- ---
按字符 800/80 172 向量 30% 25% 38% 17% 75% 0% 782 字 75% 80%
按字符 800/80 172 混合 47% 25% 50% 67% 100% 0% 792 字 --- ---
按字符 800/80 172 父块 47% 63% 38% 50% 50% 25% 631 字 --- ---
按段落 300/30 556 向量 43% 50% 50% 50% 25% 25% 1077 字 68% 82%
按段落 300/30 556 混合 70% 75% 75% 100% 25% 50% 1057 字 --- ---
按段落 300/30 556 父块 43% 63% 25% 50% 50% 25% 1899 字 --- ---
按段落 800/80 196 向量 30% 50% 25% 17% 50% 0% 839 字 94% 95%
按段落 800/80 196 混合 67% 63% 63% 83% 100% 25% 752 字 --- ---
按段落 800/80 196 父块 50% 63% 50% 50% 50% 25% 705 字 --- ---
按句子 300/30 453 向量 37% 38% 38% 33% 50% 25% 1124 字 6% 18%
按句子 300/30 453 混合 67% 75% 75% 83% 50% 25% 1139 字 --- ---
按句子 300/30 453 父块 50% 75% 38% 50% 50% 25% 1239 字 --- ---
按句子 800/80 191 向量 33% 25% 38% 17% 75% 25% 810 字 45% 69%
按句子 800/80 191 混合 57% 38% 63% 83% 100% 0% 795 字 --- ---
按句子 800/80 191 父块 50% 63% 50% 50% 50% 25% 792 字 --- ---
递归 300/30 563 向量 40% 50% 50% 33% 25% 25% 1091 字 70% 80%
递归 300/30 563 混合 73% 75% 75% 100% 25% 75% 1068 字 --- ---
递归 300/30 563 父块 43% 63% 25% 50% 50% 25% 1918 字 --- ---
递归 800/80 196 向量 30% 50% 25% 17% 50% 0% 851 字 94% 95%
递归 800/80 196 混合 67% 63% 63% 83% 100% 25% 752 字 --- ---
递归 800/80 196 父块 50% 63% 50% 50% 50% 25% 705 字 --- ---

从这张表里能拎出四件事。

召回方式的杠杆比切分大。同一切分配置下,纯向量换成混合,涨 17 到 37 个点(递归 300/30 那组,40% 直接到 73%)。但切分也不能忽略,同一召回方式下换切分,最大差 20 个点(800/80 混合,按字符 47%,按段落和递归都是 67%)。

结构完整度和命中率是脱钩的。按句子切分把表格完整度打到 6%、代码 18%,等于把每一行都切开了。可混合召回下命中率还有 67%,跟最优只差 6 个点。原因不难猜,答案片段多半落在普通文字行里,结构被毁不等于答案找不到。但模型拿到一张残缺表格照样读不懂,只看命中率,这块损失会被漏掉。

父块不是万灵药。递归和段落 300/30 的父块,平均上下文分别是 1918 字和 1899 字,都超出预算了(第一块强制装入),命中率只有 43%。子块调大之后父块变短,705 字,命中率还是低于混合。它的价值在上下文完整,不在命中。

块大小是个取舍。300 比 800 命中率高(混合 73% 对 67%),800 的结构完整度又高得多(表格 94% 对 70%)。两头都要付代价。

关键词为什么在这批语料上更强

这个结论依赖语料性质,别直接搬到别的场景。

这批语料是中文技术笔记,幂等键、defaultToolCallbacks、on-http-codes 这类专有名词密度极高。向量模型把它们压进 512 维,相邻的概念就容易糊在一起,幂等、抢坑位、限流都算「并发与护栏」,语义上贴得非常近。二元组匹配碰到专有名词几乎不失手,代价是同义改写完全无能,那一列只有 38%。

跨块那列关键词 0%,值得单独记一笔。它只能找到词对得上的那一块,拼不出跨段的答案。混合也只有 25%,跨块是所有方案的短板。真要解决,得上父块回填或者多轮检索,可父块回填在 1200 字预算下并不划算,前面 1918 字平均上下文那组数字已经说明了。

三个估出来的参数,后来都扫了一遍

混合权重 0.6 / 0.4、预算 1200 字、相似度阈值 0,这三个数一开始全是我拍的。拍出来的值不能当结论,所以后面各扫了一遍,看它到底是平台,还是某个幸运点。

向量 / 关键词 命中率
0.0 / 1.0 53%
0.2 / 0.8 67%
0.3 / 0.7 67%
0.4 / 0.6 73%
0.5 / 0.5 73%
0.6 / 0.4 73%
0.7 / 0.3 63%
0.8 / 0.2 60%
1.0 / 0.0 40%

0.4 到 0.6 这三档结果完全相同,取哪个都一样。往下到 0.3 掉到 67%,往上到 0.7 掉到 63%。是个很宽的平顶,不是某个幸运点。

预算(字) 命中率 实际平均用字
300 30% 242
600 50% 517
900 57% 770
1200 73% 1067
1800 77% 1708
2400 80% 2281
4000 83% 3892

900 到 1200 加了 16 个点,1200 到 1800 只加 4 个点,1800 之后每翻一倍换 3 个点。1200 就是「再加不划算」的那个位置。

minScore 命中率 平均取块数
-1.0 73% 4.6
0.0 73% 4.6
0.2 73% 4.6
0.3 73% 4.6
0.4 73% 4.6
0.5 60% 4.4
0.6 30% 3.2

0 到 0.4 之间结果一模一样。进预算的块,余弦本来就在 0.4 以上,阈值根本拦不到东西。拉到 0.5 开始误杀,命中率掉到 60%,再拉到 0.6 只剩 30%。所以这个语料上阈值设 0 是对的,设了也没用。

顺手看了 Python 那边的默认值

很多人是从 Python 那边过来的。我把 langchain_text_splitters 0.3.11 的源码解出来看了默认值,跟 Java 这边对不上的地方不少。

LangChain(Python) LangChain4j(Java)
默认块大小 chunk_size=4000、chunk_overlap=200 没有默认值,必须传
切分依据 字符(length_function=len) 字符,可换成 TokenCountEstimator
默认分隔符 ["\n\n", "\n", " ", ""] 段落、行、句子、词、字符,链式降级
语言预设 from_language(Language.JAVA) 按 \nclass 、\npublic 、\nif 等切 没有,要自己写 DocumentByRegexSplitter

Java 那条链我是用 javap -c 在 jar 上确认的。DocumentSplitters.recursive 依次构造段落、行、句子、词四个切分器,词那一层再降级到字符,没有「分隔符列表」这个东西。

有三个地方不能照抄。

  • 4000 字这个默认值对中文偏长。上面矩阵里 800 已经是「结构完整优先」的档了,300 的命中率还更高。直接套 4000,一块塞进去好几节,检索精度必掉。
  • " " 这一级对中文几乎没用。中文不靠空格分词,那一层基本不触发,真正生效的是换行和字符级。
  • Java 侧没有语言预设。想按类、按方法切,得自己写正则。正则写错了不报错,切出来的块只是「看起来正常」。

两个坑

坑一,Intel Mac 装不了本地中文 embedding。报错原文是这样。

vbnet 复制代码
ai.djl.engine.EngineException: Failed to load Huggingface native library.
Caused by: ai.djl.engine.EngineException: Unexpected flavor: cpu

「Unexpected flavor: cpu」把我往 ONNX 运行时的 flavor 配置上带,查了半天环境变量,什么也没查到。后来把 jar 解包看 native/lib/,才找到真因。DJL 的 tokenizers 从 0.30.0 起不再发布 osx-x86_64 的原生库,而 LangChain4j 默认拉的是 0.36.0,本机 Intel Mac,压根没有对应的 .dylib。逐版本解包确认下来,0.29.0 是最后一个还带这个库的版本。把 ai.djl:api 和 ai.djl.huggingface:tokenizers 一起钉到 0.29.0,测试才过。只钉一个会版本错配。

坑二,正则里的 matches() 让切分只切出一块。第一版的输出是这样。

复制代码
候选块:全部 31 个,值得问 31 个,涉及 31 个文档

每个文档只切出一块。Pattern.matches() 要求整行完全匹配,可我的正则带了行首锚点。改成 find() 之后是 237 块。这地方的危险在于输出看着完全正常,块数等于文档数,不报错,不警告。

相关推荐
靠谱的工程师朋友1 小时前
电子会计档案管理软件科普 核心能力与选型要点解析
网络·数据库·人工智能·电子会计档案
wflynn1 小时前
Sebastian Raschka 11 小时视频课:用 PyTorch 从零构建推理模型
人工智能·ai
买房动力十足1 小时前
秒杀商城 + AI 运营:Lua 防超卖、MQ 削峰,AI 负责选品和策划
开发语言·人工智能·lua
forest_091 小时前
《大模型原理通俗讲》系列 00|开篇 大模型到底是怎么回事
人工智能·python·prompt·agi
字节跳动视频云技术团队1 小时前
一份旅行手册,豆包做完,IGA Pages 一句话就分享出去
人工智能
海岳云舟1 小时前
springboot单元测试技术
java·后端·spring
汤姆宝库1 小时前
蒸馏与小模型深拆:软标签、轨迹搬运与能力电网
人工智能·ai
poiu12346572 小时前
使用AI辅助学习与资料整理的个人体会
人工智能·学习
清华kenny2 小时前
美业经营者如何筛选中医健康系统?避坑与推荐
大数据·人工智能·安徽晓得养