关系数据库 + Ontology + LLM:三位一体的落地设计
本文的核心命题:Ontology 不必是独立的图数据库或 OWL 文件,它可以"溶解"进「关系数据库 + LLM」这对现成基础设施,形成三位一体的协同架构。
全部内容基于 hostdb-query-agent PoC 的真实代码与实测结论(召回 100%、SQL 正确率 100%),不引用未实现的范式。
一、核心命题:为什么是「三者协同」而非「三者选一」
1.1 三者各自能做什么、不能做什么
自然语言查数据库这件事,单靠任何一方都不够:
| 角色 | 擅长 | 致命短板 |
|---|---|---|
| 关系数据库(PG) | 结构化存储、毫秒级查询、强一致、千万行聚合 | 不懂自然语言;表多了 LLM 找不到该查哪张 |
| LLM | 理解自然语言、生成 SQL、跨语言泛化 | 幻觉(编造不存在的列/表)、不懂你的私有 schema |
| Ontology | 定义"概念/实体/属性/关系"的语义骨架,给 LLM 严格约束 | 传统形态(图库/OWL)重、慢、工程门槛高 |
三角困局:
- 只用「PG + LLM」→ LLM 在万表库里迷路、幻觉列名(hostdb 实测 qwen2.5:3b 把
ip_addressesJSONB 当顶层列)。 - 引入「图数据库 Ontology」→ 性能雪崩(千万行聚合)+ LLM 难生成 Cypher + 工程成本爆炸。
- 只用「OWL Ontology」→ 推理机慢、格式冗长、LLM 难解析。
1.2 本项目的破局思路:让 Ontology「溶解」进现成基础设施
Ontology 不另起炉灶,而是拆成三层,分别长在关系数据库和 LLM 的 prompt 上:
- 概念分类长在向量库(Qdrant)—— 做 LLM 的「宏观导航」
- 实体属性长在关系库的 DDL + 注释 —— 做 LLM 的「微观字典」
- 关系与查询模式长在动态 SQL 范例 —— 做 LLM 的「语法示范」
这样三者的关系不再是"选一个",而是各司其职、闭环协同:
┌─────────────────────────────────────────────┐ │ 用户的自然语言问题 │ └──────────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ① Ontology-概念层(Qdrant 向量+标签) │ ← 解决「该查哪些表」 │ 把问题对齐到 hostdb 的概念分类 │ (召回 100%) └──────────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ② Ontology-属性层(关系库 DDL+注释) │ ← 解决「这些表有哪些列/含义」 │ 注入候选表的精确 schema + 业务语义 │ (LLM 的字典) └──────────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ③ Ontology-关系层(动态 SQL 范例) │ ← 解决「表怎么连/用什么模式」 │ 检索匹配的查询模式范例(COUNT/JOIN/JSONB)│ (防幻觉 + 防模式误用) └──────────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ LLM 生成 SQL │ ← 三层约束下生成 │ (DeepSeek-v4-pro 实测正确率 100%) │ └──────────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ 关系数据库执行(PG,只读账号 + 熔断) │ ← 落地到真实数据 └─────────────────────────────────────────────┘关键洞察:Ontology 的价值是"语义约束",而"语义约束"在 LLM 时代最优载体是prompt 里的结构化文本(DDL + 注释 + 范例),不是独立的图数据库。这就是「三位一体」的核心。
二、Ontology 基本原理(背景)
2.1 定义与四要素
Ontology = 一个领域里「概念、实体、属性、关系」的形式化定义,给模糊世界一个机器可读的概念骨架。
| 要素 | 含义 | hostdb 例子 |
|---|---|---|
| Concept/Class | 抽象类别 | 「主机」「进程」「网络连接」「未知主机」 |
| Entity/Instance | 类的具体对象 | 主机node1(host_id=a1f9c2d7...) |
| Property | 实体字段 | 主机os/ip_addresses;进程cpu_percent |
| Relation | 实体关联 | 「主机_contains_进程」(host_id软外键) |
2.2 Ontology 解决的语义鸿沟
- 用户说「node1 的 CPU」→ 机器怎么知道指向
resources.cpu_usage_percent? - 用户说「未知主机」→ 怎么区分是
unknown_hosts(未纳管)而非hosts(受控)? - 用户说「每台主机的进程数」→ 怎么知道要走
hosts _contains_ processes关系?
没有 Ontology,这些全靠人工 hardcode;有了它,概念结构化、可检索、可被 LLM 理解。
2.3 传统形态 vs 本项目形态
| 传统 Ontology | 本项目形态 | |
|---|---|---|
| 载体 | OWL/RDF 文件 或 Neo4j 图 | 关系库 DDL + 向量库 + prompt |
| 形态 | 显式图/三元组 | 隐式三层 |
| 严格度 | ★★★★★ | ★★★(够用) |
| 性能 | 大数据慢 | 毫秒级 |
| LLM 友好 | 低(Cypher/OWL 难生成) | 极高(DDL 即 prompt) |
传统 OWL 形态(<owl:Class>/<owl:ObjectProperty>)语义最严格,但重、慢、LLM 难消化——这是本项目不走该路的根本原因。
三、Ontology 实现模式分类与对比
3.1 四类模式总览
| 模式 | 载体 | 性能 | LLM 友好 | 工程成本 |
|---|---|---|---|---|
| A. 图数据库(Neo4j) | 节点+边 | 大数据慢 | 低(Cypher 难) | 极高 |
| B. OWL/RDF | 三元组 | 中 | 低(冗长) | 高 |
| C. 关系型 DDL+元数据 | PG Schema | 极快 | 高(DDL 即提示词) | 低 |
| D. 向量+标签(RAG) | Qdrant payload | 极快 | 极高 | 中 |
3.2 为什么不选 A/B(图库 / OWL)
pg_notes_ontology_features.md论证、本项目实测验证:
| 维度 | 图库 (A) | OWL (B) | 关系+向量 (C+D) ✅ |
|---|---|---|---|
| 千万行历史聚合 | 性能雪崩 | 性能雪崩 | PG 毫秒级 |
| LLM Text-to-SQL | 翻译成 Cypher,幻觉重 | OWL 冗长难解析 | DDL 进 prompt,召回 100% |
| 工程成本 | ETL + 图运维 | 推理机 + 专家 | DBA 写注释 |
| 多跳推理 | 强 | 强 | 弱(hostdb 单跳够用) |
结论:hostdb 是监控分析库(非社交网络),查询以host_id单跳 + 时间窗为主。上图库是把简单问题复杂化,DDL 注入远比 Cypher 生成可靠。
3.3 选定:C + D 混合(关系库提供属性,向量库提供路由)
本项目把 C 和 D 组合成三层隐式 Ontology,下一章详述。
四、三位一体的落地设计(核心)
4.1 三层 Ontology 的分工
| Ontology 层 | 对应要素 | 载体 | 代码/集合 | 解决的问题 |
|---|---|---|---|---|
| ① 概念分类层 | Concept/Class | Qdrant 向量+标签 | schema_catalog.ts+ 集合hostdb_schema_catalog | 该查哪些表 |
| ② 实体属性层 | Entity/Property | 关系库 DDL+注释 | catalog 的columns_ddl+columns_hint | 表有哪些列、什么含义 |
| ③ 关系/模式层 | Relation+查询模式 | 动态 SQL 范例 | sql_examples.ts+ 集合hostdb_sql_examples | 表怎么连、用什么 SQL 套路 |
4.2 层 ① 概念分类层 —— Ontology 给 LLM 做「宏观导航」
关系数据库的角色:提供 18 张表的真实结构。
Ontology 的角色:给每张表打两个维度的概念标签。
LLM 的角色:通过向量召回 + 标签过滤,把自然语言对齐到正确的表。
// src/schema_catalog.ts(实测代码)exportinterfaceCatalogEntry{table:string;domain:"inventory"|"runtime"|"security"|"app";// ← 业务域分类entity_type:"entity"|"history"|"link"|"meta";// ← 时态分类description:string;columns_ddl:string;columns_hint:string;}domain(业务域):inventory(受控主机)/runtime(进程连接资源告警)/security(未知实体)/app(应用自身)entity_type(时态):entity(当前态)/history(时序)/link(关联)/meta(元数据)
双重定位(向量 + 标签):
- 向量召回(bge-m3 + Qdrant cosine):自然语言 → Top-K 相似表,实测命中率100%(AC1)。
- 标签过滤(Qdrant filter):如
domain=runtime直接排除web_*(TC1.7 验证)。
这一层的价值:没有它,LLM 面对万表会迷路;有了它,LLM 只面对 2-5 张相关表的 DDL,token 不爆、注意力聚焦。hostdb 仅 18 表看似不需要,但这是 pg_notes「万表两级路由」的最小可验证切片。
4.3 层 ② 实体属性层 —— 关系库 DDL 即 Ontology 的「微观字典」
关系数据库的角色:DDL 本身就是最权威的实体-属性定义。
Ontology 的角色:用columns_hint(列注释)补充业务语义,把 OWL 要写 10 倍长的话用一行中文讲清。
LLM 的角色:读 DDL + hint,理解每个列的精确含义与边界。
例如resources表:
{table:"resources",columns_ddl:"CREATETABLEresources(id bigint,host_id varchar,timestamp timestamptz,cpu_usage_percent double,mem_used_percent double,disks jsonb)",columns_hint:"cpu_usage_percent:CPU使用率0-100;mem_used_percent:内存使用率0-100;disks:JSONB数组,含 mount_point/used_percent,用 jsonb_array_elements 展开.",}columns_hint是关键 —— 它直接进 prompt 喂给 LLM。这是符号主义本体论在 LLM 时代的演进:不靠推理机,靠 prompt 注入实现"严格约束"。
这一层的价值:LLM 幻觉的根源是"不知道列的精确含义"。DDL+hint 把语义边界讲死,DeepSeek-v4-pro 在此约束下达成 SQL 正确率 100%(不再把
disks内部键当顶层列)。
4.4 层 ③ 关系/模式层 —— 动态 SQL 范例做「语法示范 + 反幻觉」
这是本项目超出 pg_notes 原设计、根据实测增补的层(Phase 4b / T8)。
问题:光有 DDL 不够 —— 模型不知道"表怎么连"、“什么场景用什么 SQL 套路”。qwen2.5:3b 实测因此把 COUNT 题误接 LIMIT 1、漏 JSONB lateral join。
解法:把"关系 + 查询模式"作为第三层 Ontology,灌入第二个 Qdrant 集合,按问题动态检索。
10 种查询模式(src/sql_examples.ts):
exporttypePattern=|"aggregate_count"// 聚合计数(对冲 LIMIT 误用)|"jsonb_string_array"// JSONB 字符串数组展开(IP)|"jsonb_object_array"// JSONB 对象数组展开(disks)|"latest_row"|"time_window"|"join"|"group_by"|"top_n"|"history_trend"|"unknown_entity";16 条范例,每条含question + sql + pattern + note,note 显式标注反模式(如「聚合禁接 LIMIT」)。全部在真实 hostdb 验证可执行(16/16 通过)。
动态检索(dynamic few-shot):
- 「有多少台主机」→ 命中
aggregate_count→ 模型照计数模式生成(不误接 LIMIT) - 「node1 各磁盘」→ 命中
jsonb_object_array→ 模型照 lateral join 生成
这一层的价值:静态 few-shot 会过度泛化(3 条范例全是 latest-row,导致 COUNT 误用)。动态检索让"聚合题拿聚合范例、JSONB 题拿展开范例",各取所需。实测修复了 COUNT bug(TC8.4),DeepSeek 上 5/5 全对。
4.5 三层协同的端到端数据流(一个完整例子)
用户问「node1 当前的 CPU 使用率」,三层如何接力:
用户问题 "node1 当前的 CPU 使用率" │ ▼ [层① 概念导航] Ontology → 告诉 LLM 该查哪些表 Qdrant 检索 hostdb_schema_catalog → 召回 resources / resources_history(向量命中) → 标签确认 domain=runtime(排除 web_*) │ ▼ [层② 属性字典] 关系库 DDL → 告诉 LLM 这些表有什么列 注入 resources 的 columns_ddl + columns_hint → LLM 看到「cpu_usage_percent: CPU使用率0-100」「disks 是 JSONB 数组」 │ ▼ [层③ 模式示范] 动态范例 → 告诉 LLM 用什么 SQL 套路 Qdrant 检索 hostdb_sql_examples(独立集合) → 命中 latest_row 范例(ORDER BY timestamp DESC LIMIT 1) → 命中 aggregate_count 的 note(聚合禁 LIMIT,防误用) │ ▼ LLM 生成 SQL(三层约束下,DeepSeek-v4-pro 100% 正确) SELECT cpu_usage_percent FROM resources WHERE host_id='a1f9c2d7...' ORDER BY timestamp DESC LIMIT 1; │ ▼ 关系库执行(PG 只读账号 ontology_ro + 熔断 + guard 闸门) → 返回结果 + 执行轨迹五、反幻觉体系:三位一体的纵深防御(关键设计)
这一章单独成篇,因为反幻觉是整个三位一体架构存在的根本理由。前面的三层设计(概念/属性/关系)最终都服务于一个目标:让 LLM 在生成 SQL 时不犯错,即便犯了也被拦下。
设计哲学是纵深防御(defense in depth):不指望任何单一环节 100% 可靠,而是让幻觉在「生成前约束 → 生成后校验 → 执行前闸门 → 执行时内核」四个关卡逐层被拦。任何一层失效,数据仍安全。
5.1 LLM 幻觉的四种形态(实测归纳)
在 hostdb PoC 的实测中,qwen2.5:3b 表现出四类典型幻觉,每一类都需要专门的拦截手段:
| 幻觉形态 | 真实案例(实测) | 危险等级 |
|---|---|---|
| ① 编造表 | (弱模型罕见,但万表场景必然)LLM 引用了 schema 里不存在的表 | 中 |
| ② 编造列 | 把ip_addresses(JSONB)当顶层列;写h.ip_address(不存在);幻觉hosts.timestamp(实际是last_timestamp) | 高 |
| ③ 模式误用 | COUNT 聚合查询误接ORDER BY ... LIMIT 1(把取最新行的模式套到计数上) | 中 |
| ④ 越权写操作 | 生成DELETE/DROP/UPDATE(即便概率低,后果不可逆) | 致命 |
5.2 四道防线:每类幻觉由谁拦、怎么拦
| 幻觉形态 | 第一道(生成前约束) | 第二道(生成后校验) | 第三道(执行前闸门) | 第四道(执行时内核) |
|---|---|---|---|---|
| ① 编造表 | 层①:只把 catalog 白名单的表 DDL 注入 prompt,LLM 根本看不到别的表 | — | — | — |
| ② 编造列 | 层②:DDL 列出真实列 + hint 讲清 JSONB 边界 | R1 列校验(column_validator.ts):按information_schema验证 LLM 引用的表.列/别名.列真实存在,不存在即拒绝并触发重试 | — | — |
| ③ 模式误用 | 层③:动态范例注入正确模式 +note标注反模式(如「聚合禁接 LIMIT」) | B 错误回筒:执行失败时把 PG 错误喂回模型自纠(最多 3 次) | — | — |
| ④ 越权写 | — | — | guard 闸门:正则黑名单(drop/delete/insert/…)+ 必须 SELECT 起头 + 拒绝分号多语句 | ontology_ro只读账号:内核物理拒绝任何写操作(双保险) |
5.3 为什么需要纵深防御(不能只靠一层)
每一道防线都有失效的可能,只有多层叠加才能保证「总有一层兜得住」:
- 只靠 LLM 自觉(生成前约束)?不够。qwen2.5:3b 在三层 prompt 约束下仍会幻觉
h.ip_address(层② DDL 明明写了ip_addresses)。 - 只靠生成后校验(R1)?不够。校验器只验「列存在」,验不出「列语义对」(如把
disks.used_percent当mem_used_percent)。这是诚实的残留边界。 - 只靠 guard 闸门?不够。guard 是正则,理论上可能被绕过(注释走私、编码 trick)。
- 只靠内核只读?够安全但不够友好——靠内核拒意味着 SQL 已经跑到数据库,浪费一次往返,且错误信息可能泄露 schema。
所以四层叠加:层①②③在「生成前/后」把绝大多数幻觉消灭在 LLM 层面(DeepSeek 下 100% 不触发后两层);guard 作为执行前最后过滤;ontology_ro作为不可绕过的物理底线。
5.4 实测:四道防线各自被验证过
| 防线 | 验证用例 | 实测结果 |
|---|---|---|
| 层① 白名单 | 万表场景模拟(catalog 仅注入相关表) | bge-m3 召回 100%,不相关表不进 prompt |
| 层② R1 列校验 | TC3.2 /column_validator.ts的 alias-aware 测试 | 拦下h.ip_address、hosts.timestamp等幻觉列,触发重试 |
| 层③ 动态范例 | TC8.4(COUNT bug 回归) | 修复「COUNT 误接 LIMIT」,DeepSeek 下 5/5 正确 |
| guard 闸门 | TC2.1–2.21(21 个注入向量) | 全绿,含注释走私、多语句、DROP/DELETE/INSERT 等 |
| 内核只读 | TC3.2 双保险 | 绕过 guard 直接喂 INSERT,被ontology_ro内核拒绝,行数不变 |
5.5 诚实的残留边界
反幻觉体系覆盖了语法层和结构层的幻觉,但有一类目前无法自动拦截:
- 语义层幻觉:SQL 语法对、列也存在、也执行成功,但取错了语义。例如 qwen3b 把内存题的
mem_used_percent取成了disks[0].used_percent(磁盘使用率)—— 列校验验不出,因为它只查"列存不存在",不查"列对不对"。
要拦这类需要结果层合理性校验(如检查 SELECT 的列名与问题关键词的相关性),属进阶工作。好消息:DeepSeek-v4-pro 在三层约束下不再犯这类错(5/5 语义全对),说明强模型 + 三层约束已足够,结果层校验是给弱模型的额外补丁。
六、实现索引
6.1 关键文件
| 文件 | 三位一体中的角色 | 关键内容 |
|---|---|---|
src/schema_catalog.ts | 层①+② 数据源 | 18 表的 domain/entity_type 标签 + DDL + hint |
src/catalog_builder.ts | 层① 灌入 | embedding 进 Qdranthostdb_schema_catalog |
src/retriever.ts | 层① 检索 | 向量召回 + 标签过滤 |
src/sql_examples.ts | 层③ 数据源 | 16 条 golden SQL + 10 pattern |
src/example_builder.ts/example_retriever.ts | 层③ 灌入+检索 | 第二个 Qdrant 集合 |
src/sql_generator.ts | 三层汇聚 | 组装 DDL + 范例进 prompt,调 LLM |
src/column_validator.ts | 层② 反幻觉 | R1 校验(alias-aware) |
src/executor.ts | 关系库执行 | 只读 + 熔断 |
src/pipeline.ts | 编排 | 串联三层 + guard + CRAG 降级 |
6.2 实测验证(三位一体有效性的证据)
| 验证项 | 结果 | 证据 |
|---|---|---|
| 层① 召回准不准 | bge-m3 命中率100% | tests/output/recall_report.json |
| 层② DDL 注入够不够 | DeepSeek SQL 正确率100%(5/5,含语义) | tests/output/sql_eval_report.json |
| 层③ 模式覆盖 | 10 pattern 检索覆盖100% | TC8.5 |
| 层③ COUNT bug 修复 | 动态范例让模型不再误接 LIMIT | TC8.4 |
| 层② 列校验 | alias-aware 校验器拦幻觉列 | column_validator.tsparseAliases |
| 关系库只读 | 写操作被内核拒(双保险) | TC3.2 |
6.3 模型对比(证明架构对强模型是充分支撑)
| 模型 | 可执行率 | 语义正确 | 说明 |
|---|---|---|---|
| qwen2.5:3b(本地) | 80% | ~60% | JSONB 幻觉、模式误用 |
| deepseek-v4-pro(云端) | 100% | 100% | 三层约束下完全正确 |
含义:pipeline(三层 Ontology + guard + CRAG)对强模型已是生产级支撑。qwen3b 的失败是模型能力问题,非架构问题——换强模型后,所有脚手架正常工作且不再需"救火"。
七、总结
7.1 核心论点
Ontology 不必是独立的图数据库或 OWL 文件。把它拆成三层(概念分类 / 实体属性 / 关系模式),分别长在向量库、关系库 DDL、动态范例上,就能与 LLM 形成三位一体的闭环——既享受 Ontology 的"零幻觉严格约束"红利,又复用关系库与向量库的成熟性能,无需引入新的图数据库栈。
7.2 三位的职责一句话
- 关系数据库:数据的家 + DDL 即最权威的实体属性定义。
- Ontology(溶解态):给 LLM 提供"该查哪些表/列什么含义/怎么连"的三层语义约束。
- LLM:在三层约束下把自然语言翻译成精确 SQL。
7.3 边界(诚实)
本模式不是万能:
- 复杂多跳推理(≥3 跳):需建视图固化为宽表(Dev Plan R3 决策点)。
- 严格逻辑推断(子类继承/逆关系):需上 OWL。
- 万表规模:需两级路由(Qdrant 类目 + 视图),属后续。
但 hostdb(监控分析库、单跳关系、18 表)完全在本模式的甜区,实测召回 100% + SQL 正确率 100%。