
大家好,我是人月聊IT。今天继续聊本体论建模。
因为最近写本体论方面的文章比较多,包括前面也分享了一个基于已经有的本体元模型,通过AI编程的方式输出一个可视化的本体语义知识网络图谱。感兴趣的可以参考我前面的文章,提供了源代码下载方便大家参考。
当然这个仅仅是简单的可视化展示的示范,还是有不少朋友问当前有无一些开源的本体论建模工具可以参考。最近也查找了不少资料,找了一个斯坦福大学开源的本体建模工具。Protégé 是一个免费的开源平台,为不断增长的用户社区提供一套工具,用于构建领域模型和基于本体的知识应用。
具体软件下载地址:
https://protege.stanford.edu/software.php

感兴趣的可以自己可以下载安装试用。运行Run命令即可。如果本机没有JDK或JRE运行环境,需要你提前安装。具体的帮助文件可以参考:
https://protegeproject.github.io/protege/getting-started/
当然该软件也提供了一个在线体验环境,注册账号后可试用。具体的在线体验环境地址:
https://webprotege.stanford.edu/

接着我们再看下其官网介绍内容如下:
Protégé Desktop 是一款功能丰富的本体编辑环境,完整支持 OWL 2 Web 本体语言,并可与 HermiT 和 Pellet 等描述逻辑推理机进行直接的内存连接。
Protégé Desktop 支持在单一工作空间中通过完全可定制的用户界面创建和编辑一个或多个本体。可视化工具支持本体关系的交互式导航。高级解释功能有助于追踪不一致性问题。提供重构操作,包括本体合并、在本体间移动公理、批量重命名多个实体等。
主要特性:
在下载安装了这个桌面端的本体建模工具后,我们运行Run命令启动。注意进入系统后,我们可以选择文件菜单下的Open From URL,然后选择一个示范的URL例子的本体模型进行打开,方便我们参考学习。比如我们打开一个系统已经具备的关于披萨的本体模型如下:

注意导入后,我们点击Entity这个Tab页签,并进一步切换到Class可以看到该实体下具体的类层次和类关系图。具体如下:

当然在实体定义下面本身还分为类图,对象属性,数据属性,实例个体等进一步的扩展信息定义。
对于类和对象实例的定义大家相对来说容易理解。接着我们看下Object Property和Data Property这两个标签页。一个是对象属性,一个是数据属性。标准解释如下:对象属性连接的是两个个体(实例)之间的关系,数据属性连接的是个体与具体值(字符串、数字、日期等)之间的关系。
如果映射到数据库设计大家可能更加容易理解,即对象属性定义的是数据库表之间的主外键关联依赖关系。比如一个合同要从从属一个客户,一个合同有对应的负责人。而数据属性则是定义类里面本身的属性对象,类似合同包括了合同编号,合同名称,合同金额等信息。
有了这个基本理解后,我做了一件重要的事情。就是前面我有一个关于合同管理或合同这个本体模型的需求,具体如下:
类似我们一个简单的合同管理的原始需求如下:
#合同管理的业务场景,流程和关键业务对象描述
当前的合同系统只对销售合同进行管理,即我公司对外销售所产生的合同签订信息。当前对合同起草,签订和审批流程不进行管理。只是对合同最终签字盖章生效后进行管理。
合同应该包括了合同编号,合同名称,所属产品,所属客户,所属部门,合同签订时间,责任人,合同总金额,合同对外采购金额,合同税率,合同付款条款等信息。
其中所属产品,所属客户,所属部门,责任人应该分别引用独立的产品信息表,客户信息表,部门信息表和人员信息表。具体的各个表信息如下:
产品信息表:包括产品编号,产品类型,产品名称信息。
客户信息表:包括客户编号,客户类型,客户名称信息。
部门信息表:包括部门编号,部门名称信息。
人员信息表:包括人员编号,人员名称,所属部门信息。
由于一个合同可以客户可以分多次进行付款,因此合同付款条款是一个明细表。该表包括了付款阶段编号,付款阶段名称,付款比例几个关键信息。
当客户通知我方付款的时候,我方基于合同付款条款对应的付款阶段进行开票,然后客户根据开票进行付款。因此还有一个独立的开票明细信息表。该表包括了开票编号,对应合同编号,开票金额,开票税率,开票时间,是否收款,收款时间等关键信息。
由于合同的某一个付款阶段也可以分多次进行开票,多个付款阶段又可以一次进行开票。因此我们还需要构建一个开票信息和付款阶段的对应表,具体包括开票编号,合同编号,付款阶段编号三个字段。
#需要实现的业务功能描述
1.合同信息的录入,在录入合同基本信息的时候还需要录入合同条款信息。
2.合同开票信息的录入,录入合同开票信息,包括开票时间,开票金额,税率等关键信息。
3. 合同收款信息的录入,录入合同收款信息,收款需要对应到具体的合同,合同开票
4. 合同信息查询,需要基于合同编号,合同名称,产品线,部门,时间等对合同信息进行模糊查询。查询列表中可以点击查看该合同的详细信息。
现在我需要你基于我上面的思路,帮我在当前项目 Skills 目录夏生成这个技能包。注意先不用生成代码片段,而是先输出完整的模型定义,我需要对模型定义进行检查和确认。
那么我们写一段提示词,让大模型基于这个提示词帮我们生成一段符合OWL语义标注的完整本体模型定义。具体提示词:
我现在需要构建一个围绕合同为核心的本体模型,基于合同管理的原始需求。
具体模型构建要求如下:
很好,我现在需要你重复理解了这个元模型后,将其转换为一个符合OWL(Web Ontology Language)标注的OWL语义定义文件,输出到newdoc目录下面。
具体要求如下:
1. 输出一个符合OWL(Web Ontology Language)标注的OWL语义定义文件
2. 能够体现核心的类,子类,包括类之间的关系。
2. 能够模拟一些具体的实例
3. 能够体现相应的对象之间的行为,关系,规则等关键特点。
4. 本体中有详细的类,对象,数据,属性,属性定义规则等定义。同时形成相应的规则,关联约束等。
5. 尽量详细定义,不要丢失关键内容。
也就是我需要构建一个合同的语义本体,能够完全支撑我后续合同管理需求的实现。所以需要你仔细分析我的需求来构建这个本体。
有了这个提示词后,AI输出完整的OWL本体模型定义源代码文件如下:

接着我们把这个OWL文件导入到本体建模中。在导入后如下:

注意在这个图左边可以看到完整的类层次结构展开。
右边红色部分我们可以进一步定义规则,包括单个属性的参考完整性规则,也包括了对象间的依赖规则。绿色框是基于这个类的对象实例参考。
在对象属性中我们可以看到关键的关系定义。

比如所属产品这个对象属性定义,其主域是合同对象,其范围关联是产品对象。也就是所属产品体现了一个合同究竟属于哪个产品这个关系连接。那么对象属性这个关系和数据库设计中的主外键关系什么区别?
主要差异
1. 数据库是单向的,OWL 可以双向
数据库外键只能从子表指向主表,查反方向需要写 JOIN。OWL 里可以定义反向属性,推理器自动维护两个方向:
hasPaymentTerm(合同→付款条款)
↕ inverseOf
belongsToContract(付款条款→合同)
你只插入一个方向的事实,另一个方向推理器自动推断出来。
2. 数据库靠约束报错,OWL 靠推理得结论
数据库外键违反了就报错拒绝写入。OWL 的域/值域违反了不报错,而是推理器从中推断出新的类型归属,这是前面提到过的特点。
3. OWL 可以描述属性的逻辑特征,数据库不行
比如你的本体里用到的:
FunctionalProperty(函数属性)
→ 相当于数据库的唯一外键,一对一关系
inverseOf(反向属性)
→ 数据库没有原生对应,需要手写 JOIN
owl:minCardinality / maxCardinality
→ 类似数据库的非空约束和唯一约束,但表达能力更强
4. OWL 支持推理,数据库只能查已有数据
数据库只能查你存进去的数据。OWL 推理器可以根据已有事实推断出新事实,比如你的本体里:存在未收款开票 → 自动推断该合同属于 UnreceivedInvoiceContract 类,不需要手动标记。
一句话总结
对象属性 ≈ 数据库外键 + 双向导航 + 逻辑推理能力 数据库建模解决"数据怎么存",OWL 本体解决"概念之间是什么关系、能推断出什么"。
所以你已有的数据库设计经验完全可以平移过来理解本体建模,只是 OWL 在语义表达上比数据库更强一层。
当然,本体建模另外一个核心还是规则。
在 OWL(Web 本体语言)语义模型中,定义“规则”主要分为两种方式:一种是通过 OWL 自带的描述逻辑(Description Logics)约束来实现“隐式”的规则制约;另一种是通过引入 SWRL(Semantic Web Rule Language) 来实现复杂的“显式” IF-THEN(如果-那么)逻辑推理。
结合您在此前 contract-ontology.owl文件中的内容,我为您详细解释这两种规则是如何定义的:
OWL 自身非常擅长通过类和属性的特性来定义约束。这类“规则”不需要写成 IF-THEN 的形式,而是直接固化在数据的结构定义中。当把数据输入到推理机(如 Pellet, HermiT)时,它会自动帮你检查冲突或推导出新关系。
常见的方式包括:
函数属性(FunctionalProperty):例如您的本体中定义 cm:belongsToDepartment 为 FunctionalProperty。这条规则意味着:“一个人员最多只能属于一个部门”。如果同一个人被分配了两个不同的部门,推理机会直接报错。基数与值域约束(owl:Restriction):您在 cm:Contract(合同类)中定义了:
<owl:Restriction>
<owl:onProperty rdf:resource="cm:hasCustomer"/>
<owl:cardinality rdf:datatype="&xsd;nonNegativeInteger">1</owl:cardinality>
</owl:Restriction>“每个签单的合同必须有且仅有 1 个客户”。甚至您还用到了 xsd:minInclusive 和 xsd:maxInclusive 来定义**“付款比例必须在 0.0 到 1.0 之间”**的规则。
OWL 的约束能力虽然强,但它无法表达复杂的条件关联(比如:“如果 A 和 B 发生了,那么推出 C”)。为了解决这个问题,W3C 引入了 SWRL (语义网规则语言)。
在您的 contract-ontology.owl文件的第 6 节中,就包含了原生的 SWRL 规则定义。
一条 SWRL 规则(在本体中用 <swrl:Imp> 标签表示)通常包含两个核心部分:
您的原始设计里有一条业务规则:“如果开票记录已标记为已收款(isPaymentReceived = true),那么它就必须有实收金额(receivedAmount)。”
它在 OWL XML 源码里的结构是这样的:
<swrl:Imp>):xml<swrl:Imprdf:about="cm:Rule_InvoiceReceivedAmountRequired">?inv 必须是一个“开票记录” (<swrl:ClassAtom>)。?inv 的数据属性 isPaymentReceived 必须为 true (<swrl:DatavaluedPropertyAtom>)。?inv 必定存在数据属性 receivedAmount(并赋值给变量 ?amt)。人类可读的 SWRL 简写语法: 如果您使用 Protégé 等可视化构建工具(借助 SWRL Tab 插件),这条 XML 规则在界面上呈现的是非常简洁的逻辑表达式:
InvoiceRecord(?inv) ^ isPaymentReceived(?inv, true) -> receivedAmount(?inv, ?amt)SWRL 支持的四种测试原子(Atom)
要在 SWRL 中组合复杂的条件,一般会组合使用以下四块“拼图”:
CustomProduct(?p))invoiceAmount(?inv, 10000))hasResponsiblePerson(?contract, ?person))swrlb:greaterThan(?amount, 50000),判断金额大于 5 万)owl:Restriction 定义。swrl:Imp 通过定义 Body 和 Head 原子来实现。接着我问了第2个关键问题,即:类似某个合同如果收款全部完成,触发合同对象实例状态为已完成,这种是否可以在 OWL中定义。如果可以,给出定义的参考。
在 OWL 中实现**“状态触发或状态变更”**,和我们在 Java/数据库中编写的业务代码逻辑有一些不同。
答案是:可以实现,但需要转变一下思路。
在 OWL(语义网)的体系里,遵循**“开放世界假设(Open World Assumption)”且缺乏原生的数学聚合函数(如求和 SUM)。因此,我们通常不会去“修改”一个字段的值(比如把 status 字段由 '执行中' update 成 '已完成'),而是利用“分类推理(Classification)”或者 SWRL 规则,让推理机自动为这个合同打上“已完成合同 (CompletedContract)”的标签。**
这里为您提供三种在语义模型中实现该需求的参考方案:
因为纯 OWL/SWRL 无法把合同下的多笔“开票金额”或者“收款金额”进行 SUM 求和。所以在实际工程中,我们通常会在合同类 (cm:Contract) 上增加一个数据属性:cm:totalReceivedAmount(累计已收总金额)(这个值由外部系统计算后写入)。
然后,我们就可以写一条 SWRL 规则:当合同的“累计已收总金额” >= “合同总金额”时,推断它的状态为“已完成”,或者推断它属于“已完成合同”类。
SWRL 规则写法(人类可读格式):
Contract(?c) ^ contractTotalAmount(?c, ?total) ^ totalReceivedAmount(?c, ?received) ^ swrlb:greaterThanOrEqual(?received, ?total) -> CompletedContract(?c)(推断结果:凡是收款金额达标的合同,都会自动成为 CompletedContract 类的实例)
如果非要给一个状态字段赋值为 "Completed"(字面量):
Contract(?c) ^ contractTotalAmount(?c, ?total) ^ totalReceivedAmount(?c, ?received) ^ swrlb:greaterThanOrEqual(?received, ?total) -> hasStatus(?c, "Completed")如果您不需要算金额,而是业务定义为:“只要该合同名下的所有开票记录,都已经标记为已收款(isPaymentReceived = true),那么它就是已完成合同。”
可以在 OWL 中定义一个等价类(Equivalent Class)。这种方式纯粹依靠 OWL 自身的描述逻辑,不需要写 SWRL。
OWL/XML 定义参考:
<!-- 定义一个“已完成合同”类 -->
<owl:Class rdf:about="cm:CompletedContract">
<rdfs:label xml:lang="zh">已完成合同</rdfs:label>
<!-- 充分必要条件(EquivalentTo) -->
<owl:equivalentClass>
<owl:Class>
<owl:intersectionOf rdf:parseType="Collection">
<!-- 条件1:必须是一个合同 -->
<rdf:Description rdf:about="cm:Contract"/>
<!-- 条件2:关联的【所有(only)】开票记录,都必须来源于“已收款的开票” -->
<owl:Restriction>
<owl:onProperty rdf:resource="cm:hasInvoice"/>
<owl:allValuesFrom>
<owl:Restriction>
<owl:onProperty rdf:resource="cm:isPaymentReceived"/>
<owl:hasValue rdf:datatype="&xsd;boolean">true</owl:hasValue>
</owl:Restriction>
</owl:allValuesFrom>
</owl:Restriction>
</owl:intersectionOf>
</owl:Class>
</owl:equivalentClass>
</owl:Class>注意:由于 OWL 的开放世界假设,这种基于 allValuesFrom 的推断在没有闭包(Closure,声明该合同明确只有这几张发票,不会再有新的了)的情况下,推理机可能不敢轻易断言它。
因为 OWL 自身的限制,在真实业务落地时,处理此类“由于多笔收款累加达到阈值触发状态流转”的场景,更主流的做法是在三元组图数据库(如 GraphDB, Jena)层面,直接跑一个 SPARQL 构造(CONSTRUCT)或更新(UPDATE)语句充当规则引擎。
它可以轻松实现求和(SUM)以及状态变更:
sparql
PREFIX cm: <http://www.example.org/contract-management#>
# 插入“已完成”状态的 SPARQL 更新规则
INSERT {
?contract a cm:CompletedContract .
?contract cm:status "已完成" .
}
WHERE {
?contract a cm:Contract ;
cm:contractTotalAmount ?totalAmount .
# 统计该合同下所有已收款发票的总额
{
SELECT ?contract (SUM(?received) AS ?sumReceived)
WHERE {
?contract cm:hasInvoice ?invoice .
?invoice cm:isPaymentReceived true ;
cm:receivedAmount ?received .
}
GROUP BY ?contract
}
# 判断条件:收款总和 >= 合同总额
FILTER (?sumReceived >= ?totalAmount)}接着第3个关键问题,我一直理解事件定义在本体模型中相对重要。行为操作会导致对象状态变化,而对象状态的变化往往又产生或触发事件,这个事件被其它的本体对象所订阅,通过对事件的监听来触发下游的关键行为活动。
因此:类似一个对象状态变更,将触发另外一个对象行为,这种如何定义。一个对象状态变更实际我在软件开发里面会将其定义为一个独立的事件。
具体如下:
在软件开发(尤其是领域驱动设计 DDD 或事件驱动架构 EDA)中,我们将状态变更定义为“领域事件(Domain Event)”是非常标准的做法。而在 OWL(语义网本体定义)中,处理“动态状态变更”和“事件流转”与面向对象编程有本质的不同。
核心差异在于:
Update 操作,也不包含 Trigger 触发器机制。因此,如果想在 OWL 中定义“事件驱动”和“状态流转”,我们需要采用**事件本体(Event Ontology)**模式。
既然 OWL 擅长描述名词而不是动词,我们就可以把“行为(Action)”和“事件(Event)”本身定义为一类实体对象(Class)。
这与您在软件开发中的思路(将其定义为一个独立的事件对象)是完全一致的!
首先,我们在本体中创建一个 Event(事件)基类,并定义具体的事件子类。
<!-- 事件基类 -->
<owl:Class rdf:about="cm:Event">
<rdfs:label xml:lang="zh">生命周期事件</rdfs:label>
</owl:Class>
<!-- 具体事件:合同全部收款完成事件 -->
<owl:Class rdf:about="cm:ContractFullyPaidEvent">
<rdfs:label xml:lang="zh">合同全款收齐事件</rdfs:label>
<rdfs:subClassOf rdf:resource="cm:Event"/>
</owl:Class>
<!-- 具体行为:财务入账行为 -->
<owl:Class rdf:about="cm:FinanceBookingAction">
<rdfs:label xml:lang="zh">财务入账行为</rdfs:label>
<rdfs:subClassOf rdf:resource="cm:Event"/>
</owl:Class>
由于 OWL 本身不能“产生”新数据,如果是纯理论上的规则定义,我们可以用 SWRL 说:“只要系统中发生了一个 A 事件,那么逻辑上必定伴随一个被触发的 B 行为”。
人类可读的 SWRL 规则:
ContractFullyPaidEvent(?eventA) ^ involvesObject(?eventA, ?contract) -> FinanceBookingAction(?actionB) ^ <!-- 推理出必定存在一个入账行为 -->triggers(?eventA, ?actionB) ^ <!-- 是由事件 A 触发的 -->involvesObject(?actionB, ?contract) <!-- 该行为的客体也是这个合同 -->但是,在实际工程系统(OWL落地)中怎么做?
在真实的语义系统中(如金融风控、医疗诊断),纯靠 SWRL 推导一个“凭空生成的行为节点”并没有实际执行力(它只会告诉你图谱里多了几个点,但不能去调用外部 API)。
要想真的让系统跑起来,架构通常是:
知识图谱(描述静态语义规则) + 业务流引擎(执行动态变更)
您的本体主要职责是:定义状态,打标签(分类)。 然后由业务系统去“监听”这些因推理而产生的新标签,并执行真正的动作。
具体流程:
cm:Contract)在语义层推断分类为 cm:CompletedContract (已完成合同) 类别。CompletedContract 吗?”sparql
SELECT ?contract WHERE { ?contract rdf:type cm:CompletedContract }ContractCompletedEvent),并调用实际代码逻辑(比如:发邮件通知销售主管、在 ERP 里自动释放保证金、触发物流系统等)。您的直觉是极其准确的。无论是 OWL 语义建模,还是在微服务/DDD架构中,“把状态流转本身抽象为一个一等公民(Event 实体)”都是最高级、最解耦的做法。
区别仅在于:
<triggers> 等关系静态地连接着相关的合同对象。至于执行,则由包裹在图谱外层的业务代码来承载。最后我们再来看下该软件本身还带了基于DL Query语言的自动化推理功能。即我们可以编写DL语言执行查询,本体模型会自动基于模型今天推理。比如我们要查询合同总金额大于100的合同信息。类似DL Query如下。

因此大家可以参考我上面思路验证。
注意前面的建模并没有去实现本体模型的可视化。因此我们可以借助另外一个开源项目来实现,具体地址:
https://github.com/VisualDataWeb/WebVOWL
该开源项目有一个公网演示环境可以参考,地址:
https://service.tib.eu/webvowl/

通过这个地址,我们将OWL语义文件导入,可以得到合同本地的可视化展示效果。在该图中可以清楚地看到类,对象属性,数据属性,行为规则定义。具体参考图如下:

进一步的思路是如何构建一个完整的对象,行为,规则,数据,事件融合的本体模型。包括接着这个开源软件的思路,我们完全可以参考这个建模思路,采用自然语言的方式来构建这个模型,然后模型构建完成后可以让AI大模型来帮我们输出标注的OWL本体语言定义文件。
今天先分享到这里,进一步分享参考后续文章。