我从零搭了一个 RAG 知识库,在我那套 12 道题的评测上拿到了 100% 分——真正值钱的是踩坑调试的经验。

免责声明:这是一个演示项目,基于合成/示例产品数据搭建(虚构的 REDMI K90 系列)。与任何品牌均无关联,也不是真实的产品评测。文中数字仅作示意。

摘要

  • 不用向量数据库。SQLite 保存的是真值(实体、别名、关系、分块元数据);numpy 向量只是一个派生索引。
  • 检索 = 4 条路径(SQL 精确匹配、对比、稠密、BM25)→ 先过滤再融合 → RRF。
  • 我做了一套 12 题的评测,每题 6 项检查(归一化 / 检索 / 隔离 / 覆盖 / 幻觉核验 / 拒答),并反复迭代: 94.4% → 98.6% → 100% 。
  • 这套评测是自建的,不是基准测试——局限性见文末。

技术栈(骨架,总计约 600 行代码)

  • SQLite → products/aliases/relations/chunks 表,content_type,authority(3/2/1),source。
  • numpy → 512 维嵌入(bge-small-zh-v1.5),通过 JSON sidecar 与分块 ID 对齐。每次构建后做一致性校验。
  • 检索:4 路结果用 RRF 融合(K=60)。 过滤在融合之前进行 —— 否则无效候选会占掉前 5 个位置,把好的分块挤出去。
  • 生成后校验:用推测性措辞正则,加上「答案里出现的每个数字都必须在检索到的材料中存在」这一条。

6 条调试经验(其他都可以跳过,只看这些)

别名归一化 ≠ 子串逻辑。 红米k90 和 k90ultra 并不共享子串,但 K90 和 K90-ULTRA 共享一个 前缀 。只按模型代码前缀保留最具体的那条匹配,不要按别名字符串来保留。

SQL ORDER BY = 插入时间偏差。 路径 1 按数据库顺序返回分块,因此后加入的文档(UGC 评论)在 RRF 融合中系统性地吃亏。修法:让每个精确匹配的分块拿到 相同的排名(1) 。连按稠密分数排序都不要做——那是第 3 号偏差。

按稠密分数给精确路径排序,会干掉向量盲区。 用户说的「拍照」,永远匹配不上文档里的「影像参数」——这个分块在稠密和 BM25 里 两边 都是 #None。再按稠密分数给精确路径排序,就把它挤出了 top-5。改成统一排名就解决了。

表格内容对稠密检索是盲区。 多列规格对比表嵌入效果极差,可它恰恰是材料对比类问题最需要的内容。解决办法:对比路径把每个对比分块重复 3 次进 RRF ≈ 权重放大 。

查询扩展 > 调嵌入模型。 同义词(“拍照”→影像/相机/成像),每个变体各检索一遍,按分块取 MAX (不是 SUM——别让某一个查询变体把分数刷上去)。传感器名称“950”一查就出来了。

事后校验也可能误报。 API 超时的时候,字符串 443 混进了回答里,数字校验就把它判成了编造。只拿那些本来就该出现在材料里的数字去核对。

补充一点:API 超时是环境问题——加上重试和退避,再把超时时间从 180s 降到 60s,大部分偶发失败就消失了。

为什么「100%」这个数字要打个折扣看

  • 我自己写了 12 个问题,每个问题 6 项检查,跑在一个 60 个分块的玩具语料上。规模小、经过人工挑选,算不上基准测试。
  • 没有真实用户,没有 A/B 测试,也没有对域外查询做评测。“100%”指的就是这次评测,仅此而已。
  • 模型是 Qwen3-8B,温度设为 0.1——换成更大或不同的模型,分数会变。

哪些部分移植得很顺利

同样的引擎,换了个领域(复制了 3 个文件,替换了数据、归一化和提示词): 新的 12 题评测在修复后的首次运行同样达到了 100% 。方法可以移植;你的数字不会一样。

完整可运行的 demo:本仓库中的 GEO_kb/(geo_build.py / geo_query.py / geo_eval.py,都是对 K90 引擎的薄封装)。

欢迎批评指正,尤其是关于均匀秩精确路径和重复 3 次对比技巧这两点。这两者在这个语料上都管用,但感觉更像是工程上的务实做法,谈不上理论。

我以后想搭建几个复杂的知识库。关于 GEO,你有什么建议吗?