本节摘要:存储就位,本节装配其上的检索层。先看
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:1487的 query_with_reasoning——返回答案之外还返回可审计的推理路径。收尾对比纯向量 RAG 的盲区。
内容来源:原项目源码
semantica/vector_store/hybrid_search.py、semantica/embeddings/pooling_strategies.py、graph_embedding_manager.py、embedding_generator.py、semantica/context/context_retriever.py、docs/guides/graphrag.md。
⚠️ 注意:GraphRAG 的多跳扩展会指数级放大上下文——graphrag.md 的 Info 框明确警告"hop 数越大上下文与 token 越多,从 2–3 跳起步并监控体量";
max_expansion_hops=3(actor→基础设施→受害者→行业)已能跨四份文档带出证据链。检索质量也强依赖图谱质量:实体抽取差、去重不净,图扩展就会带跑答案。
阅读完本节,你应当能够:
score = Σ 1/(k + rank),解释 k=60 的作用与"排名可比、分数不可比"的动机。graph_expansion/max_expansion_hops/hybrid_alpha,说出 anchor_node 与 proximity_weight 的用途。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.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 行)把同一查询打到多个向量源再统一融合——联邦检索的雏形。
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, )
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 | GraphRAG |
|---|---|---|
| 命中依据 | 文本语义相似 | 语义相似 + 实体关系连通 |
| 多跳事实 | 跨文档关联常因词面不重叠而漏 | 三跳内证据链自动带出 |
| 可解释性 | "相似度高"一个分数 | reasoning_path 给出实体级遍历轨迹 |
| 代价 | 一次向量查询 | 图遍历开销 + token 放大 + 依赖图谱质量 |
guide 也如实划界:话题相似检索、单文档问答、实体关系稀疏的领域,纯向量检索足够,GraphRAG 反而过度设计;实时性苛刻的场景也要掂量遍历延迟。装配判断式就一句话——问题是否需要"跨文档连接事实":需要,装图;不需要,省下复杂度。
💡 装配要点:检索层的三级装配——①嵌入口统一走
EmbeddingGenerator,池化按领域换(长文档先试 hierarchical/attention);②多路召回用 RRF 融合(1/(k+rank),k=60),永远召回 k×2 给排序留余量,分数不可比时永远别加权平均;③有图谱就开graph_expansion(2–3 跳起步),要可审计就上query_with_reasoning——它把"答案从哪来"变成返回值的一部分。
score = Σ 1/(k + rank),k=60;只比排名不比分值,天然规避多通道量纲问题;weighted_average 是备选,multi_source_search 做联邦多源。knowledge_graph= 即激活;max_expansion_hops=3、hybrid_alpha=0.6、anchor_node + proximity_weight 精控遍历。reasoning_path 形如 A --[rel]--> B;置信度 = 平均分×0.8 + 有路径 0.2;at_time 带出双时态时间头;LLM 失败降级纯检索。下一节:检索解决"查得准",02 节解决"存得对"——本体工程四件事(OWL 生成、SHACL 约束验证、SKOS 词表、本体评估器)守住语义质量,再用 export 的 11 种格式把成果交付出去。