70/30 Hybrid Retrieval 和 Graph Anchor 怎样权衡

Vector、Keyword、Parent-Child Context 与 Verified Graph Path 分层组合,避免相似内容被误当成调用证明。

本页目录 · 11 节

工程查询同时包含两种语言:人会问「为什么刷新后仍读到旧值」,代码里却是 refreshProjectionreadMaterializedView 和 GraphQL Field。只做 Dense Retrieval 容易错过精确 Symbol;只做 Keyword Search 又跨不过中文业务描述与英文实现之间的表达差异。

System RAG Shadow 的第一轮召回把 Vector 和 Keyword 分成两条 Lane,再用 Weighted Reciprocal Rank Fusion 合并;融合结果提供 Graph Anchor,Neo4j 只负责补充有 Evidence 的关系路径。

本篇知识地图

flowchart TB
  Q["Query"] --> D["Dense Retrieval<br/>semantic candidates"]
  Q --> K["FTS + Exact Identifier<br/>lexical candidates"]
  K --> X["Exact candidate real dense score completion"]
  D --> R["Weighted RRF 70/30"]
  X --> R
  R --> T["Temporal Rerank"]
  T --> P["Child hit -> Parent context"]
  P --> A["Top-hit graph_ids"]
  A --> G["Neo4j Verified Path"]
  G --> E["Evidence Bundle"]

需要理解的知识点包括 Dense/Sparse Retrieval、RRF、Exact Candidate Completion、Parent/Child Hydration、Temporal Rerank、Graph Anchoring 和 Retrieval Evaluation。

70/30 合并的是 Rank,不是原始 Score

Cosine Similarity、BM25/FTS Rank 和 Exact Match Score 不在同一尺度。直接做:

0.7 * cosine + 0.3 * bm25

没有稳定意义,因为 BM25 的数值范围会随 Corpus、Analyzer 和 Query 改变。System Shadow 使用:

score(d) = 0.70 / (k + dense_rank(d))
         + 0.30 / (k + keyword_rank(d))

当前 Index Manifest 要求 k >= 1,配置值为 140。RRF 只关心每条 Lane 内的顺序,从而降低不同分数系统不可比的问题。

70/30 是当前 System Shadow 的版本化检索合同,不是所有路径的普适结论。 默认外部 /v1/answer 仍保留自己的 Legacy/Hybrid 开关与 Exact Quota 逻辑;没有 Cutover 前,不能把 Shadow 的公式描述成所有线上查询都已采用。权重也必须通过企业 Query Set 评估,而不是因为数字整齐就永久固定。

Exact Identifier 属于 Keyword Lane,不是第三份权重

ISSUE-1234RebuildProjectionJob 和一个完整 File Path 的身份价值高于一般 Token。System Keyword Index 先查 Exact Alias,再查 FTS;两者共同形成 Keyword Rank,仍然只占 30%。

一个难点是:精确候选可能不在 Dense Top-K。如果直接把它塞进最终结果,就等于绕过 70/30;如果给它伪造一个 Dense Rank,又会夸大语义相关性。

当前实现向 Qdrant 按 Point ID 请求这些候选的真实 Cosine Score,把它们放回 Dense 排序,再执行 RRF。这叫 Exact Candidate Completion:保证精确对象有参赛资格,但不修改比赛规则。

Parent/Child 解决「命中准」和「读得懂」的冲突

小 Chunk 有利于命中一个 Method、Heading 或 Table Row,但直接交给回答模型容易缺上下文;大 Chunk 上下文完整,却可能把多个主题平均进同一个向量。

当前做法是:

  1. Child 作为精确召回单位;
  2. Payload 保留 parent_id
  3. 命中后从 Keyword DB 加载 Parent Text;
  4. Parent 只作为 Context,不凭 Hydration 获得额外 Rank;
  5. External Answer Path 还可加入前后 Parent Neighbor,但标为 context_only

这种结构比固定 Top-K 多一次读取,却避免「因为扩大上下文而重复计分」。Parent/Child 也不能修复错误切块:如果 Parser 已丢掉 Table Header 或 Code Symbol,回溯 Parent 仍然无法还原证据,因此它依赖前一章的 Typed IR。

Temporal Rerank:相关不等于当前

RRF 之后,System Shadow 再做 Source-specific Temporal Rerank:

  • Confluence 在 Query Time 用 Effective Updated Timestamp 重算 Freshness,Multiplier 为 0.5 + 0.5 * freshness
  • Code 依据 currentness_valid,Current 使用 1.0,非 Current 使用 0.25;
  • 其他 Source 默认不应用同一种时间衰减。

时间信号只调整排序,不进入 Chunk/Embedding Identity。一个旧页面仍可作为历史证据,只是不能轻易压过当前代码;一个当前性未知的 Code Hit 可以帮助定位,不能成为高 Confidence 实现结论。

Graph Expansion 从哪个 Anchor 开始

Vector/Keyword 召回的是相关实体,Neo4j 查询的是实体间关系。如果从所有 Top-K 候选同时扩图,系统很容易从次要候选借一条真实路径,拼到主要答案上。

System Shadow 的 Controller 只使用第一个拥有 graph_ids 的最高排名 Hit作为 Graph Root,并只取少量相关 Graph ID。Graph Query 再限制 Relation Allowlist、方向、Hop 和 Evidence。Incoming Query(例如「谁调用它」)使用反向 Pattern,其他查询默认 Outgoing。

默认 External API 是另一条更通用的 Query Runtime:Rewrite 后首轮 Graph Depth 1 与 Vector/Keyword 并行;Controller 根据复杂度决定是否执行唯一一次第二轮和 1–3 Depth 扩展。System Shadow search_verified_paths 当前允许最多 4 Hop。两条路径都强调 Bounded Search,但具体上限不同。

Context Compression 为什么不是越多越好

External Runtime 在候选较多时可启用 Compression,只保留 Query Similarity 高于配置阈值的 Vector/Keyword Candidate;Graph Evidence 由 Graph-priority Policy 保护,并保留 Verified/Candidate 标签。

更多 Candidate 会提高 Recall 上限,也会增加 Rerank 延迟、Prompt 长度和无关证据拼接机会。「Lost in the Middle」一类研究也说明长上下文并不保证模型有效利用中间信息。因此当前默认 Rerank 先用本地规则压到数十条,再做一次 Codex Rerank,最多保留十几条;深度 Batch Rerank 只用于专项排查。

怎样评估,而不是只看一次漂亮答案

Hybrid Retrieval 至少要把 Query 分组:

Query 类别 主要指标 常见失败
Exact Symbol/Issue Key MRR、Top-1 Exact、Repo/Commit Match Dense 把相似类名排在真正 Identifier 前
中文业务描述 → 英文代码 Recall@K、Verified Anchor Coverage Keyword 无跨语言能力
跨仓库调用链 Verified Path Coverage、False Path Rate 多 Anchor 拼出偏题真路径
历史设计查询 Citation Coverage、Freshness Calibration 旧文档被一律丢弃或一律置顶
无答案问题 Insufficient Precision 检索总能找到「像答案」的文本

Recall、Rerank 和 Answer Faithfulness 要分开测。最终回答正确不代表 Retriever 找得好,Retriever 找到正确页面也不代表 Generator 引用完整。

失败模式与取舍

失败模式 当前策略 Trade-off
Dense 与 FTS Score 不可比 Weighted RRF 按 Rank 融合 丢失原始 Score 间距信息
Exact Hit 不在 Dense Top-K 查询真实 Dense Score 后补入 多一次 Qdrant Point Scoring
Child 太短 Hydrate Parent/Neighbor Context 增加读取量,但不额外计分
Top-K Anchor 过多 只从最高排名实体扩图 Precision 提高,可能漏掉次要真实分支
Graph Depth 太深 External 1–3,Shadow Bounded Hop 牺牲部分长链 Recall,控制组合爆炸
旧文档干扰 Source-specific Temporal Rerank Freshness 参数需要离线校准
Candidate 太多 Compression + Prefilter + Single Rerank 过强阈值可能误删弱信号

可复核的项目证据

  • src/mt_rag/retrieval/ranking.pyHybridWeights、Weighted RRF 与 Temporal Rerank;
  • src/mt_rag/retrieval/system_index.py:Exact Dense Completion、Keyword DB、Parent Context 和 System Hybrid Contract;
  • src/mt_rag/retrieval/hybrid.py:Exact Terms、Lexical Token 与 External Hybrid/Quota Helper;
  • src/mt_rag/query/controller.py:最高排名 Hit Anchor 与两轮上限;
  • src/mt_rag/system_graph/query.py:Verified Path、Direction 与 Hop Bound;
  • config/rag-runtime.json:Shadow 70/30、RRF K、Compression 和 External Controller Depth;
  • docs/02-runbooks/local-llm-service.md:External Parallel Search、Context-only Neighbor 与 Rerank Strategy。

参考资料

Hybrid Retrieval 的目标不是让每条 Lane 都「赢一点」,而是让自然语言、精确身份和关系证据各做自己擅长的事,并在每一次融合处保留可解释边界。

相关文章

在做类似的事情?

很乐意就分布式系统、交付流程和应用 AI 交换意见。

[email protected]