深入实现、巧妙之处、边界与代码地图
本章讲什么: 给要读源码的人。前两章讲了"怎么转",这里讲"为什么这么写、妙在哪、会在哪崩",末尾是可 grep 的代码地图。
1. 深入:PPR 种子权重的三个设计
种子权重(reset 概率)的质量直接决定 PPR 扩散准不准。graph_search_with_fact_entities(HippoRAG.py:1551)在这上面做了三个不显然的处理。
1.1 IDF 式的实体降权
事实分赋给实体节点前,先除以"该实体出现的块数":
# 真实逻辑(HippoRAG.py:1604-1611)
if len(self.ent_node_to_chunk_ids.get(phrase_key, set())) > 0:
weighted_fact_score /= len(self.ent_node_to_chunk_ids[phrase_key])
phrase_weights[phrase_id] += weighted_fact_score
number_of_occurs[phrase_id] += 1
妙在:ent_node_to_chunk_ids 记录每个实体被多少块引用(建图时 add_fact_edges 填的,HippoRAG.py:917-918),相当于文档频率。除以它 = 越普遍的实体越不值钱,把扩散预算留给有区分度的实体。随后还会对同一实体的多条事实分取平均(HippoRAG.py:1615-1625),避免重复计数。
1.2 只保留 top-k 种子,其余清零
算完实体权重,get_top_k_weights(HippoRAG.py:1512)只留下 linking 分最高的 link_top_k 个实体,其余权重强制归零,并有断言把关:
# HippoRAG.py:1548
assert np.count_nonzero(all_phrase_weights) == len(linking_score_map.keys())
目的:种子越集中,PPR 扩散越聚焦,噪声实体不会把游走带偏。
1.3 图信号与向量信号的加权融合
最终 reset 向量 = phrase_weights + passage_weights(HippoRAG.py:1645),而段落权重被 passage_node_weight=0.05 压得很小(HippoRAG.py:1640)。这是一个刻意的主辅配比:
- 实体种子(图联想)是主力,权重量级大;
- DPR 段落分只作"温和先验",防止图完全脱离字面相关性。
还有个硬断言 assert sum(node_weights) > 0(HippoRAG.py:1651):若所有事实的实体都不在图里,宁可报错也不给出无意义的扩散。
2. 巧妙之处(可借鉴的技术)
| 妙在哪 | 一句话 | 依据 |
|---|---|---|
| 内容哈希做 id | 文本 MD5 当节点 id,天然去重、天然幂等,增量/删除都靠它精准定位 | compute_mdhash_id(utils/misc_utils.py:141) |
| 事实是边不是节点 | 图只有实体+段落两类节点,三元组化成实体间的边,图更小、PPR 更快 | add_fact_edges(HippoRAG.py:872) |
| 大模型改写后模糊还原 | 识别记忆里大模型可能改写事实文本,用 difflib 匹配回原候选,不丢索引 | rerank.py:122-125 |
| 降级即安全网 | 无相关事实时退回纯 DPR,保证不弱于普通 RAG | HippoRAG.py:472-474 |
| 增量只算新旧之间的同义边 | 同义边只在"新实体×全体"间建,增量成本不随图爆炸 | HippoRAG.py:990 注释 + add_synonymy_edges |
| 两套查询指令 | 同一问题分别按"对事实"" |