Ontology 最小 MVP:和普通业务系统的区别,以及一步一步怎么做

---
title: Ontology 最小 MVP:和普通业务系统的区别,以及一步一步怎么做
published_at: 2026-08-05
language: zh
---

# Ontology 最小 MVP:和普通业务系统的区别,以及一步一步怎么做

做 Ontology MVP,不能一上来就说“建几个对象、连几个关系、做个风险判断”。那样听起来和正常开发一个业务系统没有区别,也确实没有解释清楚 Ontology 的价值。

正确顺序应该是:

```text
先讲清楚它和普通业务系统有什么区别;
再讲清楚本体层的优势在哪里;
再讲清楚 AI 为什么需要这层本体;
最后用一个具体场景,一步一步搭出最小 MVP。
```

本文用“订单延期风险分析”作为例子,分别讲三条路线:

```text
路线 A:使用 Palantir 官方平台搭 MVP;
路线 B:仍使用 Palantir,但不用官方 OSDK,直接调 Ontology REST API;
路线 C:完全不用 Palantir,自建一个轻量 Ontology Runtime。
```

目标不是泛泛讲架构,而是让这个 MVP 能真的一步一步搭起来。

## 一、普通业务系统和 Ontology 系统有什么区别

如果只做一个订单延期风险系统,普通开发方式通常是:

```text
建数据库表;
写后端接口;
写前端页面;
写风险规则;
加任务创建按钮;
接一个 AI 解释接口。
```

这没有问题,但它本质上还是一个普通业务系统。

普通业务系统的特点是:

```text
业务含义藏在代码里;
对象关系藏在 SQL join 里;
动作藏在前端按钮和后端接口里;
权限分散在不同接口里;
AI 需要额外手写 tool 和 prompt 才能理解系统。
```

例如“订单”和“客户”的关系,可能只是某个接口里的 join:

```sql
select *
from orders
join customers on orders.customer_id = customers.customer_id;
```

“创建处理任务”这个动作,可能只是一个接口:

```text
POST /tasks
```

前端知道什么时候显示按钮,后端知道怎么写任务表,但系统整体并没有显式声明:

```text
Order 是什么;
Order 和 Customer 有什么关系;
Order 能执行哪些动作;
谁能执行这些动作;
AI 应该如何围绕 Order 组织上下文。
```

Ontology 系统的关键不同在这里。

它不是只写一个订单风险应用,而是把业务世界抽成一层可复用的模型:

```text
对象类型:Order、Customer、Product、Part、Supplier、Inventory、Risk、Task
对象属性:订单号、交付日期、数量、风险等级、负责人
对象关系:Order -> Customer,Order -> Product,Product -> Part
对象动作:CreateMitigationTask、ChangePriority、RequestInventoryTransfer
权限规则:谁能看对象,谁能看字段,谁能执行动作
AI 上下文:围绕对象、关系、状态和动作自动生成
```

也就是说,普通业务系统是“页面和接口中心”;Ontology 系统是“业务对象中心”。

页面、报表、自动化流程、AI 助手,都消费同一套对象模型。

## 二、本体层的优势是什么

Ontology 的优势不是让你少写一个 CRUD 页面,而是把业务语义从具体应用里抽出来,变成可复用、可组合、可治理的一层。

### 1. 统一业务语言

企业里不同系统可能对同一件事有不同叫法:

```text
ERP 叫 sales_order;
CRM 叫 customer_order;
仓储系统叫 fulfillment_request;
财务系统叫 invoice_source。
```

Ontology 把它们映射成业务人员理解的对象:

```text
Order
Customer
Product
Inventory
Supplier
```

这样业务人员、开发者、数据分析师和 AI 都在同一套语言里工作。

### 2. 关系变成一等公民

普通系统里,对象关系通常藏在 SQL、接口和页面逻辑里。

Ontology 里,关系本身被显式建模:

```text
Order belongs to Customer
Order contains Product
Product requires Part
Part supplied by Supplier
Part has Inventory
```

这样系统可以通用地做:

```text
关系追踪;
影响分析;
上下游依赖分析;
AI 上下文组装;
对象级权限过滤。
```

### 3. 动作绑定到对象上

普通系统里,动作通常是一个按钮加一个接口。

Ontology 里,动作属于对象模型:

```text
Order 可以创建处理任务;
Order 可以调整优先级;
Part 可以发起库存调拨;
Supplier 可以发送加急请求。
```

每个动作都有明确的:

```text
作用对象;
参数 schema;
执行条件;
权限要求;
副作用;
审计记录。
```

这使系统不只是“能看数据”,而是“能围绕对象行动”。

### 4. 权限和审计下沉到对象层

普通系统里,权限经常散落在不同接口里。

Ontology 更理想的做法是把权限绑定到:

```text
对象;
字段;
关系;
动作。
```

例如:

```text
销售可以看自己客户的订单,但不能调整排产;
供应链经理可以创建风险处理任务;
财务可以看发票字段,但不能看生产细节;
AI 只能调用当前用户有权限执行的动作。
```

这对企业级 AI 尤其重要,因为 AI 不能绕过人的权限。

## 三、AI 的优势在哪里体现

AI 的优势不是替代普通业务系统,也不是替你写 CRUD。

如果只是查询订单、展示库存、创建任务,普通系统更稳定。

AI 的优势体现在四件事上。

### 1. 自然语言查询

用户可以问:

```text
未来两周哪些战略客户订单有延期风险?
```

AI 可以借助 Ontology 知道:

```text
战略客户来自 Customer.tier;
订单来自 Order;
订单和客户通过 Customer 关系连接;
延期风险来自 Risk;
风险原因可能涉及 Part、Inventory、Supplier。
```

没有 Ontology,AI 面对的是表名、字段名和接口名,很难可靠理解业务含义。

### 2. 沿关系解释原因

报表可能只显示:

```text
SO-1001 风险高
```

AI 可以沿对象关系解释:

```text
SO-1001 属于战略客户 A 公司,订单需要 500 个工业控制器。
每个工业控制器需要 1 个芯片 A,所以需要 500 个芯片 A。
当前芯片 A 可用库存只有 200 个,缺口 300 个。
供应商甲交期 14 天,因此存在延期风险。
```

这个解释不是自由发挥,而是来自对象链条。

### 3. 在合法动作空间里给建议

如果 Ontology 里定义了动作:

```text
CreateMitigationTask
RequestInventoryTransfer
NotifyAccountOwner
SendSupplierExpediteRequest
```

AI 就不是随便建议“你可以想办法处理”,而是在已有动作空间里组合:

```text
先创建处理任务;
如果其他仓库有库存,发起库存调拨;
如果没有替代库存,联系供应商加急;
如果仍然可能延期,通知客户负责人。
```

### 4. 协助执行但不绕过权限

AI 可以说:

```text
我可以为订单 SO-1001 创建一个处理任务,负责人建议为供应链经理。是否执行?
```

用户确认后,AI 调用 Ontology Action。

关键是:

```text
AI 只能执行模型里定义过的动作;
AI 只能执行当前用户有权限的动作;
动作参数有 schema 校验;
执行结果有审计记录。
```

这才是 AI + Ontology 的真正价值。

## 四、MVP 场景:订单延期风险分析

本文的 MVP 场景固定为:

> 用户打开一个订单,看到它的客户、产品、零件、库存和供应商;系统判断延期风险;AI 解释原因;用户创建一个处理任务。

最终用户路径是:

```text
进入订单列表;
找到高风险订单;
打开订单详情;
看到客户、产品、零件、库存、供应商;
理解为什么有延期风险;
让 AI 解释风险;
创建一个处理任务;
在任务列表里看到任务。
```

最小对象:

```text
Customer    客户
Order       订单
Product     产品
Part        零件
Supplier    供应商
Inventory   库存
Risk        风险
Task        处理任务
```

最小关系:

```text
Customer -> Order
Order -> Product
Product -> Part
Part -> Supplier
Part -> Inventory
Order -> Risk
Order -> Task
```

下面开始讲三条实现路线。

# 路线 A:使用 Palantir 官方平台一步一步搭 MVP

这一条路线假设你已经有 Palantir Foundry / Ontology / AIP 环境。

官方做法的核心顺序是:

```text
Dataset → Object Type → Link Type → Risk Transform → Action Type → Workshop → AIP
```

下面拆成可执行步骤。

## A1. 准备 6 个 CSV 数据源

先不要接真实 ERP。MVP 用 CSV 最快。

### customers.csv

```csv
customer_id,name,tier,owner
C001,A 公司,strategic,张三
C002,B 公司,normal,李四
```

### orders.csv

```csv
order_id,customer_id,product_id,quantity,due_date,status,priority
SO-1001,C001,P001,500,2026-08-20,in_production,high
SO-1002,C002,P002,100,2026-08-25,planned,normal
```

### products.csv

```csv
product_id,name
P001,工业控制器
P002,传感器模块
```

### parts.csv

```csv
part_id,product_id,name,required_quantity_per_product,supplier_id
PART-A,P001,芯片 A,1,S001
PART-B,P001,外壳 B,1,S002
PART-C,P002,传感器 C,1,S003
```

### suppliers.csv

```csv
supplier_id,name,lead_time_days,reliability_score
S001,供应商甲,14,0.82
S002,供应商乙,5,0.95
S003,供应商丙,7,0.90
```

### inventory.csv

```csv
part_id,available_quantity,reserved_quantity
PART-A,200,50
PART-B,1000,100
PART-C,500,50
```

产出:6 个原始数据文件。

验收:至少能看出 `SO-1001` 需要 500 个产品,而 `PART-A` 库存只有 200。

## A2. 在 Foundry 创建 Dataset

在 Foundry 里依次上传 6 个 CSV,创建 Dataset:

```text
customers
orders
products
parts
suppliers
inventory
```

检查字段类型:

```text
customer_id: string
order_id: string
product_id: string
part_id: string
quantity: integer
due_date: date
lead_time_days: integer
reliability_score: double
available_quantity: integer
```

产出:6 个 Foundry Dataset。

验收:每个 Dataset 都能预览;主键字段没有空值;`orders.due_date` 是日期类型,不是普通字符串。

## A3. 创建 Object Types

进入 Ontology 管理界面,创建对象类型。

### Customer

```text
Object Type: Customer
Dataset: customers
Primary Key: customer_id
Display Name: name
Properties:
- customer_id
- name
- tier
- owner
```

### Order

```text
Object Type: Order
Dataset: orders
Primary Key: order_id
Display Name: order_id
Properties:
- order_id
- customer_id
- product_id
- quantity
- due_date
- status
- priority
```

### Product

```text
Object Type: Product
Dataset: products
Primary Key: product_id
Display Name: name
Properties:
- product_id
- name
```

### Part

```text
Object Type: Part
Dataset: parts
Primary Key: part_id
Display Name: name
Properties:
- part_id
- product_id
- name
- required_quantity_per_product
- supplier_id
```

### Supplier

```text
Object Type: Supplier
Dataset: suppliers
Primary Key: supplier_id
Display Name: name
Properties:
- supplier_id
- name
- lead_time_days
- reliability_score
```

### Inventory

```text
Object Type: Inventory
Dataset: inventory
Primary Key: part_id
Display Name: part_id
Properties:
- part_id
- available_quantity
- reserved_quantity
```

产出:6 个 Object Types。

验收:在 Ontology 里搜索 `SO-1001`,能打开一个 Order 对象并看到属性。

## A4. 创建 Link Types

创建对象关系。

### Customer has Orders

```text
From: Customer.customer_id
To: Order.customer_id
Cardinality: one-to-many
```

### Order contains Product

```text
From: Order.product_id
To: Product.product_id
Cardinality: many-to-one
```

### Product requires Parts

```text
From: Product.product_id
To: Part.product_id
Cardinality: one-to-many
```

### Part supplied by Supplier

```text
From: Part.supplier_id
To: Supplier.supplier_id
Cardinality: many-to-one
```

### Part has Inventory

```text
From: Part.part_id
To: Inventory.part_id
Cardinality: one-to-one
```

产出:5 个 Link Types。

验收:打开 `SO-1001`,能沿关系看到:

```text
Order SO-1001
→ Customer A 公司
→ Product 工业控制器
→ Parts 芯片 A、外壳 B
→ Supplier 供应商甲、供应商乙
→ Inventory PART-A 可用 200
```

## A5. 用 Transform 生成 Risk Dataset

创建一个 Transform / Pipeline,输出 `order_risks`。

输出字段:

```text
risk_id
order_id
risk_level
risk_reason
affected_part_id
shortage_quantity
estimated_delay_days
```

计算逻辑:

```text
required_total = order.quantity * part.required_quantity_per_product
shortage = required_total - inventory.available_quantity
remaining_days = order.due_date - today

如果 shortage > 0 且 supplier.lead_time_days > remaining_days:high
如果 shortage > 0:medium
否则:low
```

伪 SQL:

```sql
select
  concat(o.order_id, '-', p.part_id) as risk_id,
  o.order_id,
  case
    when o.quantity * p.required_quantity_per_product > i.available_quantity
     and s.lead_time_days > datediff(o.due_date, current_date)
    then 'high'
    when o.quantity * p.required_quantity_per_product > i.available_quantity
    then 'medium'
    else 'low'
  end as risk_level,
  concat(
    p.name,
    ' 库存不足,缺口 ',
    cast(o.quantity * p.required_quantity_per_product - i.available_quantity as string),
    ';供应商 ',
    s.name,
    ' 交期 ',
    cast(s.lead_time_days as string),
    ' 天。'
  ) as risk_reason,
  p.part_id as affected_part_id,
  greatest(o.quantity * p.required_quantity_per_product - i.available_quantity, 0) as shortage_quantity,
  greatest(s.lead_time_days - datediff(o.due_date, current_date), 0) as estimated_delay_days
from orders o
join parts p on o.product_id = p.product_id
join inventory i on p.part_id = i.part_id
join suppliers s on p.supplier_id = s.supplier_id;
```

产出:`order_risks` Dataset。

验收:`SO-1001` 至少有一条 high 或 medium risk,原因里包含 `芯片 A` 和 `缺口 300`。

## A6. 创建 Risk Object Type,并连接 Order

创建对象:

```text
Object Type: Risk
Dataset: order_risks
Primary Key: risk_id
Display Name: risk_level
Properties:
- risk_id
- order_id
- risk_level
- risk_reason
- affected_part_id
- shortage_quantity
- estimated_delay_days
```

创建关系:

```text
Order has Risks
From: Order.order_id
To: Risk.order_id
Cardinality: one-to-many
```

产出:Risk 对象和 Order -> Risk 关系。

验收:打开 `SO-1001`,能看到风险对象,风险原因可读。

## A7. 创建 Task Dataset / Object Type

创建用于动作写入的任务数据集或对象类型。

字段:

```text
task_id
order_id
title
assignee
status
created_at
```

创建 Object Type:

```text
Object Type: Task
Primary Key: task_id
Display Name: title
Properties:
- task_id
- order_id
- title
- assignee
- status
- created_at
```

创建关系:

```text
Order has Tasks
From: Order.order_id
To: Task.order_id
Cardinality: one-to-many
```

产出:Task Object Type。

验收:可以手工创建一条 Task 测试,并从 Order 反查到 Task。

## A8. 创建 Action Type:CreateMitigationTask

在 Ontology 中创建 Action Type。

```text
Action Type: CreateMitigationTask
Applies To: Order
Purpose: 为高风险订单创建处理任务
```

参数:

```text
order: Order
assignee: string
description: string
```

默认值:

```text
title = 处理订单 {order.order_id} 的延期风险
status = open
created_at = now()
```

提交条件:

```text
Order 必须存在;
assignee 不能为空;
当前用户必须有 supply_chain_manager 角色;
如果订单没有 high/medium risk,可以给 warning 或禁止提交。
```

副作用:

```text
创建 Task 对象;
关联到当前 Order;
记录 action audit。
```

产出:一个可在 Order 上执行的 Action。

验收:打开 `SO-1001`,执行 `CreateMitigationTask`,生成一条 Task,且 Order -> Task 关系能看到它。

## A9. 配置权限

最小角色:

```text
viewer:能看 Customer、Order、Product、Part、Supplier、Inventory、Risk;
supply_chain_manager:继承 viewer,并能执行 CreateMitigationTask;
admin:能管理 Object Type / Link Type / Action Type。
```

至少配置:

```text
Order: viewer 可读;
Risk: viewer 可读;
Task: viewer 可读;
CreateMitigationTask: supply_chain_manager 可执行。
```

产出:对象和动作权限。

验收:viewer 可以看风险,但看不到或不能执行 `CreateMitigationTask`;supply_chain_manager 可以执行。

## A10. 用 Workshop 搭 3 个页面

### 页面 1:Orders

数据源:Order 对象集。

列:

```text
order_id
customer.name
due_date
status
priority
risk_level
```

交互:点击订单进入详情页。

### 页面 2:Order Detail

展示区域:

```text
订单基本信息;
客户信息;
产品信息;
零件列表;
库存情况;
供应商情况;
风险原因;
关联任务。
```

动作按钮:

```text
CreateMitigationTask
```

### 页面 3:Tasks

数据源:Task 对象集。

列:

```text
task_id
order_id
title
assignee
status
created_at
```

产出:一个最小业务应用。

验收路径:

```text
Orders 页面打开 SO-1001;
Order Detail 看到芯片 A 库存不足;
点击 CreateMitigationTask;
Tasks 页面出现新任务。
```

## A11. 接入 AIP

在 AIP 中创建一个面向供应链运营的助手。

它只做三件事:

```text
解释订单风险;
总结影响对象;
建议下一步动作。
```

给它的上下文来自 Ontology:

```text
当前 Order;
linked Customer;
linked Product;
linked Parts;
linked Suppliers;
linked Inventory;
linked Risks;
当前用户可执行 Actions。
```

示例指令:

```text
你是供应链运营助手。只基于 Ontology 上下文回答,不要编造系统里没有的数据。
回答订单延期风险时,必须说明:风险等级、主要原因、影响对象、建议动作。
如果用户要求执行动作,只能调用当前用户有权限的 Action。
```

产出:AIP 助手。

验收:问它:

```text
为什么 SO-1001 有延期风险?
```

它应该回答:

```text
因为 SO-1001 需要 500 个芯片 A,但可用库存只有 200,缺口 300;供应商甲交期 14 天,因此存在延期风险。
```

再问:

```text
下一步怎么做?
```

它应该建议执行 `CreateMitigationTask`,而不是编造一个系统里不存在的动作。

# 路线 B:使用 Palantir,但不用官方 OSDK,直接 REST API

这一条路线适合:Ontology 已经建在 Foundry 里,但你不想用 Palantir 官方 OSDK,而是自己写前端或后端,直接调 HTTP API。

整体结构:

```text
自建前端 / 后端
  ↓ REST API
Palantir Ontology API
  ↓
Object Types / Link Types / Action Types
```

## B1. 准备 API 访问凭证

你需要准备:

```text
Foundry base URL
Access token
Ontology identifier / rid
Object Type API name
Action Type API name
```

这些值在不同组织环境里命名可能不同,但最小需要知道:

```text
我要访问哪个 Ontology;
我要读哪个 Object Type;
我要对哪个对象执行哪个 Action;
当前 token 对这些对象和动作有没有权限。
```

产出:一组环境变量。

```bash
export FOUNDRY_BASE_URL="https://your-foundry.example.com"
export FOUNDRY_TOKEN="..."
export ONTOLOGY="your-ontology-api-name-or-rid"
```

验收:用 token 调一个最简单的 list ontology / metadata 接口能返回 200。

## B2. 读取 Object Type 元数据

先不要直接写业务页面。第一步先读元数据。

目标:拿到 `Order` 的属性、主键、可用 link、可用 action。

伪请求:

```bash
curl -H "Authorization: Bearer $FOUNDRY_TOKEN" \
  "$FOUNDRY_BASE_URL/api/v2/ontologies/$ONTOLOGY/objectTypes/Order"
```

你要确认返回里至少能看到:

```text
Order 的 properties;
Order 的 primary key;
Order 的 links;
Order 可执行的 actions。
```

产出:前端或后端可以动态知道 Order 是什么。

验收:不要手写“Order 有哪些字段”;先从 API 读出来。

## B3. 查询订单列表

伪请求:

```bash
curl -H "Authorization: Bearer $FOUNDRY_TOKEN" \
  "$FOUNDRY_BASE_URL/api/v2/ontologies/$ONTOLOGY/objects/Order"
```

查询条件可以先简单:

```text
limit 50;
按 due_date 或 risk_level 排序;
只取 order_id、customer_id、due_date、status、priority。
```

产出:自建应用里的订单列表。

验收:页面能显示 `SO-1001`。

## B4. 查询单个订单对象

伪请求:

```bash
curl -H "Authorization: Bearer $FOUNDRY_TOKEN" \
  "$FOUNDRY_BASE_URL/api/v2/ontologies/$ONTOLOGY/objects/Order/SO-1001"
```

产出:订单详情基础信息。

验收:返回里有:

```text
order_id = SO-1001
quantity = 500
due_date = 2026-08-20
```

## B5. 查询 linked objects

接下来不要自己写 SQL join,而是通过 Ontology Link 查询关联对象。

要查:

```text
Order -> Customer
Order -> Product
Product -> Parts
Part -> Supplier
Part -> Inventory
Order -> Risks
Order -> Tasks
```

伪请求:

```bash
curl -H "Authorization: Bearer $FOUNDRY_TOKEN" \
  "$FOUNDRY_BASE_URL/api/v2/ontologies/$ONTOLOGY/objects/Order/SO-1001/links/order_product"
```

然后继续沿 Product 查 Parts,沿 Part 查 Supplier 和 Inventory。

产出:一个由 REST API 组装出来的 Order Graph。

验收:自建后端返回统一结构:

```json
{
  "order": {},
  "customer": {},
  "product": {},
  "parts": [],
  "risks": [],
  "tasks": []
}
```

## B6. 在自建后端做 Context Builder

不要让前端直接到处调 Foundry API。建议加一层自己的后端:

```text
GET /api/orders/{order_id}/context
```

后端做:

```text
1. 读 Order;
2. 读 linked Customer;
3. 读 linked Product;
4. 读 linked Parts;
5. 读 linked Supplier / Inventory;
6. 读 linked Risks;
7. 读当前用户可执行 Actions;
8. 返回一个给页面和 AI 共用的结构。
```

产出:统一上下文接口。

验收:页面和 AI 都用这个接口,不各自拼数据。

## B7. 执行 Action Type

当用户点击“创建处理任务”时,不要直接写自己的任务表,而是调用 Foundry Ontology Action。

伪请求:

```bash
curl -X POST \
  -H "Authorization: Bearer $FOUNDRY_TOKEN" \
  -H "Content-Type: application/json" \
  "$FOUNDRY_BASE_URL/api/v2/ontologies/$ONTOLOGY/actions/CreateMitigationTask/apply" \
  -d '{
    "parameters": {
      "order": "SO-1001",
      "assignee": "供应链经理",
      "description": "检查芯片 A 替代库存或联系供应商加急"
    }
  }'
```

产出:Task 对象在 Foundry 里被创建。

验收:重新查询 `Order -> Tasks`,能看到新任务。

## B8. 自建 AI 接口,但上下文来自 Ontology API

接口:

```text
POST /api/assistant/orders/{order_id}/explain-risk
```

后端流程:

```text
1. 调 /api/orders/{order_id}/context;
2. 将 context 传给 LLM;
3. 要求 LLM 只基于 context 回答;
4. 如果建议动作,只能从 available_actions 里选择。
```

产出:不用 OSDK 的 AI 解释层。

验收:AI 解释包含真实对象链路,并且不会建议不存在或无权限的动作。

# 路线 C:完全不用 Palantir,自建轻量 Ontology MVP

这一条路线适合学习和原型验证。

目标不是复制 Palantir,而是做一个最小 Ontology Runtime:

```text
PostgreSQL:存对象数据;
Object Registry:登记对象类型;
Link Registry:登记对象关系;
Action Registry:登记动作类型;
FastAPI:提供对象、关系、风险、动作 API;
Permission Middleware:过滤字段和动作;
Action Log:记录审计;
Context Builder:给 AI 生成结构化上下文;
React / Next.js:展示最小页面;
LLM API:解释风险、生成建议。
```

## C1. 创建项目目录

```bash
mkdir ontology-mvp
cd ontology-mvp
mkdir -p backend/app backend/registry frontend
```

目录结构:

```text
ontology-mvp/
├── backend/
│   ├── app/
│   │   ├── main.py
│   │   ├── db.py
│   │   ├── seed.py
│   │   ├── risk.py
│   │   ├── context_builder.py
│   │   ├── permissions.py
│   │   └── actions.py
│   ├── registry/
│   │   ├── object_types.json
│   │   ├── link_types.json
│   │   └── action_types.json
│   └── requirements.txt
├── frontend/
└── docker-compose.yml
```

产出:项目骨架。

验收:目录存在。

## C2. 启动 PostgreSQL

`docker-compose.yml`:

```yaml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: ontology_mvp
      POSTGRES_USER: ontology
      POSTGRES_PASSWORD: ontology
    ports:
      - "5432:5432"
    volumes:
      - ontology_pg:/var/lib/postgresql/data

volumes:
  ontology_pg:
```

启动:

```bash
docker compose up -d
```

验收:

```bash
docker ps
```

能看到 postgres 容器运行。

## C3. 创建数据库表

连接数据库:

```bash
psql "postgresql://ontology:ontology@localhost:5432/ontology_mvp"
```

建表:

```sql
create table customers (
  customer_id text primary key,
  name text not null,
  tier text not null,
  owner text not null
);

create table products (
  product_id text primary key,
  name text not null
);

create table suppliers (
  supplier_id text primary key,
  name text not null,
  lead_time_days integer not null,
  reliability_score numeric not null
);

create table parts (
  part_id text primary key,
  product_id text not null references products(product_id),
  name text not null,
  required_quantity_per_product integer not null,
  supplier_id text not null references suppliers(supplier_id)
);

create table inventory (
  part_id text primary key references parts(part_id),
  available_quantity integer not null,
  reserved_quantity integer not null
);

create table orders (
  order_id text primary key,
  customer_id text not null references customers(customer_id),
  product_id text not null references products(product_id),
  quantity integer not null,
  due_date date not null,
  status text not null,
  priority text not null
);

create table risks (
  risk_id text primary key,
  order_id text not null references orders(order_id),
  risk_level text not null,
  risk_reason text not null,
  affected_part_id text references parts(part_id),
  shortage_quantity integer,
  estimated_delay_days integer,
  created_at timestamp not null default now()
);

create table tasks (
  task_id text primary key,
  order_id text not null references orders(order_id),
  title text not null,
  assignee text not null,
  status text not null,
  created_at timestamp not null default now()
);

create table action_log (
  action_id text primary key,
  actor text not null,
  object_type text not null,
  object_id text not null,
  action_name text not null,
  parameters jsonb not null,
  created_at timestamp not null default now()
);
```

产出:业务对象表和审计表。

验收:

```sql
\dt
```

能看到 9 张表。

## C4. 插入样例数据

```sql
insert into customers values
('C001', 'A 公司', 'strategic', '张三'),
('C002', 'B 公司', 'normal', '李四');

insert into products values
('P001', '工业控制器'),
('P002', '传感器模块');

insert into suppliers values
('S001', '供应商甲', 14, 0.82),
('S002', '供应商乙', 5, 0.95),
('S003', '供应商丙', 7, 0.90);

insert into parts values
('PART-A', 'P001', '芯片 A', 1, 'S001'),
('PART-B', 'P001', '外壳 B', 1, 'S002'),
('PART-C', 'P002', '传感器 C', 1, 'S003');

insert into inventory values
('PART-A', 200, 50),
('PART-B', 1000, 100),
('PART-C', 500, 50);

insert into orders values
('SO-1001', 'C001', 'P001', 500, '2026-08-20', 'in_production', 'high'),
('SO-1002', 'C002', 'P002', 100, '2026-08-25', 'planned', 'normal');
```

验收:

```sql
select * from orders;
select * from inventory where part_id = 'PART-A';
```

能看到 `SO-1001` 和 `PART-A` 库存 200。

## C5. 创建 Object Registry

`backend/registry/object_types.json`:

```json
{
  "Order": {
    "table": "orders",
    "primary_key": "order_id",
    "display_name": "order_id",
    "properties": ["order_id", "customer_id", "product_id", "quantity", "due_date", "status", "priority"],
    "actions": ["create_mitigation_task"]
  },
  "Customer": {
    "table": "customers",
    "primary_key": "customer_id",
    "display_name": "name",
    "properties": ["customer_id", "name", "tier", "owner"]
  },
  "Product": {
    "table": "products",
    "primary_key": "product_id",
    "display_name": "name",
    "properties": ["product_id", "name"]
  },
  "Part": {
    "table": "parts",
    "primary_key": "part_id",
    "display_name": "name",
    "properties": ["part_id", "product_id", "name", "required_quantity_per_product", "supplier_id"]
  },
  "Supplier": {
    "table": "suppliers",
    "primary_key": "supplier_id",
    "display_name": "name",
    "properties": ["supplier_id", "name", "lead_time_days", "reliability_score"]
  },
  "Inventory": {
    "table": "inventory",
    "primary_key": "part_id",
    "display_name": "part_id",
    "properties": ["part_id", "available_quantity", "reserved_quantity"]
  },
  "Risk": {
    "table": "risks",
    "primary_key": "risk_id",
    "display_name": "risk_level",
    "properties": ["risk_id", "order_id", "risk_level", "risk_reason", "affected_part_id", "shortage_quantity", "estimated_delay_days"]
  },
  "Task": {
    "table": "tasks",
    "primary_key": "task_id",
    "display_name": "title",
    "properties": ["task_id", "order_id", "title", "assignee", "status", "created_at"]
  }
}
```

产出:对象元数据。

验收:后端能读取这个 JSON,并知道 `Order` 对应 `orders` 表。

## C6. 创建 Link Registry

`backend/registry/link_types.json`:

```json
{
  "links": [
    {
      "name": "order_customer",
      "from_type": "Order",
      "from_property": "customer_id",
      "to_type": "Customer",
      "to_property": "customer_id",
      "cardinality": "many-to-one"
    },
    {
      "name": "order_product",
      "from_type": "Order",
      "from_property": "product_id",
      "to_type": "Product",
      "to_property": "product_id",
      "cardinality": "many-to-one"
    },
    {
      "name": "product_parts",
      "from_type": "Product",
      "from_property": "product_id",
      "to_type": "Part",
      "to_property": "product_id",
      "cardinality": "one-to-many"
    },
    {
      "name": "part_supplier",
      "from_type": "Part",
      "from_property": "supplier_id",
      "to_type": "Supplier",
      "to_property": "supplier_id",
      "cardinality": "many-to-one"
    },
    {
      "name": "part_inventory",
      "from_type": "Part",
      "from_property": "part_id",
      "to_type": "Inventory",
      "to_property": "part_id",
      "cardinality": "one-to-one"
    },
    {
      "name": "order_risks",
      "from_type": "Order",
      "from_property": "order_id",
      "to_type": "Risk",
      "to_property": "order_id",
      "cardinality": "one-to-many"
    },
    {
      "name": "order_tasks",
      "from_type": "Order",
      "from_property": "order_id",
      "to_type": "Task",
      "to_property": "order_id",
      "cardinality": "one-to-many"
    }
  ]
}
```

产出:关系元数据。

验收:后端可以根据 Registry 知道 `Order -> Product -> Part -> Supplier` 怎么走。

## C7. 创建 Action Registry

`backend/registry/action_types.json`:

```json
{
  "create_mitigation_task": {
    "display_name": "创建延期风险处理任务",
    "object_type": "Order",
    "parameters": {
      "assignee": { "type": "string", "required": true },
      "description": { "type": "string", "required": true }
    },
    "required_role": "supply_chain_manager",
    "side_effect": "insert_task",
    "audit": true
  }
}
```

产出:动作元数据。

验收:AI 和页面都可以读取“Order 当前能执行 create_mitigation_task”。

## C8. 创建 FastAPI 后端

`backend/requirements.txt`:

```text
fastapi
uvicorn
psycopg[binary]
pydantic
python-dotenv
```

安装:

```bash
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

`backend/app/db.py`:

```python
import os
import psycopg
from psycopg.rows import dict_row

DATABASE_URL = os.getenv(
    "DATABASE_URL",
    "postgresql://ontology:ontology@localhost:5432/ontology_mvp",
)

def query(sql, params=None):
    with psycopg.connect(DATABASE_URL, row_factory=dict_row) as conn:
        with conn.cursor() as cur:
            cur.execute(sql, params or {})
            return cur.fetchall()

def execute(sql, params=None):
    with psycopg.connect(DATABASE_URL, row_factory=dict_row) as conn:
        with conn.cursor() as cur:
            cur.execute(sql, params or {})
            try:
                return cur.fetchall()
            except psycopg.ProgrammingError:
                return []
```

产出:后端可以连接数据库。

验收:启动后端时不报数据库连接错误。

## C9. 实现订单列表和订单详情 API

`backend/app/main.py` 先写基础接口:

```python
from fastapi import FastAPI, HTTPException, Header
from app.db import query, execute
from app.risk import calculate_risk
from app.context_builder import build_order_context
from app.actions import create_mitigation_task

app = FastAPI()

@app.get("/objects/orders")
def list_orders():
    return query("""
        select o.*, c.name as customer_name
        from orders o
        join customers c on o.customer_id = c.customer_id
        order by o.due_date asc
    """)

@app.get("/objects/orders/{order_id}")
def get_order(order_id: str):
    rows = query("select * from orders where order_id = %(order_id)s", {"order_id": order_id})
    if not rows:
        raise HTTPException(status_code=404, detail="order not found")
    return rows[0]
```

启动:

```bash
uvicorn app.main:app --reload
```

验收:

```bash
curl http://localhost:8000/objects/orders
curl http://localhost:8000/objects/orders/SO-1001
```

能返回订单数据。

## C10. 实现 Order Graph API

`backend/app/context_builder.py`:

```python
from app.db import query

def build_order_graph(order_id: str):
    order = query("select * from orders where order_id = %(order_id)s", {"order_id": order_id})
    if not order:
        return None
    order = order[0]

    customer = query(
        "select * from customers where customer_id = %(id)s",
        {"id": order["customer_id"]},
    )[0]

    product = query(
        "select * from products where product_id = %(id)s",
        {"id": order["product_id"]},
    )[0]

    parts = query(
        "select * from parts where product_id = %(id)s",
        {"id": product["product_id"]},
    )

    enriched_parts = []
    for part in parts:
        supplier = query(
            "select * from suppliers where supplier_id = %(id)s",
            {"id": part["supplier_id"]},
        )[0]
        inventory = query(
            "select * from inventory where part_id = %(id)s",
            {"id": part["part_id"]},
        )[0]
        enriched_parts.append({
            **part,
            "required_total": order["quantity"] * part["required_quantity_per_product"],
            "supplier": supplier,
            "inventory": inventory,
        })

    risks = query("select * from risks where order_id = %(id)s", {"id": order_id})
    tasks = query("select * from tasks where order_id = %(id)s", {"id": order_id})

    return {
        "order": order,
        "customer": customer,
        "product": product,
        "parts": enriched_parts,
        "risks": risks,
        "tasks": tasks,
    }

def build_order_context(order_id: str, role: str):
    graph = build_order_graph(order_id)
    if graph is None:
        return None

    actions = []
    if role == "supply_chain_manager":
        actions.append({
            "name": "create_mitigation_task",
            "description": "创建延期风险处理任务",
        })

    return {
        **graph,
        "available_actions": actions,
    }
```

在 `main.py` 加:

```python
@app.get("/objects/orders/{order_id}/graph")
def get_order_graph(order_id: str):
    graph = build_order_context(order_id, role="viewer")
    if graph is None:
        raise HTTPException(status_code=404, detail="order not found")
    return graph
```

验收:

```bash
curl http://localhost:8000/objects/orders/SO-1001/graph
```

必须看到:

```text
order
customer
product
parts
parts[].supplier
parts[].inventory
```

## C11. 实现风险计算 API

`backend/app/risk.py`:

```python
from datetime import date

LEVEL_SCORE = {"low": 1, "medium": 2, "high": 3}

def calculate_risk(graph):
    order = graph["order"]
    due_date = order["due_date"]
    if isinstance(due_date, str):
        due_date = date.fromisoformat(due_date)

    remaining_days = (due_date - date.today()).days
    candidates = []

    for part in graph["parts"]:
        required = part["required_total"]
        available = part["inventory"]["available_quantity"]
        lead_time = part["supplier"]["lead_time_days"]
        shortage = max(required - available, 0)
        estimated_delay = max(lead_time - remaining_days, 0)

        if shortage > 0 and lead_time > remaining_days:
            level = "high"
            reason = f"{part['name']} 库存不足,缺口 {shortage};供应商 {part['supplier']['name']} 交期 {lead_time} 天。"
        elif shortage > 0:
            level = "medium"
            reason = f"{part['name']} 库存不足,缺口 {shortage}。"
        else:
            level = "low"
            reason = f"{part['name']} 当前库存充足。"

        candidates.append({
            "risk_level": level,
            "risk_reason": reason,
            "affected_part_id": part["part_id"],
            "shortage_quantity": shortage,
            "estimated_delay_days": estimated_delay,
        })

    return max(candidates, key=lambda x: LEVEL_SCORE[x["risk_level"]])
```

在 `main.py` 加:

```python
@app.get("/objects/orders/{order_id}/risk")
def get_order_risk(order_id: str):
    graph = build_order_context(order_id, role="viewer")
    if graph is None:
        raise HTTPException(status_code=404, detail="order not found")
    return calculate_risk(graph)
```

验收:

```bash
curl http://localhost:8000/objects/orders/SO-1001/risk
```

返回应包含:

```text
risk_level
risk_reason
shortage_quantity
```

`shortage_quantity` 应该是 300。

## C12. 实现 Action:创建处理任务

`backend/app/permissions.py`:

```python
def require_role(role: str, required: str):
    if role != required:
        return False
    return True
```

`backend/app/actions.py`:

```python
import uuid
import json
from app.db import execute
from app.permissions import require_role


def create_mitigation_task(order_id: str, assignee: str, description: str, actor: str, role: str):
    if not require_role(role, "supply_chain_manager"):
        return {"error": "permission denied"}

    task_id = "TASK-" + uuid.uuid4().hex[:8]
    title = f"处理订单 {order_id} 的延期风险"

    execute(
        """
        insert into tasks(task_id, order_id, title, assignee, status)
        values (%(task_id)s, %(order_id)s, %(title)s, %(assignee)s, 'open')
        """,
        {
            "task_id": task_id,
            "order_id": order_id,
            "title": title,
            "assignee": assignee,
        },
    )

    execute(
        """
        insert into action_log(action_id, actor, object_type, object_id, action_name, parameters)
        values (%(action_id)s, %(actor)s, 'Order', %(object_id)s, 'create_mitigation_task', %(parameters)s)
        """,
        {
            "action_id": "ACT-" + uuid.uuid4().hex[:8],
            "actor": actor,
            "object_id": order_id,
            "parameters": json.dumps({"assignee": assignee, "description": description}),
        },
    )

    return {
        "task_id": task_id,
        "order_id": order_id,
        "title": title,
        "assignee": assignee,
        "status": "open",
    }
```

在 `main.py` 加:

```python
from pydantic import BaseModel

class CreateTaskRequest(BaseModel):
    assignee: str
    description: str

@app.post("/objects/orders/{order_id}/actions/create-mitigation-task")
def create_task(order_id: str, body: CreateTaskRequest, x_role: str = Header(default="viewer"), x_actor: str = Header(default="anonymous")):
    result = create_mitigation_task(
        order_id=order_id,
        assignee=body.assignee,
        description=body.description,
        actor=x_actor,
        role=x_role,
    )
    if "error" in result:
        raise HTTPException(status_code=403, detail=result["error"])
    return result
```

验收 1:viewer 不能执行。

```bash
curl -X POST http://localhost:8000/objects/orders/SO-1001/actions/create-mitigation-task \
  -H 'Content-Type: application/json' \
  -H 'X-Role: viewer' \
  -d '{"assignee":"供应链经理","description":"检查芯片 A 替代库存"}'
```

应返回 403。

验收 2:supply_chain_manager 可以执行。

```bash
curl -X POST http://localhost:8000/objects/orders/SO-1001/actions/create-mitigation-task \
  -H 'Content-Type: application/json' \
  -H 'X-Role: supply_chain_manager' \
  -H 'X-Actor: zhangsan' \
  -d '{"assignee":"供应链经理","description":"检查芯片 A 替代库存"}'
```

应返回 Task。

验收 3:审计记录存在。

```sql
select * from action_log order by created_at desc limit 5;
```

## C13. 实现 AI Context Builder 接口

在 `main.py` 加:

```python
@app.get("/assistant/orders/{order_id}/context")
def get_ai_context(order_id: str, x_role: str = Header(default="viewer")):
    context = build_order_context(order_id, role=x_role)
    if context is None:
        raise HTTPException(status_code=404, detail="order not found")
    context["risk"] = calculate_risk(context)
    return context
```

验收:

```bash
curl -H 'X-Role: viewer' http://localhost:8000/assistant/orders/SO-1001/context
```

`available_actions` 应为空或不包含创建任务。

```bash
curl -H 'X-Role: supply_chain_manager' http://localhost:8000/assistant/orders/SO-1001/context
```

`available_actions` 应包含:

```text
create_mitigation_task
```

这一步是自建版 Ontology 和普通业务系统的关键分水岭:AI 拿到的是经过权限过滤的对象上下文,而不是裸数据库。

## C14. 实现 AI 解释接口

接口:

```text
POST /assistant/orders/{order_id}/explain-risk
```

最小实现可以先不接真实 LLM,先返回模板解释;接 LLM 后,再把 context 传给模型。

模板版:

```python
@app.post("/assistant/orders/{order_id}/explain-risk")
def explain_risk(order_id: str, x_role: str = Header(default="viewer")):
    context = build_order_context(order_id, role=x_role)
    if context is None:
        raise HTTPException(status_code=404, detail="order not found")
    risk = calculate_risk(context)
    order = context["order"]
    customer = context["customer"]
    product = context["product"]

    return {
        "answer": f"订单 {order['order_id']} 的风险等级为 {risk['risk_level']}。主要原因是:{risk['risk_reason']} 该订单属于客户 {customer['name']},产品为 {product['name']}。建议从可执行动作中选择处理方式。",
        "risk": risk,
        "available_actions": context["available_actions"],
    }
```

接 LLM 时,prompt 应该这样组织:

```text
你是供应链运营助手。
只基于给定 JSON 上下文回答,不要编造不存在的信息。
请输出:风险等级、主要原因、影响对象、建议动作。
如果建议动作,只能从 available_actions 中选择。

上下文:
{context_json}
```

验收:

```bash
curl -X POST -H 'X-Role: supply_chain_manager' \
  http://localhost:8000/assistant/orders/SO-1001/explain-risk
```

返回应包含:

```text
风险等级;
芯片 A;
缺口 300;
create_mitigation_task。
```

## C15. 做最小前端

前端只需要三个页面。

### 页面 1:订单列表

调用:

```text
GET /objects/orders
```

展示:

```text
订单号
客户
交付日期
状态
优先级
```

点击订单进入详情。

### 页面 2:订单详情

调用:

```text
GET /assistant/orders/{order_id}/context
GET /objects/orders/{order_id}/risk
```

展示:

```text
订单信息;
客户信息;
产品信息;
零件需求;
库存情况;
供应商情况;
风险原因;
可执行动作。
```

按钮:

```text
AI 解释风险
创建处理任务
```

### 页面 3:任务列表

可以先加一个接口:

```text
GET /tasks
```

展示:

```text
任务 ID
关联订单
标题
负责人
状态
创建时间
```

验收路径:

```text
打开订单列表;
进入 SO-1001;
看到芯片 A 缺口 300;
点击 AI 解释风险;
点击创建处理任务;
任务列表出现新任务。
```

## C16. 最小部署

本地 MVP 可以这样跑:

```bash
# 1. 数据库
docker compose up -d

# 2. 后端
cd backend
source .venv/bin/activate
uvicorn app.main:app --reload

# 3. 前端
cd frontend
npm run dev
```

如果先不做前端,也可以只用 Swagger:

```text
http://localhost:8000/docs
```

验收完整 API 顺序:

```bash
curl http://localhost:8000/objects/orders
curl http://localhost:8000/objects/orders/SO-1001/graph
curl http://localhost:8000/objects/orders/SO-1001/risk
curl -H 'X-Role: supply_chain_manager' http://localhost:8000/assistant/orders/SO-1001/context
curl -X POST -H 'X-Role: supply_chain_manager' -H 'Content-Type: application/json' \
  http://localhost:8000/objects/orders/SO-1001/actions/create-mitigation-task \
  -d '{"assignee":"供应链经理","description":"检查芯片 A 替代库存"}'
```

# 最小验收标准

这个 MVP 是否成立,不看页面好不好看,而看下面这些问题:

```text
1. 业务对象是否显式建模?
2. 对象关系是否显式建模?
3. 用户是否能沿关系理解风险来源?
4. 风险是否由对象关系和规则计算出来?
5. 动作是否绑定在对象上,而不是只是孤立接口?
6. 权限是否能过滤对象字段和可执行动作?
7. AI 是否通过 Context Builder 获取上下文?
8. AI 是否只能建议和调用当前用户有权限的动作?
9. 所有动作是否有审计记录?
```

如果这些问题都能回答“是”,这个 MVP 才不是普通 CRUD,而是一个真正的 Ontology MVP。

# 最后总结

普通业务系统是:

> 为一个具体流程写页面、接口和规则。

Ontology MVP 是:

> 把这个流程背后的业务对象、关系、权限和动作显式建模,让页面、流程和 AI 都复用同一套模型。

Palantir 官方路线是:

```text
Dataset → Object Type → Link Type → Risk Transform → Action Type → Workshop → AIP
```

不用官方 OSDK 但仍使用 Palantir 的路线是:

```text
Ontology API → 自建 Context Builder → 自建页面 / AI → 调用 Action API
```

完全自建路线是:

```text
PostgreSQL → Object Registry → Link Registry → Action Registry → FastAPI → Context Builder → AI → 前端
```

AI 的优势不是替你开发系统,而是:

> 基于这套模型理解业务上下文,沿对象关系解释原因,在合法动作空间里给建议,并在权限和审计约束下协助执行。

所以,最小 MVP 的正确搭建顺序是:

```text
先定义对象;
再定义关系;
再定义风险规则;
再定义动作;
再加权限和审计;
再做 Context Builder;
最后接 AI 和页面。
```

这样做出来的,才不是一个普通订单风险页面,而是一个可以继续扩展到库存、供应商、生产、客户成功等场景的本体项目起点。