知识图谱不再是"画出来好看":三条工程路线打通 RAG 的语义断层
摘要:向量检索的语义漂移,是 RAG 在金融、ERP 这类强关系场景失准的根因。本文给出知识图谱三条落地方案:API 元数据建图(SPARQL)、金融文档图谱(Cypher 生成)、多智能体自动建图(实体对齐),附可复用代码与避坑要点。真实项目 + 生产环境验证。
一、先说结论:向量检索解决不了"关系"
RAG 在通用问答上表现不错,一到强关系场景就出问题。举个具体的:问"某公司的机构投资者有哪些",向量检索可能召回一段讲"客户基础"的文本——因为"investor"和"customer base"在语义空间中很近。答案看起来像那么回事,实际完全不是那回事。
这不是模型的问题。向量检索按语义相似度找片段,它天然不擅长回答"谁持有谁"“哪个部件装在哪台设备上”"哪个 API 依赖哪个 API"这类结构化关系问题。
知识图谱补的正是这一块:把关系显式建成边,将模型的注意力强制锚定在合规的实体关系上。本质是用符号逻辑校准统计学习。
下面三条路线,按落地难度从低到高。
二、路线一:API 元数据建图(解决 Agent 找错接口)
痛点很具体:企业有几十上百个 API 服务,Agent 每次回答都要花大量 token 和时间试错找接口。图能把这件事的搜索空间直接缩掉。
真实数据规模(真实项目 + 生产环境验证):39 个服务、101 个实体集、120 个导航关系、2000+ 属性定义。这个规模的元数据用文档描述没人看得完,建成图后可查询。
图谱的模式直接来自 OData 的 EDMX 规范:
Service 服务 │ contains ▼ EntityType 实体类型 ──nav──► EntityType(多重性 1 或 *) │ has ▼ Property 属性(类型/最大长度/是否主键/是否可查询) │ belongs to ▼ EntitySet 实体集(实体类型实例的逻辑容器)每个实体类型恰好属于一个实体集。导航属性定义了"一端到另一端"的路径,比如"采购订单项 → 采购订单"多重性为 1(一项恰好属于一单),"采购订单 → 采购订单项"多重性为*(一单可有多个项)。
用 SPARQL 的 CONSTRUCT 查询建图(不是 SELECT——SELECT 只返回结果表,CONSTRUCT 才输出新三元组):
CONSTRUCT { ?service a :Service ; :name ?serviceName ; :description ?desc ; :version ?ver . } WHERE { ?s a :RawService ; :serviceName ?serviceName ; :description ?desc ; :version ?ver . BIND(IRI(CONCAT("http://ex/", ?serviceName)) AS ?service) }WHERE子句定义要在输入数据中匹配的图模式,BIND生成新标识符,CONSTRUCT定义输出图的结构。这套映射方法的适用范围不限于 OData——任何有规范描述的 API 都能这么建图。
避坑要点:建图的价值不在"图",在缩小搜索空间。如果只是把 API 文档搬进图数据库但不定义导航关系,Agent 依然要遍历全部节点,等于没建。关系边才是资产。
三、路线二:金融文档图谱(三段式构建 + Cypher 生成)
场景:SEC 的 10-K 财报(非结构化)+ 13-F 持仓表(结构化),目标是回答投资分析类问题。
构建:提取 → 增强 → 扩展
这个三段式是整个方法论里最可复用的部分。
| 阶段 | 做什么 |
|---|---|
| 提取 | 10-K 文本分块(chunking)生成文本节点;13-F CSV 抽取公司与基金经理实体;地址字符串解析成结构化地址节点 |
| 增强 | 文本块加向量嵌入;地址节点加经纬度 point 字段并建地理空间索引 |
| 扩展 | 建关系边串联:公司→表单→章节→文本块、经理→地址、经理→持股公司 |
具体到关系设计(这是最需要产品思维的地方):
(chunk)-[:PART_OF]->(form) # 文本块属于表单 (chunk)-[:NEXT]->(chunk) # 同节内的块按 sequenceID 串成链表 (form)-[:SECTION]->(chunk) # 表单直连每节首块(sequenceID=0),用于快速导航 (manager)-[:OWNS_STOCK_IN]->(company) # 带 reportCalendarOrQuarter / shares / value (company)-[:FILED]->(form) # 公司提交了表单 (manager)-[:LOCATED_AT]->(address) # 地理关系SECTION这条边是个很巧的设计:没有它,从表单找某一节要顺着NEXT链表一路走;有了它,直接跳到sequenceID = 0的块。
检索:两条链的对比
真实项目里跑了两条链做对照(真实项目 + 生产环境验证):
- 基础链:只返回向量匹配的单个 chunk
- 投资链:从 chunk 出发,经
PART_OF → FILED → OWNS_STOCK_IN路径提取 Top 10 投资者,格式化成自然语言句子(“X 机构持有 Y 股,市值 Z”)前置到 LLM 输入
结果很典型:
| 问题类型 | 基础链 | 投资链 |
|---|---|---|
| “某公司主营业务?” | 相近 | 相近 |
| “某公司的投资者有哪些?” | 生成模糊推断,机构名是编的 | 精准列出真实机构名单 |
结论很清楚:文本型问题和关系型问题需要两套检索路径。只做一套,必然有一类问题答不准。
两个可以抄的工程细节
① chunk 窗口用变长路径而不是固定长度。
MATCH (c:chunk {id: $chunkId})-[:NEXT*0..2]-(neighbor) RETURN neighbor固定长度的NEXT路径在首块/末块上会失败(没有前驱或后继)。[:NEXT*0..2]自适应捕获 0 到 2 跳,所有块都能生成有效窗口。这个模式应该作为图谱 RAG 的标准写法。
② 金融数值必须转整数并做千分位格式化。
f"{int(value):,}"# 而不是直接拼 str(value)原因:原始字符串可能是123456789.0,模型会误读;加了千分位123,456,789后,模型对量级的理解明显改善。所有金融数值注入环节都应该强制这一步。
避坑要点:分块时按
sequenceID = 0判断"节首块"会踩坑——同一份表单的不同 Part(如 Item 7 和 Item 7A)都有 sequenceID 为 0 的块。要用(form, item)组合做唯一判定,否则导航会串节。
Cypher 生成:少量示例就够了
用少样本提示引导模型生成 Cypher,实测的递进关系很清晰:
| 示例数量 | 模型能力 |
|---|---|
| 1 个(“某城市的投资公司”) | 能正确替换 WHERE 里的城市名 |
| 2 个(增加"某地附近的投资公司") | 自主调用空间距离函数构造地理查询 |
| 3 个(增加"某公司的业务描述") | 组合全文检索 + 多跳遍历(公司→表单→章节→文本块)并返回摘要 |
少量高质量示例 + 严格 schema 约束 + 链式编排,就能稳定产出生产级 Cypher。不需要几十个示例,需要的是示例覆盖"查询类型"而不是"查询内容"。
地理查询这块还有个实用能力:拼写纠错 + 10 公里围栏的组合,构成"语义容错 + 空间校验"的双重消歧。在命名不规范、地址格式混乱的文档场景里,这一招比纯字符串匹配强得多。
四、路线三:多智能体自动建图(把 LLM 幻觉隔离在提案阶段)
前两条路线是人工设计 schema,这条是让 Agent 自动提案。适用于业务复杂到人设计不过来的情况。
三层代理架构
┌──────────────────┐ │ Root Agent │ 用户交互 + 任务分发 └────────┬─────────┘ ┌────────────┴────────────┐ ▼ ▼ 结构化数据代理 非结构化数据代理 (CSV/JSON) (Markdown) │ │ ▼ ▼ 意图澄清 → 文件采样 意图澄清 → 文件定位 → Schema 提案/评审 → 实体与事实抽取提案 │ │ └────────────┬────────────┘ ▼ Graph Builder(工具引擎)关键设计:代理之间靠**委托(delegation)**协作,不是简单工具调用。协调代理识别意图后把控制权交给专用子代理,对话历史和状态在代理间透传。
三个决定成败的机制
① Proposer / Critic 双代理对抗。
Schema 不是生成一次就完事,而是"提案 → 评审 → 迭代"。Critic 不只看语法对不对,还要验证 schema 能不能满足用户的原始目标。比如目标是"支持根因分析",那 schema 必须包含 供应商 → 部件 → 产品 → 投诉 的完整链路。
② 提案阶段禁止出现具体实例。
事实类型抽取只提通用三元组模式:
✅ (product)-[has_issue]->(issue) ✅ (person)-[likes]->(beverage) ❌ (某产品名)-[likes]->(咖啡)这条约束看起来是细节,实际上决定了整个方案的可控性。提案只定义"什么可被提取",具体提取由后续工具执行。职责分离后,LLM 幻觉被隔离在提案环节,不会污染数据导入。
③ 所有关键决策写入 Session State,不依赖对话历史。
用户目标确认、文件推荐批准这类决策,必须通过工具写入状态(如approved_user_goal键),而不是靠 LLM 记住对话内容。
这条是整个架构里最重要的工程原则:LLM 负责设计,代码负责实施。状态一旦落库,系统就具备可审计性和可回溯性。
数据导入:先节点后关系
1. 建唯一性约束 CREATE CONSTRAINT ON (p:Product) ASSERT p.productId IS UNIQUE 2. 节点批量导入 LOAD CSV WITH HEADERS ... MERGE(1000 行/批事务化) 依据唯一列去重,动态 SET n += {props} 3. 关系批量导入 MATCH(定位两端)→ MERGE(建关系)→ SET(赋属性) 关系不需要唯一约束,靠两端节点唯一性兜底MERGE天然幂等,多条"同一人喜欢同一饮品"的记录只留一条。这比纯 LLM 驱动的数据清洗在工程上稳得多。
导完之后一定要做采样验证:把所有预期的关系类型展开,逐个模式匹配,确认每种关系至少存在一个实例。不验证就导库,后面出问题极难定位。
跨图实体对齐:四步闭环
当结构化域图谱(CSV)和文本域图谱(Markdown)需要连通时,就得做实体对齐。
| 步骤 | 做法 |
|---|---|
| 动态 Schema 发现 | 查 Cypher 实时提取两边同类节点的全部属性键,不预设对齐字段 |
| 键名归一化 | 统一处理前缀(product_name→name)、大小写、空格 |
| 模糊匹配 | 用模糊相似度算法(阈值 0.9)自动配对高相关键 |
| 字符串距离验证 | 用编辑距离算法(阈值 0.1)触发MERGE创建CORRESPONDS_TO关系,记录创建/更新时间 |
避坑要点:不要对所有实体类型用同一套阈值。产品名匹配精度高,问题描述这类自由文本差异大,统一阈值会漏联关键实体。合理策略是分层:强标识字段(ID、名称)用高阈值(0.05),弱标识字段(自由文本摘要)引入向量相似度做二次校验。
五、写在最后
知识图谱在 RAG 里的位置已经清楚了:它不是"让演示更漂亮的可视化",是补向量检索的语义断层。
三条路线的选择很简单:
- 有规范描述的 API/元数据 → 路线一(SPARQL 建图)
- 有强关系需求的垂直文档 → 路线二(三段式 + Cypher 生成)
- 业务复杂到 schema 设计不过来 → 路线三(多 Agent 提案 + 评审)
三个可以马上做的动作:
- 把
[:NEXT*0..2]变长路径加进现有的图谱检索,解决首末块的窗口缺失; - 金融/数值字段全部改成整数 + 千分位再注入 LLM;
- 挑一个"向量检索老答错"的问题,看看是不是关系型问题——如果是,别调提示词了,缺的是边。