第 9 章 · 01 HybridSearch 混合检索与 GraphRAG


第 9 章 · 01 HybridSearch 混合检索与 GraphRAG

本节摘要:存储就位,本节装配其上的检索层。先看 embeddings/(11 个文件):EmbeddingGenerator 统一嵌入口,pooling_strategies 提供五种池化,GraphEmbeddingManager 补上图结构嵌入;再拆 hybrid_search.py 的 MetadataFilter 过滤与核心算法 RRF(Reciprocal Rank Fusion)——多路召回按 1/(k+rank) 融合排序;然后沿 docs/guides/graphrag.md 装配 GraphRAG:向量检索命中种子实体→多跳图扩展带出关联子图→上下文增强;最后读 context_retriever.py:1487query_with_reasoning——返回答案之外还返回可审计的推理路径。收尾对比纯向量 RAG 的盲区。

内容来源:原项目源码 semantica/vector_store/hybrid_search.pysemantica/embeddings/pooling_strategies.pygraph_embedding_manager.pyembedding_generator.pysemantica/context/context_retriever.pydocs/guides/graphrag.md

⚠️ 注意:GraphRAG 的多跳扩展会指数级放大上下文——graphrag.md 的 Info 框明确警告"hop 数越大上下文与 token 越多,从 2–3 跳起步并监控体量";max_expansion_hops=3(actor→基础设施→受害者→行业)已能跨四份文档带出证据链。检索质量也强依赖图谱质量:实体抽取差、去重不净,图扩展就会带跑答案。

学习目标

阅读完本节,你应当能够:

  1. 说出 embeddings 模块三件套(生成/池化/图嵌入)各自的职责与代表类。
  2. 手推 RRF 公式 score = Σ 1/(k + rank),解释 k=60 的作用与"排名可比、分数不可比"的动机。
  3. 配出 GraphRAG 三件套:graph_expansion/max_expansion_hops/hybrid_alpha,说出 anchor_nodeproximity_weight 的用途。
  4. 复述 query_with_reasoning 的五步流水线、返回字典六字段与置信度算式。
  5. 讲清 GraphRAG 相对纯向量 RAG 的三点优势与两个代价。

一、embeddings:向量从哪来

semantica/embeddings/ 三件套。其一,生成EmbeddingGenerator(embedding_generator.py:34)是统一门面——set_text_model(method, model_name)(93 行)换模型、get_methods_info(119 行)报告当前方法栈、generate_embeddings(135 行)支持多种 data_type、compare_embeddings(217 行)内置余弦/欧氏相似度(241/252 行两个私有实现)。混合检索里它负责把查询字符串变成查询向量。

其二,池化:pooling_strategies.py 一文件五策略,工厂按名创建(173-191 行):

189 "mean": MeanPooling, 190 "max": MaxPooling, 191 "cls": CLSPooling,

MeanPooling.pool 取 token 向量均值(54 行 np.mean(embeddings, axis=0))、MaxPooling 逐维取最大(74 行)、CLSPooling 取 [CLS] 位、AttentionPooling(97 行)以均值向量作 query 打分后 softmax 加权——125 行 scores - np.max(scores) 是教科书级的数值稳定处理、HierarchicalPooling(134 行)分段池化再聚合。池化决定"一段文本变成一个向量"的方式,直接框定检索质量上限。

其三,图嵌入:GraphEmbeddingManager(graph_embedding_manager.py:29)把图谱拓扑也向量化——prepare_for_graph_db(67 行)为落库打包、embed_entities(160 行)嵌入实体、embed_relationships(230 行)嵌入关系、create_node_embeddings/create_edge_embeddings(308/331 行)批量产节点与边向量。纯文本嵌入看不见拓扑,图嵌入补上"这个实体在图里处于什么位置"的信息——与第 3 章 kg/node_embeddings 同源。

二、HybridSearch:先过滤再召回,多路结果再融合

HybridSearch.search(hybrid_search.py:276)构造时就定下融合策略(270-273 行):

271 self.ranker = SearchRanker( 272 config.get("ranking_strategy", "reciprocal_rank_fusion") 273 )

本地路径流水线:字符串查询先过 EmbeddingGenerator 生成查询向量(352-370 行,2D 结果会取第一行);MetadataFilter 先行过滤——链式条件 API(69-99 行)eq/ne/gt/gte/lt/lte/contains/in_list 逐个往 conditions 追加字典,matches(101 行)对每条元数据求值;过滤后的候选集做向量检索,且故意召回 k * 2(452 行注释:Get more results for ranking)给排序器留余量,最后截回 top-k。若传了 vector_store 则走后端委托路径,结果统一归一化成 {id, score, distance, metadata} 四键形状——distance 缺失置 None 而非拿 score 反推,注释特意说明各后端度量(L2/内积/余弦)语义不同,猜了就是胡说。还有两处防御性细节(313-331 行):遗留参数 top_k 必须 pop 而非只读,否则转发给 sqlite/pgvector 后端时会与显式 top_k=k 撞名报"got multiple values"。

真正的多路融合在 SearchRanker(140 行)。RRF 实现(hybrid_search.py:148-197):

168 scores: Dict[str, float] = {} 170 for result_list in results: 171 for rank, result in enumerate(result_list, start=1): 173 result_id = result.get("id", str(id(result))) 174 score = 1.0 / (k + rank) 175 scores[result_id] = scores.get(result_id, 0.0) + score 177 # Sort by score 178 ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)

公式 score = Σ 1/(k + rank)(k 默认 60):每路结果只贡献排名不贡献原始分——排第 1 得 1/61、第 2 得 1/62……同一文档出现在多路时分数累加,两路都靠前的自然浮顶。RRF 的妙处是回避了不同检索通道分数量纲不可比:向量余弦 0.87 与 BM25 分 12.3 没法直接相加,排名却天然可比。186-197 行按融合分排序重建结果,result["score"] 被覆写为 RRF 分。备选策略 weighted_average(188 行起)保留加权分数融合,权重数不匹配时自动均摊(1.0/len(results));rank(227 行)按策略名分发,未知策略回落 RRF。multi_source_search(530 行)把同一查询打到多个向量源再统一融合——联邦检索的雏形。

三、GraphRAG:向量命中种子,图扩展带出关联

docs/guides/graphrag.md(584 行)给出完整配方,标准工作流六步:Ingest→Build Graph→Retrieve→Expand Context→Reason→Answer。第一步装配三对象:

vs = VectorStore(backend="faiss", dimension=768) graph = ContextGraph(advanced_analytics=True) context = AgentContext( vector_store=vs, knowledge_graph=graph, graph_expansion=True, # enable multi-hop traversal from seed nodes max_expansion_hops=3, # APT29 → infrastructure → victim → sector is 3 hops hybrid_alpha=0.6, # 60% graph influence, 40% vector similarity decision_tracking=True, # record analyst queries as auditable decisions )

guide 强调:GraphRAG 没有独立开关——传了 knowledge_graph= 即自动激活,hybrid_alpha 控制图结构与向量相似度的配比。第二步 context.store(..., extract_entities=True, extract_relationships=True, link_entities=True) 写向量的同时跑 NER/关系抽取/实体链接把图建起来。第三步检索:

results = context.retrieve( "APT29 tactics against healthcare", use_graph=True, max_results=10, expand_graph=True, max_hops=3, )

机制是向量检索命中 top-k 文档→取其中实体作种子沿边多跳扩展→带出结构关联的事实。guide 用威胁情报例子说明价值:APT29 → HAMMERTOSS → LifeCare → 医疗行业这条三跳链横跨四份文档,纯向量检索可能因关键词不重叠漏掉后几环,图扩展却把它们顶上来——返回的 score 是"向量相关度与图连通性的透明混合"。已知锚点时还可显式指定遍历起点并加权邻近:

apt29_intel = context.retrieve( "C2 infrastructure beaconing patterns", use_graph=True, anchor_node="APT29", proximity_weight=0.7, # strongly favour nodes close to APT29 max_hops=3, max_results=8, )

四、query_with_reasoning:答案 + 可审计推理路径

ContextRetriever.query_with_reasoning(context_retriever.py:1487)在 retrieve 之上走五步(1545-1610 行):

1546 retrieved_context = self.retrieve( 1547 query, max_results=max_results, 1547 use_graph_expansion=True, **kwargs) ... 1560 for ctx in retrieved_context: 1561 query_entities.extend(ctx.related_entities) ... 1572 reasoning_paths = self._build_reasoning_path( 1572 unique_entities, max_hops=max_hops) ... 1585 response = self._generate_reasoned_response( 1585 query, retrieved_context, reasoning_paths, llm_provider, ...) ... 1598 path_parts.append(entity_name) 1599 if i < len(relationships) and relationships[i].get("type"): 1600 path_parts.append(f"--[{relationships[i]['type']}]-->")

检索(带图扩展)→ 收集上下文关联实体并按 id 去重(1565-1569 行)→ 沿图构建推理路径 → 把"上下文 + 路径"喂给 LLM 生成答案 → 路径格式化成 实体A --[关系]--> 实体B 的可读链(取第一条路径展示)。签名里还有时态钩子:at_time 参数接受 datetime 或 ISO 串,给 LLM 上下文块前置 [Graph context valid as of: ...] 结构化时间头(1493-1496 行)——第 7 章双时态在检索出口的回响。返回字典六字段(1631-1638 行):response/reasoning_path/sources(content 截 200 字)/confidence/num_sources/num_reasoning_paths。置信度是透明算式(1618-1621 行):

1618 confidence = 0.0 1619 if retrieved_context: 1620 avg_score = sum(ctx.score for ctx in retrieved_context) / len(...) 1621 confidence = min(1.0, avg_score * 0.8 + (0.2 if reasoning_paths else 0.0))

上下文平均分占八成,有推理路径再加 0.2。LLM 失败也不空手而归:except 分支(1641-1657 行)退回"只给检索结果不给生成"的降级响应,confidence 兜底 0.5。guide 点破要害:reasoning_path 是 GraphRAG 与黑盒 LLM 调用的分界线——分析师问"你怎么知道 APT29 针对医疗?",你展示的是系统在自己文档上走过的确切遍历,不是模型训练记忆里的断言。

五、与纯向量 RAG 的对比

维度 纯向量 RAG GraphRAG
命中依据 文本语义相似 语义相似 + 实体关系连通
多跳事实 跨文档关联常因词面不重叠而漏 三跳内证据链自动带出
可解释性 "相似度高"一个分数 reasoning_path 给出实体级遍历轨迹
代价 一次向量查询 图遍历开销 + token 放大 + 依赖图谱质量

guide 也如实划界:话题相似检索、单文档问答、实体关系稀疏的领域,纯向量检索足够,GraphRAG 反而过度设计;实时性苛刻的场景也要掂量遍历延迟。装配判断式就一句话——问题是否需要"跨文档连接事实":需要,装图;不需要,省下复杂度。

💡 装配要点:检索层的三级装配——①嵌入口统一走 EmbeddingGenerator,池化按领域换(长文档先试 hierarchical/attention);②多路召回用 RRF 融合(1/(k+rank),k=60),永远召回 k×2 给排序留余量,分数不可比时永远别加权平均;③有图谱就开 graph_expansion(2–3 跳起步),要可审计就上 query_with_reasoning——它把"答案从哪来"变成返回值的一部分。

本节要点回顾

  1. embeddings 三件套:EmbeddingGenerator 统一嵌入口;五种池化(mean/max/cls/attention/hierarchical)经工厂按名创建;GraphEmbeddingManager 把实体/关系/节点拓扑向量化。
  2. HybridSearch 流水线:查询向量化→MetadataFilter 链式过滤→k×2 召回→SearchRanker 融合截 top-k;后端结果归一化四键形状,遗留 top_k 参数要 pop 防撞名。
  3. RRFscore = Σ 1/(k + rank),k=60;只比排名不比分值,天然规避多通道量纲问题;weighted_average 是备选,multi_source_search 做联邦多源。
  4. GraphRAG 配置knowledge_graph= 即激活;max_expansion_hops=3hybrid_alpha=0.6anchor_node + proximity_weight 精控遍历。
  5. query_with_reasoning:五步流水线产出六字段;reasoning_path 形如 A --[rel]--> B;置信度 = 平均分×0.8 + 有路径 0.2;at_time 带出双时态时间头;LLM 失败降级纯检索。
  6. 边界:多跳连接与可解释是 GraphRAG 的价值;上下文指数放大与图谱质量依赖是它的代价。

下一节:检索解决"查得准",02 节解决"存得对"——本体工程四件事(OWL 生成、SHACL 约束验证、SKOS 词表、本体评估器)守住语义质量,再用 export 的 11 种格式把成果交付出去。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U