语义工程-03.电商导购案例(V2.0):真正的本体出现了

V1.0回顾:我们有了语义层,但还没有真正的本体

在上文中,我们通过一个电商案例搭建了一个简单的语义层。

通过这个语义层,我们可以建立以下数据和领域概念之间的关系。

  • 安踏 → 品牌属性
  • 黑色 → 产品颜色属性
  • 透气 → 产品特性
  • 42码 → 尺码变体筛选器
  • 300元以下 → 价格约束
  • 2天内发货 → 发货时间限制
  • 跑步 / 打篮球 / 通勤 / 户外 → 场景语义映射到具体特性组合
    当用户说"适合跑步的鞋"时,本体层还会扩展为:
  • features CONTAINS ALL ["透气", "轻便", "缓震"]

但是在V1.0中,我们识别领域概念时采用的是硬编码

代码示例:

python 复制代码
@dataclass

class OntologyConcept:
    name: str
    domain: str           # Which entity this concept applies to
    concept_type: str     # "attribute" | "variant" | "constraint" | "category"
    valid_values: list[str] = field(default_factory=list) 
  

CONCEPTS = {
    "brand": OntologyConcept(
        name="brand", domain="Product", concept_type="attribute",
        valid_values=["安踏", "李宁", "361"]
    ),

    "color": OntologyConcept(

        name="color", domain="Product", concept_type="attribute",

        valid_values=["黑色", "白色", "红色", "蓝色", "灰色", "绿色"]

    ),

    "feature": OntologyConcept(

        name="feature", domain="Product", concept_type="attribute",

        valid_values=["透气", "缓震", "防滑", "轻便", "耐磨", "包裹性"]

    )
}
  
# Attribute validity rules: which features are INVALID for which categories

INVALID_ATTRIBUTE_RULES: dict[str, list[str]] = {

    "sneakers": [],  # 运动鞋可使用全部特性

} 

SCENE_TO_FEATURES: dict[str, list[str]] = {

    # 跑步场景 → 需要透气、轻便、缓震

    "跑步": ["透气", "轻便", "缓震"],

    "跑": ["透气", "轻便", "缓震"],

    "马拉松": ["透气", "轻便", "缓震"],

    "慢跑": ["透气", "轻便", "缓震"],

}

这种硬编码的方式有很大的局限性,包括:

  • 无法基于别名/同义词扩展 ,例如"跑鞋"和"跑步鞋"要分别写规则
  • 无上下位推理,例如不知道"篮球鞋"是"运动鞋"的一种;
  • 特征匹配太严格 ,例如入要求所有特性同时满足,容易为空,这是基于封闭假设的结果;
  • 无多跳关系,例如不能推荐"买了这双的人还买了..." |

我们需要一个真正的使用本体语言构建和表达的语义层。

在此之前我们需要掌握一下本体表达基础知识。

本体的表达

要突破硬编码的局限,我们需要一套形式化的语言来描述领域中的概念、关系和规则。这套语言必须能让机器理解"篮球鞋是一种运动鞋"这类语义,支持同义词扩展,并能在开放世界假设下进行推理。业界已经形成了成熟的标准体系:RDF 提供事实表达的基本图模型,OWL 在此基础上定义概念间的逻辑约束与推理规则。掌握这两者的语法与语义,是构建真正可演化语义层的前提。

什么是 RDF?

RDF 全称是资源描述框架。RDF把现实世界中的信息拆成一个个"主语---谓语---宾语"的关系,再把这些关系连成一张图。

例如:

css 复制代码
Subject  --Predicate-->  Object

语法结构:

  • 核心为三元组(主体、谓词、客体)
  • 主体和谓词必须是URI或空白节点,客体可以是 URI、字面量(字符串/数字)或空白节点
  • 支持多种序列化格式(如 XML、Turtle),但底层逻辑统一为三元组

详细示例:

turtle 复制代码
@prefix ex: <http://example.org/> .  # 声明命名空间前缀

ex:Alice ex:hasAge "30"^^xsd:integer .  # 主体=ex:Alice, 谓词=ex:hasAge, 客体=整数字面量
ex:Alice ex:hasName "Alice"^^xsd:string .  # 字符串字面量
ex:Alice ex:worksAt ex:CompanyX .  # 客体为另一个URI

什么是 OWL?

OWL 全称是网络本体语言。如果说 RDF 解决的是"怎么把事实表示成图",那么 OWL 解决的就是:这些图里的概念、关系、约束和规则,到底是什么意思。

你在 OWL 里不只是定义 CustomerOrderProduct, 你还可以定义:

  • 谁是谁的子类
  • 哪些类彼此互斥
  • 某个关系的起点和终点应该是什么
  • 一个概念由哪些条件"定义出来"
  • 满足什么条件后可以被自动归类
    RDF 提供"事实如何表达"的图模型,OWL 提供"这些事实背后的语义如何定义和推理"的语言。

语法结构:

  • 基于 RDF/RDFS,支持更复杂的逻辑表达(如等价类、属性限制、唯一性约束)
  • 核心构造:
    • owl:equivalentClass:声明两个类等价
    • owl:Restriction:定义属性约束(如 owl:allValuesFrom, owl:someValuesFrom
    • owl:InverseOf:声明属性的反向关系
    • owl:hasKey:定义唯一标识符

详细示例:

turtle 复制代码
@prefix ex: <http://example.org/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# 等价类声明
ex:Human owl:equivalentClass ex:Mammal .  
# 属性限制:定义"仅包含哺乳动物的动物是哺乳动物"
ex:Animal a owl:Class ;
    owl:equivalentClass [
        a owl:Restriction ;
        owl:onProperty ex:hasMember ;
        owl:allValuesFrom ex:Mammal
    ] .

# 属性反向关系
ex:hasParent owl:inverseOf ex:hasChild .  

本体表达的基本元素

IRI

IRI是本体的全局标识符,IRI能在全局命名空间标识本体相关元素,包括类、属性或实例。借助全局命名空间,可避免不同本体间的命名冲突,确保知识在分布式语义网中被无歧义地引用与共享,例如本案例电商的命名空间可以声明为"ec":

turtle 复制代码
@prefix ec: <http://example.org/semantic-rag/ec#> .

类别(Class)

类别是对事物的分类。药物动物城市 都是类别。类别是对一组具有某些共同特征的实例的描述。类别以层级结构排列:狗是动物 的子类,动物又是生物 的子类。

类是本体模型中的名词。

实例(Individuals)

实例是属于某个类的特定事物。狸花猫是猫类的一个实例。深圳是城市类的一个实例。这是数据层------关于世界的具体事实。

属性

在OWL语法中,属性是非常重要的概念。它有两个作用,一个是定义类别之间的关系,一个是定义类的和特性。

通常明确划分为对象属性(Object Properties)和数据属性(Data Properties)。

对象属性(Object Properties)

定义:用于建立个体与个体之间(或实例与实例之间)的关系,其取值是另一个本体实例。

特征 说明
连接对象 个体(Individual)→ 个体(Individual)
定义域/值域 通常是某个类(Class/Concept)
可具有的特性 传递性、对称性、反对称性、自反性、函数性等
逆属性 可以定义逆关系(如 hasPartisPartOf

示例:

  • hasAuthor(某本书 → 某位作者)
  • locatedIn(某企业 → 某城市)
  • isFriendOf(张三 → 李四)
数据属性(Data Properties)

定义:用于描述个体的具体数据特征,其取值是字面量(Literal),即具体的数据值。

特征 说明
连接对象 个体(Individual)→ 数据值(Literal)
值域类型 数据类型(如字符串、整数、日期、布尔)
定义域 通常是某个类
不可有逆属性 字面量不是个体,因此不能建立双向关系

示例:

  • hasAge(张三 → 整数 25)
  • hasName(某产品 → 字符串 "iPhone 15")
  • foundedDate(某公司 → 日期 "1987-06-05")

公理(Axioms)

公理有是约束和定义本体的逻辑陈述。公理是显式定义的逻辑约束,可以理解为一种"先验知识"。公理使我们能够根据本体结构自动推断新的事实。

公理既可以基于类进行陈述也可以基于实例(事实)

公理的类型和示例:

公理类型 OWL语法示例 推理层级
类层级关系 Dog SubClassOf Animal TBOX
等价类定义 DogOwner EquivalentTo Person and hasPet some Dog TBOX
属性限制 Person EquivalentTo (hasPet some Dog) and (hasAge max 100) ABOX
个体断言 Fido Type Dog ABOX
在上述自公理中,SubClassOf表示"是一类",EquivalentTo表示"等价于",Type表示"是",haspet表示"宠物是",hasage表示"年龄是"。

推理(Reasoning)

推理是利用已有的知识和规则(本体模式)自动得出新事实或结论的过程。

要理解推理就要理解OWL规范中的TBOX和ABOX。

在本体工程中,本体及其描述逻辑通常被划分为两个主要组成部分:TBoxABox

TBox(Terminological Box)

TBox 是本体的"模式层"。TBox定义了有意义的领域术语,包括领域内的结构、概念以及概念之间的逻辑关系。

  • 内容:类、属性以及公理。
  • 逻辑本质:它规定了"什么是真"。
  • 示例
    • "男人是人中的一个子类。"
    • "每辆车必须至少有三个轮子。"
    • "父母是拥有孩子的人。"

ABox

ABox 是本体的"实例层"。

ABox是实例的一句话事实陈述,包括了属性、状态或所属类别。

  • 内容:实例以及这些实例所属的类或它们之间的关系。
  • 逻辑本质:它陈述了"现实世界中发生了什么"。
  • 示例
    • "张三是一个男人。"(概念断言)
    • "张三拥有那辆车。"(关系断言)

推理是通过 TBox 中的规则去检查或扩充 ABox 中事实的过程。例如:

  • TBox 规则:"所有年满60周岁的旅客都属于'铁路重点旅客/静音车厢优先服务对象'(可自动享受购票优先分配下铺等便利)。"
  • ABox 事实:"张阿姨今年68岁,购买北京南至上海虹桥的高铁票。"
  • 推理结果:系统自动推断"张阿姨属于铁路下铺优先分配对象",因此在出票时会自动为她分配下铺(即使她自己没有勾选任何特殊服务选项)。

使用OWL表达本体的完整例子

1.定义类别

复制代码
Ontology: <http://example.org/ontology>
Class: Animal
Class: Dog SubClassOf: Animal
Class: Person
Class: City

说明:

http://example.org/ontology声明本体的唯一标识符(IRI),相当于本体的"身份证号",用于区分不同本体。

SubClassOf表明DogAnimal的一个子类

2.定义实例

owl 复制代码
Individual: Fido Types: Dog
Individual: London Types: City
Individual: Alice Type Person, hasPet Fido, hasName "Alice"

说明:

Types定义了实例所属的类型,Individual: Fido Types: Dog 表示 Fido 是类 Dog 的实例。

hasPethasname是实例Alice的属性。

3.定义属性

owl 复制代码
ObjectProperty: hasPet
    Domain: Person
    Range: Dog
DataProperty: hasName
  Domain: Person
  Range: xsd:string

说明:

Domain规定哪些类的实例可以拥有该属性型,Domain: Person表示只有Person类的个体(如 Alice)才能使用hasName属性。

Range规定属性值的类型,Range: Dog表示haspet的取值类型必须是DogRange: xsd:string表示hasName的值必须是字符串(如"Alice")。

4.定义公理

owl 复制代码
Class: DogOwner Equivalent To: Person and hasPet some Dog

说明:

上述公理的含义是DogOwner(狗主人)必须是一个至少一条狗的人。

5.产生推理

owl 复制代码
Individual: Alice
    Types: 
        Person,
        DogOwner [Inferred]  # 这是推理机自动推导出来的隐性类型!
    Facts: 
        hasPet  Fido,
        hasName "Alice"

说明:我们在第四部分定义了 DogOwner(狗主人)的公理。虽然我们在声明 Alice从未显式写过 Alice Types: DogOwner,但推理机能够通过逻辑规则,自动把 Alice 归类到 DogOwner 类别。

电商案例V2.0

V1.0 完成的功能

1. 数据层

Neo4j 中 seed 了 9 个中国运动鞋产品,包括数据:

  • 品牌:安踏、李宁、361
  • 颜色:黑色、白色、红色、蓝色、灰色、绿色
  • 特性:透气、缓震、防滑、轻便、耐磨、包裹性
  • 尺码:36-45 标准鞋码
  • 价格:人民币 ¥100-¥900
  • 产品示例:安踏 KT9 篮球鞋、李宁驭帅18 篮球鞋等

2. 语义层

  • 硬编码本体;
  • 正则规则识别用户查询,包括品牌、颜色、特性、尺码、价格、发货时间
  • 场景映射:跑步→透气/轻便/缓震,篮球→包裹性/缓震/耐磨/防滑 等

3. 查询层

精确 Cypher 查询,支持品牌、颜色、特性、尺码、价格、发货时间过滤;

4. 编排层

  • LangGraph 5 节点流水线:
    1. resolve_intent(语义解析)
    2. validate_constraints(约束校验)
    3. query_knowledge_graph(查 Neo4j)
    4. build_prompt(组装 Grounded Prompt)
    5. generate_response(LLM 生成)
  • 支持语义层开关控制
  • 支持模型切换(gpt-oss-120b / gemini3-flash)

5. 前端界面

  • Streamlit 实现
  • 聊天式交互
  • 产品卡片展示
  • 流水线追踪面板
  • 语义层开关、模型选择器

V1.0 的痛点:

  • 本体概念是 Python 字典里的字符串,无法跨系统复用
  • Neo4j 节点用 name 字符串匹配,容易出现同名不同义、同义不同名
  • 没有形式化语义,无法做标准推理(上下位、等价、互斥)
  • 领域规则被困在代码里,新增场景需要改 Python

V2.0 要解决的问题

让本体从"代码里的注释"变成"可加载、可推理、可共享的形式化知识资产"。

  • 真正本体模型:硬编码本体迁移到 OWL/TTL 文件
  • Neo4j 节点增加 iri 属性,与本体概念一一对应
  • Cypher 查询从字符串匹配升级为 IRI 匹配
  • 保持现有正则解析和 Pipeline 不变(向后兼容)

三、总体架构

#mermaid-svg-C5IMwotePem8NRIB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C5IMwotePem8NRIB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C5IMwotePem8NRIB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C5IMwotePem8NRIB .error-icon{fill:#552222;}#mermaid-svg-C5IMwotePem8NRIB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C5IMwotePem8NRIB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C5IMwotePem8NRIB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C5IMwotePem8NRIB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C5IMwotePem8NRIB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C5IMwotePem8NRIB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C5IMwotePem8NRIB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C5IMwotePem8NRIB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C5IMwotePem8NRIB .marker.cross{stroke:#333333;}#mermaid-svg-C5IMwotePem8NRIB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C5IMwotePem8NRIB p{margin:0;}#mermaid-svg-C5IMwotePem8NRIB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C5IMwotePem8NRIB .cluster-label text{fill:#333;}#mermaid-svg-C5IMwotePem8NRIB .cluster-label span{color:#333;}#mermaid-svg-C5IMwotePem8NRIB .cluster-label span p{background-color:transparent;}#mermaid-svg-C5IMwotePem8NRIB .label text,#mermaid-svg-C5IMwotePem8NRIB span{fill:#333;color:#333;}#mermaid-svg-C5IMwotePem8NRIB .node rect,#mermaid-svg-C5IMwotePem8NRIB .node circle,#mermaid-svg-C5IMwotePem8NRIB .node ellipse,#mermaid-svg-C5IMwotePem8NRIB .node polygon,#mermaid-svg-C5IMwotePem8NRIB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C5IMwotePem8NRIB .rough-node .label text,#mermaid-svg-C5IMwotePem8NRIB .node .label text,#mermaid-svg-C5IMwotePem8NRIB .image-shape .label,#mermaid-svg-C5IMwotePem8NRIB .icon-shape .label{text-anchor:middle;}#mermaid-svg-C5IMwotePem8NRIB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C5IMwotePem8NRIB .rough-node .label,#mermaid-svg-C5IMwotePem8NRIB .node .label,#mermaid-svg-C5IMwotePem8NRIB .image-shape .label,#mermaid-svg-C5IMwotePem8NRIB .icon-shape .label{text-align:center;}#mermaid-svg-C5IMwotePem8NRIB .node.clickable{cursor:pointer;}#mermaid-svg-C5IMwotePem8NRIB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C5IMwotePem8NRIB .arrowheadPath{fill:#333333;}#mermaid-svg-C5IMwotePem8NRIB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C5IMwotePem8NRIB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C5IMwotePem8NRIB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C5IMwotePem8NRIB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C5IMwotePem8NRIB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C5IMwotePem8NRIB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C5IMwotePem8NRIB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C5IMwotePem8NRIB .cluster text{fill:#333;}#mermaid-svg-C5IMwotePem8NRIB .cluster span{color:#333;}#mermaid-svg-C5IMwotePem8NRIB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C5IMwotePem8NRIB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C5IMwotePem8NRIB rect.text{fill:none;stroke-width:0;}#mermaid-svg-C5IMwotePem8NRIB .icon-shape,#mermaid-svg-C5IMwotePem8NRIB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C5IMwotePem8NRIB .icon-shape p,#mermaid-svg-C5IMwotePem8NRIB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C5IMwotePem8NRIB .icon-shape .label rect,#mermaid-svg-C5IMwotePem8NRIB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C5IMwotePem8NRIB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C5IMwotePem8NRIB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C5IMwotePem8NRIB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户查询
正则解析
本体字典
IRI 解析
Neo4j IRI 查询
LLM 生成

V2 在 V1 的流水线中插入 IRI 解析层IRI 查询层

  1. 正则解析:从用户查询中提取关键词,例如"黑色""透气""跑步"。
  2. 本体字典:把关键词映射到领域概念,例如"黑色"→Color、"透气"→Feature、"跑步"→Scene。
  3. IRI 解析 :通过 ontology/loader.py 查询 OWL/TTL 文件,把概念转换为全局唯一的 IRI。例如"透气"→http://example.org/semantic-rag/ec#Breathable,并生成 Neo4j 中存储的短 IRI ec#Breathable。这一步让概念从字符串变成可共享、可推理的形式化标识。
  4. Neo4j IRI 查询 :Cypher 查询从字符串匹配改为 IRI 匹配。例如颜色过滤从 toLower(col.name) = toLower($color) 升级为 col.iri IN $color_iris,产品特征过滤从 feat.name IN $features 升级为 f.iri IN $feature_iris。这样可以避免同名不同义、同义不同名的问题,同时让查询结果直接携带 IRI 供下游追踪。
  5. LLM 生成:基于图谱返回的已验证事实生成自然语言回复。

关键改造点

1. 新增 OWL/TTL 形式化本体

文件ontology/ecommerce.ttl

定义内容:

类型 示例
ec:Productec:Brandec:Colorec:Featureec:Sizeec:Categoryec:ShippingOption
子类 ec:BasketballShoe ⊑ ec:Sneakerec:RunningShoe ⊑ ec:Sneaker
属性 ec:hasBrandec:hasColorec:hasFeatureec:hasSizeec:hasPriceec:hasShippingDays
实例 ec:ANTAec:LI-NINGec:361ec:Blackec:Breathableec:Size42
约束 ec:hasFeature 的 domain 是 ec:Product,range 是 ec:Feature

命名空间与 IRI 设计:

turtle 复制代码
@prefix ec: <http://example.org/semantic-rag/ec#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

IRI 采用 前缀 + domain + name 结构:

  • 完整 IRI:http://example.org/semantic-rag/ec#Breathable
  • Neo4j 中存储的短 IRI:ec#Breathable(仅保留 domain + name,前缀由企业统一配置)
  • 本项目的 domain 为 ec(e-commerce 缩写)

这种设计的核心意义:

  • 领域边界清晰ec#Breathablefinance#Account 不会冲突
  • 企业级统一:前缀可在配置中心管理,不同系统共享 domain
  • 向后兼容:后续新增 domain 无需改动 Neo4j schema
  • 为推理和多跳查询奠基:所有概念都有全局唯一标识

2. 新增本体加载器

新增 ontology/loader.py,用 rdflib 加载 TTL 文件,提供标签到 IRI、IRI 到标签、场景到特征等映射接口,作为语义层和 Neo4j 之间的翻译层。

3. 修改 Neo4j seed 数据

给 Product、Feature、Color、Size、Brand、Category 节点增加 iri 属性,采用短 IRI 格式如 ec#Breathable,与 TTL 中的概念一一对应。

4. 修改 Cypher 查询

查询条件从字符串匹配改为 IRI 匹配,例如颜色过滤从 toLower(name) = toLower($color) 升级为 col.iri IN $color_iris,避免同名歧义。

5. 修改语义层参数转换

ResolvedIntent 新增 to_iri_params() 方法输出 IRI 列表,同时保留 V1 的 to_kg_params() 作为 fallback。

6. 修改编排层调用

Pipeline 优先使用 IRI 参数查询 Neo4j,失败时自动回退到 V1 字符串模式,保持 5 节点架构不变。

未完待续...