Metabase Metabot 核心技能解读:用construct_notebook_query以 MBQL 5 JSON 构建 Notebook 查询
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
本指南围绕 Metabase 开源仓库中面向 AI Agent(Metabot)的技能文档 construct-notebook-query-core.md 展开,完整讲解如何通过construct_notebook_query工具,将自然语言需求翻译为一段 MBQL 5(Metabase 查询语言)JSON 描述,交给 Metabase 校验、修复与执行。读完本文,你将掌握该工具的调用契约、子句形状、字段引用规则、最小可行示例以及最容易触发的反模式,并理解这些规则背后的服务端实现管线,可直接用于二次开发、Agent 集成或排查"构造出的查询不可运行"类问题。
技能定位:为什么要在首次构造查询前加载
construct-notebook-query-core是 Metabase Agent(Metabot)技能体系中的一份核心技能(skill),其 frontmatter 中标注了id: construct-notebook-query-core、tools: [construct_notebook_query]、priority: 60。从 skills.clj 的实现可以看到,技能通过load_skill工具按需加载,返回形如<skill id="...">...</skill>的指令体注入对话流。该技能在描述中明确要求:在你第一次构造查询之前加载它,以便先掌握子句形状、字段引用以及规则/反模式,避免在首个查询上就犯错。
它并不是孤立文档,而是三份配套技能之一:
- construct-notebook-query-core(本文主体):子句形状、字段引用、基础规则与反模式,对应
["op", {}, ...args]通用形态与 portable FK; - construct-notebook-query-advanced:join(显式/隐式)、多阶段查询、
source-card、metrics/measures/segments、表达式与聚合引用; - construct-notebook-query-operators:完整的聚合、过滤、表达式与时间分桶算子目录。
其"最终真相"位于 src/metabase/lib/schema/ 下的aggregation.cljc、filter.cljc、temporal_bucketing.cljc与expression/*.cljc。理解这一点很重要:技能文档是给 LLM 看的速查手册,而 malli schema 才是服务端实际校验的依据。
工具契约:query/title/description/visualization
调用construct_notebook_query需要返回四个字段,其中只有visualization可选:
| 字段 | 必填 | 说明 |
|---|---|---|
query | 是 | 一个 JSON对象(绝不能是带引号的字符串)。目标数据库从第一个 stage 的source-table(或source-card)推断,因此要使用 search /read_resource/ 元数据工具报告的精确数据库名 |
title | 是 | 简短、人类友好的查询名称,写法类似一个已保存问题的名称 |
description | 是 | 一句话描述查询返回什么 |
visualization | 否 | 可选的{"chart_type": "bar"},是query的兄弟字段,绝不能内嵌进query里 |
从源码看,这个契约与 construct.clj 中的construct-notebook-query-args-schema一一对应:[:query [:map {:json-schema ...}]]、[:title :string]、[:description :string]、可选[:visualization construct-visualization-schema]。值得注意的是,args schema 对:query只断言"是 map",真正的结构校验发生在入口边界(HTTP defendpoint 的 string-transformer 与lib.normalize/normalize ::lib.schema/query),这正是为了让修复层(repair)有机会补救 LLM 常见的手误(如缺失{}选项位)。
另外,工具定义处还有一段手写的 JSON Schema(construct-notebook-query-json-schema,见 construct.clj):它不参与校验,只是替换呈现给 LLM 的 schema 描述——因为 malli 对开放的 property-less map 会输出空properties,弱模型(如 gpt-4.1-mini)会误读为"该对象没有字段"而返回{}。这一实现细节解释了为什么技能文档里反复强调"query 是 JSON 对象而非字符串"。
Slackbot 变体的差异
注意:该工具的 Slackbot 变体契约不同——
reasoning必填、title可选、没有description,且使用display(Slack 专用的可视化类型枚举)代替visualization。详见 Slackbot 系统提示词(仓库中的 slackbot.selmer 与 streaming.clj)。
最小示例:按月统计订单数量
技能文档给出的最小完整查询如下:
{"lib/type": "mbql/query", "stages": [{"lib/type": "mbql.stage/mbql", "source-table": ["Sample Database", "PUBLIC", "ORDERS"], "aggregation": [["count", {}]], "breakout": [["field", {"temporal-unit": "month"}, ["Sample Database", "PUBLIC", "ORDERS", "CREATED_AT"]]]}]}这个例子同时演示了技能文档强调的两条最容易违反的规则:
- 每个子句都是
["op", {}, ...args],位置 1 必须有强制存在的{}选项 map; - 每个字段引用都在最后一个槽位使用4 段 portable FK(数据库/模式/表/字段)。
在 construct.clj 的 JSON Schema 描述中,source-table被定义为 3 元素数组[db-name, schema-or-null, table-name],而字段引用则是 4+ 元素数组,二者形状一致地贯穿整个系统。
通用子句形状:["op", {options}, ...args]
每个操作都是["<operator>", {options}, ...args],选项 map始终存在,即使为空:
- 正确:
["count", {}]、["field", {}, ["DB", "SCH", "TBL", "COL"]]、["sum", {}, <expr>] - 错误:
["count"]、["field", ["DB", "SCH", "TBL", "COL"]]
服务端管线确实会"修复"缺失的{}槽位——construct.clj 的注释明确说明 args schema 故意保持宽松,就是为了让 repair 层有机会修补这类 shortcut。但技能文档要求:你写出的输出应该与后续检查看到的结果一致,即直接写规范形态,不要把修复机制当作文档兜底。
顶层查询与 Stage 形状
顶层:
"lib/type": "mbql/query"——必需的类型标记;"stages": [...]——至少一个 stage。
Stage("lib/type": "mbql.stage/mbql"为必需标记):
source-table或source-card——二选一,仅限第一个 stage;后续 stage 隐式消费上一个 stage 的输出;- 可选键:
filters、aggregation、breakout、expressions、fields、joins、order-by、limit。
特别强调:LLM 契约中不存在顶层database:字段,数据库完全从 source 推导。这一点在源码中得到了精确印证——resolve-database-id-from-first-stage 明确注释:顶层database:是 spec 规定的冗余字段,修复 pass 会在解析出数据库 id 之后才把它盖章写回;而且当source-table是 portable FK 时,按数据库名查找,重名会抛出:ambiguous-database-name,查无此库抛:unknown-database。这正是技能文档要求"使用精确数据库名(如"Sample Database"而非"Sample")"的底层原因——近似名不会静默匹配,而是直接报错。
字段引用:Portable FK 与跨阶段字符串名
字段引用通用形式:
["field", {}, ["<db-name>", "<schema-or-null>", "<table-name>", "<field-name>"]]第三个槽位是portable field FK——一个 4+ 元素的字符串数组。关键变体:
- 无 schema 的数据库(MongoDB 等)在 schema 槽位用
null:["Mongo", null, "orders", "created_at"]; - JSON 展开字段会追加额外段:
["DB", "SCH", "TBL", "PARENT", "CHILD"]。
在后续 stage中引用上一个 stage 产生的列时,改用字符串名而不是 portable FK:["field", {}, "count"]、["field", {}, "PRODUCT_ID"]。
字段选项(全部可选):
| 选项 | 作用 |
|---|---|
temporal-unit | 对日期/时间字段分桶,如"month"、"day"、"hour";完整列表见 construct-notebook-query-operators 技能 |
join-alias | 显式 join 内的每个字段引用必须携带 |
source-field | 源表上 FK 列的 portable FK;仅在隐式 join 自动填充有歧义时使用 |
source-field-name | 当 FK 列来自上一 stage 输出时的列名(罕见,不会自动填充) |
source-field-join-alias | FK 列所属的显式 join 别名(通常自动填充) |
binning | 对数值字段分桶 |
base-type在跨 stage 引用时会被自动填充——不要手写。
从源码看,portable field FK 的解析逻辑在 portable-field-fk-table:要求向量、≥4 个元素、第 0 位是字符串、第 1 位为nil或字符串、第 2 位是字符串。字段引用同时承担着权限检查的职责——referenced-table-fks会递归收集查询中所有表引用并逐一执行api/query-check(见 check-source-table-query-permissions!),且对:sensitive/:retired字段与隐藏表一律拒绝(visible-table/visible-field实现)。
各类子句的完整示例
技能文档逐一给出了核心子句的写法,下面全部继承并标注要点。
过滤(比较 + 布尔组合):
"filters": [["and", {}, [">", {}, ["field", {}, ["Sample Database", "PUBLIC", "ORDERS", "TOTAL"]], 100], ["=", {}, ["field", {}, ["Sample Database", "PUBLIC", "ORDERS", "STATUS"]], "paid"]]]聚合(字段上的sum+count):
"aggregation": [["sum", {}, ["field", {}, ["Sample Database", "PUBLIC", "ORDERS", "TOTAL"]]], ["count", {}]]带时间分桶的 Breakout:
"breakout": [["field", {"temporal-unit": "month"}, ["Sample Database", "PUBLIC", "ORDERS", "CREATED_AT"]]]排序(方向包裹引用,可作用于字段引用或聚合引用):
"order-by": [["desc", {}, ["field", {}, ["Sample Database", "PUBLIC", "ORDERS", "CREATED_AT"]]], ["desc", {}, ["aggregation", {}, 0]]]标量/列表形式:"limit": 50与"fields": [<field-ref>, ...]是直白的标量与列表写法。
注意order-by中["aggregation", {}, 0]是"第 0 个聚合"的 0 基索引引用——这与 advanced 技能中的"聚合引用"规则衔接:内联形式必须与aggregation列表中的条目逐字完全一致(同样的 op 与参数),不确定时优先用["aggregation", {}, <0-based-idx>]。越界索引会得到一条清晰错误,列出所有可用聚合及其索引。
规则与常见错误
技能文档将易错点分为"形状规则"与"反幻觉规则"两组。
形状规则:
- 每个子句都要写
{}选项,即使为空。["count"]是错的,必须是["count", {}]; - query 是 JSON 对象而非字符串,直接作为调用的
"query"字段传入; - 使用 search /
read_resource报告的精确数据库名作为每个 portable FK 的第一个元素。近似名会得到Unknown database而非静默选库;跨数据库查询不受支持; - 使用 portable FK 而非数字 ID。无 schema 数据库用
null;JSON 展开字段追加路径段; - 子句头是小写连字符风格:
"count"、"sum-where"、"time-interval"、"get-day-of-week"。不要下划线,不要驼峰; - 绝不臆造
source-card的 entity_id:必须是 21 字符字符串,逐字复制自 search /read_resource——没有模式、没有数字 id、没有card__<id>; source-card的列用输出名引用(槽位 3 的字符串),不是 portable FK。
从源码角度,source-cardentity_id 查找发生在 resolve-database-id-from-first-stage:用 entity_id 查卡片并取其:database_id,未知 id 抛:unknown-card。而metabase://...URI 误写进source-table会被专门的 detect-metabase-uri-source-table! 捕获并给出定向错误——该正则故意宽松,匹配一切以metabase://开头的内容,确保错误消息始终是指令性的。
反幻觉规则:
- 不要用
-相减日期。计算两个时间值之间相差的整数单位要用["datetime-diff", {}, <left>, <right>, "<unit>"]; - 多值分类过滤用
in/not-in,不要用=加列表字面量。工具虽会重写列表形式,但应写规范形式:["in", {}, <field>, "a", "b"]; - 提取的季度值是数字
1, 2, 3, 4,绝不是"Q1"之类的字符串; - 不要在同一 stage 对同一个底层字段 breakout 两次。如果已按
{"temporal-unit": "month"}breakout,不要再按原始字段 breakout; visualization是query的兄弟字段,绝不内嵌;- 按内联聚合排序必须与
aggregation:条目完全一致(同 op 同参数)。不确定就用["aggregation", {}, <0-based-idx>]; - 绝不把
metabase://...URI 写进source-table或source-card——那些 URI 是给read_resource用的,不是查询源; - 不要把
[aggregate, ...]、[filter, ...]、[order-by, ...]、[breakout, ...]、[limit, ...]写成子句头——这些是 stage 的容器键,不是子句。内部子句直接放进 stage 的aggregation:/filters:等数组中; - 常见拼写错误(
count-if、variance、stddev-pop、count-distinct、dayofweek、hour-of-day、month-of-year、quarter-of-year、temporal-diff、relative-date)会被自动纠正,但应写规范名(count-where、var、stddev、distinct、get-day-of-week、get-hour、get-month、get-quarter、datetime-diff、relative-datetime),使工具输出与后续检查结果一致。
底层实现:从 JSON 到可执行查询的完整管线
技能文档描述的是"契约",而 construct.clj 中的execute-representations-query*才是契约的落地者。理解这条管线,有助于判断哪些错误可重试、哪些必须重写:
- 边界校验:将 keyword 键的输入按
::lib.schema/external-query校验,捕获结构性错误——缺失stages、拼错的 stage 键(如aggreagation)、错误的顶层lib/type等; - 转 portable 形式:转换为修复管线操作的 string-keyed portable 形式,并断言所有 stage 键已知(防止 LLM 拼写错误被
lib.schema静默丢弃); - 解析数据库:从
stages[0]的source-table/source-card解析数据库 id,构建基于应用数据库的MetadataProvider; - 修复(repair):填充缺失的
{}选项、缺失的lib/type标记、盖章顶层database:、为隐式 join 自动接线source-field、把内联order-by聚合改写为引用等; - 形状复查:对修复后的 portable 形式再做 schema 校验;
- 解析与归一化:把 portable FK 解析为数字 ID,并基于 metadata-provider 通过
lib.schema/query归一化; - 可运行性后门:镜像前端
canRun的 schema 校验(query-not-runnable-explanation),再跑前端表达式编辑器自带的诊断(assert-editor-accepts-expressions!)——任一失败都是可重试的:agent-error?; - 导出:把最终的数字 MBQL 5 导回 portable 形式,作为 LLM 可见的
:query-json/query-content输出。
权限检查(check-source-table-query-permissions!)刻意放在修复/解析之前,确保 metadata-provider 支撑的管线永远不会检视当前用户无权使用的表/卡片元数据。最终结果会附带:result-columns(result-columns-for-query通过lib/returned-columns生成),让 Agent 在下一轮就能引用实际会执行的字段路径。整个工具入口construct-notebook-query-tool(construct.clj)还会联动创建图表,返回Chart链接与下一步指令;若查询构造失败且错误带:agent-error?或 403,则把消息原样作为工具输出返回给 LLM 自行纠正,而不是抛出堆栈。
配套技能与算子目录:何时继续深入
本文覆盖的 core 技能足以处理绝大多数"单表 + 过滤 + 聚合 + 分组 + 排序"场景。当查询需要以下能力时,应加载配套技能而非在 core 文档内硬编:
- **join(显式
joins条目、join-alias、四种strategy)与隐式 join(source-field自动填充、:ambiguous-fk/:no-fk-path错误)**→ construct-notebook-query-advanced.md; - 多阶段查询(后置聚合过滤、再聚合、跨 stage 字符串名引用、
distinct聚合输出名为count等细节)→ 同上; - 比率/占比(
count-where ÷ count的单 stage 或双 stage 写法,以及"表达式内不允许聚合"的边界)→ 同上; - **
source-card查询已保存问题/模型、metrics(["metric", {}, "<entity_id>"])、measures/segments(不透明 id 子句优先)**→ 同上; - 完整算子目录(聚合、过滤、表达式、时间分桶单位的精确名称与参数个数,如
time-interval、inside、temporal-extract的独立枚举、offset窗口函数仅限aggregation:/order-by:)→ construct-notebook-query-operators.md,其标注的真相来源是 src/metabase/lib/schema/ 下的 schema 文件。
这三份技能文档本身服务于同一个工具,且系统提示词中对应的工具说明位于 resources/metabot/prompts/tools/construct_notebook_query.md——它是技能的"常驻精简版",还额外包含"翻译请求"一节(约束条件是 filter 而非 breakout、不要擅自加分析、显式日期用绝对过滤等请求意图映射准则),与技能文档互为补充。
小结
construct_notebook_query的核心使用心法可以浓缩为三句话:每个子句都写成["op", {}, ...args]、每个字段都写成 4+ 段 portable FK、整个 query 是一个 JSON 对象而非字符串。在此基础上,善用"精确数据库名 + 不臆造 entity_id + 规范算子名"这三条纪律,就能避开绝大多数修复失败与幻觉错误。而当你需要了解这些规则为何如此时,src/metabase/metabot/tools/construct.clj 中的数据库解析、权限预检与修复管线,就是最完整的答案。
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考