本体论语义建设新思路,另类RAG来解决检索问题
来源:互联网
时间:2026-06-30 14:58:23
有没有想过一个问题,本体论里一半的篇幅都在讲怎么定义标准数据、怎么描述数据之间的关系。之所以要这么较真,是因为所有的分析和Action都需要一个精准的上下文。从这个角度看,它本质上是一个高维度的RAG问题。
只不过普通RAG搜索的目标大多是大量文本对象,而Ontology操作的目标更偏向数据库对象。这意味着,我们可以借鉴为RAG设计的系统框架来设计Ontology的数据层。比如,这次要聊的SAG(Structured Aggregated Graph),就是一个很有参考价值的方案。
在回答复杂问题时,单靠向量匹配搜索出来的chunk往往不够用。因为很多隐含条件并没有体现在字面上,需要借助关系来查找字面上没有出现的实体。SAG的思路是把关系与向量结合起来做召回和重排序,在多跳问答数据集上的验证结果相当亮眼。核心在于:它不维护重型知识图谱,而是建立三种轻量索引(`chunk → event`、`event ↔ entities`、`chunk → entities`),用“双存储协同 + 多跳扩展”来弥补纯向量检索在多跳场景下的不足。
## 索引方式
SAG把一个chunk拆成了三个部分:事件、实体和关系。事件是对chunk的摘要,实体是从事件中提取出的主体,关系则是事件与实体之间建立的连接。
对于每一个chunk,让LLM提取事件和实体,并建立关联。
这就像是一个图:两个事件之间如果存在相同的实体,它们就产生了关联。
整个索引流程是一个五步流水线:
`chunks → processor(LLM调用) → filter(过滤) → parser(解析) → sa ver(持久化)`
每个chunk经过一次LLM调用,融合成**恰好一个**自包含的事件,再加上若干实体。这和传统的“一句一三元组”是完全不同的思路。对chunk的提取会产出两大类成果:结构化数据和向量化数据。
MySQL负责存储结构化数据,通过event和entity的id来关联,实现精确的关系遍历,用于Step3的通道1(entity→event)、Step5的多跳扩展以及Step8的chunk回溯。
ES则存储向量化数据,负责模糊语义召回和打分,用于Step2的实体召回、Step3的通道2以及Step6的粗排。
结构化数据在MySQL中通过id记录event/entity之间的关系,可以通过entity_id进行精确的关联查询:
```
stmt = select(EventEntity.event_id).where(EventEntity.entity_id.in_(entity_ids) # 精确 JOIN
).join(SourceEvent...).where(source_config_id.in_(...))
```
向量化数据存放在ES中,供向量搜索使用:
| ES 索引 | 向量来源 | 用途 |
| --- | --- | --- |
| `event_vectors` | 事件标题、`title+content` 分别 embed | 事件语义召回 |
| `entity_vectors` | `entity.name` embed | 实体向量召回(NER命中后找相似实体) |
| `event_entity_vectors` | `EventEntity.description` embed | 关联关系检索 |
## 检索:8步流水线的逐层职责
| 步骤 | 职责 | 存储 | 关键参数 |
| --- | --- | --- | --- |
| **Step1** NER | query → 实体名 | LLM(multi)/ BM25(multi_es) | — |
| **Step2** 实体召回 | 实体名 → entity_ids | ES `entity_vectors` | top_k=20, 阈值 0.9 |
| **Step3** 双通道召回 | 召回初始事件 | MySQL JOIN + ES kNN | **k=20(入口窄)** |
| **Step4** 事件详情 | 取 content + 关联 entities | MySQL / ES | — |
| **Step5** 多跳扩展 | 沿实体图遍历补全桥梁 doc | MySQL JOIN / ES 反查 | max_hops=1(默认) |
| **Step6** 粗排 | 向量相似度去噪打分 | ES kNN | **max_events=100(5倍冗余)** |
| **Step7** LLM 精选 | 多跳推理选 top_k | LLM | top_k=5/10,**不看分数** |
| **Step8** chunk 回溯 | event → 原始 chunk | MySQL | chunk_id 去重 |
### 多跳扩展:解决“语义断裂”问题
在多跳问答场景里,答案所在的doc可能与query在语义上并不相关(query里没有答案实体的字面信息)。纯向量检索无法召回这类doc。**Step5的多跳扩展沿着 entity↔event 的关系图进行遍历,把那些“图可达但语义远”的doc拉进候选池**。
基于真实MuSiQue 4跳样本的验证结果如下:
| hop | gold doc 的 query 语义相关性 | 召回方式 |
| --- | --- | --- |
| hop1(query含实体) | 高 | Step3 向量直接召回 |
| **hop2(中间桥梁)** | **极低(主题域不交叉)** | **只能靠 Step5 图遍历** |
| hop3-4 | 中-高 | 向量 + 图遍历互补 |
### Step3(k=20)与 Step6(max=100)的5倍冗余
```
Step3 入口窄(k=20,严苛语义筛选)
↓
Step5 多跳注入(绕过相似度,图可达性注入)
↓
Step6 缓冲池宽(max=100,5倍冗余给注入doc留存活空间)
↓
Step7 LLM 不看分数(候选池内一律平等,靠推理选)
```
这实际上是说,在做向量搜索时用K=20限制了向量召回的数量,把一部分空间留给了用MySQL做精确关联的event。然后将双搜索召回的event放到一起做重排序。有意思的是,这里的重排序用的是LLM,而不是简单的reranker。
### Step7 用 LLM 而非 reranker:任务定义不同
| 方面 | 传统 reranker | SAG Step7 |
| --- | --- | --- |
| 任务 | query-doc 语义匹配度 | doc对**多跳推理链**的贡献度 |
| 能力 | 相似度打分 | 理解 “First find X, then find Y” |
| 成本 | 毫秒级 | 秒级(万 token 量级) |
Reranker无法识别那种“跟query不像但却是推理链必经桥梁”的doc,而LLM可以。简单说,就是把召回的100条event依次让LLM重新判断,看哪个event对回答问题更有帮助。当然,它也提供了**fast 模式(multi_es)用数值公式替代 LLM**,用来节约时间和成本。
## 在RAG上存在的问题
### 文档格式强依赖
首先,文档格式依赖非常强。SAG的Load模块**只认markdown**,而且heading_strict切分方式强制依赖ATX风格的标题(`#`号)来定义chunk边界。如果你的文档没有标题、不是markdown格式(比如PDF、Word、HTML),这套流程基本就凉了。换句话说,SAG的Load只能处理结构清晰的数据,否则很容易出问题。
benchmark数据集里的corpus都很干净(title/text齐全),完美避开了生产环境中那些乱七八糟的格式预处理问题。真实部署的话,你不得不在前端加一层格式转换。
### 图遍历与向量打分的固有张力
多跳扩展靠图可达性来召回,Step6又靠向量相似度来排序——这两者之间可能根本不相关。深跳(3-4跳)的答案在向量上可能几乎不相关,因此在Step6被100名的截断淘汰的可能性很大。这是SAG架构的固有代价,也是MuSiQue(其中48%是3-4跳的样本)比HotpotQA更难的根本原因。
### 成本,还是成本
在整个抽取和检索过程中都需要调用LLM,产生的成本是普通RAG的数倍。
| 阶段 | 每次 input token 量级 |
| --- | --- |
| **抽取** | 每个 chunk ~500-2000 token + system prompt + few-shot |
| **检索** | NER 较小;rerank 100 候选 × ~200 token = ~20000 token |
## 基于SAG的语义层?
如果要用图数据库来定义本体之间的关系,常见的做法是把两张表定义为两个本体,然后用某种关系连接起来。但问题在于,两个本体之间可能存在多种关联关系。
从数据层面看,可能有外键关联;从其他维度看,可能有某些维度字段关联,比如城市、商品类目。通常情况下,图数据库建模都不建议在两个节点之间直接定义多种关系,要么造一个中间节点来处理,要么通过专门的查询条件来避免笛卡尔积。
如果参考SAG的构建方式,把每条数据看作一个chunk/event,把关联字段看作SAG中的实体,就可以自然地建立多种关系。
但要注意,不能直接用LLM来处理数仓中的每一行数据,那token的费用可能比整个数据团队的工资还高。
经过反复取舍和测试,业内逐渐形成了一种结合wiki和cube的多层结构混合存储与检索方案,大致思路如下:
1. 为每张表建立一个wiki,详细描述表的内容、业务含义、适用场景、可能的关联关系等;
2. 把这个wiki作为一个chunk,提取它的event和entity,存入MySQL和ES;
3. 按照cube的标准,定义关联字段、视图等;
4. 使用SAG的检索流程,进行相关表的检索;
5. 综合表、wiki和cube的定义,生成一个或多个SQL语句,进行查询和聚合,并最终生成答案。
简化来看,就是这样一个流程:
```
用户 query
↓
查询意图分类(LLM)
├── 明细查询 → SAG 检索(召回行)
├── 聚合查询 → CubeSQL(生成 SQL)
└── 混合查询 → SAG 召回 + SQL 聚合
```
但在工程实践中,还有很多落地的细节需要处理,比如多个表之间的同义entity如何保证一致性,如何分解query需求,在解答用户或其他系统的问题时是否要采用ReAct模型进行多步检索等等。这些问题,都值得继续深入探讨。