Metabase Metabot 核心技能解读:用 `construct_notebook_query` 以 MBQL 5 JSON 构建 Notebook 查询
2026/9/14 17:03:34 网站建设 项目流程

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-coretools: [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.cljcfilter.cljctemporal_bucketing.cljcexpression/*.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"]]]}]}

这个例子同时演示了技能文档强调的两条最容易违反的规则

  1. 每个子句都是["op", {}, ...args],位置 1 必须有强制存在的{}选项 map
  2. 每个字段引用都在最后一个槽位使用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-tablesource-card——二选一,仅限第一个 stage;后续 stage 隐式消费上一个 stage 的输出;
  • 可选键:filtersaggregationbreakoutexpressionsfieldsjoinsorder-bylimit

特别强调: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-aliasFK 列所属的显式 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;
  • visualizationquery的兄弟字段,绝不内嵌;
  • 按内联聚合排序必须与aggregation:条目完全一致(同 op 同参数)。不确定就用["aggregation", {}, <0-based-idx>]
  • 绝不把metabase://...URI 写进source-tablesource-card——那些 URI 是给read_resource用的,不是查询源;
  • 不要把[aggregate, ...][filter, ...][order-by, ...][breakout, ...][limit, ...]写成子句头——这些是 stage 的容器键,不是子句。内部子句直接放进 stage 的aggregation:/filters:等数组中;
  • 常见拼写错误count-ifvariancestddev-popcount-distinctdayofweekhour-of-daymonth-of-yearquarter-of-yeartemporal-diffrelative-date)会被自动纠正,但应写规范名(count-wherevarstddevdistinctget-day-of-weekget-hourget-monthget-quarterdatetime-diffrelative-datetime),使工具输出与后续检查结果一致。

底层实现:从 JSON 到可执行查询的完整管线

技能文档描述的是"契约",而 construct.clj 中的execute-representations-query*才是契约的落地者。理解这条管线,有助于判断哪些错误可重试、哪些必须重写:

  1. 边界校验:将 keyword 键的输入按::lib.schema/external-query校验,捕获结构性错误——缺失stages、拼错的 stage 键(如aggreagation)、错误的顶层lib/type等;
  2. 转 portable 形式:转换为修复管线操作的 string-keyed portable 形式,并断言所有 stage 键已知(防止 LLM 拼写错误被lib.schema静默丢弃);
  3. 解析数据库:从stages[0]source-table/source-card解析数据库 id,构建基于应用数据库的MetadataProvider
  4. 修复(repair):填充缺失的{}选项、缺失的lib/type标记、盖章顶层database:、为隐式 join 自动接线source-field、把内联order-by聚合改写为引用等;
  5. 形状复查:对修复后的 portable 形式再做 schema 校验;
  6. 解析与归一化:把 portable FK 解析为数字 ID,并基于 metadata-provider 通过lib.schema/query归一化;
  7. 可运行性后门:镜像前端canRun的 schema 校验(query-not-runnable-explanation),再跑前端表达式编辑器自带的诊断(assert-editor-accepts-expressions!)——任一失败都是可重试的:agent-error?
  8. 导出:把最终的数字 MBQL 5 导回 portable 形式,作为 LLM 可见的:query-json/query-content输出。

权限检查(check-source-table-query-permissions!)刻意放在修复/解析之前,确保 metadata-provider 支撑的管线永远不会检视当前用户无权使用的表/卡片元数据。最终结果会附带:result-columnsresult-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-intervalinsidetemporal-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询