◈本体论工程手册
方法论

七 · 一个完整案例:五周与三个事故

18 个类、5 条约束、二十余人日、每周对账

前六章讲的是「应该怎么做」。这一章讲「实际怎么发生」——一家中型制造企业用五周时间,把三个反复出错的口径问题,做成一套能拒绝写入、能每周对账的操作本体。所有数字与产物都按真实项目的量级构造,命名为通用化处理,不涉及任何一家具体公司的数据。

选择供应链履约作为案例,是因为它同时具备三个特征:跨系统(ERP、WMS、QMS、BI 四套)、有监管(到货与质检要能追溯)、口径争议多(准时率、合格率、周转天数各自都有三种算法)。这正是本体能产生价值、也最容易被做成无用文档的场景。

起点:三个真实发生的事故

不是设想,是立项的直接原因。

事故一:同一个指标,三个部门三个值。 「准时到货率」在运营看板是 98.2%,供应链周报是 95.1%,财务的供应商考核是 91.7%。差异不是数据错误,是三个定义:运营按「承诺到货日」算、供应链按「订单确认日 + 标准交期」算、财务按「发票结算周期倒推」算,分母还分别是「全部订单」「非取消订单」「已收货订单」。

三个数都对。这就是本体要解决的第一类问题——不是算错,是没有唯一的口径定义。

事故二:被冻结的供应商仍在供货。 供应商冻结状态存在 ERP 的一个备注字段(自由文本),WMS 收货时看不到它。结果是一家因质量问题被冻结的供应商,在冻结后 11 天又送了两批货,且正常入库。

这是第二类问题——约束存在,但不在写入路径上。

事故三:新增物料分类后,三张报表静默少数据。 质管部新增了一个「返修件」分类,下游三张报表的过滤器里没有它,于是这部分数据被静默排除。没有人报错,两周后才发现某指标掉了 4 个点。

第三类问题——变更的影响面不可见。

三个事故对应本体的三种能力:唯一口径、门控写入、变更影响面。这三件事正好是这本书反复讲的三个价值点,所以这个案例的结构就是围绕它们展开。

第 0 周:20 条问题,不是 200 个类

建模的第一件事不是画类图,是写问句。团队花了三天,从三个部门收集了四十多条问句,去重合并成 20 条 competency question(CQ)。其中 10 条最关键:

#问句由谁提出
Q1上个月每个供应商的准时到货率是多少?口径是什么?供应链
Q2哪些订单的到货时间晚于承诺日超过 3 天?运营
Q3某个物料当前可用的供应商有几家,其中被冻结的是哪几家?采购
Q4每一批到货对应的质检结论是什么,谁签的字?质管
Q5某个供应商在过去 12 个月里被冻结过几次,原因是什么?合规
Q6某个指标的口径最近一次变更是什么时候,谁批的?财务
Q7如果我把「返修件」并入统计,哪些报表会变?质管
Q8订单的承诺到货日、实际到货日分别由哪个系统提供?数据团队
Q9哪些订单既没有质检结论也没有豁免记录?审计
Q10上周新增的约束拦下了多少条数据,都是什么类型?管理层

这 20 条问句做了三件事:定义了范围(只涉及订单、供应商、物料、批次、质检、指标六类对象)、暴露了争议(Q1 的三个口径必须选一个)、以及给出了验收标准(每一条后面都要能被一条查询回答)。

第 7 条(Q7)后来成了整个项目最有价值的一条——它把「影响面」从抽象概念变成了一个具体的、有人等着要的答案。

一个反直觉的发现:四十多条问句里,有十余条是「谁改的/什么时候改的/谁批的」。也就是说,近四分之一的需求是溯源,不是查询。这直接决定了后面的设计:所有变更必须留痕,且溯源信息要能被人读到,而不只是存在日志里。

第 1 周:18 个类、6 条关系、2 个枚举

范围定在六类对象,展开成 18 个类:

Supplier ──供应──> Material ──包含──> OrderLine
   │                                      │
   └─冻结记录─> FreezeEvent            PurchaseOrder ──到货──> Batch
                                              │                    │
                                              └─承诺/实际到货日      └─质检─> Inspection
                                                                          │
                                                                     Finding(缺陷项)

Metric(指标) ──定义于──> MetricDefinition ──变更──> DefinitionChange
   └── owner ──> OrgUnit
6 条关系:supplies / contains / arrivesAs / inspectedBy / definedBy / ownedBy
2 个枚举:FreezeReason(quality / compliance / financial / other)
          InspectionResult(pass / fail / conditional)

三个设计决定,每个都有代价

决定一:不用 OWL,用 SHACL + 少量 RDFS。 理由来自第 11 章的实测:OWL 在开放世界假设下不会因为「缺一个责任人」而报错,而 Q9 要的正是「哪些订单缺东西」。选 OWL 等于选了一个不能回答核心问题的工具。

代价:放弃推理。Q1、Q3 这类问题本来可以靠推理从「物料属于某类别」推出「该类别的供应商集合」,现在必须显式建模或在查询里 JOIN。团队接受了这个代价——因为这些问题用 SPARQL 一样能回答,而「拒绝写入」没有替代品。

决定二:指标的口径作为一等对象,而不是注释。 MetricDefinition 单独成类,带 numerator、denominator、timeBasis、version、approvedBy。这是本案例最关键的一步:把「准时到货率怎么算」从文档里搬进模型,让它有版本、有审批人、有生效日期。

代价:指标定义变更要走变更流程,比改一段 SQL 慢。这个代价正是价值所在。

决定三:冻结状态不在备注字段里,而是独立事件。 把 ERP 备注里的「冻结」抽成 FreezeEvent(供应商、原因、时间、操作人、解除时间)。这是一次数据迁移,花了 1.5 天。

代价:ERP 侧的备注字段仍然存在,两边会不同步。解决办法是单向同步 + 每周对账(第 4 周做)。

这三个决定合起来,就是第 10 章那个 A 形态:不引入图数据库、不引入推理机、不建新端点。模型是文件,校验是命令,对外只有一张视图和一份报表。

第 2 周:5 条约束,首跑约 4% 违规

第一批只写了 5 条约束。选择标准很功利:每条约束必须对应一个已经发生的事故或一条 CQ。

# C1 对应事故二:订单必须指向供应商与物料(引用完整性)
ex:OrderLineShape a sh:NodeShape ;
  sh:targetClass ex:OrderLine ;
  sh:property [ sh:path ex:supplier ; sh:minCount 1 ; sh:class ex:Supplier ;
                sh:message "订单行必须有供应商" ] ;
  sh:property [ sh:path ex:material ; sh:minCount 1 ; sh:class ex:Material ;
                sh:message "订单行必须有物料" ] .

# C2 对应事故二:已冻结供应商不得产生新批次(跨对象一致性)
ex:BatchShape a sh:NodeShape ;
  sh:targetClass ex:Batch ;
  sh:sparql [
    sh:message "批次来自已冻结供应商" ;
    sh:select """
      SELECT $this WHERE {
        $this ex:fromSupplier ?s .
        ?s ex:hasFreeze ?f .
        ?f ex:freezeEnd ?end .
        FILTER (?end > $this ex:arrivalDate || !BOUND(?end))
      }
    """ ] .

# C3 对应 Q9:质检结论必须引用批次与签字人
ex:InspectionShape a sh:NodeShape ;
  sh:targetClass ex:Inspection ;
  sh:property [ sh:path ex:inspects ; sh:minCount 1 ; sh:class ex:Batch ] ;
  sh:property [ sh:path ex:signedBy ; sh:minCount 1 ] .

# C4 对应事故一:指标定义必须有口径四要素(分子/分母/时间基准/审批人)
ex:MetricDefinitionShape a sh:NodeShape ;
  sh:targetClass ex:MetricDefinition ;
  sh:property [ sh:path ex:numerator ; sh:minCount 1 ] ;
  sh:property [ sh:path ex:denominator ; sh:minCount 1 ] ;
  sh:property [ sh:path ex:timeBasis ; sh:minCount 1 ] ;
  sh:property [ sh:path ex:approvedBy ; sh:minCount 1 ] .

# C5 枚举值越界(对应事故三的同源问题:分类值不规范)
ex:BatchShape sh:property [
  sh:path ex:freezeReason ;
  sh:in ( "quality" "compliance" "financial" "other" ) ;
  sh:message "冻结原因必须是四类之一" ] .

首跑结果

数据规模:订单行约 8,000、批次约 2.2 万、供应商约 300、物料约 5,000、质检约 1.9 万,合计约 50–60 万三元组。

pyshacl -s shapes.ttl data.ttl -f human --abort-on-first-error false
约束违规数占比判断
C1 引用完整性00%通过,但说明这层已被 ERP 外键保证
C2 冻结供应商仍有批次约 40约 0.2%真实问题,两个供应商 + 一次迁移遗漏
C3 质检缺签字人约 1,200约 6%规则错:历史数据(系统上线前)本来就没有签字人字段
C4 指标缺口径约 120100%规则错:MetricDefinition 是新类,全部新建,字段还没填
C5 枚举越界约 30约 0.1%真实问题:中英混用(质量/quality/Quality)

总违规约 1,400 条,占 4% 上下。但真正的问题只有六十余条(C2 + C5);其余绝大部分是规则写得不对。

两条教训

教训一:100% 违规几乎总是规则错。 C4 的一百多条全是新对象,字段天然为空。把「新对象的必填字段」写成「全量必填」,等于给自己造了一个永远红的流水线。正确做法:约束先作用在「生效日期之后创建的对象」上,历史对象走豁免。

教训二:枚举值必须先归一,再约束。 C5 报的三十来条里,有近一半是同一个值的三种写法。如果先改枚举定义再加约束,能省掉一轮返工。顺序是:归一 → 约束,反过来会得到一堆假阳性,然后团队会开始不信校验。

这两条经验后来被写进第 12 章「反模式」——它们听起来像常识,但几乎每个项目第一次都会踩。

修正后的第二轮:C3 加 sh:severity sh:Warning 并对生效日期前的数据豁免;C4 拆成两条(新对象必填 + 存量统计);C5 先跑了三天的归一脚本。二轮违规数降到六十余条,全部可解释、可指派。

第 3 周:让一个动作被拒绝

前两周的价值是「发现了六十余条坏数据」,这一周的价值是「让它不再发生」。

收货动作定义:

Action: ReceiveBatch(收货)
  参数:poId, batchNo, supplierId, materialId, qty, arrivalDate
  前置条件:
    P1  订单存在且状态为「已确认」
    P2  供应商当前未冻结(无未解除的 FreezeEvent)
    P3  批次号在该订单下不重复
    P4  materialId 必须在订单行中(收货不得超出订单范围)
  副作用:
    - 新建 Batch
    - 若 qty > 订单余量,写 OverDelivery 标记(不拒绝,但告警)
  后置条件:
    - 该批次的 Inspection 必须在 48 小时内创建,否则进入「待质检」对账清单
  权限:仓储操作员
  结果:成功 / 拒绝(带拒绝码与原因)

P2 正是事故二的解药。实现方式有两种,团队选了第二种:

  • 方案一:收货时调用 ERP 的冻结接口。依赖 ERP 团队排期,2 周。
  • **方案二:收货前查本体的 FreezeEvent,冻结态由 ERP 单向同步过来(每天 + 事件触发)。** 当天能上,代价是同步延迟最长 1 天。

拒绝时的返回必须是结构化的,否则操作员只会看到「失败」然后找 IT:

{
  "result": "rejected",
  "code": "SUPPLIER_FROZEN",
  "message": "供应商 S-2041 于 2026-10-12 因质量问题被冻结,尚未解除",
  "details": {
    "supplier": "S-2041",
    "freezeEvent": "FE-2026-0043",
    "reason": "quality",
    "since": "2026-10-12",
    "operator": "王××",
    "contact": "采购部 张××"
  }
}

「contact」这一行是项目里被引用最多的设计。 被拒绝的人需要知道找谁,否则流程会卡在他那里。技术上这是一个字段,组织上这是让约束被接受的前提。

上线三周内,这个动作拒绝了约 20 次,其中一半以上是冻结供应商,其余是超订单范围与重复批次。第 4 次被拒绝之后,采购部自己去更新了冻结流程——因为它第一次有了确定的执行点。

这是全书最重要的一个观察:约束不会因为「被写下来」而被遵守,只会因为「有一个动作会因此失败」而被遵守。

第 4 周:每周对账

对账不是校验的重复。校验看「模型内部是否自洽」,对账看「模型与源系统是否一致」。

三张对账表:

对账项比较对象频率差异处理
供应商冻结态ERP 备注字段 ↔ FreezeEvent每日差异 → 工单给采购,48 小时闭环
订单金额汇总本体视图 ↔ ERP 总账每周差异 > 0.5% → 阻塞发布
批次-质检配对批次表 ↔ QMS 检验单每周未配对超 48 小时 → 进「待质检」清单

第一周对账发现几条冻结态不一致——都是「ERP 里解除了,本体没同步」。这类差异在校验里永远看不见,因为它们各自都自洽。

对账的输出不是报表,是工单。 一张没人处理的差异清单,两周后会变成没人看的清单。所以每一条差异都必须有指派人、有期限、有闭环状态。这一点比技术实现重要得多。

第 5 周:一次兼容性变更

第四周结束时,财务提出要求:MetricDefinition 必须记录「数据来源系统」。这是一个新增必填字段。

如果直接加 sh:minCount 1,结果是:现存一百多个指标定义全部违规,流水线立刻变红,团队会以「先关掉校验」收场。

实际做法是三步:

  1. 加字段,但先不设必填(第 1 天)。字段可选,只在新建时提示。
  2. 补数据(第 2–9 天)。一百多个定义逐个人工填来源,其中十来个发现「其实没有稳定来源」,转成「手工维护」标记。
  3. 设为必填,并给 30 天豁免给存量(第 10 天)。豁免记录带负责人与到期日,到期未补自动变红。

这个流程后来固化成第 5 章的三条底线:新增必填字段必须分两步、豁免必须带期限、变更必须能回答影响面。

Q7(「并入返修件会影响哪些报表」)在这一步得到了回答:影响面计算扫出 3 张报表、1 个 API、2 个下游模型引用该分类。其中 1 张报表是没人知道的——它由一位离职同事两年前建的,还在每天跑。

成本:五周,二十余人日

阶段人日谁
收集与合并 CQ3–4数据团队 1 人 + 三个部门各 1 天
建模与评审4–5数据团队 1 人,业务方评审 2 次共 3 小时
约束编写与两轮修正4–5数据团队
动作门控与联调4–5数据团队 1.5 + 后端 2.5
对账与工单流程2数据团队 1 + 运营 1
变更流程与影响面2数据团队

合计二十余人日,没有采购任何软件,没有新建存储。 工具链是 pyshacl + rdflib + 一个 200 行的 Python 生成脚本,加上 GitLab CI 的一个 job。

对照:如果走「买一个知识图谱平台」的路线,仅 POC 阶段通常就要 30+ 人日,且五周内出不了可用的拒绝逻辑。

六个月后回看

奏效的三件事:

  1. **MetricDefinition 作为一等对象。** 口径争议从「会上吵」变成「查版本」。六个月里发生了 7 次口径变更,每次都有记录,财务的供应商考核不再和运营对不上。
  2. 收货动作的门控。 冻结供应商供货从「偶发事故」变成「不可能发生」。这一条是唯一被管理层主动提及的成果。
  3. 每周对账。 差异从第一周的 3 条降到稳定 0–1 条;它成了数据团队与业务之间的固定会议议程。

失效或打折扣的三件事:

  1. 枚举归一没有坚持。 第 3 个月新来了一个物料分类,又出现了中英混用,因为归一脚本没进 CI。教训:归一必须是自动的,靠人记得就会退化。
  2. 豁免变成了永久。 30 天豁免到期后没有人处理,系统自动变红,然后有人把到期日改到了下个季度。这暴露了豁免设计的缺陷:到期应该升级给人,而不是变红等人来改。
  3. 溯源信息没人查。 变更历史记录得很完整,但六个月里只有 2 次被真正查询——都是审计。这说明「四分之一的需求是溯源」这个判断在立项时被高估了。真实的溯源需求是低频、高压的:平时不用,用的时候必须有。

可复用的六件事

如果只从这个案例带走六条:

  1. 从事故出发,不从类出发。 三个事故定义了范围,比任何建模方法论都有效。
  2. 先写 20 条问句。 它们同时是范围、验收标准和争议清单。
  3. 约束第一轮一定会写错。 100% 违规 = 规则错;先跑小样本,再全量。
  4. 让一个动作失败。 约束的接受度取决于有没有人因此被挡住、以及他知不知道找谁。
  5. 对账输出工单,不输出报表。 没有指派人的差异清单等于没有。
  6. 新增必填字段分两步走。 直接加必填,等于邀请别人关掉校验。

这个案例的规模(18 个类、5 条约束、二十余人日)刻意做得很小。它不是「本体能做什么」的上限,而是「本体在多小的投入下能开始产生价值」的下限。 对绝大多数组织来说,先摸到这个下限,比先设计一个完整的蓝图重要得多。