Skip to content

InterX - 多通道检索系统 ​

上一篇聊了数据预处理层如何把 40 本中英文手册从脏数据变成可用的结构化内容。数据准备好了,接下来的问题就是:用户问一个问题,怎么从上万个 chunk 里精准找到最相关的那几个?

这篇就来拆解 InterX 的检索层——一个五重分词、双通道召回、领域特化 Rerank、三级层次聚合的多通道混合检索系统。

五重分词:不是一种分词器不够用,是五种一起上 ​

为什么需要五重分词?因为纯 Jieba 分词在产品手册场景下会翻车。

举个例子:用户问"DCD791 怎么调转速",Jieba 会把"DCD791"切成"DC"、"D"、"791"——好好的产品型号被拆得面目全非。再比如"化油器调节螺钉",Jieba 可能切成"化油器"、"调节"、"螺钉",但如果用户搜的是"化油器调节",bigram 就能兜底召回。

所以我们做了五层分词叠加,每层解决不同的问题:

原始文本 → 层1: ASCII Token     (英文/数字,精确匹配产品型号)
         → 层2: Jieba 中文分词   (语义词汇,主力分词器)
         → 层3: 中文单字符       (Unigram 回退,最细粒度)
         → 层4: Bigram           (相邻二字组合,兜底分词错误)
         → 层5: Trigram          (相邻三字组合,捕获短语)
         → BM25Okapi 索引

效果是:产品型号通过 ASCII Token 精确命中,专业术语即使被 Jieba 切散了,bigram/trigram 也能兜底。五层信号同时构建索引,任意一层命中都能参与最终排序。

说白了,这是一种"用冗余换鲁棒"的策略——单个分词器总会犯错,但五层叠加后,错误被其他层覆盖的概率极高。

双通道召回 + RRF 融合:Dense 和 BM25 各有所长 ​

分词解决了 BM25 端的问题,但检索精度还取决于另一个维度:语义理解。

BM25 擅长精确匹配——用户搜"DCD791",BM25 能直接命中包含这个型号的 chunk。但 BM25 的软肋是同义表达:用户问"空调不制冷",手册里写的是"制冷效果差",字面不匹配就召回失败。

Dense 向量检索正好相反——它能理解"不制冷"和"制冷效果差"是同一件事,但它会平滑掉精确术语。用户搜"DCD791",Dense 可能返回一堆"相似型号"的 chunk,反而把目标淹没了。

所以我们同时跑两个通道,然后用 RRF(Reciprocal Rank Fusion)基于排名融合:

  • Dense 通道(权重 0.65):使用 DashScope 的 1024 维向量,查询端带 query_prompt 前缀,文档端带 document_prompt 前缀,引导模型在不同语义子空间表示
  • BM25 通道(权重 0.35):五重分词构建索引,显式过滤 score <= 0 的噪声结果
  • 召回过生成:两通道各召回 20 个候选,RRF 融合后保留 20 个给 Rerank

为什么 Dense 权重更高?因为产品手册场景下,用户的表述和手册原文往往差异较大(口语 vs 书面语),语义理解能力更重要。但那 0.35 的 BM25 权重不可省略——没有它,产品型号和精确参数就丢掉了。

领域特化 Rerank:让排序模型"懂"产品手册 ​

RRF 融合之后,排序质量还不够。通用 Rerank 模型不知道在产品手册场景下,哪些信息更重要。

我们的做法是给 Rerank 模型一个领域特化的 prompt,明确告诉它:

偏好包含具体步骤、安全指南、故障排除、规格参数的段落。

同时,输入给 Rerank 模型的不是原始 chunk 文本,而是带面包屑路径的 retrieval_text——也就是"手册名 > 章节 > 小节 > chunk 内容"这种结构化文本。这让 Rerank 模型能感知到 chunk 的位置上下文。

Rerank 使用的是 qwen3-rerank 模型。如果 Rerank API 调用失败,系统会静默降级到 RRF 融合排序——用户感知不到任何异常,只是排序质量略有下降。这种"优雅降级"的设计贯穿整个检索链路。

三级层次聚合:从小块精准到大块完整 ​

检索返回的是 small chunk(220 tokens),但用户需要的往往不只是一个精确答案,还需要完整的操作步骤和背景上下文。

所以我们做了三级层次聚合:small chunk 命中后,通过 ID 链向上聚合到 mid chunk(520 tokens)和 big chunk(900 tokens)。

聚合的核心规则:

  • 分数继承:父 chunk 的 score 继承子 chunk 的最大值,不被弱证据稀释
  • 双关键字排序:(first_rank, -max_score),先按首次出现排名,再按最高分排序
  • 截断:small 取 10 个,mid 取 5 个,big 取 3 个

这意味着:small 层面能找到最精确的事实(比如"极速制冷模式按下极速键"),mid 层面能看到完整的操作流程(4 步改善方法),big 层面能了解背景信息(空调制冷原理概述)。三层证据分别送给三层 LLM 并行回答,最后 ensemble 融合。

查询改写:让检索系统"听懂"用户的话 ​

用户问"那个按钮在哪?"——检索系统一脸懵,"那个"是什么?

查询改写是 answer 层在检索前做的关键步骤。LLM 会生成三种变体:

  1. 同义改写:"那个按钮在哪?" → "空调遥控器上的极速按钮在什么位置?"
  2. 跨语言翻译:"空调制冷效果差怎么办?" → "What should I do if my air conditioner isn't cooling?"
  3. 具体分解:"空调制冷效果差怎么办?" → "空调制冷效果差是否因为滤网堵塞或制冷剂不足?"

三种变体分别参与检索,扩大召回覆盖面。同义改写解决口语化表达,跨语言翻译命中英文手册内容(我们的系统同时覆盖中英文手册),具体分解把模糊问题拆成更精确的子问题。

查询改写使用独立的 LLM 端点,温度设为 0.7,保证多样性。记忆上下文也只在这一步暴露——LLM 看到完整对话历史后,把带代词的问题改写为自包含的独立问题,后续的检索和回答层完全不感知对话历史。

全链路容错 + 模块级缓存 ​

检索链路很长,任何一环出错都不应该让用户感知到。我们的容错策略是:

阶段故障模式降级策略
Dense 检索API 超时静默降级到纯 BM25
RerankAPI 故障静默保留融合排序
上下文组装预算不足二分搜索截断,至少保留 1 个 block

注意"静默"这个词——用户不会看到任何错误提示,系统在后台自动切换到降级路径。

模块级缓存方面,语料和 BM25 索引首次加载后驻留内存,后续请求直接复用。reload() 接口支持热更新,手册内容更新后不需要重启服务。

实际效果 ​

拿案例来看:用户问"空调制冷效果差怎么办?"

  • 查询改写生成 3 个变体
  • 层级检索返回 28 个 small hits(其中 8 个来自 KG 扩展)+ 5 个 mid hits + 3 个 big hits
  • small 上下文 7,883 字符,mid 上下文 2,103 字符,big 上下文 1,304 字符
  • 三层并行回答后 ensemble 融合,最终回答 281 字 + 1 张图片

整个检索耗时 13.7 秒,其中 Dense 向量检索、BM25 检索、RRF 融合、Rerank、层次聚合串行完成。查询改写的三种变体各自独立检索,最终结果合并去重。

这就是 InterX 检索层的全貌——五重分词兜底精确匹配,双通道召回兼顾语义和关键词,领域特化 Rerank 提升排序质量,三级层次聚合从小块精准扩展到大块完整。下一讲我们会聊知识图谱如何进一步扩展跨章节的证据发现能力。