首页/设计深潜/语义编译边界

语义编译边界

语义优先的系统里,每条问题都要先经过语义模型编译。核心设计问题是: 模型没覆盖到的地方怎么办——全有全无地拒绝,等于把「开箱即用」的旧毛病 换个地方长出来;无声放行,等于放弃语义边界。Trove 的答案是 分级逃生梯:能编译的部分权威化、缺的部分交给生成通道带着契约补齐、 结构性错误响亮拒绝并反问着把模型长大。这页讲四级梯子的分界线、 两把校验尺,以及「AI 起草、确定性验证、按档入库」的扩展闭环。

边界不是一条线,是一架梯子

语义模型(semantics.yml)是生成的主视角与可答边界。 但「边界」有两种画法:一条线(在模型里就编译、不在就拒绝)和一架梯子 (按缺口性质分级:能保真的保真,能补齐的补齐,真错的才拒绝)。 线上方案的失败模式被 Trove 的早期版本实测过:一条没有「月均余额」声明的数据源, 对「上个月月均余额多少」的回答不该是拒绝——那是词表缺口, 不是能力缺口。

于是编译器的返回值本身就是一架梯子 compiler.py:1340 CompileResult / compiler.py:1374 PartialCompile / compiler.py:1361 CompileMiss, 三种类型、四条出口:

flowchart TB
    Q["query_sketch: 按语义模型编译计划"]:::gen --> C{"编译结果"}:::gate
    C -->|"覆盖内"| OK["CompileResult
权威 SQL 骨架注入 plan"]:::gen C -->|"软 MISS"| P["PartialCompile
可解析部分权威化
未解析组件记 compile_misses"]:::gen C -->|"硬 MISS"| M["CompileMiss
触发 refuse 出口"]:::gate OK --> EXEC["execute_sql
compiled_sql_matches · 照抄校验"]:::exec P --> GEN["gen_sql 按 plan 补齐未解析组件"]:::gen GEN --> EXEC2["execute_sql
skeleton_preserved · 骨架保真校验"]:::exec M --> REF["refuse: 拒绝 + 反问扩展模型"]:::gate M -->|"模型整体缺失"| INIT["引导先建模 /kb init"]:::gate classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px; classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px; classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px;
图 1 · 四条出口。编译产物不同,执行侧用的校验尺也不同——partial 走骨架保真,compiled 走照抄。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。
出口触发条件交付语义
权威编译 计划落在模型覆盖内 编译器拼出完整 SQL 骨架注入 plan,生成通道以它为规格
PartialCompile 骨架 软 MISS:词表/值/口径未声明(未声明 metric/字段/枚举值/时间分桶) 可解析部分权威化(join/过滤/分组),未解析组件交 gen_sql 按 plan 文本补齐——回答照常交付
拒绝 + 反问 硬 MISS:结构性错误——fan-out、join 二义、未覆盖表、坏派生定义(环/超深/未解析) 不生成查询;同时 LLM 起草一份最小扩展声明,走确认流程后重答
整体拒绝 数据源根本没有语义模型 明确提示先 /kb init——没有模型就没有边界可谈

硬与软:闭集分类,未知按硬处理

分级不是模糊判断,是闭集分类: MISS_REASONS 列举全部 34 种编译缺口 compiler.py:1396 MISS_REASONS, 再劈成两半——HARD_MISS_REASONS 13 种 compiler.py:1437 HARD_MISS_REASONS、 SOFT_MISS_REASONS compiler.py:1456 SOFT_MISS_REASONS。 分界判据是「跳过这个组件继续编译,产出还安全吗」:

分类表的维护纪律写在源码注释里:新增 MISS 分支时必须归入其中一边—— 该集合是路由判定的唯一权威。而 is_hard_miss compiler.py:1483 is_hard_miss 对未知 reason 默认返回硬——宁可拒绝,不冒险产错 SQL; 漏分类会被 eval 归因暴露,而放行过的错误 SQL 不会自己浮出来。

骨架保真:partial 与 compiled 的两把尺

骨架权威化的关键问题是下游是否真的照骨架走。execute_sql 对两条通道用两把不同的尺 (execute_sql.py:192 skeleton_preserved / execute_sql.py:194 compiled_sql_matches):

compiled_sql_matches|照抄校验skeleton_preserved|骨架保真
用在 完整编译(CompileResult) 软 MISS 骨架(PartialCompile)
比什么 编译期抽好的结果形状签名:聚合/过滤值/投影宽度/表/投影与过滤的列身份 join 边、WHERE 条件、分组宽度——投影列不参与比较(LLM 需要补未解析组件)
容忍什么 别名、格式、大小写、COUNT(*) vs COUNT(col)、裸列 vs 限定列(语义等价) join 顺序、分桶表达式方言差异
打回的边界 生成 SQL 解析不了也打回——它连是不是查询都定不下来,不能算保真(此前这种情况静默放行,是 A1 收口的 fail-open) 骨架的退化形态(计划本身只可部分解析)会放松 WHERE 比对——把「计划文本恰好能解析的部分」冻成权威,会把生成侧的正确改写打回

两把尺共用一个工程判断:校验读契约,不反推 SQL。 编译器在编译期就把结果形状抽成纯 JSON 契约(join 边/过滤/分组宽度 + 缺口列表)随 state 走(state.py:188 contract), 校验节点直接读字段——不再拿 compiled_sql 用 AST 反推回结构。 骨架的覆盖情况也在 state 上留下痕迹:compile_partial state.py:180 compile_partial 与 compile_misses state.py:181 compile_misses (未覆盖组件 [{reason, component}])——它们既是执行侧的判据,也是学习侧的输入(见下)。

拒绝:不是死胡同,是模型的生长期

硬 MISS 出口在 refuse 节点 (refuse.py:69 _no_model_message): 每条拒绝都随一份一键下一步(next_actions,形状统一为 [{id, kind, label, href, admin_only, payload?}],后端给全双语 label、 前端只渲染不分类)。数据源整体没模型时,这一出口直接给 /kb init 引导, 管理端待办里对应 datasource_uninitialized 一类 (overview.py:102 todo kind)。

拒绝的同一节点还做第二件事:起草扩展。LLM 根据缺口草拟一份 最小补充声明(字段或指标) semantic_draft.py:229 draft_refusal_extension, 但草稿能不能落、能不能免确认,全部由确定性门说了算:

flowchart LR
    MISS["硬 MISS 原因"]:::gate --> DRAFT["LLM 草拟最小扩展声明
(字段 / 指标,不出表达式新语义)"]:::gen DRAFT --> CONF{"确定性冲突检测
同名 / 表达式不可解析 / 数据集未声明"}:::gate CONF -->|"agg_duplicate 已有同口径"| TIP["不落库
引导换问法加过滤条件"]:::entry CONF -->|"可人工修正的冲突"| PEND["落 pending 收件箱
管理员修正后新建"]:::gate CONF -->|"无冲突"| GATE{"A / B 档确定性验证门"}:::gate GATE -->|"A 档: 字段在物理表中确认存在"| AUTO["自动入库(跳过 pending)
同轮路由回重答"]:::gen GATE -->|"B 档: 机械聚合 + 完整编译 + 真实执行过 shape"| AUTO GATE -->|"任一不过"| PEND classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px; classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px; classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px;
图 2 · 扩展草稿的确认梯。免人工的唯一通道是确定性验证门;其余一律 pending 等管理员。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

三道门的细节,每道都在拦一类具体错误:

门位置拦什么
确定性冲突检测 refuse.py:595 _detect_conflict,code 闭集:同名/表达式不可解析/数据集未声明/… 草稿与现有模型冲突分两类:可人工修正的(重名、表达式错)照落 pending 待审;agg_duplicate(已有同聚合签名指标)既不落库也不给建档动作——落一条必然重复的指标是诱导管理员确认错误,补救是换问法加过滤条件
A 档:物理列字段 refuse.py:678 _physical_column_field 字段名在物理表里确认存在(读连接器 catalog,零 LLM)——「这个列真的存在」是结构事实,不需要人再背书一次
B 档:机械聚合指标 refuse.py:729 _mechanical_metric_ok 四步全过才免人工:①表达式是单个值无关机械聚合(count/sum/avg,白名单 _MECHANICAL_AGGS,带 CASE/WHERE/FILTER/子查询 = 值被写死,不算);②单数据集、表可达、引用列存在(超大表跳过真实执行退回人工);③草稿指标并入模型副本走权威编译器——完整编译(软 MISS 骨架不算)+ guardrail;④真实执行权威 SQL、过 rules 的 shape 链(单行单列标量)。任一不过 → pending 走人工

免确认入库后,auto_confirmed 标记让图同轮路由回重答—— 用户感知是「系统补充了声明,正在重新回答」而不是一次拒绝 (refuse.py:1220 A/B 判定)。 防打转的护栏是 AUTO_CONFIRM_MAX_ROUNDS = 2 refuse.py:719 单问上限: 同一问题连续补两个字段还编译不过,就停止自动、回退人工——自动确认是省人工的便道, 不是绕过评审的暗门。

软 MISS 的生长方向:候选收件箱

软 MISS 的语义是「回答照常交付,但计划里有用例模型没声明的组件」—— 这正是语义层该生长的地方。候选收件箱 candidates.py 模块头 把这些缺口翻译成 pending 语义草稿,落进管理端待办。三条硬约束值得逐条看:

证据门槛同样是分类型的,各有各的「硬」:字段类只收解析失败的理由 (未声明字段/时间字段未声明/分析分区列未解析——「模型里缺这个声明」就是缺口本身); 指标类只收 no_metric_match,计划给的聚合表达式原文即权威定义,可原样落为 expression;值类别(enum_value_unresolved)仅当字段带实测取值 且提问字面量命中其一才落别名——命中即「库里确实这么存」; 不命中/未探测一律不猜(机器无法从翻译缺口推出目标码,自映射会把错字面量 冻结进骨架)。单轮捕获上限 MAX_CANDIDATES_PER_RUN = 3 candidates.py:50 单轮上限—— 一个多缺口计划不许把收件箱刷屏。

向用户披露:答案来源四档

编译梯子的每一级对应一种答案来源,披露装置把它们显示成四档 answer_source.py:27 AnswerSource: certified(复用已认证模板)/ reused(复用 KB 资产,未认证)/ compiled(编译器产物)/ generated(LLM 生成,含编译失败回退与软 MISS 骨架)。 拆成四档而不是三档的原因写在模块头:「机制」与「有没有人背书」是两个维度—— 压成一档会把「根本没进过模型的 SQL」说成「LLM 生成」。 判定只有一份 answer_source.py:94 resolve (答案渲染、SSE、评测归因、台账落账共用一个入口),方向刻意不对称: 判定不出的一律 GENERATED——把编译器产物标低,用户少得一点信息; 把模型编的标高,用户得到一个不该有的信任。同类纪律还有快径的治理门: fast_match 的第五道门只放行 status == "certified" 的模板 (fast_match.py:19 治理门)。

怎么用

编译决策的观测字段(compiled / compile_partial / compile_misses / contract)恒写在 state 上 (state.py:173 compiled_sql)—— eval 归因按 path: compiled|llm 分账,学习回路( 闭环与契约)也读同一份缺口记录。

取舍

选择放弃了什么为什么值得
分级逃生梯而非一条边界线 「不在模型里就拒绝」的简单叙事 词表缺口占真实提问的多数;全有全无会让语义优先退化成「建模不完备就不能用」。梯子把「能不能安全跳过」当成真正的判据
软 MISS 骨架交由生成补齐 未解析组件的确定性(那部分由 LLM 补) 保真校验(骨架保真 + rules 链 + 版本回归)三层兜底把风险压在「结构被改」上;跳过组件本身是安全的
未知 MISS 原因按硬处理 新分支漏分类时的「宽容」 方向不对称:错拒一次可以靠 eval 归因发现并修;放行一条会产错结果的 SQL,没人会回头查
编译期抽契约(纯 JSON)而不是校验时反推 一份契约字段的维护成本 反推要把 SQL 重新解析回结构,各节点各推一次迟早推歪;契约随 state 走,校验方与编译方看的是同一份事实
A/B 免确认是白名单式的(物理列 / 机械聚合) 「AI 草稿大多是对的,都自动确认」的吞吐 免人工的资格按可确定性验证发:列存在与否、单值无关聚合能否跑出标量,都是机器能证的事实;派生/比率/复杂口径仍走人工——那才是语义风险所在
agg_duplicate 连 pending 都不落 「先收进收件箱再说」的省事 落一条必然重复的指标是诱导管理员确认错误;正确补救是换问法(加过滤条件),不是补声明——收件箱的工作量也是资产,不能被噪声淹没
值类别别名只在「实测命中」时落 对未探测值的推断 自映射会把错字面量冻结进骨架,聚合题 0 行过滤没有守卫兜底——静默算错比拒绝更糟
来源四档 + 判定不出倒向 GENERATED 更漂亮的「编译占比」数字 披露装置的资产是它说的话可信;虚报一次,四档一起作废

优势在哪

继续读

想了解去哪
语义模型的形状、编译出的 SQL 长什么样语义层
骨架交出去之后:生成、校验、反思回滚的闭环闭环与契约
判定规则也用同一套「安全由构造保证」的表达语言判定内核
KB 的治理与写入纪律知识库
本页源码锚点的基准提交:anchor-base: 211ae86 GitHub 仓库 文档首页