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 里不只是定义 Customer 、 Order 、 Product, 你还可以定义:
- 谁是谁的子类
- 哪些类彼此互斥
- 某个关系的起点和终点应该是什么
- 一个概念由哪些条件"定义出来"
- 满足什么条件后可以被自动归类
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) |
| 可具有的特性 | 传递性、对称性、反对称性、自反性、函数性等 |
| 逆属性 | 可以定义逆关系(如 hasPart ↔ isPartOf) |
示例:
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。
在本体工程中,本体及其描述逻辑通常被划分为两个主要组成部分:TBox 和 ABox。
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表明Dog是Animal的一个子类
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 的实例。
hasPet和hasname是实例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的取值类型必须是Dog, Range: 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 节点流水线:
- resolve_intent(语义解析)
- validate_constraints(约束校验)
- query_knowledge_graph(查 Neo4j)
- build_prompt(组装 Grounded Prompt)
- 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 查询层:
- 正则解析:从用户查询中提取关键词,例如"黑色""透气""跑步"。
- 本体字典:把关键词映射到领域概念,例如"黑色"→Color、"透气"→Feature、"跑步"→Scene。
- IRI 解析 :通过
ontology/loader.py查询 OWL/TTL 文件,把概念转换为全局唯一的 IRI。例如"透气"→http://example.org/semantic-rag/ec#Breathable,并生成 Neo4j 中存储的短 IRIec#Breathable。这一步让概念从字符串变成可共享、可推理的形式化标识。 - Neo4j IRI 查询 :Cypher 查询从字符串匹配改为 IRI 匹配。例如颜色过滤从
toLower(col.name) = toLower($color)升级为col.iri IN $color_iris,产品特征过滤从feat.name IN $features升级为f.iri IN $feature_iris。这样可以避免同名不同义、同义不同名的问题,同时让查询结果直接携带 IRI 供下游追踪。 - LLM 生成:基于图谱返回的已验证事实生成自然语言回复。
关键改造点
1. 新增 OWL/TTL 形式化本体
文件 :ontology/ecommerce.ttl
定义内容:
| 类型 | 示例 |
|---|---|
| 类 | ec:Product、ec:Brand、ec:Color、ec:Feature、ec:Size、ec:Category、ec:ShippingOption |
| 子类 | ec:BasketballShoe ⊑ ec:Sneaker、ec:RunningShoe ⊑ ec:Sneaker |
| 属性 | ec:hasBrand、ec:hasColor、ec:hasFeature、ec:hasSize、ec:hasPrice、ec:hasShippingDays |
| 实例 | ec:ANTA、ec:LI-NING、ec:361、ec:Black、ec:Breathable、ec: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#Breathable与finance#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 节点架构不变。
未完待续...