拆开这个 24 小时涨星 2000+ 的本体项目:Auto Ontology 的自动化边界比宣传的窄
NVIDIA 的 auto-ontology 最近在 GitHub Trending 上冲得很猛,社区统计 24 小时新增 2,000+ star,到 11 月初仓库累计约 4.4k star。名字里的 “auto” 和 “ontology” 两个词都很容易让人兴奋:前者暗示自动建模,后者暗示一套表达力完备的领域本体。把仓库从 auto_ontology/semantic/ 一路读到 skills/,结论有点反高潮——它自动化的是从物理元数据起草语义映射这一段,而不是“自动造出一个本体”;它存储的也不是 OWL 意义上的公理体系,而是一张能回读、能被 SQL 验证的映射表。
这个边界并不丢人,反而是这个项目最值得学的地方。下面用源码和官方技能文档把这条边界画清楚。
一次语义编译,究竟自动了什么
入口在 语义编译总控:先 compile_semantic_layer 做逐表分类,再 resolve_semantic_fks 补语义外键,最后是桥表与 SQL 属性建议。真正“自动”的魔法都集中在第一阶段。
第一阶段的工作清单不是“所有表”,而是还没有 Term 的表——fetch_all_tables_without_term 的 SQL 里直接写了 NOT EXISTS (SELECT ... FROM table__term ...)。这意味着编译天然是增量的:一张表一旦被赋过 Term,后续编译不会再碰它,除非你手动重置编译。这也解释了为什么“自动”在这里更接近“首次冷启动批量补齐”,而不是“持续自我演化”。
并发模型写得很克制。pipeline.py 只有 3 个工作线程,LLM 调用并行,但提交阶段按数据库名/模式名/表名的 canonical 顺序串行,用 _OrderedCommitQueue 保证同名 Term 的归属是确定的:谁先拿到未限定的名字,由排序决定,而不是由线程调度决定。这个细节很能说明项目的气质——宁可慢,也要可复现。
单表处理在 visit_enter.py,几个常量几乎定义了整套“自动”的精度上限:
- 每列采样最多 1,000 行,每列只保留最常见的 5 个值;
- 字符串样本超过 30 字符不落库,日期/时间/UUID 类型直接不采样;
- 文本列若基数不超过 25,改用
DISTINCT全量抓取类别值,避免稀有枚举被高频值淹没; - 采样查询超时 120 秒,连续 3 次失败就熔断本次编译的采样,理由是“86 张表 × 120 秒接近三小时,且下游没有任何收益”。
然后是三个 LLM 角色:判外键/主键/桥表的 fk_suggester.py、提 Term 与列显示名的 term_extractor.py、以及从查询历史里挑可复用表达式的 sql_attribute_suggester.py。它们的提示词里藏着大量“别乱来”的约束,比如外键建议要求列名以 _id 结尾且前缀指向另一个实体,明确排除 weight、height、name 这类度量与标签列;桥表判定要求“至少两个外键角色”,并显式列出卫星表、查找表、自层级表都不算。
最关键的约束在 term_extractor.py 的系统提示词第 4 条:
4. Do not propose IS_A, PART_OF, or ROLE relationships.
也就是说,自动环节只产出最浅的一层分类学:一个表对应一个 Term(只有真正的联结表才允许拆成两个),每个列分配到某个 Term 并拿到一个人类可读的显示名。IS_A、PART_OF、ROLE 这些真正体现“本体”味道的关系,被明确禁止由 LLM 提议。同样地,semantic/constants.py 里定义了 7 种语义关系,但编译链路只自动生成 HAS_ATTRIBUTE、REPRESENTS、SEMANTIC_FK 这一层结构边。
还有一处容易被忽略但很说明问题:LLM 输出会经过 _sanitize_result 清洗,列名必须在候选集合里、不能重复分配、Term 名要匹配 ^[A-Z][A-Za-z0-9]*(?: [A-Za-z][A-Za-z0-9]*)*$;如果 LLM 完全失败,还有 _fallback_result 用表名生成一个占位 Term。这条兜底路径保证了流水线不崩,但也说明——“有 Term”不等于“有语义”,占位符和人工确认过的定义在库里长得一模一样。
人必须亲手做的四件事
如果只读 README,很容易以为这是一套端到端的自动化。实际上至少有四个关口是设计上留给人的。
第一,新概念没有公开的创建入口。 项目自带的建模规范 modeling.md 写得很直白:Auto Ontology 没有直接创建空 Term 的公开端点,也没有完整的结构化关系变更工作流,遇到表达不了的设计应当返回“模型缺口”,而不是伪造一个操作。翻 OpenAPI 描述 可以印证:81 条路径里,/api/terms 只有 GET,/api/terms/{term_id} 只有 GET 和 PATCH。概念的产生只能经由编译、数据接入或 YAML 模型导入。
第二,认证是人打的布尔位。 术语路由 的 PATCH 请求体里是 name_certified、description_certified;列属性的补丁里是 certified。这些标记再在 dal/terms.py 里聚合成 certified / partial / pending 三态。换句话说,系统不会自己判断一个定义“对不对”,它只忠实记录“有人确认过”。
第三,发布不是原子操作。 publication.md 给出的流程是:先按数据库 ID 范围导出 YAML 并保存 SHA-256 作为回滚物,在隔离环境先 apply,再把同一范围重新导出,逐项比对 ID、结构、属性、关系、计数,然后跑正反例的 readiness / answerability / SQL / 行 / 措辞探针,最后才提升。文档同时点名已知缺陷:替换导入可能残留被省略的语义外键。技能文档反复强调一句话——API 返回成功、导入成功、编译完成、回答非空,都不构成“语义已正确发布”的证据。
第四,MCP 是只读的。 MCP 工具清单 里只有 search_terms、get_term、get_term_columns、get_term_sql_attributes、get_sql_attribute、describe_table、check_answerable 这七个读工具,没有任何写工具。这是刻意设计:Agent 可以消费语义层,但不能顺手改它。所有写操作必须走 Next.js 的 /api/... 网关并经过权限校验。
把这四件事和第一节对照,项目的分工就清楚了:机器负责“起草 + 建议 + 补全结构边”,人负责“定义 + 认证 + 发布”。它更像一个带 LLM 辅助的语义建模工作台,而不是一个自动本体工程师。
为什么不引入独立图数据库
组件表里写得很清楚:Postgres + pgvector 一个库同时承担应用元数据、数据目录、语义层和向量存储。pyproject.toml 的依赖里也找不到 neo4j、kuzu、nebula 之类的图引擎。仓库的架构图也印证了这一点——所有组件围绕一个 PostgreSQL 汇聚:
“图”在实现层面是邻接表:dal/schema.py 里是 column__semantic_fk、column__has_attribute、column_attribute__term、table__term 这些普通的关系表。更值得注意的是同一文件里关于 join_path_edge 视图的注释:这个曾经用于路径查找的视图已经没有任何 Python 代码读取它,路径遍历改成了直接读 column__semantic_fk 和 column__has_attribute;视图被保留下来,只是因为“删掉它属于迁移决策,而不是遍历方式变更的副作用”。这条注释基本等于官方承认:这里的图查询就是应用层 BFS,不是 Cypher。
路径查找的实现在 dal/attributes.py 的 _find_table_join_hops,用 deque 做广度优先,最大跳数由 max_hops = (MAX_PATH_DEPTH + 2) // 4 推出,MAX_PATH_DEPTH = 30,也就是最多 8 跳。这段代码里有一处非常典型的手写图约束:SEMANTIC_FK 只在“目标列是该表自身声明的主键”时才允许反向成边。否则两个都指向同一 status_code 的外键列会被判成可互相 join,造出一条假路径——注释的原话是“两列只是引用了同一个东西,并不意味着它们互相引用”。
不引入图数据库的理由,从代码里能推出四条:
- 一致性优先于查询表达力。 一次编译要同时写 Term、ColumnAttribute、关系表和向量索引。放在一个库里,这些写入可以共享事务与连接池;拆成两个库,就得处理双写与补偿,而项目的发布流程本来就已经在跟原子性较劲。
- 图规模是元数据级,不是行级。 节点是库、模式、表、列、属性、分析定义,量级在万到十万;真正需要“多跳”的是 join 路径,而这类查询最终要翻译成源库上的 SQL,图数据库帮不上执行。
- 语义层与目录必须能互相校验。 列属性要绑定物理列,语义外键要绑定声明的外键与主键,这种“同库可 join、可回读”的能力,是发布流程里“精确回读”的前提。
- 向量检索已经被顺手解决了。 vdb/postgres.py 里有一段很实在的调优记录:pgvector 的 HNSW 索引对
vector类型上限 2000 维,而模型输出 2048 维;建表达式索引只能服务带 cast 的查询,而PGVectorStore生成的是不带 cast 的embedding <=> $1,Postgres 会静默走顺序扫描且不报错。解法是把列类型改成halfvec(2048),让参数侧做 cast。注释给出的实测是 51,200 行上从 175ms 降到 0.36ms。这类问题在图数据库里同样存在,只是换了个地方处理。
代价也要说清楚:图约束只能靠代码里的 if 表达,不能靠 schema 表达;跨库跨模式的关系遍历需要应用层拼装;join_path_edge 这种“留着但不用”的遗留物,本身就是演进过程中付出的维护成本。
与学术本体:可验证映射优先于表达力
学术本体的价值坐标是表达力与可推理性:OWL 公理、描述逻辑、推理机、SPARQL 查询、SHACL 校验,追求的是“用形式化语言把领域知识说清楚,并让机器推出新知识”。
Auto Ontology 走的是另一条路。它的词表只有 Term、ColumnAttribute、SqlAttribute、TextAttribute、Analysis,加 7 种关系常量;模型导入导出走 YAML,还兼容 Apache Ossie 格式,但仓库里没有 TTL、没有 OWL、没有推理机、没有 SPARQL 端点。前面提到的 Do not propose IS_A, PART_OF, or ROLE 更直接:连这些关系都不让模型自动生成。
那它靠什么保证“本体是对的”?靠验证,而不是靠公理。
一是查询前的覆盖率检查。 entity_coverage/agents/coverage.py 的模块注释就写着 “Deterministic coverage grade”:先用向量距离阈值过滤候选,再用命中记录反查列属性上下文,最后算一个 覆盖实体数 / 问题实体数 的比例。整数除法,没有 LLM 参与,同一个问题永远得到同一个分数。
二是执行后的三层校验。 query-validation.md 要求把“问题意图、SQL、结果行、自然语言答案”分开验证:join 是否用了批准的键与方向、明细行是否先聚合再关联、一对多扇出是否导致重复累加、空值是否被当成 0、截断结果是否被说成全量。任何一项缺失就 fail closed,保留 SQL 并报告失败,而不是把结果包装成结论。
三是语义契约的可反驳性。 modeling.md 要求每个度量记录表达式、行粒度、允许的分组粒度、物理单位、可加维度、分子分母、空值与零分母行为、有效期与版本;每条关系记录方向、基数、空键行为、观测到的 join 覆盖率与扇出风险。文档里有两句判词很硬:“相似的名字或相等的当前行集,不能证明两个概念等价”“匹配的列名不是 join 的证明”。
最能体现这种取向的是 SQL 属性的提取规则。sql_attribute_suggester.py 先把查询历史里的表达式按三个月执行次数排序,取前 10 个交给模型判断,规则里明确要求:凡是把列和字面量比较的表达式(status = 'active'、amount > 1000)都应被提升为具名属性,因为字面量编码的就是业务阈值与分类;但纯 surrogate id 的等值查询、start_date < end_date 这种结构完整性检查、以及主键列上的 IS NULL(其实是 LEFT JOIN 是否命中的探针)都不算业务逻辑。
这套规则不优雅,但它可执行、可测试、可被数据反驳。相比之下,一条 OWL 公理很难回答“上季度华东区退货率”这个具体问题到底算得对不对。
所以两者的差别不是先进与落后,而是优化目标不同:一边优化“能不能推出新知识”,另一边优化“能不能证明这次回答用的是正确的定义”。后者在 Agent 场景里更稀缺,也更难伪装。
落地前该知道的边界
如果你的场景是“已有数据仓库、术语在各系统之间打架、希望 Agent 查数时不再靠猜表名”,这个项目的设计是对症的:编译负责起草,MCP 负责只读分发,校验技能负责把“SQL 跑通了”升级成“答案站得住”。
但如果你的期待是下面任何一条,就会失望:想从零自动生成领域本体;想靠推理机推导隐含关系;想把语义关系直接当业务动作执行;想有一个通用的结果行写回接口(publication.md 明确说没有,只能写到外部受控表再回灌)。
真正上手前,还有三件事值得单独排期:一是连接凭据——README 写明未完整配置 Vault 时,UI 添加的数据源密码会以明文 JSON 存在 Postgres 里,而不完整的 Vault 配置会回退到明文;二是采样与向量化的数据外发边界,列样本会进入提示词与 embedding 请求;三是模型变更的演练,按导出、隔离 apply、精确回读、回滚这条链路至少走一遍,别把“导入返回 200”当成发布成功。
回到标题。这个项目 24 小时涨 2000+ star,涨的是“给 Agent 一个可信语义层”这个叙事;而代码里真正的自动化,是从元数据里起草 Term、列描述、候选外键和候选 SQL 属性,并且把每一步都做成可排序、可取消、可回读的批处理。把 “auto” 理解成“自动起草、人工定稿”,你会觉得它的工程取舍相当克制;理解成“自动完成本体建模”,你就会在第一个 IS_A 关系上卡住。
后者正是宣传语和源码之间那道不宽但很真实的缝。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




