我的本体论理解

——从"人月聊IT"出发,谈本体如何让业务知识成为系统的第一公民


一、我理解的世界

软件行业有个老笑话:程序员最痛苦的事,是需求改了,但代码已经写完了。更痛苦的事是,代码改完了,系统里还有三处依赖这个逻辑没改。更更痛苦的事是,测试通过了,上线了,业务人员说"这不是我要的"。

问题的根源只有一个:业务知识和系统实现之间,隔着一层翻译。 业务人员说"这个人借调去了北京",程序员要在数据库里找到一个叫 current_region_id 的字段,然后在三个 service 里改五处逻辑。这个翻译过程一旦出错,系统就会在运行时以 bug 的形式暴露出来——而这时候,业务已经发生了。

我一直在想,有没有一种方式,让业务知识本身成为系统的一部分?不是注释,不是文档,而是系统能够理解、验证、执行的东西?

这个方向,就是本体论。


二、什么是本体论

本体论(Ontology)本来是哲学的概念,研究"存在"本身。但在软件工程里,它有了新的含义:用形式化的语言,精确描述一个业务领域里有什么(对象)、能做什么(行为)、必须遵守什么(规则)、如何协作(流程)。

类比一下:传统软件设计,是先写代码,然后通过注释和文档来解释"为什么要这样写"。而本体驱动的做法,是先写业务的形式化定义(也就是 YAML 模型文件),然后由运行时引擎自动生成代码、API、数据库结构、事件订阅。

业务人员/领域专家
        ↓  编写 YAML 本体模型(用业务语言,不是代码)
本体模型(YAML)
        ↓  运行时引擎解析执行
系统行为(API、数据库、事件)

这个转变的意义在于:业务知识第一次成为了系统的第一公民,而不是第三公民。


三、本体论解决什么问题

3.1 业务知识不再需要"翻译"

传统开发中,产品经理写的需求文档,程序员要花大量时间理解、质疑、翻译成代码。这个翻译过程丢失信息,而且翻译质量参差不齐。

本体模型用结构化的 YAML 描述业务对象和关系。拿我们的 HR 系统来说,“区域”“人员”“薪资方案”"借调单"这些概念,在 YAML 里就直接叫这个名字,而不是某个数据库表里一个叫 region_code 的 VARCHAR 字段。业务人员和程序员看到的是同一种语言。

3.2 规则变更不需要改代码

HR 系统里有一条业务规则:社保缴纳基数必须在当地政府规定的上下限之间。传统做法是在代码里写一个 CLAMP(grossSalary, baseLower, baseUpper) 的函数,然后散落在各处。

本体驱动的做法是:把这条规则写进 M3-Rule.yaml,注明它适用于哪些场景(薪资计算),注明它的输入参数(grossSalary, baseLower, baseUpper)和计算逻辑。运行时引擎读取这条规则,在薪资计算时自动执行。

如果某天政府调整了社保基数,程序员不需要找代码,只需要在 YAML 里改两个数字,重启服务。

3.3 系统行为可以被"元编程"

因为 YAML 是系统的输入,系统本身就可以根据输入的不同,生成不同的行为。同一个运行时引擎,读入 HR 行业的本体模型,就生成 HR 系统;读入电商的本体模型,就生成电商系统。系统的行为由业务定义决定,而不是由代码决定。

这在概念上类似于数据库 schema 的作用——你改了 schema,数据结构就变了。但本体论更进一步:它不仅定义数据,还定义行为、规则、流程、权限、补偿。

3.4 业务知识可以被验证

本体模型在运行前可以被静态验证:比如一个规则引用了某个不存在的字段,系统在启动时就能报错,而不是等到运行时才发现 salary 计算出来是个 null。

同样重要的是,本体模型可以成为业务人员和程序员之间的共同语言,减少"需求理解不一致"导致的返工。


四、我们走过的弯路和现在的方向

4.1 V1.0 的教训

我们第一版尝试用一个扁平结构,把所有东西塞进一个大 YAML 文件里,然后用一堆 if-else 来处理不同场景。结果是:YAML 变得和代码一样复杂,而且一旦出问题,根本不知道去哪里找。

4.2 V2.0 的改进

我们把一个大 YAML 拆成了多个(M1、M2、M3…),引入了更清晰的模型边界。但结构还是扁平的——解析器只是一堆函数,没有分层的概念,测试也难写。

4.3 V3.0 的思路:五层架构 + 事件驱动

现在的方向是两个核心决策:

决策一:五层架构。

把系统分成五层,每一层只关心自己的事:

L1 基础层:日志、配置、错误定义
L2 基础设施层:数据库连接、Kafka、Redis
L3 领域服务层:聚合根操作、规则引擎、事件路由
L4 运行时引擎层:读 YAML,生成数据库结构、注册路由、编译规则
L5 应用层:HTTP 接口、认证鉴权、请求处理

为什么要分层?因为业务越做越复杂。薪资计算链条跨了考勤聚合、薪资批次聚合、社保方案聚合,规则执行又需要读 Redis 的幂等性记录。如果这些逻辑混在一起,出问题时根本没法定位。分层后,每个问题都知道去哪一层找。

决策二:事件驱动(Transactional Outbox)。

传统做法是"更新数据库,然后发一条 Kafka 消息"。问题是:如果数据库更新成功了,但 Kafka 发失败了,数据就不一致了——财务显示工资已算,但事件没有发出去,下游系统不知道。

Transactional Outbox 的做法是:在同一个数据库事务里,同时写入业务数据表和一张"发件箱"表(Outbox)。Kafka 的转发进程读取发件箱,发送成功后标记已发送。这样,要么都成功,要么都失败,没有中间状态。


五、我们现在的本体模型体系

我们定义了 8 个模型(8-Model System):

模型 回答什么问题 举例子
M1 对象模型 业务里有哪些东西,它们什么关系? 区域、人员、项目、借调单、薪资批次
M2 行为模型 业务能做什么? 创建借调单、提交考勤、计算薪资
M3 规则模型 业务必须遵守什么? 社保基数必须在上下限之间、借调天数必须大于0
M4 场景模型 业务流程怎么走? 考勤提交 → 触发薪资批次创建 → 计算 → 审核 → 发放
M5 主体模型 谁可以做什么? 财务可以计算薪资,区域管理员可以审批借调
M6 补偿模型 失败了怎么办? 薪资发放失败 → 回退到已审核状态 + 通知财务
M7 质量模型 系统要做到什么程度? 500人薪资计算必须在30秒内完成
ME 事件模型 跨聚合的信息怎么流通? 考勤提交财务 → 触发薪资批次自动创建

这 8 个模型合在一起,就是一个 HR 业务的完整"描述文件"。运行时引擎读取这个描述文件,生成对应的数据库表、API 路由、规则执行逻辑、事件订阅关系。


六、我们想达到的结果

说到底,我想达到三个结果:

第一,业务人员能够直接看懂系统在做什么。

打开 M1-Object.yaml,能看到"人员"有哪些属性;打开 M2-Behavior.yaml,能看到"借调"这个操作需要什么权限、会产生什么事件。程序员和业务人员看到的是同一份描述,不会再有"需求理解不一致"的问题。

第二,规则变更不需要动代码。

如果明天社保政策变了,程序员只需要改 M3-Rule.yaml 里的两行,重启服务。不需要找代码、读代码、改代码、测代码。

第三,系统行为可以被追溯和补偿。

因为所有业务操作都通过事件链驱动,系统的任何状态变更都有迹可循。如果薪资计算时数据库和 Kafka 之间出现了不一致,Outbox 机制保证不会丢事件;如果借调激活失败了,Saga 补偿机制把相关状态回退到一致状态。


七、这条路对不对,我不确定

说了这么多,我必须承认一件事:我不知道这条路最终是不是对的。

本体驱动的好处,在概念上讲起来很美。但它的代价是:系统的前期投入很大,引擎本身需要有人维护,团队需要理解这套新的方法论。

如果业务相对稳定、不需要频繁调整规则,传统的 CRUD + 代码方式,可能反而更高效。

但如果业务的特点是:

  • 规则经常变(政策、行业需求)
  • 跨聚合的业务流程复杂(HR、财务、项目管理)
  • 需要完整的审计追溯(金融、医疗、HR)

那本体驱动的方法,能够让业务知识真正成为系统的一部分,而不是藏在代码注释里等它过时。

这是我现在的理解。路对不对,走着看。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐