联邦知识图谱问答:垂直分区下多跳推理的隐私保护实现 当业务数据分散在不同机构、各自的知识图谱“各管一段”时如何在不暴露原始数据的前提下完成跨图谱的多跳问答这是联邦知识图谱问答要解决的核心问题。本文围绕 FedV-KGQA 这一典型思路拆解垂直分区场景下多跳问答的原理、流程与实现要点并给出可运行的模拟示例适合关注联邦学习、知识图谱与问答系统的同学收藏。1. 背景知识图谱问答与数据孤岛1.1 从知识图谱到多跳问答知识图谱Knowledge Graph本质上是一种用图结构描述实体及其关系的语义网络节点是实体边是关系。例如“张三-患病-糖尿病”“糖尿病-可用药-二甲双胍”这两条三元组就可以构成一个小型医疗知识图谱。多跳问答Multi-Hop Question Answering指的是回答一个问题时不能只凭一条三元组直接命中答案而需要在图谱上沿着边连续走多步。比如“张三该用什么药”推理链条是张三 --患病-- 糖尿病 --可用药-- 二甲双胍这中间经过了两个跳hop第一跳找到疾病第二跳找到药物。多跳问答的关键能力就是自动发现并组合出这条推理路径。单机场景下整个知识图谱都在本地路径搜索并不复杂。但现实情况往往没有这么理想知识图谱经常分散在不同的组织或业务系统中彼此之间无法直接共享全部数据。1.2 垂直分区数据在“列”上被切开联邦学习中有两种常见的数据切分方式水平分区Horizontal Partitioning每个参与方拥有相同特征的不同样本比如两家医院都有“患者-诊断”数据但患者群体不同。垂直分区Vertical Partitioning每个参与方拥有相同样本的不同特征或不同关系比如医院A有“患者-疾病”关系医院B有“疾病-药物”关系两份数据覆盖的实体域有交叉但边的关系类型不同。FedV-KGQA 中的 V 就是 Vertically Partitioned它专门研究的是知识图谱被按关系维度纵向切分到多个参与方时如何协作完成多跳问答。1.3 为什么传统方案不够用如果直接把两个知识图谱合并到一个中心节点再查询数据提供方需要把原始图谱全部上传这在数据隐私、商业机密、合规要求下往往不可行。另一种做法是“各查各的”先问A拿到中间实体再拿去问B。但这样做有几个问题中间实体可能涉及敏感信息直接明文传递会泄露隐私。多跳路径搜索过程中A不确定该返回哪些实体给BB也不确定该向A要哪些实体。如何跨参与方计算答案置信度、如何让路径组合过程可追溯都需要专门设计。FedV-KGQA 的出发点就是在这类“数据不出域、推理跨图谱”的约束下实现可用的多跳问答。2. FedV-KGQA 核心概念与整体架构2.1 问题定义假设有 N 个参与方各自维护一个局部知识图谱 ( G_i )。这些局部图谱面向同一批实体集合实体 ID 可以对齐但各自只包含一部分关系。目标是回答用户提出的自然语言问题答案可能分布在多个参与方的图谱中推理路径要跨越多个参与方。一个典型的垂直分区示例参与方 A (张三, 患病, 糖尿病) (李四, 患病, 高血压) 参与方 B (糖尿病, 可用药, 二甲双胍) (高血压, 可用药, 硝苯地平)用户问“张三应该用什么药”单靠 A 或单靠 B 都无法回答。A 知道张三患病是糖尿病但不知道糖尿病对应什么药B 知道糖尿病的药但不知道张三患有糖尿病。问答系统必须把 A、B 两段信息组合起来。2.2 系统角色划分从工程角度看FedV-KGQA 类系统通常包含三类角色角色职责关键能力客户端Client持有局部知识图谱处理本地查询子图检索、实体消歧、本地路径展开服务端Server协调客户端之间的交互汇总路径调度、中间结果路由、结果聚合与排序安全组件Secure Channel保证实体 ID 与关系查询过程中的隐私加密传输、差分隐私、混淆机制这里的“服务端”不一定要持有任何业务数据它更像一个“编排者”负责告诉每个客户端下一步该查什么实体。真实的 FedV-KGQA 论文实现还会有更细致的数学模型但整体思路都是“客户端本地算服务端协调拼”。2.3 与单机多跳问答的三大区别维度单机多跳问答FedV-KGQA图谱可见性查询器能看到全部三元组每个参与方只能看到本地图谱路径搜索空间全局统一搜索分布式搜索需要跨方传递中间实体隐私约束无额外约束不能直接暴露中间实体明文需要保护机制这也是 FedV-KGQA 在实现时最难的部分跨客户端路径组合的每一步都会产生中间实体而中间实体本身可能就是敏感信息。3. 方法原理FedV-KGQA 的关键阶段拆解3.1 阶段一问题理解与实体链接无论图谱是否分区问答系统第一步都是把自然语言问题映射到图谱中的起始实体。例如问题“张三应该用什么药”实体链接模块需要识别出“张三”是图谱中的实体节点并把问题分类为“查找该实体经过两条关系边后到达的实体”。在联邦场景中实体链接通常由服务端或某个指定客户端完成因为实体 ID 可以共享对齐。这个阶段的输出是起始实体张三 查询意图疾病 - 药物 最大跳数23.2 阶段二本地子图检索服务端把查询任务分发给相关客户端。每个客户端在自己的局部知识图谱上对当前候选实体集合执行一轮检索返回“当前实体在本地能走到哪些相邻实体以及对应关系”。举例来说参与方 A 接收到“查询张三的相邻节点”本地返回张三 --患病-- 糖尿病参与方 B 接收到“查询糖尿病的相邻节点”本地返回糖尿病 --可用药-- 二甲双胍这一阶段的关键是每个客户端只暴露相邻实体和关系不暴露无关图谱结构。3.3 阶段三中间结果的安全交互这里有一个核心矛盾服务端需要把 A 返回的中间实体“糖尿病”传给 B让 B 继续查询但直接传明文实体名可能带来隐私泄露风险。FedV-KGQA 通常有几种处理策略实体 ID 映射参与方之间使用不可逆的假名 ID查询时通过 ID 映射表转换。差分隐私扰动在返回的候选实体数量、频次上加入噪声避免攻击者推断敏感关系。安全多方计算使用秘密共享等技术让参与方在密文上完成实体匹配。在工程落地时最简单的方式是服务端维护一份“实体别名表”每个实体对外只暴露随机编号。下面的演示代码也会使用这一思路用哈希值代替真实实体名做跨方传递。3.4 阶段四路径组合与答案排序当所有客户端的检索结果汇总到服务端后服务端会把不同参与方返回的边拼接成完整路径并按一定策略选出最终答案。常见的答案排序方法包括路径覆盖度多条路径指向同一答案时答案置信度更高。关系置信度客户端可以返回本地关系的语义相似度分数。支持度投票让多个客户端对候选答案进行本地投票再聚合。FedV-KGQA 的实践中这一步还会结合语义编码模型对问题和路径做相似度对齐但在流程抽象上可以简化成“拼接路径 - 打分 - 排序”。下面用一个简化流程图表示整体处理过程用户问题 | v [服务端] 实体链接与任务分发 | ----- [客户端A] 本地子图检索 - 中间实体1 | ----- [客户端B] 本地子图检索 - 中间实体2 | v [服务端] 跨方路径组合与答案排序 | v 最终答案4. 环境准备与演示项目结构4.1 运行环境本文的模拟示例以 Python 为基础核心逻辑不依赖外部框架。环境要求如下Python 3.8 及以上版本。不需要安装深度学习框架仅使用标准库即可运行。如果需要把客户端接口改造成 HTTP 服务可以选装 Flask但这不是必须的。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。4.2 项目目录结构fedv_kgqa_demo/ ├── client.py # 客户端实现本地图谱检索 ├── server.py # 服务端实现任务调度与路径拼接 ├── data.py # 模拟数据两个垂直分区的知识图谱 ├── privacy.py # 隐私保护工具实体 ID 哈希映射 └── main.py # 主入口运行一次多跳问答下面逐个文件编写。5. 完整实战案例模拟两个垂直分区知识图谱的多跳问答5.1 定义模拟数据data.py用来构造两个参与方的局部知识图谱。为了贴近垂直分区场景这里设计两个图谱参与方 A持有“患者-疾病”关系。参与方 B持有“疾病-药物”关系。# 文件路径fedv_kgqa_demo/data.py CLIENT_A_GRAPH { 张三: [(患病, 糖尿病)], 李四: [(患病, 高血压)], 王五: [(患病, 哮喘)], } CLIENT_B_GRAPH { 糖尿病: [(可用药, 二甲双胍), (可用药, 胰岛素)], 高血压: [(可用药, 硝苯地平)], 哮喘: [(可用药, 沙丁胺醇)], }这个设计里张三、李四、王五只出现在 A 端药物信息只出现在 B 端。中间实体“糖尿病”“高血压”“哮喘”在两份图谱中都存在但对应的关系完全不同这正是垂直分区的特征。5.2 客户端实现本地检索每个客户端需要提供两个能力本地查询给定实体返回该实体在本地图谱中的所有相邻边。对外展示返回的实体名通过隐私模块转换成假名避免直接暴露真实实体。# 文件路径fedv_kgqa_demo/client.py from privacy import hash_entity class KnowledgeGraphClient: 模拟联邦问答中的一个参与方客户端。 每个客户端只持有自己的局部知识图谱 对外只暴露“实体ID - 邻居假名列表”的查询接口。 def __init__(self, client_id: str, graph: dict): self.client_id client_id self.graph graph def local_query(self, entity: str): 查询某个实体在本地图谱中的相邻节点。 参数: entity: 真实实体名仅在客户端内部使用 返回: list[tuple]: 列表每个元素为 (关系, 目标实体假名) if entity not in self.graph: return [] results [] for relation, target in self.graph[entity]: # 目标实体以哈希假名形式返回保护真实实体名 results.append((relation, hash_entity(target))) return results这里要说明hash_entity只是单向映射服务端拿到假名后无法直接反推出真实实体。但服务端可以在下一轮把假名发给另一个客户端由另一个客户端在自己的“假名-真实实体”映射表中还原。5.3 隐私工具实现privacy.py提供统一的实体哈希映射。为了保证两个客户端能对同一个实体生成一致的假名这里使用相同的盐值和哈希算法。# 文件路径fedv_kgqa_demo/privacy.py import hashlib # 实际项目中这个盐值应该由安全组件统一管理 # 并且定期轮换。这里仅为演示逻辑。 _SALT kgqa-demo-salt def hash_entity(entity: str) - str: 把实体名转换为固定长度的哈希假名。 raw f{_SALT}:{entity} return hashlib.sha256(raw.encode(utf-8)).hexdigest()[:16]需要注意这个哈希方案只是为了让演示流程能跑通。真实系统中这种固定哈希容易被彩虹表攻击通常还要加随机化或使用密钥散列消息认证码HMAC。5.4 服务端协调逻辑服务端是整个问答流程的编排者。它的职责是接收用户问题中的起始实体。向持有该实体的客户端发起第一跳查询。把返回的中间假名分发给其他客户端继续查询。重复直到达到最大跳数。把跨客户端的边拼成完整路径输出答案。# 文件路径fedv_kgqa_demo/server.py from privacy import hash_entity class FedVKGQAServer: 联邦垂直分区知识图谱问答服务端。 def __init__(self, clients: list): # 用一个字典记录实体假名到真实实体名的映射 # 服务端在聚合路径时需要还原可读结果。 self.clients clients self.entity_alias_map {} def _register_alias(self, real_entity: str): alias hash_entity(real_entity) self.entity_alias_map[alias] real_entity return alias def _query_all_clients(self, real_entity: str): 在所有客户端上查询某个真实实体的邻接信息。 edges [] for client in self.clients: # 客户端内部会返回假名但服务端需要建立映射关系 local_results client.local_query(real_entity) for relation, alias in local_results: # 如果服务端还不认识这个假名记下对应真实实体 if alias not in self.entity_alias_map: # 这里需要客户端把真实实体额外上报。 # 为了演示我们直接在服务端反向注册。 # 更严谨的做法是客户端通过安全通道单独上报。 pass edges.append((client.client_id, relation, alias)) return edges def answer(self, start_entity: str, max_hops: int 2): 执行多跳查询返回所有路径。 current_entities [start_entity] all_paths [] for hop in range(max_hops): next_entities set() hop_edges [] for entity in current_entities: for client in self.clients: local_results client.local_query(entity) for relation, alias in local_results: hop_edges.append((entity, relation, alias, client.client_id)) # 在演示中通过客户端内部映射拿到真实目标实体 real_target self._resolve_alias(client, alias) if real_target: next_entities.add(real_target) all_paths.append(hop_edges) if not next_entities: break current_entities list(next_entities) return self._build_readable_paths(all_paths, start_entity) def _resolve_alias(self, client, alias: str): 根据客户端的本地映射将假名还原为真实实体。 演示代码中为了让逻辑可运行直接在客户端维护一个 反向映射表。生产环境应通过安全通道或可信组件完成。 return client.alias_reverse_map.get(alias) def _build_readable_paths(self, all_paths, start_entity): 把每一跳的边组合成可读路径。 paths [] # 这里简化处理把每一跳的边按实体串联起来 current start_entity readable [(start_entity, START, )] for hop_edges in all_paths: matched None for src, relation, alias, client_id in hop_edges: if src current: real_target self._resolve_alias_by_client(client_id, alias) readable.append((current, relation, real_target)) current real_target matched True break if not matched: break if len(readable) 1: paths.append(readable) return paths def _resolve_alias_by_client(self, client_id: str, alias: str): for client in self.clients: if client.client_id client_id: return client.alias_reverse_map.get(alias, alias) return alias上面的服务端代码有一个问题KnowledgeGraphClient还没有维护反向映射。为了让示例完整可运行我们需要在客户端里加入反向映射表。5.5 完善客户端反向映射在实际系统中假名到真实实体的映射只应该保存在客户端本地。服务端查询时可以让客户端返回假名的同时把真实实体通过安全通道发给服务端或者由服务端在后续轮次中继续使用该假名与其他客户端通信。为了保持演示简单、可读我们改一下客户端设计# 文件路径fedv_kgqa_demo/client.py完善版 from privacy import hash_entity class KnowledgeGraphClient: 模拟联邦问答中的一个参与方客户端。 def __init__(self, client_id: str, graph: dict): self.client_id client_id self.graph graph # 反向映射假名 - 真实实体名。仅保存在客户端本地。 self.alias_reverse_map {} def _to_alias(self, real_entity: str) - str: alias hash_entity(real_entity) self.alias_reverse_map[alias] real_entity return alias def local_query(self, entity: str): 查询某个真实实体在本地图谱中的相邻节点。 if entity not in self.graph: return [] results [] for relation, target in self.graph[entity]: alias self._to_alias(target) results.append((relation, alias, target)) # 第三个字段仅用于演示内部解析 return results这里我加了第三个返回字段target目的是让服务端能够拿到真实目标实体并继续下一跳查询。严格来说这破坏了隐私保护因为服务端直接看到了明文实体。但在演示代码中这是为了让流程可运行。真实系统中应该通过安全多方计算或加密实体对齐协议来实现。为了让代码逻辑清晰且不误导读者我在服务端代码中采用“边缘返回明文目标实体”的简化方式并在注释中说明隐私风险。5.6 调整服务端代码# 文件路径fedv_kgqa_demo/server.py完善版 class FedVKGQAServer: 联邦垂直分区知识图谱问答服务端。 def __init__(self, clients: list): self.clients clients def answer(self, start_entity: str, max_hops: int 2): 执行多跳查询返回所有可读路径。 参数: start_entity: 起始实体 max_hops: 最大跳数 返回: list[list[tuple]]: 路径列表每条路径是 (实体, 关系, 目标实体) 的列表 current_entities [start_entity] all_edges [] for _ in range(max_hops): hop_edges [] next_entities set() for entity in current_entities: for client in self.clients: local_results client.local_query(entity) for relation, alias, target in local_results: hop_edges.append((entity, relation, target, client.client_id)) next_entities.add(target) if not hop_edges: break all_edges.append(hop_edges) current_entities list(next_entities) return self._build_paths(all_edges, start_entity) def _build_paths(self, all_edges, start_entity): 把所有跳的边拼成完整推理路径。 paths [] current start_entity path [] for hop_edges in all_edges: for src, relation, target, client_id in hop_edges: if src current: path.append((src, relation, target, client_id)) current target break if path: readable [(start_entity, START, )] for src, relation, target, client_id in path: readable.append((f{relation}{client_id}, target)) paths.append(readable) return paths在_build_paths中我用relationclient_id的方式标注这条边来自哪个客户端便于后续分析路径的跨方属性。5.7 主入口与运行验证# 文件路径fedv_kgqa_demo/main.py from data import CLIENT_A_GRAPH, CLIENT_B_GRAPH from client import KnowledgeGraphClient from server import FedVKGQAServer def main(): # 1. 初始化两个垂直分区的客户端 client_a KnowledgeGraphClient(clientA, CLIENT_A_GRAPH) client_b KnowledgeGraphClient(clientB, CLIENT_B_GRAPH) # 2. 初始化服务端 server FedVKGQAServer([client_a, client_b]) # 3. 多跳问答 question 张三应该用什么药 print(f问题{question}) print(推理过程) print( 第1跳张三 --患病-- 糖尿病 (来自 clientA)) print( 第2跳糖尿病 --可用药-- 二甲双胍 (来自 clientB)) paths server.answer(张三, max_hops2) print(\n最终结果) for path in paths: print( - .join([str(item) for item in path])) if __name__ __main__: main()运行命令cd fedv_kgqa_demo python main.py预期输出问题张三应该用什么药 推理过程 第1跳张三 --患病-- 糖尿病 (来自 clientA) 第2跳糖尿病 --可用药-- 二甲双胍 (来自 clientB) 最终结果 (张三, START, ) - (患病clientA, 糖尿病) - (可用药clientB, 二甲双胍)5.8 结果说明从输出可以看到第一条边由clientA提供完成“张三 - 糖尿病”的推理。第二条边由clientB提供完成“糖尿病 - 二甲双胍”的推理。两条边在服务端被拼接成一条完整的跨客户端推理路径。这个例子说明在没有合并原始图谱的条件下只要每个客户端暴露“本地一跳查询”能力服务端就能通过编排完成多跳问答。真实 FedV-KGQA 系统还会对中间实体做更严格的隐私保护但整体流程骨架是一样的。6. 常见问题与排查思路6.1 常见报错与解决方案问题现象常见原因解决思路查询结果为空起始实体不在任何客户端图谱中检查实体链接是否正确确认实体 ID 对齐方式路径在第1跳后中断两个客户端之间没有共享中间实体检查垂直分区设计确认中间实体在双方图谱中都有出现返回的答案过多没有设置最大跳数或答案筛选阈值增加 max_hops 限制添加候选答案置信度阈值隐私工具被逆向破解固定哈希容易建立枚举映射改用带随机盐的 HMAC或接入安全多方计算协议分布式查询延迟高客户端接口串行调用使用多线程并发查询不同客户端再汇总结果6.2 关键排查流程如果多跳问答没有输出预期路径可以按以下顺序排查确认起始实体在所有客户端中是否存在。打印每一跳的中间实体确认上一跳返回的实体在下一跳客户端中能查到边。检查实体 ID 是否对齐。垂直分区场景最常见的坑是A 中叫“糖尿病”B 中叫“diabetes”两边没有做实体对齐。检查客户端是否返回了正确的相邻边而不是本地全量图。确认服务端拼接路径时是否把不同客户端的边按正确顺序串联。6.3 一个典型的实体对齐坑假设参与方 A 的图谱中实体是中文名参与方 B 的图谱中实体是英文名CLIENT_A_GRAPH { 张三: [(患病, 糖尿病)], } CLIENT_B_GRAPH { Diabetes: [(可用药, Metformin)], }如果服务端直接把“糖尿病”传给 B 查询B 返回空列表路径就断了。解决方式是维护一份“实体对齐表”把同一实体的不同表示映射到同一个内部 IDENTITY_ALIGNMENT { 糖尿病: DB0001, Diabetes: DB0001, }服务端在分发查询时先把实体转换为统一 ID再把 ID 传给每个客户端。7. 最佳实践与工程建议7.1 客户端接口设计原则在实际工程中客户端接口建议设计成“最小暴露”的形态只提供单跳查询能力不要提供全量图谱导出接口。每一次查询都要有权限校验。对查询频率做限流防止恶意调用者通过大量查询反推图谱结构。7.2 隐私保护注意点本文演示中直接返回真实中间实体是为了简化流程。生产环境至少需要做以下几件事使用密钥散列消息认证码HMAC代替固定哈希定期轮换密钥。对客户端返回的候选实体数量做限制避免差分攻击。敏感场景下使用秘密共享或同态加密方案让服务端在密文上完成路径拼接。记录查询审计日志但日志中不应包含原始实体明文。7.3 服务端编排优化服务端是跨客户端路径查询的瓶颈建议对客户端查询做并发调用降低端到端延迟。缓存高频实体的本地查询结果但要注意缓存带来的隐私风险缓存中只保留脱敏后的路径片段。路径拼接时限制最大跳数防止指数级路径爆炸。7.4 与中心化知识图谱问答的对比选型如果业务允许数据集中存储中心化方案在性能和实现成本上通常更优。FedV-KGQA 适合以下场景参与方之间存在信任边界数据无法集中。合规要求禁止原始数据离开本地。需要保留各参与方的数据自主权。如果只是公司内部多个团队共享数据优先考虑数据中心化或基于中间表的统一查询不必一开始就上联邦方案。8. 总结与学习路线这篇文章围绕 FedV-KGQA 的标题展开核心是理解“垂直分区的知识图谱上如何实现多跳问答”。读完你应该能说清楚什么是知识图谱多跳问答。垂直分区对比水平分区的区别。FedV-KGQA 四阶段流程实体链接、本地检索、安全交互、路径组合。一个最小可运行的跨客户端多跳问答演示如何编写。中间实体隐私保护的工程难点在哪里。如果你想继续深入可以按以下路线学习先掌握单机知识图谱问答基础熟悉实体链接、关系抽取、图谱路径搜索。学习联邦学习的基本概念重点看纵向联邦学习的实体对齐方法。研究知识图谱表示学习理解如何把实体和关系映射为向量。查阅 FedV-KGQA 论文中的数学模型与训练目标本文只覆盖了系统骨架。动手改造本文示例加入 HMAC 实体假名、并发查询、路径打分排序。实际项目中最需要优先关注的风险是隐私保护方案是否足够强以及实体对齐是否可靠。建议先用小规模模拟数据验证流程再逐步扩展到真实业务图谱。如果这篇文章对你有帮助可以收藏备用。后续我会再整理联邦知识图谱问答中的隐私对齐算法和路径打分优化欢迎持续关注。