循序渐进 · 教学

本体结构指南

上一篇讲的是"原则",这一篇讲落地:属性怎么放、关系怎么连、权限怎么切、名字怎么起。 同样用 9 步拆开讲,配 4 个可以亲手点的互动演示。

原文 Palantir Foundry · Structural guidance 预计阅读 14 分钟 4 个互动演示 + 6 道测验
本文来源 · Source 内容整理自 Palantir Foundry 官方文档:
https://www.palantir.com/docs/foundry/ontology/ontology-structural-guidance/
原始标题:Ontology design: Structural guidance

一句话速览

本体结构指南:上一篇讲的是"原则",这一篇讲落地:属性怎么放、关系怎么连、权限怎么切、名字怎么起。同样用 9 步拆开讲,配 4 个可以亲手点的互动演示。

1 这一篇在讲什么
上一篇《最佳实践》给你的四条原则像是"宪法";这一篇是"施工规范",回答的是更具体的问题
2 一个事实只存一处
假设 Manager 想知道自己有多少个直属下属
3 那什么时候可以"抄一份"
不是所有计算都要放到查询时做
4 当"关系"本身也携带信息
每一条链接都应该回答一个清晰的领域问题
1

这一篇在讲什么?

原文一句话:如何在本体内部组织属性、关系和访问控制。

上一篇《最佳实践》给你的四条原则像是"宪法";这一篇是"施工规范", 回答的是更具体的问题:

同一个值要不要复制一份?
→ 规范化与派生属性
一个属性其实是一组字段怎么办?
→ Struct 结构体
多个类型长得一样怎么办?
→ Interface 接口
关系本身带信息怎么办?
→ 对象支撑链接
名字怎么起才不返工?
→ 命名规范
谁能看哪些格子?
→ 安全设计
先建立直觉:这六个话题其实在回答同一个问题——怎样让"现实世界的一个事实"在本体里只出现一次、出现在正确的位置、并且被恰当地保护起来。
2

核心铁律:一个事实只存一处

规范化(Normalization)与派生属性(Derived properties)。

假设 Manager 想知道自己有多少个直属下属。最直觉的做法是:在 Manager 上放一个 directReportCount 整数属性,有人入职就 +1,有人离职就 -1。

问题在于:这份"人数"其实不是 Manager 的属性,而是"有多少 Employee 链接到他"这个事实的结果。 你把它抄了一份,就得负责让这份副本永远不出错。

动手试试 · 点几下看看会发生什么
✗ 手动维护的整数属性
5
Manager.directReportCount
每次 action 都得记得改它
✓ 派生属性(查询时计算)
5
Manager.directReportCount
统计链接过来的 Employee 对象数

真实下属人数:5 人。点"入职/离职",看看哪一边会失准。

当某个值依赖于通过 action 发生的变更时,每一个可能影响它的 action 都必须同时更新它。只要有一个 action 忘了,这个值就会一直错下去,直到有人发现。

要避开什么

  • 同一个值被存成多个对象类型上的属性
  • 属性是别处维护的值的副本,因而逐渐过期
  • 更新一个现实世界的事实,需要写多个对象
  • 计数类属性靠人工维护,而不是从链接计算得出
3

那什么时候可以"抄一份"?

预计算(Pre-computed) vs 动态派生(Dynamically derived)。

不是所有计算都要放到查询时做。区分标准是:输入会不会被 Ontology 层的操作改变。

类型特征推荐工具例子
预计算 同一个对象上的属性算出;输入很少变,或只随 pipeline 摄入而变。 Pipeline transform fullName = firstName + " " + lastName
输入稳定且在同一条 pipeline 里更新,预计算既安全又零运行时开销。
动态派生 依赖链接对象或会经 action、自动化等 Ontology 层操作改变的值。 Derived property directReportCount
员工会通过 action 调岗、入职、离职。用派生属性在查询时统计,自动保持正确。
✗ 手动维护 + 复制字段
Manager - directReportCount: 5 (人工维护的整数, 员工进出都要改) Employee - managerName: "Alice" (从链接的 Manager 抄过来; 经理改名就失效)
✓ 查询时派生 + 只存一处
Manager - directReportCount(派生): 在查询时统计链接的 Employee 对象 Employee - manager(链接到 Manager)

该怎么做

  1. 每个事实只存一处放在它在语义上真正所属的那个对象上。
  2. 用派生属性在查询时从链接对象计算或聚合数值。
  3. 随着规模增长监控性能如果派生属性在大数据量下带来不可接受的延迟,再考虑有选择地反范式化。
  4. 任何反范式化都要写清楚记录理由、事实来源(source of truth)、以及副本的同步更新策略。
4

Struct:把语义相关的字段打包

一个"属性"如果天生就是多字段的,就别摊平成一堆属性。

S

结构体 Structs

Group semantically related fields into structs.

地址天然由街道、城市、州、邮编组成。摊平成 10 个平级属性后, 它们之间唯一的联系只剩下命名前缀——这是很脆弱的"约定"。 用 struct 则保留了语义分组,还能顺手把元数据一起带上。

什么时候该用 struct

场景例子
多字段的值地址(street / city / state / postalCode)、坐标(geopoint / altitude)
带元数据的值AI 生成的输出,附带置信度、来源引用、推理过程
带选择逻辑的多值属性多个电话号码,用 reducer 选出主号
✗ 摊平成十个属性
Facility - addressStreet - addressCity - addressState - addressPostalCode - addressCountry - addressGeopoint - addressLastOccupied - addressDatasource - addressLlmConfidence - addressLlmReasoning 十个互不相关的属性, 唯一的联系是命名约定
✓ 一个语义概念
Facility - address(struct 数组) - street(主字段) - city(主字段) - state(主字段) - postalCode(主字段) - country(主字段) - geopoint - lastOccupied(用于排序) - datasource - llmConfidence - llmReasoning 一个语义概念: 主字段 + 结构化子字段

该怎么做

  1. 识别多字段属性字段之间语义相关,且总是被一起使用。
  2. 定义 struct字段名和类型都要清晰。
  3. 指定主字段(main field)让 struct 在大多数场景下表现得像一个简单属性。
  4. 用 reducer 处理多值 struct从多个候选中浮出最相关的那个值。
  5. 把元数据一起收进 struct来源、置信度、时间戳——尤其对 AI 生成的输出。
5

Interface:把"重复的形状"抽出来

这是实现"不要重复自己"和"开闭原则"的主力工具。

I

接口 Interfaces

Use interfaces to build reusable, future-proof abstractions.

接口定义了一个共享形状(属性、链接、动作),多个对象类型都可以实现它。 这样工作流就可以面向接口而不是面向具体类型来写。

✗ 三份属性 + 三份动作
Vehicle - lastInspectionDate - inspectionStatus (重复动作:安排车辆检查) Equipment - lastInspectionDate - inspectionStatus (重复动作:安排设备检查) Facility - lastInspectionDate - inspectionStatus (重复动作:安排设施检查) 三份副本,各自维护
✓ 一个接口 + 一个共享动作
Interface: Inspectable - lastInspectionDate - inspectionStatus (共享动作:安排检查) Vehicle implements Inspectable - make, model, mileage, ... Equipment implements Inspectable - serialNumber, warrantyExpiry, ... Facility implements Inspectable - address, capacity, ... 一个接口,三个实现类型

该怎么做

  1. 识别共同的形状多个类型共享属性、链接或动作时,就抽一个接口出来。
  2. 围绕"能力"或"分类"设计接口能力型:InspectableSchedulableBillable;分类型:MilitaryAssetMedicalDevice
  3. 工作流面向接口动作、函数、应用尽量建在接口上。
  4. 接口可以继承接口通过扩展构建分层的抽象。
  5. 先搭骨架,后收敛即使因平台能力暂时受限、某些工作流还得按类型复制一份,也先把接口定义出来。
6

链接:当"关系"本身也携带信息

直接链接 vs 对象支撑链接(Object-backed link)。

每一条链接都应该回答一个清晰的领域问题:

「这位患者去过哪家医疗机构?」
「这名员工属于哪个团队?」
「这张工单用了哪台设备?」

两种链接怎么选

链接类型什么时候用例子
直接链接关系有意义,但本身不带元数据EmployeeDepartment
对象支撑链接关系自带元数据(日期、角色、状态、占比)。EmployeeVentureStaffingVenture(带 rolestartDateallocation
并不是每个"中间对象"都要在所有场景里露面。有些工作流关心这段关系的细节,有些只想要那条直达的连线。 对象支撑链接的好处是:两种视图你都能给。
✗ 直连,或塞进源对象
Employee → Venture(直接链接) 无法记录每次派驻的角色、 开始日期、投入占比 — 或者 — Employee - ventureRole - ventureStartDate (一人参与多个项目时 语义就含糊了)
✓ 中间对象承载关系
Employee → VentureStaffing → Venture VentureStaffing - role - startDate - allocationPercentage - status 工作流可按需暴露: - 简版:Employee → Venture - 详版:Employee → Staffing → Venture

练一练:这三个场景该用哪种?

链接设计错了会怎样

问题后果
元数据丢失直接链接无法表达关系"何时、为何、以什么身份"存在。
多重链接含糊ventureRole 这样放在源对象上的属性,一旦实体参与多重关系就说不清了。
无意义的链接只因为两个数据集共享一个外键就建的链接,会往本体里加噪声,干扰导航。

该怎么做

  1. 先验证语义别只因为两个数据集共享外键就建链接,要问:这个关系在业务里说得通吗?
  2. 判断关系是否携带元数据如果带(日期、角色、状态),就用对象支撑链接把它记下来。
  3. 暴露合适的信息粒度按场景决定给"直达关系"还是"经过中间对象的详细关系"。
  4. 命名要让两个方向都读得通链接名要能从两端各自描述这段关系。
7

命名规范:最划算的一笔投资

为"人能读懂"和"AI 能导航"而优化。

一致且具描述性的命名,是你能为本体的质量做的最有价值的投资之一;而且一旦本体投入使用,它就极难纠正。
元素约定好例子坏例子
对象类型单数、具体的名词,领域专家一眼认得出Patient, WorkOrder, FlightSegmentData, Item, Record
属性简洁、自解释;不编码类型信息或实现细节age, status, lastInspectionDatedtLastInspMod, nVAL01, fieldX
链接从两个方向都读得自然department(员工→部门)
employees(部门→员工)
relatedItems, link1
日期全本体统一遵循一种约定createdDate, updatedDate, effectiveDate混用 createdDate 和 dateOfCreation
含糊的词加上限定,明确具体含义monetaryValue, quantityOnHand, riskScorevalue, quantity, score

命名诊所:点开看看推荐怎么改

类型
✗ 坏名字
✓ 推荐名字(点击左侧「看推荐」)
对象类型
Item
看推荐 →
属性
dtLastInspMod
看推荐 →
属性
value
看推荐 →
属性
quantity
看推荐 →
链接
Item → Related Item
看推荐 →
链接
Employee → Related Employee
看推荐 →

该怎么做

  1. 动手前先定规范日期、状态、标识符、链接的命名模式先达成一致。
  2. 遵循本体已有的约定已经在用 createdDate,就别再引入 dateOfCreation
  3. 给含糊的属性加限定monetaryValuequantityOnHandriskScore;别用 valuequantityscore
  4. 按关系给链接命名Employee → Department 叫 department;反过来叫 employees
  5. 和最终用户一起过一遍名字构建者觉得清楚的名字,使用者可能觉得含糊。用分析演练去观察用户会搜什么词、期待找到什么关系。
8

安全设计:按业务语义切权限

遵循最小权限原则,并且用"领域语言"而非"基础设施语言"来表达。

用户看一眼安全配置,就应该能明白"保护的是什么、为什么要保护"。 关键是:不要为了权限去复制对象类型——那正好撞上"不要重复自己"这条原则。

三层安全模型

控制什么例子
行级 Row-level用户能看到哪些对象VIP 患者仅资深员工可见
列级 Column-level对可见对象,用户能看到哪些属性临床记录仅护理团队可见
单元格级(两者交叉)行与列限制的交集VIP 患者的临床记录仅资深护理团队可见

动手试试:切换身份,看哪些格子亮起来

可见    被策略屏蔽
行级规则:VIP 患者仅资深员工可见 · 列级规则:diagnosis / clinicalNotes 仅护理团队;mentalHealthRecords 仅精神科团队
✗ 拆成两个对象类型来做权限
PublicPatient - name - dob - diagnosis RestrictedPatient - name - dob - diagnosis - clinicalNotes - mentalHealthRecords 模式重复;靠拆类型实现安全。 给一个类型加属性时, 很容易忘了另一个
✓ 一个类型 + 策略
Patient(单一对象类型) - name - dob - diagnosis(列级限制:护理团队) - clinicalNotes(列级:护理团队) - mentalHealthRecords(列级:精神科) 行级安全: - VIP 患者:仅资深员工 一个类型;安全由策略实现。 领域边界驱动访问规则

安全设计错了会怎样

为安全而复制类型模式逐渐不同步;加到一个类型上的属性很容易在另一个上被漏掉。违反"不要重复自己"。
默认过度宽松先放开、日后收紧,意味着在收紧完成前敏感数据可能已经暴露。
用临时过滤代替策略安全逻辑散落在应用代码里、而不是在本体层强制执行,既脆弱又难审计。
边界与领域不对齐不跟随领域边界的安全边界更难推理,也更容易出现漏洞。

该怎么做

  1. 从严格开始,按需放开默认是最小访问权限;而不是先全开再慢慢收紧。
  2. 行级与列级组合使用两者交叉才能得到细粒度的单元格级访问控制。
  3. 让安全边界对齐领域边界区域经理看本区域数据、护理团队看自己的患者——用本体关系和安全策略来建模,而不是在应用层临时过滤。
  4. 不要为了安全复制对象类型一个类型配好策略,永远优于多个类型重复模式。
  5. 新增路径时复查权限一致性新加的链接、类型、属性都要确认没有绕过既有的保护。

一页带走

① 一个事实一处
能算出来的就别存;必须抄副本就写明来源与同步策略。
② 成组的用 struct
多字段 + 带元数据,指定主字段。
③ 同形状抽接口
工作流面向接口写,先搭骨架后收敛。
④ 关系带信息就加中间对象
对象支撑链接,两种视图都能给。
⑤ 命名先定规范
要具体、要一致、要让两端都读得通。
⑥ 安全按领域切
行 × 列 = 单元格级;绝不靠复制类型实现权限。

最后一句原文提醒:先确保安全边界对齐领域边界,再去查具体的配置文档。 边界对齐了,配置只是实现细节。

延伸阅读 · 相关页面

按主题横向跳转,不必顺着目录一篇篇读。

常见问题速答 · FAQ

关于「本体结构指南」,读者最常问的几个问题。

这一篇在讲什么?
上一篇《最佳实践》给你的四条原则像是"宪法";这一篇是"施工规范",回答的是更具体的问题。
核心铁律:一个事实只存一处是什么?
假设 Manager 想知道自己有多少个直属下属。最直觉的做法是:在 Manager 上放一个 directReportCount 整数属性,有人入职就 +1,有人离职就 -1。
那什么时候可以"抄一份"?
不是所有计算都要放到查询时做。区分标准是:输入会不会被 Ontology 层的操作改变。
链接:当"关系"本身也携带信息是什么?
每一条链接都应该回答一个清晰的领域问题。