Skip to main content

SVO 语义检索的系统化方案

先定义"语义相关"是什么,再倒推索引结构,最后选算法——这个顺序不能颠倒,否则就是拿着锤子找钉子。

一、第一性原理:先定义"相关"

"检索相关语义"这句话太模糊,必须拆开。对 SVO 表达式而言,相关性其实是一个有层级的谱系,不同层级需要不同的索引手段:

L1 概念相关——查"汽车"时召回含"轿车""SUV"的表达式。这是词项语义相似,靠 embedding 解决。

L2 命题相关——查"Karpathy 表示 AGI 还远"时召回"Karpathy 认为通用人工智能尚未到来"。这是整个三元组语义相似,核心词、动作、宾语都可能被同义替换,但结构角色必须对齐(施事还是施事,受事还是受事,不能错位)。

L3 情境相关——查"在低资源场景下的模型压缩"时,召回的文档其前提 >> 必须涉及"低资源",而不是只在正文里提一句。这是作用域匹配

L4 蕴含相关——查"所有学生通过考试",应该召回"没有学生没通过"(逻辑等价),但不应召回"某些学生通过"(强度不同)。这涉及量词、否定、模态的逻辑推理。

L5 结构类比——查"A 促使 B 认识到 C",可以召回结构相同但实体完全不同的致使句。这在科研文献检索、案例检索里很有用。

关键洞察:大多数检索系统失败,是因为把这五层揉在一个向量里。SVO 的算子结构恰恰让我们能把五层拆开,各自用最合适的手段索引,然后在排序时加权融合。这是 SVO 相比纯文本最大的红利,也是系统化方案的根基。

二、分解原则:把表达式打成"语义原子"

SVO 表达式不应该作为整体被索引。一条复杂表达式应该在入库时被分解成三类语义原子,分别建索引:

原子一:概念簇 (Concept)

任何被 : 链绑定起来的核心词单元就是一个概念簇。(OpenAI:创始:元老):Karpathy 是一个概念簇,代表一个具体实体。(协作式:中间态) 也是一个概念簇,代表一个抽象概念。概念簇是 SVO 中名词性的最小语义单位,对应现实世界的实体或属性集合。

原子二:命题 (Proposition)

任何 主体 > 动作 > 受事 的三元组就是一个命题,是 SVO 中陈述性的最小语义单位。复合的 &| 在这一层展开成多个命题。嵌套的宾语命题独立成一条记录,通过 ID 与父命题关联——这一步至关重要,否则递归结构无法被搜索。

原子三:情境 (Context)

>> 左侧的所有内容构成一个情境,情境本身也是一个命题(或概念簇)。情境通过 ID 链接到它所辖的所有命题上,作为这些命题的"作用域标签"。

举例。这条表达式:

(前:负责人):Karpathy >> 今天:表示 > ((该计划 > 不:公开) >> (该计划 > 无法:获得 > 认可))

入库后会变成:

  • 概念簇:(前:负责人):Karpathy该计划认可
  • 命题 P1:Karpathy > 表示 > P2(状语:今天)(情境:身份=前负责人)
  • 命题 P2:该计划 > 无法获得 > 认可(情境:该计划不公开)
  • 命题 P3:该计划 > 不公开(作为 P2 的情境)

四个命题、三个概念簇,各自独立可检索,但通过 ID 保留了原始的嵌套关系。这就是结构化分解 + 关系保留

三、三层索引架构

基于上面的分解,建三个并列的索引,每个索引服务不同的相关性层级:

索引 A:概念向量索引(服务 L1)

每个概念簇编码成一个向量。编码方式不是简单地把词拼起来过 sentence encoder——那样会丢掉 : 的限定关系。正确做法是按绑定链从右到左聚合:核心词的 embedding 作为基底,每个修饰词的 embedding 作为加性偏置(类似 TransE 的思路),或者用一个小型注意力层让修饰词加权调制核心词。这样 (前:负责人):KarpathyKarpathy 在向量空间里既相近又可区分。

索引 B:命题倒排索引 + 命题向量索引(服务 L2、L4)

对每个命题三元组,同时建两套索引:

  • 倒排索引:按施事、动作、受事分别建倒排表,支持精确的角色查询("找所有施事是 Karpathy 的命题")。这是 SPARQL/RDF 那一套的成熟做法,工程上极其便宜。
  • 命题向量索引:把整个三元组编码成单个向量,但编码时必须区分角色——施事位、动作位、受事位的 embedding 要拼接(或加位置编码),不能简单平均,否则 "猫吃鱼" 和 "鱼吃猫" 就成一回事了。否定、模态、量词作为额外的特征位拼上去,这样 L4 的逻辑区分才有依据。

这两套索引互补:倒排管精确,向量管语义模糊。

索引 C:情境向量索引(服务 L3)

每个情境单独编码一个向量(复用索引 B 的命题编码器即可)。每个命题记录里挂一个"所属情境 ID 列表"。检索时,用户的"场景词"被编码后查情境索引,然后顺着 ID 反查到该情境下的所有命题。

这三个索引不是替代关系,是协同关系。它们分别解决不同维度的相关性,在排序阶段融合。

四、检索流程:从查询到结果

把一次查询想象成漏斗。用户给一个 SVO 表达式(或自然语言,先过解析器转成 SVO),系统按以下步骤走:

步骤 1:查询分解。用同样的规则把查询表达式拆成概念簇、命题、情境三类原子。注意识别哪些是"必须项"(用户查询里出现的实体/动作)和"软需求"(可被同义替换的部分)。

步骤 2:多路召回。每类原子去对应的索引召回:

  • 概念簇 → 索引 A → 召回相似概念,以及含这些概念的命题 ID
  • 命题 → 索引 B 倒排 → 角色严格匹配的命题(高精度低召回);同时索引 B 向量 → 语义相似的命题(高召回低精度)
  • 情境 → 索引 C → 含相似情境的命题

把所有路召回的命题 ID 取并集,得到候选池(通常几百到上千)。

步骤 3:逻辑过滤。这一步是 SVO 相比传统向量检索的杀手锏。利用算子的逻辑语义做硬过滤:

  • 极性检查:查询是肯定句,候选是否定句应降权或过滤(除非用户明确要找反例)
  • 量词检查:查询是 所有:X,候选是 某:X 不能算等价
  • 模态检查:必须:X可能:X 区分开
  • 情境覆盖:如果查询有 >> 前提,候选的情境必须能覆盖或被覆盖(看你要的是 entailment 方向)

这些规则可以用一个小型规则引擎实现,几十行代码搞定,但能把"语义看似相近、实则相反"的脏数据干掉。这是纯向量检索做不到的。

步骤 4:命题级精排。对剩下的命题候选,用一个学习排序模型打分。特征包括:概念向量相似度、命题向量相似度、情境向量相似度、倒排命中数、逻辑约束满足度。这五维特征做加权或者过一个轻量 LightGBM/MLP,输出最终分数。

步骤 5:回溯与聚合。命题是检索单位,但用户要的是能解释这个命题来源的完整表达式。所以最后一步是顺着命题 ID 回溯到它所属的原始 SVO 表达式,把同一表达式下被多次命中的命题加分,然后按表达式输出结果。这一步保证了"检索到的是命题,呈现给用户的是完整语义单元"。

五、为什么这套方案系统且可行

系统在哪:它从"相关性的五层定义"出发,每一层都有对应的索引和算法负责,职责清晰,不重复不遗漏。每个组件都可以独立评估和优化——概念召回不准就调索引 A,逻辑过滤漏掉反例就加规则,这种可解释、可调试的检索系统是纯神经方案给不了的。

可行在哪:三个索引全都用现成基础设施就能搭。倒排索引用 Elasticsearch,向量索引用 Qdrant 或 Milvus,情境关联用 PostgreSQL 的外键即可,根本不需要图数据库。编码器用现成的 BGE 或 E5 微调,微调数据可以用 SVO 表达式的同义改写自动构造。整套系统两个工程师两个月能跑起来,不需要任何前沿研究突破。

未来在哪:这个架构留了清晰的升级路径。短期可以不训练任何模型,纯靠预训练 embedding + 规则,先跑起来。中期把概念编码器和命题编码器用对比学习微调,精度上一个台阶。长期如果要追 SOTA,可以把命题向量索引换成图神经网络编码,把逻辑过滤换成可微的逻辑层(比如 neural theorem prover)。每一步升级都不需要推倒重来。

六、最容易被忽视但最关键的一件事

编码器必须知道算子的存在。如果你拿一个普通的 sentence encoder 去编码 SVO 表达式,它会把 :>>> 当成无意义符号忽略掉,效果还不如直接编码原始中文句子。

正确做法是构造对比学习数据,让编码器学会区分:

  • A > 杀 > B vs B > 杀 > A(角色翻转,必须远)
  • A > 喜欢 > B vs A > 爱 > B(同义动作,必须近)
  • 不:适用 vs 适用(极性翻转,必须远)
  • 所有:学生 > 通过 vs 某:学生 > 通过(量词不同,必须中等距离)
  • (战时)>> A > 训练 vs (和平)>> A > 训练(情境翻转,必须远)

这五类对比样本各造几万条,微调一个 BGE-base,就能得到一个真正"懂 SVO"的编码器。这一步是整个方案的技术核心,其他都是工程组装。没有这个编码器,前面所有架构都只是文本检索的复杂版本;有了这个编码器,SVO 的算子语义才真正进入了向量空间。


一句话收束:把 SVO 表达式分解成概念、命题、情境三类原子,各自建索引,检索时多路召回 + 逻辑过滤 + 学习排序,最后按表达式回溯聚合。这套方案的精髓不是任何单一算法,而是承认"语义相关性是分层的",并让架构忠实地反映这个分层。SVO 的算子设计本来就是为这种分层准备的——不利用就太可惜了。