70/30 Hybrid Retrieval 和 Graph Anchor 怎样权衡
Vector、Keyword、Parent-Child Context 与 Verified Graph Path 分层组合,避免相似内容被误当成调用证明。
本页目录 · 11 节
工程查询同时包含两种语言:人会问「为什么刷新后仍读到旧值」,代码里却是 refreshProjection、readMaterializedView 和 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-1234、RebuildProjectionJob 和一个完整 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 上下文完整,却可能把多个主题平均进同一个向量。
当前做法是:
- Child 作为精确召回单位;
- Payload 保留
parent_id; - 命中后从 Keyword DB 加载 Parent Text;
- Parent 只作为 Context,不凭 Hydration 获得额外 Rank;
- 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.py:HybridWeights、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。
参考资料
- Cormack、Clarke、Büttcher 的 Reciprocal Rank Fusion 原始论文;
- Qdrant Hybrid Queries 官方文档;
- Qdrant Hybrid Search + Reranking 官方教程;
- Nogueira 与 Cho 的 Passage Re-ranking with BERT;
- Lost in the Middle;
- Azure AI Search Parent-child Index Projection;
- Microsoft GraphRAG Local Search。
Hybrid Retrieval 的目标不是让每条 Lane 都「赢一点」,而是让自然语言、精确身份和关系证据各做自己擅长的事,并在每一次融合处保留可解释边界。
相关文章
为什么 Vector、Keyword 和 Neo4j 要使用不同存储
语义相似、精确标识符和关系遍历是三种查询问题,强行放入一个数据库会牺牲正确性与可解释性。
RAG 不是一条必经流水线,而是一种按需取证的能力
先理解目标、先看当前源码,只有跨来源信息真的能改善判断时,才调用 Local RAG 去找证据候选。