数据截至 (上游 commit c617143f0147)
深入实现、巧妙之处、边界与代码地图
本章讲什么: 给要读源码的人。前两章讲了"怎么转",这里讲"为什么这么写、妙在哪、会在哪崩",末尾是可 grep 的代码地图。
1. 深入:PPR 种子权重的三个设计
种子权重(reset 概率)的质量直接决定 PPR 扩散准不准。graph_search_with_fact_entities(HippoRAG.py:1544)在这上面做了三个不显然的处理。
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:912-913),相当于文档频率。除以它 = 越普遍的实体越不值钱,把扩散预算留给有区分度的实体。随后还会对同一实体的多条事实分取平均(HippoRAG.py:1608-1618),避免重复计数。
1.2 只保留 top-k 种子,其余清零
算完实体权重,get_top_k_weights(HippoRAG.py:1505)只留下 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:1638),而段落权重被 passage_node_weight=0.05 压得很小(HippoRAG.py:1633)。这是一个刻意的主辅配比:
- 实体种子(图联想)是主力,权重量级大;
- DPR 段落分只作"温和先验",防止图完全脱离字面相关性。
还有个硬断言 assert sum(node_weights) > 0(HippoRAG.py:1644):若所有事实的实体都不在图里,宁可报错也不给出无意义的扩散。
2. 巧妙之处(可借鉴的技术)
| 妙在哪 | 一句话 | 依据 |
|---|---|---|
| 内容哈希做 id | 文本 MD5 当节点 id,天然去重、天然幂等,增量/删除都靠它精准定位 | compute_mdhash_id(utils/misc_utils.py:141) |
| 事实是边不是节点 | 图只有实体+段落两类节点,三元组化成实体间的边,图更小、PPR 更快 | add_fact_edges(HippoRAG.py:867) |
| 大模型改写后模糊还原 | 识别记忆里大模型可能改写事实文本,用 difflib 匹配回原候选,不丢索引 | rerank.py:122-125 |
| 降级即安全网 | 无相关事实时退回纯 DPR,保证不弱于普通 RAG | HippoRAG.py:467-469 |
| 增量只算新旧之间的同义边 | 同义边只在"新实体×全体"间建,增量成本不随图爆炸 | HippoRAG.py:985 注释 + add_synonymy_edges |
| 两套查询指令 | 同一问题分别按"对事实""对段落"编码,各走各的相似度通道 | prompts/linking.py + get_query_embeddings |
| OpenIE 结果落盘复用 | 最贵的抽取步按 chunk 哈希缓存,重复实验不重抽 | load_existing_openie(HippoRAG.py:1022) |
3. 边界与局限(诚实)
- 建索引依赖大模型质量。 OpenIE 抽错三元组,图就带噪;谓语不进节点,关系表达全压在实体和边上。抽取用
temperature=0(config_utils.py:50)求稳定,但幻觉/漏抽仍是根因误差。 - 同义边治标不治本。 阈值
0.8的余弦相似度做实体缝合(config_utils.py:172),偏保守;跨语言、缩写、别名仍可能割裂成孤立节点,多跳链因此断裂。 - 默认不切块。
TextPreprocessor默认"一篇文档=一块"(preprocessing.py:15-27),长文档会让单次 OpenIE 上下文过载、抽取变差——README 也提示长段落需自行切块。 - PPR 全图跑。
personalized_pagerank在整张图上算(HippoRAG.py:1736),语料极大时在线延迟随图规模上升;项目靠"种子集中 + prpack 实现"缓解,但没有子图裁剪。 - 识别记忆每查一次大模型。 每个 query 的事实过滤都要调一次 LLM(
rerank.py:95),是在线延迟和成本的主要来 源之一。 eval()解析事实字符串。 候选事实内容还原成元组:检索主路径HippoRAG.py:1693仍用eval,rerank 侧已换成安全版ast.literal_eval(rerank.py:125);来源是自建向量库、可控,但对不可信语料要留意。- 图节点计数一致性靠运行时修补。
prepare_retrieval_objects(HippoRAG.py:1287)大量 warning + 重建逻辑,暗示图与向量库可能不同步,需要防御式对齐。
4. 横向对比(rag-context 货架)
三种典型 RAG 的取舍:
| 维度 | 普通向量 RAG(DPR) | GraphRAG(社区摘要类) | HippoRAG 2 |
|---|---|---|---|
| 索引产物 | 段落向量 | 实体图 + LLM 生成的社区摘要 | 实体图 + 段落向量(事实是边) |
| 多跳能力 | 弱(只看字面相似) | 强,但靠离线 LLM 摘要,很贵 | 强,靠在线 PPR 扩散 |
| 离线建图成本 | 最低 | 高(大量 LLM 摘要调用) | 中(OpenIE + 便宜的图算法) |
| 在线延迟 | 最低 | 中/高 | 中(多一次事实过滤 + PPR) |
| 简单任务 | 好 | 可能过度设计 | 好(有 DPR 降级兜底) |
| 增量更新 | 易 | 难(社区要重算) | 易(哈希 id + 局部同义边) |
一句话定位:HippoRAG 2 想在"GraphRAG 的多跳能力"和"普通 RAG 的低成本/低延迟"之间取中间点——把"联想"从离线 LLM 摘要挪到了在线的图算法(PPR),从而离线更便宜、在线仍可控。
延伸阅读:本子库 index.md 的顶层全景;同货架其他 rag-context 子库可对照"检索信号如何融合"这一共同关切。
5. 代码地图(导航索引)
可按 symbol grep 定位(行号会漂,符号名稳)。
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 顶层门面 / 初始化 | src/hipporag/HippoRAG.py | HippoRAG.__init__ |
| 建索引主流程 | src/hipporag/HippoRAG.py | HippoRAG.index |
| 事实边(实体↔实体,共现权重) | src/hipporag/HippoRAG.py | HippoRAG.add_fact_edges |
| 段落边(段落→实体) | src/hipporag/HippoRAG.py | HippoRAG.add_passage_edges |
| 同义边(KNN 缝合近义实体) | src/hipporag/HippoRAG.py | HippoRAG.add_synonymy_edges |
| 灌边入 igraph | src/hipporag/HippoRAG.py | HippoRAG.add_new_edges |
| 在线检索主循环 | src/hipporag/HippoRAG.py | HippoRAG.retrieve |
| 事实打分 | src/hipporag/HippoRAG.py | HippoRAG.get_fact_scores |
| 事实候选 + 识别记忆入口 | src/hipporag/HippoRAG.py | HippoRAG.rerank_facts |
| 种子权重 + PPR 调度 | src/hipporag/HippoRAG.py | HippoRAG.graph_search_with_fact_entities |
| top-k 种子筛选 | src/hipporag/HippoRAG.py | HippoRAG.get_top_k_weights |
| 个性化 PageRank | src/hipporag/HippoRAG.py | HippoRAG.run_ppr |
| 纯向量检索(降级/融合) | src/hipporag/HippoRAG.py | HippoRAG.dense_passage_retrieval |
| 检索对象预热/一致性修补 | src/hipporag/HippoRAG.py | HippoRAG.prepare_retrieval_objects |
| 删除文档 | src/hipporag/HippoRAG.py | HippoRAG.delete |
| QA 读段落生成答案 | src/hipporag/HippoRAG.py | HippoRAG.qa |
| IRCoT 多步检索 | src/hipporag/HippoRAG.py | HippoRAG.retrieve_ircot |
| OpenIE:NER + 三元组 | src/hipporag/information_extraction/openie_openai.py | OpenIE.ner · OpenIE.triple_extraction · OpenIE.batch_openie |
| 识别记忆过滤器 | src/hipporag/rerank.py | DSPyFilter.rerank · DSPyFilter.parse_filter |
| 识别记忆提示 | src/hipporag/prompts/filter_default_prompt.py | best_dspy_prompt |
| 查询指令(对事实/对段落) | src/hipporag/prompts/linking.py | get_query_instruction |
| NER / 三元组提示模板 | src/hipporag/prompts/templates/ner.py · triple_extraction.py | prompt_template |
| 向量库(默认 Parquet)+ 工厂 | src/hipporag/embedding_store.py | EmbeddingStore · get_embedding_store |
| 批量 KNN(建同义边) | src/hipporag/utils/embed_utils.py | retrieve_knn |
| 哈希 id / 三元组工具 | src/hipporag/utils/misc_utils.py | compute_mdhash_id · extract_entity_nodes · flatten_facts · min_max_normalize |
| 全局配置(所有超参) | src/hipporag/utils/config_utils.py | BaseConfig |
| 实验入口 | main.py | main |
6. 关键超参速查
| 参数 | 默认 | 作用 | 位置 |
|---|---|---|---|
linking_top_k | 5 | 候选事实数 / 保留的种子实体数 | config_utils.py:184 |
retrieval_top_k | 200 | 每次检索返回的段落数 | config_utils.py:188 |
qa_top_k | 5 | 喂给 QA 大模型的段落数 | config_utils.py:203 |
damping | 0.5 | PPR 阻尼(继续游走 vs 跳回种子) | config_utils.py:192 |
passage_node_weight | 0.05 | DPR 段落分在种子里的权重(辅信号) | config_utils.py:91 |
synonymy_edge_sim_threshold | 0.8 | 建同义边的余弦相似度阈值 | config_utils.py:172 |
synonymy_edge_topk | 2047 | 同义边 KNN 的 k | config_utils.py:160 |
is_directed_graph | False | 图是否有向(默认无向) | config_utils.py:176 |
temperature | 0 | OpenIE / 过滤的采样温度 | config_utils.py:50 |
读到这里,你应该能对着源码复述 HippoRAG 2 的全貌了:离线抽三元组建图,在线用事实种子 + PPR 扩散做多跳检索,配 DPR 降级兜底。