本体结构指南
上一篇讲的是"原则",这一篇讲落地:属性怎么放、关系怎么连、权限怎么切、名字怎么起。 同样用 9 步拆开讲,配 4 个可以亲手点的互动演示。
https://www.palantir.com/docs/foundry/ontology/ontology-structural-guidance/
原始标题:Ontology design: Structural guidance
一句话速览
本体结构指南:上一篇讲的是"原则",这一篇讲落地:属性怎么放、关系怎么连、权限怎么切、名字怎么起。同样用 9 步拆开讲,配 4 个可以亲手点的互动演示。
这一篇在讲什么?
原文一句话:如何在本体内部组织属性、关系和访问控制。
上一篇《最佳实践》给你的四条原则像是"宪法";这一篇是"施工规范", 回答的是更具体的问题:
核心铁律:一个事实只存一处
规范化(Normalization)与派生属性(Derived properties)。
假设 Manager 想知道自己有多少个直属下属。最直觉的做法是:在 Manager 上放一个
directReportCount 整数属性,有人入职就 +1,有人离职就 -1。
问题在于:这份"人数"其实不是 Manager 的属性,而是"有多少 Employee 链接到他"这个事实的结果。 你把它抄了一份,就得负责让这份副本永远不出错。
Manager.directReportCount每次 action 都得记得改它
Manager.directReportCount统计链接过来的 Employee 对象数
真实下属人数:5 人。点"入职/离职",看看哪一边会失准。
要避开什么
- 同一个值被存成多个对象类型上的属性
- 属性是别处维护的值的副本,因而逐渐过期
- 更新一个现实世界的事实,需要写多个对象
- 计数类属性靠人工维护,而不是从链接计算得出
那什么时候可以"抄一份"?
预计算(Pre-computed) vs 动态派生(Dynamically derived)。
不是所有计算都要放到查询时做。区分标准是:输入会不会被 Ontology 层的操作改变。
| 类型 | 特征 | 推荐工具 | 例子 |
|---|---|---|---|
| 预计算 | 由同一个对象上的属性算出;输入很少变,或只随 pipeline 摄入而变。 | Pipeline transform | fullName = firstName + " " + lastName输入稳定且在同一条 pipeline 里更新,预计算既安全又零运行时开销。 |
| 动态派生 | 依赖链接对象或会经 action、自动化等 Ontology 层操作改变的值。 | Derived property | directReportCount员工会通过 action 调岗、入职、离职。用派生属性在查询时统计,自动保持正确。 |
该怎么做
- 每个事实只存一处放在它在语义上真正所属的那个对象上。
- 用派生属性在查询时从链接对象计算或聚合数值。
- 随着规模增长监控性能如果派生属性在大数据量下带来不可接受的延迟,再考虑有选择地反范式化。
- 任何反范式化都要写清楚记录理由、事实来源(source of truth)、以及副本的同步更新策略。
Struct:把语义相关的字段打包
一个"属性"如果天生就是多字段的,就别摊平成一堆属性。
结构体 Structs
Group semantically related fields into structs.地址天然由街道、城市、州、邮编组成。摊平成 10 个平级属性后, 它们之间唯一的联系只剩下命名前缀——这是很脆弱的"约定"。 用 struct 则保留了语义分组,还能顺手把元数据一起带上。
什么时候该用 struct
| 场景 | 例子 |
|---|---|
| 多字段的值 | 地址(street / city / state / postalCode)、坐标(geopoint / altitude) |
| 带元数据的值 | AI 生成的输出,附带置信度、来源引用、推理过程 |
| 带选择逻辑的多值属性 | 多个电话号码,用 reducer 选出主号 |
该怎么做
- 识别多字段属性字段之间语义相关,且总是被一起使用。
- 定义 struct字段名和类型都要清晰。
- 指定主字段(main field)让 struct 在大多数场景下表现得像一个简单属性。
- 用 reducer 处理多值 struct从多个候选中浮出最相关的那个值。
- 把元数据一起收进 struct来源、置信度、时间戳——尤其对 AI 生成的输出。
Interface:把"重复的形状"抽出来
这是实现"不要重复自己"和"开闭原则"的主力工具。
接口 Interfaces
Use interfaces to build reusable, future-proof abstractions.接口定义了一个共享形状(属性、链接、动作),多个对象类型都可以实现它。 这样工作流就可以面向接口而不是面向具体类型来写。
该怎么做
- 识别共同的形状多个类型共享属性、链接或动作时,就抽一个接口出来。
- 围绕"能力"或"分类"设计接口能力型:
Inspectable、Schedulable、Billable;分类型:MilitaryAsset、MedicalDevice。 - 工作流面向接口动作、函数、应用尽量建在接口上。
- 接口可以继承接口通过扩展构建分层的抽象。
- 先搭骨架,后收敛即使因平台能力暂时受限、某些工作流还得按类型复制一份,也先把接口定义出来。
链接:当"关系"本身也携带信息
直接链接 vs 对象支撑链接(Object-backed link)。
每一条链接都应该回答一个清晰的领域问题:
两种链接怎么选
| 链接类型 | 什么时候用 | 例子 |
|---|---|---|
| 直接链接 | 关系有意义,但本身不带元数据。 | Employee → Department |
| 对象支撑链接 | 关系自带元数据(日期、角色、状态、占比)。 | Employee → VentureStaffing → Venture(带 role、startDate、allocation) |
练一练:这三个场景该用哪种?
链接设计错了会怎样
| 问题 | 后果 |
|---|---|
| 元数据丢失 | 直接链接无法表达关系"何时、为何、以什么身份"存在。 |
| 多重链接含糊 | 像 ventureRole 这样放在源对象上的属性,一旦实体参与多重关系就说不清了。 |
| 无意义的链接 | 只因为两个数据集共享一个外键就建的链接,会往本体里加噪声,干扰导航。 |
该怎么做
- 先验证语义别只因为两个数据集共享外键就建链接,要问:这个关系在业务里说得通吗?
- 判断关系是否携带元数据如果带(日期、角色、状态),就用对象支撑链接把它记下来。
- 暴露合适的信息粒度按场景决定给"直达关系"还是"经过中间对象的详细关系"。
- 命名要让两个方向都读得通链接名要能从两端各自描述这段关系。
命名规范:最划算的一笔投资
为"人能读懂"和"AI 能导航"而优化。
| 元素 | 约定 | 好例子 | 坏例子 |
|---|---|---|---|
| 对象类型 | 单数、具体的名词,领域专家一眼认得出 | Patient, WorkOrder, FlightSegment | Data, Item, Record |
| 属性 | 简洁、自解释;不编码类型信息或实现细节 | age, status, lastInspectionDate | dtLastInspMod, nVAL01, fieldX |
| 链接 | 从两个方向都读得自然 | department(员工→部门) employees(部门→员工) | relatedItems, link1 |
| 日期 | 全本体统一遵循一种约定 | createdDate, updatedDate, effectiveDate | 混用 createdDate 和 dateOfCreation |
| 含糊的词 | 加上限定,明确具体含义 | monetaryValue, quantityOnHand, riskScore | value, quantity, score |
命名诊所:点开看看推荐怎么改
该怎么做
- 动手前先定规范日期、状态、标识符、链接的命名模式先达成一致。
- 遵循本体已有的约定已经在用
createdDate,就别再引入dateOfCreation。 - 给含糊的属性加限定用
monetaryValue、quantityOnHand、riskScore;别用value、quantity、score。 - 按关系给链接命名Employee → Department 叫
department;反过来叫employees。 - 和最终用户一起过一遍名字构建者觉得清楚的名字,使用者可能觉得含糊。用分析演练去观察用户会搜什么词、期待找到什么关系。
安全设计:按业务语义切权限
遵循最小权限原则,并且用"领域语言"而非"基础设施语言"来表达。
用户看一眼安全配置,就应该能明白"保护的是什么、为什么要保护"。 关键是:不要为了权限去复制对象类型——那正好撞上"不要重复自己"这条原则。
三层安全模型
| 层 | 控制什么 | 例子 |
|---|---|---|
| 行级 Row-level | 用户能看到哪些对象 | VIP 患者仅资深员工可见 |
| 列级 Column-level | 对可见对象,用户能看到哪些属性 | 临床记录仅护理团队可见 |
| 单元格级(两者交叉) | 行与列限制的交集 | VIP 患者的临床记录仅资深护理团队可见 |
动手试试:切换身份,看哪些格子亮起来
行级规则:VIP 患者仅资深员工可见 · 列级规则:diagnosis / clinicalNotes 仅护理团队;mentalHealthRecords 仅精神科团队
安全设计错了会怎样
| 为安全而复制类型 | 模式逐渐不同步;加到一个类型上的属性很容易在另一个上被漏掉。违反"不要重复自己"。 |
| 默认过度宽松 | 先放开、日后收紧,意味着在收紧完成前敏感数据可能已经暴露。 |
| 用临时过滤代替策略 | 安全逻辑散落在应用代码里、而不是在本体层强制执行,既脆弱又难审计。 |
| 边界与领域不对齐 | 不跟随领域边界的安全边界更难推理,也更容易出现漏洞。 |
该怎么做
- 从严格开始,按需放开默认是最小访问权限;而不是先全开再慢慢收紧。
- 行级与列级组合使用两者交叉才能得到细粒度的单元格级访问控制。
- 让安全边界对齐领域边界区域经理看本区域数据、护理团队看自己的患者——用本体关系和安全策略来建模,而不是在应用层临时过滤。
- 不要为了安全复制对象类型一个类型配好策略,永远优于多个类型重复模式。
- 新增路径时复查权限一致性新加的链接、类型、属性都要确认没有绕过既有的保护。
一页带走
最后一句原文提醒:先确保安全边界对齐领域边界,再去查具体的配置文档。 边界对齐了,配置只是实现细节。
延伸阅读 · 相关页面
按主题横向跳转,不必顺着目录一篇篇读。
常见问题速答 · FAQ
关于「本体结构指南」,读者最常问的几个问题。