目录
[四、路线二:非结构化(文档 / 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
步骤
-
连数据源 → introspect 元数据:表清单、每列(名/类型/可空)、主键、外键、样例行
-
建 Object Type :表 → object type(实体名默认 = 表名);列 → properties(SQL 类型映射 XSD);主键 →
primaryKey(identity 元数据) -
建 Link Type: 外键 → link(
from子表 →to父表) -
加 logic 层:SHACL 红线(必填/枚举/范围/唯一)+ 派生 properties
-
加 action 层:为每个可写回对象定义 Action Type(前置条件 + 机器可读拒绝 + 写回 + authority map:source-backed / ontology-owned / derived)
-
加 security 层:行/列级权限
-
落库 :把类型层定义写入
ontology_revisions(JSONB,先存 draft 版本,带 revision 号) -
人审 → 批准 → 发布:draft → published,revision-guarded;旧版保留、可回滚
-
交付方式:本体当上下文 按问题检出类型切片(类型 + 属性 + 物理映射)
示例: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
步骤
-
解析 + 分块 :文档识别 → 解析器(PDF→pdfplumber / PyMuPDF、DOCX→python-docx、扫描件→OCR)→ 归一为带结构文本 → 切块,每 chunk 带
docId / page / section / chunkId -
全量抽取候选(类型级,不抽实例) :每个 chunk 抽所有 候选类型 / 关系 / 属性------只抽类型层,JSON Schema(
strict)锁输出形状,类型名开放 + 软种子引导复用;每条回写chunkId -
对齐归一(命门) :同义类型 / 关系 embedding 聚类 + 人审拍 canonical 名 ("供应商 / 厂商 / 供货方" →
Supplier);可选建 is-a,产出 = master 本体库(中央池) -
编本体 JSON ( canonical ) :组装
data(objectTypes / linkTypes / properties)+logic(SHACL:必填 / 枚举 / 范围 / 唯一)+action(写回对象配 Action Type)+security(行 / 列级 scope);每元素挂authority(source-backed / derived)+provenance(PROV-O) -
人审 → 版本化发布 :概率抽取必审;候选 → 批准 → 存
ontology_revisions(draft → published,可回滚) -
按需检出场景本体 :按场景需要的类型,从 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 就完事" ,每层都要工程 + 人审兜底;最易被低估的是对齐归一 和人审闭环两块
五、统一循环
同一个循环:抽取 → 对齐 → 约束 → 人审 → 发布 → 检出
