Featured image of post AI 写的 SQL 没报错,58% 的答案却是错的:语义层正在成为 Agent 查数据的命门!

AI 写的 SQL 没报错,58% 的答案却是错的:语义层正在成为 Agent 查数据的命门!

我在 Hacker News 上刷到一条标题,前后读了两遍还是觉得别扭:

The SQL ran fine. 58% of the answers were wrong.

翻译过来是:SQL 跑得好好的,58% 的答案是错的。

具体场景是这样的——你让一个 agent 帮你在数据仓库里查「上个季度总营收」,它生成的 SQL 语法没毛病,跑得飞快,几秒钟返回一个数。你顺手贴进周报。问题在于,那个数把已经取消的订单也算了进去:1630 万美元,而正确答案是 1460 万。差出来的那 170 万不会报错,不会抛异常,你甚至看不出来。

这不是谁手滑写错一行 SQL,而是现在几乎每一个「让 AI 查数据」的项目都会撞上的一堵墙。最近冒出来的一个开源项目 semlayer,连同它背后那套「语义层」的思路,给这件事提供了一个挺锋利的解释框架。我把它的仓库、规范文档、benchmark 从头翻了一遍。这篇就讲讲我看懂了什么,以及为什么我觉得这层东西会比模型本身更值钱。

先分清两件事:模型的智商,和仓库的脏

很多人第一反应是「模型不够强,等下一代就好了」。但数据是反过来的。

semlayer 的 README 里引了一组我很喜欢的数字:在 Spider 2.0 这个面向真实企业级数据仓库的评测集上,当年最强的前沿模型发布时的通过率只有 21.3%——而同样的模型在更老的学术 benchmark Spider 1.0 上有 约 91%。即便到了今天,最好的那些 agent 脚手架,也只爬到 30% 上下。

同一批模型,换个数据集,从 91% 掉到 21%。这说明瓶颈不在「模型会不会写 SQL」——它当然会。卡住它的是别的东西:企业仓库里的列名是 tot_amt、sts_cd 这种缩写,没有任何声明的外键,业务规则藏在工程师脑子里而不是表结构里。「平均订单金额」这个词在任何一张表里都没有对应的列,它是某个人做的一个决定:分子是什么,分母是什么。

这就引出今天的主角。

语义层到底在记什么

行业里其实早就有「语义层(Semantic Layer)」这个词。dbt Semantic Layer、Looker 的 LookML、Cube、Snowflake 的 Semantic Views 都是干这个的:把「指标怎么定义、维度怎么切、表之间怎么连」写成一份机器可读的文档,让 BI 工具和 SQL 都照着来。

它们有一个共同点——都得靠人手写。

一个数据团队往往要花一个季度,把几百张表的业务含义一行行敲成 YAML,然后再花精力维护它。而它最脆弱的地方在于:任何人跑一条 ALTER TABLE,这份文档就可能悄悄过期了。过期的语义层比没有更危险,因为它会一本正经地给你一个错的数字。

semlayer 想改的就是这一点。它的口号很直接——「the open-source semantic layer that infers itself(会自我推断的开源语义层)」。你不用手写,把它指向你的仓库,它自己去读、去猜、去验证,然后写出一份带置信度、来源和生命周期的语义层,用 MCP 喂给任何 agent。

这里有个关键的观念转换,我觉得值得单独拎出来:语义层记录的不是数据,是决定。

「Average order value」不是一个列。它是某人关于『除以什么』做的一个决定,而两个都站得住脚的答案之间可以差三倍。这就是为什么两个人各自正确地拉了同一个指标,走进同一场会,却拿着两个不同的数字——没人犯错,他们只是用了不同的定义。

semlayer 要做的,就是把这个决定白纸黑字写下来:哪些行算数,分母是什么,证据是什么。信任不只是「算对一个数」,而是同一个问题,对每个人、每一次,都返回同一个数。

扒开 semlayer:一条四段的推断流水线

它的 infer 命令底下是一条四段流水线,每一段的产物都带置信度和来源。我按源码结构讲:

第一段 Profile(profile/)——对每张表做批量的统计剖析,而且是每张表两次宽 SELECT,不是每列一次往返查询。它跑一遍有序规则来给列做语义分类(状态码?金额?PII?)。置信度低于 0.7 的列才升级给 LLM,每张表一次批量调用。所以它在 --no-llm 模式下也能完全确定性地跑。

第二段 Link(link/)——找外键。这里有个很克制的规则,我特别欣赏:统计特征永远不单独自动纳入一个外键,必须「强命名一致 + LLM 认为合理」两者互相印证,否则丢进待审队列。它的测试里有个硬门槛:在 TPC-DS 风格的命名上找到 104/104 个未声明外键,F1 = 1.0,而种进去的假外键陷阱一个都没被自动接受。

第三段 Describe(describe/)——两遍上下文传播。第二遍重新描述每张表时,已经知道了它在连接图里的邻居长什么样。

第四段 Enrich(enrich/)——确定性地做字典解码(把 C=Completed, X=Cancelled 这种对照从它自己发现的 decode 维表里 join 出来)、指标候选、聚合对账。最后这一项还顺手发现了业务规则:如果一张聚合表只有在 sts_cd <> 'X' 时才和明细对得上,那这条规则就被假设检验挖了出来,并且只在指标口径上生效(scope: measures),绝不用到行数统计上。

这就是它和 dbt 那类工具最本质的区别——规则是「发现」出来的,不是「写」出来的。

一组我很想让你看的实测数字

空谈不如上表。semlayer 把完整的评测脚手架(fixture、gold、competency questions、runner)都放进了仓库,python fixtures/build.py && pytest tests/ -q 就能自己复现。它在那个「乱仓库」上的成绩是这样的:

场景(fixture) 问题数 答题模型 只看裸 schema 加语义层 语义层 + SQL 校验
messy_mart(乱) 38 claude-haiku-4-5 0.42 0.87 0.89
messy_mart(乱) 38 claude-sonnet-5 0.55 0.84 —
fan_trap(扇形陷阱) 8 claude-haiku-4-5 0.88 0.75 0.88
tpcds_clean(干净) 12 claude-haiku-4-5 0.67 0.58 0.58

这张表里有三个反直觉的点,每一个都值得多想一秒。

第一,在乱仓库上,语义层把通过率从 42% 拉到 87%(相对提升 107%),再加一道 SQL 校验到 89%。

第二,换更强的模型几乎没用。 Sonnet 读裸 DDL 比 Haiku 强(0.55 vs 0.42),但加上语义层之后反而略低于 Haiku(0.84 vs 0.87)。作者的结论是:「答题模型不是瓶颈。」同一份语义层,把两个档次的模型拉到了几乎同一个水平线上。语义层带来的提升,Sonnet 是 +52%,Haiku 是 +107%——弱模型吃到的红利更大。

第三,也是最诚实的一点:在干净规范的 TPC-DS 上,语义层帮了倒忙。 裸 DDL 反而跑出 0.67,高于语义层的 0.58。作者的解读是:干净的列名本身就承载了足够的语义,而语义层塞进去的上下文是 DDL 的三倍,反而稀释了信号。

我特别喜欢它在 README 里把这句「负面结果」原样登出来的做法——「如果你的是整洁的 TPC-DS,你可能不需要我们」。一个工具敢于说清楚自己在哪里没用,才值得你去信它在哪里有用。

把「沉默的错误」变成结构上不可能

现在回到最开始那 58% 的错误。semlayer 最狠的地方不在推断,而在它的规范(spec/SPEC.md)——它定义了一份消费者契约,把那些「SQL 能跑通、数字却是错的」的情况,从「不明智」升级成了「不合规(non-conforming)」。

其中几条我抄下来给你看:

信任要分级。 每条被推断出来的断言都带生命周期和置信度,消费者必须照章办事:

lifecycle confidence 消费方行为
certified / reviewed 任意 可信任
inferred ≥ 0.9 可用,但必须标注「推断、未复核」
inferred 0.6 – 0.9 仅在带标注的前提下可用
inferred < 0.6 禁止用于回答
deprecated 任意 禁止新查询,必须提示替代对象
orphaned 任意 禁止使用(底层对象已不存在)

扇形连接必须拒绝。 当一次聚合要穿过一个 fanout_risk: true 的关系时,编译器要么套用声明的安全策略,要么拒绝并给出解释。静默地在扇形连接上求和,直接判定为不合规。规范里写得很直白:这是手搓语义层里最常见的正确性事故,而这份格式让它「在结构上不可能」。

SCD2 必须走时间窗。 标记了 temporal: scd2 的维度列,必须通过开启 asof 的关系用生效日期去解析;对一张有历史版本的维表做「当前行」直连,不合规。

宁可拒绝,不许猜。 一个合规的编译器在以下情况必须拒绝而不是瞎猜:group-by 落在不可达的维度上、时间查询打到没有时间维度的指标上、过滤条件引用了没建模的列、或者指标/表已经废弃。而且每一次拒绝都必须是「建设性的」——说清原因,列出合法的替代方案。拒绝之后,不许偷偷回退到自己手搓 SQL。

还有它那个 check_sql(CLI 里是 semlayer lint):对任何一段 SQL(你的,或者 agent 现写的)做确定性校验——漏了必需过滤器、扇形求和、读了废弃表、幻觉出来的列、SCD2 缺时间窗、空转的关联子查询。文档里有一句我读着挺解气的话:

check_sql 报错,但你那条查询明明跑得通?这正是重点——它校验的是语义有效性,不是语法。一条执行得很欢的查询,照样可能在求和已取消的订单,或者通过扇形连接把总额翻了三倍。

这就是那 42% → 89% 里,最后 2 个百分点的来处——而且是「执行报错修复」永远看不见的那一类错误,因为它在执行层面根本不报错。

我的判断:这层东西会成为 Agent 数据栈的「编译层」

翻完之后,我自己有几个结论,说出来供你参考,也欢迎不同意。

第一,别再等模型变强来解决这件事了。 Sonnet 加语义层反而略低于 Haiku 加语义层,这个数字已经把话说死了:真正决定准确率的是「模型周围那层接口」,不是模型本身。战场从模型能力,转移到了模型外面那层契约。 这也是我为什么觉得语义层值得单独写一篇——它跟「把 agent harness 的规则外置成协议」是同一个大趋势的两张脸。你可以对照着看我们之前拆过的 《UHP 深度拆解:继 MCP 之后,Agent Harness 也要有自己的协议了!》,思路几乎一模一样。

第二,「会自我推断」会把整个语义层的商业模式掀翻。 过去语义层的价值,很大一块在于「我帮人写了这份文档」——一份人力密集、按项目计费的咨询型产品。semlayer 的做法是:推断归引擎,人工只负责 review 那一步。 成本被压到大约 每 100 张表 0.7 美元(用你自己的 key,走便宜模型档),而且大约 80% 的列靠纯统计就能定型,LLM 只是兜底。一个季度的活被压到一杯咖啡的钱,那些靠「手写 YAML」立命的方案,日子会很难过。

第三,那个「负面结果」其实暴露了市场的真实形状。 它在干净仓库上没用、只在乱仓库上翻倍——这意味着它的市场不在「新建的漂亮数仓」,而在存量的脏数据。这一点很关键:愿意为一个「清理历史遗留」的工具付费的,是有既得利益、又跑不动 AI 项目的大公司,而不是刚起步的小团队。人群画像相当清晰。

第四,一个更锋利的设计——compile, don't execute。 semlayer 的 MCP server 不碰你的仓库:它不持凭证、不开连接、看不到一行数据,只读那份 layer 文件,然后把正确的 SQL 当文本交出来,由你自己的 warehouse MCP server 去执行。作者管这叫「semlayer 是地图,不是路」。我喜欢这个切分,因为它意味着语义层正在变成 Agent 数据栈里的「编译器」:agent 不再直接手搓 SQL,而是先请求编译、再执行。凭证、权限、数据都不经过它——它只负责「把意图翻译成一段语义上正确的查询」。这个位置,比「再做一个数据查询 agent」聪明得多。

当然,也有我还不放心的地方。 它现在是 beta,仓库只有个位数的 star,支持面是 Snowflake / BigQuery / DuckDB(Iceberg 靠 DuckDB 桥接),LLM 只走 Anthropic。规范里那套「置信度是经过校准的」主张,最终要看它发布的 calibration 报告能不能被第三方复现——「校准」这两个字,谁都能印在 README 上,值不值钱得看独立验证。还有一层隐性成本:lifecycle 里 certified 那一步永远是人的活,谁来长期维护这份 review 状态,是个治理问题,不是技术问题。工具能替你把 80% 的活干掉,但最后那 20% 的判断,还是得有人签字。

想自己试一下的话

它的上手路径短到有点不像话,我把命令原样贴给你,可以直接照做:

pip install -e ".[warehouses]"

semlayer init snowflake                       # 生成最小权限的授权脚本
semlayer infer snowflake -o layer.yaml \
  --context ./docs/ --context ./etl-repo/CLAUDE.md   # 可选:把你的 wiki/字典当先验
semlayer review layer.yaml                    # 人工接受/驳回引擎推断出来的东西
semlayer mcp layer.yaml                        # 把它作为 MCP server 喂给任意客户端
semlayer lint layer.yaml query.sql             # 拿任何 SQL 对着语义层校验
semlayer drift layer.yaml snowflake            # 捕捉 schema 变更(适合 cron / CI)

几个我实测/文档里确认过、能省你时间的点:semlayer mcp 走的是 stdio,你不用自己跑,是客户端去拉起它,配置里一定要用 layer 文件的绝对路径(客户端的工作目录跟你的 shell 不一样,这是「工具不出现」的头号原因)。另外还有 --no-llm(零 API 调用、纯确定性,评测里 typing 0.780 / role 0.834,只比有模型时低一点点)和 --no-sample-egress(不把任何单元格的值发给 LLM,且几乎不掉准确率,实测 0.809 / 0.842)两个开关——对走采购流程、或者数据敏感的团队,这两个选项基本决定了它能不能落地。

写在最后

我越来越觉得,2026 年这一整轮「让 AI 干活」的浪潮,真正的分水岭不在模型那一边。

模型负责「会做题」,但要让它「做对题」,得有人在它和真实世界之间,铺一层把规矩写死的账本。Agent 查数据是语义层,Agent 调工具是 MCP 和网关,Agent 动真格的副作用是审批和权限——它们本质上是同一件事:给一个太会说话、但太容易自信地犯错的东西,立一套它绕不过去的契约。

那 58% 的错误,答案从来不在更大的模型里,而在那层没人愿意动手写的规矩里。现在有人把它自动化了。剩下的问题是:你愿不愿意把最后那 20% 的签字权,交出去。

相关阅读

By AI博士 万戈