本体构建方法

目录

一、总体定义

二、两条路线对比

三、路线一:结构化

[四、路线二:非结构化(文档 / PDF / Word / MD)](#四、路线二:非结构化(文档 / PDF / Word / MD))

五、统一循环


一、总体定义

本体 = data(objectTypes / linkTypes / properties)+ logic + action + security

其中:

  • 结构化抽取:读数据库 schema 机械抽取,确定、可复现、几乎不用审

  • 非结构化抽取:用 LLM 从文档抽取,概率性结果,必须人审 + 溯源

前提:只构建类型层本体, 产出 objectTypes / linkTypes / properties / logic / action / security 六类定义,不存实例;业务数据留在原库

二、两条路线对比

路线一:结构化

  • 输入:关系库连接(PG / MySQL / SQLite)

  • 抽取器:schema introspection

  • 实体从哪来:表 → object type

  • 关系从哪来:外键 → link

  • 属性从哪来:列 → attribute

  • 人审强度:低

  • 产出:本体 JSON(类型层)

路线二:非结构化

  • 输入:文档(PDF / MD / Word)+ 范围说明

  • 抽取器:LLM 结构化抽取

  • 实体从哪来:LLM 列候选类型

  • 关系从哪来:LLM 抽关系类型

  • 属性从哪来:LLM 抽每类属性

  • 人审强度:高

  • 产出:本体 JSON(类型层)


三、路线一:结构化

输入:关系库 / Foundry dataset

产出:本体 JSON

步骤

  1. 连数据源 → introspect 元数据:表清单、每列(名/类型/可空)、主键、外键、样例行

  2. 建 Object Type :表 → object type(实体名默认 = 表名);列 → properties(SQL 类型映射 XSD);主键 → primaryKey(identity 元数据)

  3. 建 Link Type: 外键 → link(from 子表 → to 父表)

  4. 加 logic 层:SHACL 红线(必填/枚举/范围/唯一)+ 派生 properties

  5. 加 action 层:为每个可写回对象定义 Action Type(前置条件 + 机器可读拒绝 + 写回 + authority map:source-backed / ontology-owned / derived)

  6. 加 security 层:行/列级权限

  7. 落库 :把类型层定义写入 ontology_revisions(JSONB,先存 draft 版本,带 revision 号)

  8. 人审 → 批准 → 发布:draft → published,revision-guarded;旧版保留、可回滚

  9. 交付方式:本体当上下文 按问题检出类型切片(类型 + 属性 + 物理映射)

示例:Customer / Orders 表 → 本体

① 表结构(SQL DDL + 样例行)

sql 复制代码
CREATE TABLE Customer (
  customer_id INTEGER PRIMARY KEY,   -- 主键 PK
  name        TEXT    NOT NULL,
  region      TEXT
);

CREATE TABLE Orders (
  order_id    INTEGER PRIMARY KEY,   -- 主键 PK
  customer_id INTEGER,               -- 外键 FK → Customer.customer_id
  status      TEXT,
  total       INTEGER,
  FOREIGN KEY (customer_id) REFERENCES Customer(customer_id)
);

样例行:

  • Customer 表:(1, '北京医药', '华北')

  • Orders 表:(100, 1, 'pending', 250)

② 语义对照(数据库 → 类型层本体)

  • 一张表 → 一个实体类型(object type)

例:Customer 表 → 类型 Customer(显示名"客户");Orders 表 → Orders(显示名"订单")

  • 一列 → 一个属性(property)

例:name、region → 客户的"名字""地区"

  • 主键 → 类型的 identity(customer_id 唯一标识一个客户),作为元数据保留

  • 外键 → 一条关系(link)

例:Orders.customer_id → 订单指向客户,many-to-one

③ 最终本体 JSON

python 复制代码
{
  "data": {
    "objectTypes": [
      { "name": "Customer", "primaryKey": "customer_id",
        "properties": { "customer_id": "integer", "name": "string", "region": "string" },
        "iri": "http://example.org/Customer" },
      { "name": "Orders", "primaryKey": "order_id",
        "properties": { "order_id": "integer", "customer_id": "integer", "status": "string", "total": "integer" },
        "iri": "http://example.org/Orders" }
    ],
    "linkTypes": [
      { "name": "Orders_Customer", "from": "Orders", "to": "Customer",
        "kind": "many-to-one",
        "viaForeignKey": "Orders.customer_id -> Customer.customer_id",
        "refPropertyIri": "http://example.org/Orders#ref-customer_id" }
    ]
  }
}

要点:表名 = 类,列 = 属性, 主键 = identity, 外键 = 关系边

具体实现:introspect → 映射函数

整条链路是纯函数:输入 schema 元数据,输出本体 JSON,确定性、可复现

① 获取元数据(introspect)

PostgreSQL(information_schema + pg_catalog):

sql 复制代码
-- 表
SELECT table_name FROM information_schema.tables WHERE table_schema='public';
-- 列(名 / 类型 / 可空)
SELECT column_name, data_type, is_nullable
FROM information_schema.columns WHERE table_name='orders';
-- 主键
SELECT a.attname FROM pg_index i
JOIN pg_attribute a ON a.attrelid=i.indrelid AND a.attnum=ANY(i.indkey)
WHERE i.indrelid='orders'::regclass AND i.indisprimary;
-- 外键(link 的来源)
SELECT conname, conrelid::regclass AS child, a.attname AS child_col,
       confrelid::regclass AS parent, af.attname AS parent_col
FROM pg_constraint c
JOIN pg_attribute a  ON a.attrelid=c.conrelid  AND a.attnum=ANY(c.conkey)
JOIN pg_attribute af ON af.attrelid=c.confrelid AND af.attnum=ANY(c.confkey)
WHERE c.contype='f';

SQLite 同义:PRAGMA table_info('表')(列 + 主键)、PRAGMA foreign_key_list('表')(外键)MySQL 用 information_schema,语法与 PG 基本一致

② 映射函数(Direct Mapping 转换)

python 复制代码
SQL_TYPE_MAP = {"INTEGER": "integer", "TEXT": "string", "VARCHAR": "string",
                "TIMESTAMP": "dateTime", "DATE": "date", "BOOLEAN": "boolean"}

def map_type(sql_type: str) -> str:                 # 列类型 → XSD
    return SQL_TYPE_MAP.get(sql_type.split("(")[0].upper(), "string")

def to_ontology(catalog: dict) -> dict:             # catalog = introspect 结果
    object_types, link_types = [], []
    for t in catalog["tables"]:
        object_types.append({
            "name": t["name"],                                   # 表 → 类
            "primaryKey": t["pk"][0] if len(t["pk"]) == 1 else t["pk"],
            "properties": {c: map_type(ty) for c, ty in t["columns"]},   # 列 → 属性
            "iri": f"http://example.org/{t['name']}",
        })
        for fk in t["fks"]:                                      # 外键 → link
            link_types.append({
                "name": f"{t['name']}_{fk['parent']}",
                "from": t["name"], "to": fk["parent"], "kind": "many-to-one",
                "viaForeignKey": f"{t['name']}.{fk['col']} -> {fk['parent']}.{fk['ref']}",
                "refPropertyIri": f"http://example.org/{t['name']}#ref-{fk['col']}",
            })
    return {"objectTypes": object_types, "linkTypes": link_types}

换数据库只改 ① 的取数 SQL,② 的转换逻辑一字不动

存储

构建产物是一份本体 JSON(类型层),业务数据留在原库不出域。

PG 落库(推荐):

sql 复制代码
CREATE TABLE ontology_revisions (
  id            BIGSERIAL PRIMARY KEY,
  ontology_name TEXT NOT NULL,
  revision      INT  NOT NULL,           -- 版本号,revision-guarded
  status        TEXT NOT NULL,           -- draft / published
  canonical     JSONB NOT NULL,          -- 整份类型层定义
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (ontology_name, revision)
);

难点:结构化路线到底难在哪

机械骨架几乎无瓶颈(表 / 列 / PK / FK → 类 / 属性 / identity / link 全是纯函数),但"整个结构化本体实现"的难点在映射之外。

工程层难点:

  • 多对多中间表:

    student_course(学生, 课程) 这种只有两个外键、没业务字段的表,会被误当成"实体"

  • 自引用层级:

    表上有指向自己的列,建本体时要认出这是层级关系、不是普通关系

  • 码表 / 字典表:

    order_status(code, name) 这种小表其实是值域,会被误当业务表

  • 隐式关系:

    很多库没建 FK 约束,只约定"cust_id 就是客户 ID",Direct Mapping 读不到这种约定

  • schema 演进:

    表后来加列 / 改类型 / 删表,本体怎么跟着变

语义层难点:

  • 列语义缺失:

    status='A' 是什么、单位是元还是分、口径,数据库只说它是 TEXT,说不出含义

  • 多源类型对齐:

    CRM 的 Customer、ERP 的 Customer、财务的 Customer,是不是同一个 object type?同名不同义(口径不同)、同义不同名(Client vs Customer)都要归一

  • 口径冲突:

    CRM"订单金额"含税、财务"订单金额"不含税,同名同列含义不同

一句话:企业级多源统一本体,难点全在 语义对齐 + 隐式关系 + 多源类型对齐


四、路线二:非结构化(文档 / PDF / Word / MD)

输入:文档(PDF/Word/MD/扫描件)

产出:本体 JSON

步骤

  1. 解析 + 分块 :文档识别 → 解析器(PDF→pdfplumber / PyMuPDF、DOCX→python-docx、扫描件→OCR)→ 归一为带结构文本 → 切块,每 chunk 带 docId / page / section / chunkId

  2. 全量抽取候选(类型级,不抽实例) :每个 chunk 抽所有 候选类型 / 关系 / 属性------只抽类型层,JSON Schema(strict)锁输出形状,类型名开放 + 软种子引导复用;每条回写 chunkId

  3. 对齐归一(命门) :同义类型 / 关系 embedding 聚类 + 人审拍 canonical 名 ("供应商 / 厂商 / 供货方" → Supplier);可选建 is-a,产出 = master 本体库(中央池)

  4. 编本体 JSON ( canonical ) :组装 data(objectTypes / linkTypes / properties)+ logic(SHACL:必填 / 枚举 / 范围 / 唯一)+ action(写回对象配 Action Type)+ security(行 / 列级 scope);每元素挂 authority(source-backed / derived)+ provenance(PROV-O)

  5. 人审 → 版本化发布 :概率抽取必审;候选 → 批准 → 存 ontology_revisions(draft → published,可回滚)

  6. 按需检出场景本体 :按场景需要的类型,从 master 投影过滤(checkout_scenario(needed_types, depth)),产子本体 JSON,带 derivedFrom 指母库

示例:供应商管理规范.md → 本体

① 文档片段 + 分块

"客户(Customer)拥有唯一客户编号与所属大区;一名客户可下达多笔订单(Order),订单记录状态(待支付 / 已发货 / 已取消)与金额。"

② LLM 结构化抽取( JSON 强约束输出候选)

sql 复制代码
{ "entityTypes": [
    { "name": "Customer", "properties": ["customer_id", "region"] },
    { "name": "Order",    "properties": ["status", "total"] } ],
  "relationTypes": [
    { "from": "Customer", "to": "Order", "kind": "one-to-many", "label": "places" } ] }

③ 最终 canonical JSON (注意 source 指向文档,且带 authority + provenance + logic )

sql 复制代码
{
  "data": {
    "objectTypes": [
      { "name": "Customer", "primaryKey": "customer_id",
        "properties": { "customer_id": "string", "region": "string" },
        "iri": "http://example.org/Customer",
        "authority": "source-backed",
        "provenance": { "sourceDoc": "供应商管理规范.md", "chunk": "chunk-3", "page": 2, "method": "llm-extract", "model": "gpt-4o" } },
      { "name": "Order", "primaryKey": "order_id",
        "properties": { "status": "string", "total": "integer" },
        "iri": "http://example.org/Order",
        "authority": "source-backed",
        "provenance": { "sourceDoc": "供应商管理规范.md", "chunk": "chunk-3", "page": 2 } }
    ],
    "linkTypes": [
      { "name": "Customer_Order", "from": "Customer", "to": "Order", "kind": "one-to-many",
        "authority": "source-backed",
        "provenance": { "sourceDoc": "供应商管理规范.md", "chunk": "chunk-3", "page": 2 } }
    ]
  },
  "logic": [
    { "target": "Customer", "property": "customer_id", "constraint": "minCount 1, unique" },
    { "target": "Order",    "property": "status",       "constraint": "in [待支付, 已发货, 已取消]" }
  ]
}

和结构化的唯一区别 :objectTypes / linkTypes 的形状完全一样 ,只是 provenance.sourceDoc 指向文档而非数据库表,authority 标了这条元素是"文档明写"还是"LLM 推断"


具体实现:全量抽取成库 → 按需检出场景本体

"文档多、场景多、业务说不清将来要啥",更稳的做法是先全量抽、后按需检出:把所有能抽的类型 / 关系都抽出来汇成中央本体库,建具体场景时再"检出"需要的那部分

具体做法(三层)

  • 第 1 层 抽取候选(corpus-wide induction):

不抽实例,只抽类型 + 关系 + 属性这个 schema 层,每个 chunk 让 LLM 吐候选类型 / 关系(schema-free 或轻引导),先不论对错汇进池

  • 第 2 层 对齐归一(命门):

候选类型名做 embedding,聚类同义("供应商 / 厂商 / 供货方"向量近 → 一簇);每簇由人审拍一个 canonical 名 + 定义;关系同理("供应 / 供货 / 提供" → supplies);可选建 is-a 层级("螺栓" is-a "Material"),产出 = master canonical(fourfold:data / logic / action / security + provenance),这就是"本体构建库"

  • 第 3 层 检出场景本体(checkout):

输入"这个场景要哪些类型",对 master 做一次投影过滤,产出子 canonical,纯函数,不碰原库:

python 复制代码
def checkout_scenario(master, needed_types, depth=1):
    # master: fourfold canonical dict;needed_types: 如 ["Supplier","Material"]
    objs = {n: o for n, o in master["data"]["objectTypes"].items() if n in needed_types}
    # 只保留这些类型之间的 linkTypes(from/to 都在集合内)
    links = {lid: l for lid, l in master["data"]["linkTypes"].items()
             if l["from"] in objs and l["to"] in objs}
    # depth>1 时按跳数做图遍历,把相距 ≤depth 跳的类型也拉进来
    # (真实实现用邻接表 BFS/DFS,此处省略连通性扩展)
    return {
        "data": {"objectTypes": objs, "linkTypes": links,
                 "properties": {k: v for k, v in master["data"]["properties"].items()
                                if k in objs}},
        "logic": {k: v for k, v in master["logic"].items() if k in objs},  # 仅保留相关 SHACL
        "provenance": master["provenance"],                                # 保留来源
    }

检出结果单独存一条 ontology_revisions 记录,canonical 字段存上面这份子 canonical,并带 derivedFrom = master.revision_id 指向母库版本,可追溯

存储

直接复用路线一的 ontology_revisions(canonical JSONB),master 存一条 published 记录,每个场景本体存一条 derivedFrom = master 的记录


难点:非结构化路线到底难在哪

LLM 抽取不是"套上就完事"------输入是自由文本、模型是概率的,难点在"抽取质量 + 抽出来怎么归一成人能信的本体",和结构化相反:结构化的难点在映射之外,非结构化的难点在抽取本身

工程层难点:

  • 多格式解析质量:

PDF 表格抽乱、扫描件 OCR 错字,底层文本就错了,上游 LLM 再强也抽不出真东西

  • 分块切断语义:

一个概念跨两页被切成两 chunk,各抽一半合起来残缺

  • 调用成本与时延:

千页文档 = 千次 LLM 调用,钱和时间是问题

  • 版本增量更新:

文档改一版不能全量重抽

语义层难点:

  • 幻觉 / 编造:

LLM 没见过的也能编个类型/关系出来

  • 遗漏(召回不足):

长文档重要类型没抽出来

  • 同义命名爆炸:

"供应商/厂商/供货方"三种名指一物,不合并就是垃圾场

  • 粒度漂移:

有时抽"订单"有时抽"采购订单",粗细不一

  • 关系方向错:

from/to 搞反("客户下订单"变"订单下客户")

  • 属性类型推断错:

金额/日期被当字符串,后面做不了范围约束

  • 跨文档冲突:

文档 A 说订单有状态枚举,B 没提或口径不同(含税/不含税)

  • is-a 层级自动构建难:

让 LLM 自动说"螺栓 is-a 物料"容易错 / 漏

一句话 :非结构化的难点全在"概率抽取的不确定性 + 抽出来怎么归一成人能信的本体"------幻觉 / 遗漏 / 同义 / 粒度 / 方向是抽取质量,对齐归一(同义合并 + is-a + 冲突)是主要成本,没有一项是"套个 LLM 就完事" ,每层都要工程 + 人审兜底;最易被低估的是对齐归一 和人审闭环两块


五、统一循环

同一个循环:抽取 → 对齐 → 约束 → 人审 → 发布 → 检出

相关推荐
深蓝AI29 分钟前
Meta FAIR 开源 e-means:k-means 十分之一的轮数跑出更低损失,已并入 Faiss
人工智能·程序员
IT_陈寒31 分钟前
Vue这个奇葩的响应式问题让我加班到凌晨
前端·人工智能·后端
yunhuibin39 分钟前
AI分类任务种类
人工智能·自然语言处理·分类
东坡肘子40 分钟前
参与学生移动应用大赛有感 -- 肘子的 Swift 周报 #161
人工智能·swiftui·swift
天天被压力42 分钟前
【A股分钟级高频数据完全手册 #11】券商实时行情数据接口:实时交易(券商源)接入与字段
java·人工智能·python
唐老板1 小时前
agent 撑爆 Git 之后,两家改的不是同一层
人工智能·ai编程
总裁余(余登武)1 小时前
【python项目】:1操作EXCEl自动化开发
python·自动化·excel
芝梵家电数据笔记1 小时前
用字段表核对暖菜板长期使用条件:结构、暖区、质保边界
python·家电·数据校验·dataclass·选购
天天被压力1 小时前
【A股分钟级高频数据完全手册 #12】股票五档盘口数据接口:买卖五档实时盘口解析
java·人工智能·python