mcpField
S
semantic-model

semantic-model

github.com/revosai数据与数据库044 次安装3 天前更新
代码仓库

关于 semantic-model

通过 RevOS MCP 服务中的组织实时语义模型回答业务问题。该 Skill 说明如何发现可查询字段、构建 Cube 查询,并处理关联路径与结果限制。

主要功能

  1. 01发现 Cube 及可查询成员
  2. 02构建聚合与记录查询
  3. 03应用筛选条件、细分和时间范围
  4. 04组合 Cube 前核实关联路径
  5. 05处理行数统计和结果限制

适用场景

  1. 01汇总指定日期范围内的销售数据
  2. 02按维度拆分指标
  3. 03查看可用的 Cube 和字段

MCP 相关

通过 RevOS MCP 服务的 `api_read` 工具,从 `cubes` 资源读取语义模型元数据并查询行数据。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/revosai/skills --skill semantic-model

安装后在 Claude Code 中输入/semantic-model确认已加载。

附带文件evals/evals.json

详细介绍

语义模型

通过 RevOS MCP 服务查询组织的实时语义模型,并直接在聊天中呈现结果,以回答业务问题。

MCP 服务提供了一小组通用工具;语义模型通过其中一个工具 api_read,针对 cubes 资源访问:

调用获取内容
api_read({ resource: "cubes", method: "meta", params: {…} })编译后的语义模型——包含每个 cube/view 及其度量、维度、分段和联接
api_read({ resource: "cubes", method: "query", body: { query: {…} } })行

api_details({ resource: "cubes", operation: "query" }) 会返回端点强制执行的确切架构,该架构读取自端点自己的 OpenAPI 文档。如果这里的内容与 api_details 不一致,以 api_details 为准——调用被拒绝且原因不明显时,请使用它。

此外,api_read({ resource: "cubes" }) 不带 method 时,会列出已存储的 cube 定义——即有人编写的 YAML。这与“我能查询什么”是不同的问题;要回答业务问题,请始终使用 meta。

步骤 1:探索可查询内容

在猜测成员名称之前,请先读取目录。meta 是一个列表端点, 和其他端点一样,因此应对其进行投影,而不是拉取整个文档:

{
  "resource": "cubes",
  "method": "meta",
  "params": { "fields": "name,title,description", "pageSize": 100 }
}

整个目录只有几 KB,读取并从中选择的成本很低。pageSize 默认为 100, meta 的上限为 1000;如果响应包含 metadata.nextPageToken,请将其作为 params.pageToken 传回,以获取剩余内容。

目录中可能会显示内部的、由系统生成的立方体,以及你预期会看到的业务立方体——例如,为“最新分数”汇总提供支持的辅助立方体、内部细分立方体,或类似的基础结构。这些立方体的存在是为了支持另一个立方体的连接,而不是供你直接查询。RevOS 生成的立方体标题为 [Table] … 或 [Segment] …(名称为 model_stream_…___local、segment_…),表的视图命名为 model_…;其他立方体可能表现为描述稀少或缺失,或名称/标题听起来像是机械生成的。如果搜索结果同时匹配到名称清晰、符合业务语义的立方体和这类立方体,除非问题明确针对内部立方体,否则请介绍并查询业务立方体。

然后,移除 fields 并筛选出问题所需的立方体,以获取其完整定义:

{
  "resource": "cubes",
  "method": "meta",
  "params": { "filter": "name == \"gold_order_items_enriched\"" }
}

返回结果包含你可以引用的准确 measures、dimensions(在相关情况下包含时间粒度)、segments 和 joins——引用时始终使用 <CubeName>.<member> 的形式,例如 gold_order_items_enriched.count 或 gold_order_items_enriched.order_date。关于 joins 提供的内容及其不提供的内容,请参阅步骤 2。

params.filter 是针对 name、title、description 和 type 的 CEL 表达式——name.contains("invoice")、!name.endsWith("_local")、 type == "view"。无法筛选成员:你不能根据立方体包含的字段来搜索立方体,因此上面投影的目录是正确的切入点。

如果用户的问题纯粹是探索内容——“我的语义模型里有什么”、“我可以查询什么”、“列出这些立方体/数据集”——到此为止:用文字或简短表格概述这些立方体(如果你获取了完整定义,也包括其字段),并跳过步骤 3–6。不要运行用户未要求的查询。

绝不要硬编码或猜测成员名称。如果无法根据 meta 返回的内容回答问题,请如实说明,不要编造字段。

即使问题提到了源系统,也要使用语义模型。 例如,“有多少个不同的 HubSpot 所有者至少有一个联系人”虽然提到了 HubSpot,但仍然是一个聚合问题——应从语义模型中回答(例如,使用基于 gold_contacts 构建、已去重且完全同步的 users 立方体),而不是转而使用直接的 HubSpot/Zammad/Exact Online/其他源连接器,即使某个连接器可用,而且通过关键词搜索看起来更符合问题的措辞。直接源连接器通常会分页,并返回不完整的实时快照,而不是完整的同步数据集——在任务中途改用这类连接器,可能会悄无声息地返回相差几个数量级的错误答案,而且不会报错,因为连接器调用本身完全成功;它只是没有查看与语义模型相同的数据。直接源连接器仅用于查询一条已确定记录的信息(例如“联系人 X 的邮箱是什么”、“HubSpot 中公司 Y 的所有者是谁”),绝不要用于对整个数据集进行计数、求和或分组统计。

第 2 步:跨立方体查询——读取 joins,并了解它的两个盲点

meta 会为每个立方体返回一个 joins 数组,其中列出它连接到的每个其他立方体的 { name, relationship, sql }。编写跨多个立方体的查询之前,先阅读它:如果问题需要同时用到 gold_order_items_enriched 和 gold_users_with_order_stats,先选其中一个,并检查另一个是否出现在它的 joins 中。不要仅仅因为两个立方体都恰好有一个名称相似、形似“account id”的列,就推断它们之间存在关联。

有两个盲点;如果你把 joins 当作完整信息,相信它们就会让你踩坑:

这里只列出出站关联。 立方体只报告自己声明的关联,不包括其他立方体指向它的关联。此代码库中的关联是单向定义的——deals 声明了与 customers 的关联,但 customers 没有声明反向关联——而 Cube 的规划器可以从任一侧遍历这类关联。因此,一个所有立方体都关联到它的维度型立方体,可能报告零个关联,但却能从十多个地方关联到它。切勿将空的或较短的 joins 数组理解为“这个立方体与任何东西都没有关联”:在得出不存在路径的结论前,也要检查另一个立方体的 joins。

伪造的 joinHints 路径不会被拒绝。 当两个立方体之间存在多条路径时,joinHints 会固定所走的路线:它是一个路径列表,每条路径都是一个有序的立方体名称数组,指定要经过的立方体——直接关联时包含两个名称,多跳关联时则更多——例如 [["gold_order_items_enriched", "gold_users_with_order_stats"]] 或 [["orders", "line_items", "products"]]。路径中某一对立方体之间如果没有已声明的关联,不会导致错误:Cube 会退回到自动解析实际路径,返回的结果会基于你没有选择的路线计算得出。只有一条实际路径时,这没有影响——Cube 会忽略错误的提示,结果仍然正确——但当架构中存在两条有效路径的真正分叉时,就会悄然产生错误,因为伪造的提示无法可靠地固定其中任何一条。查询成功并不能证明这条路径真实存在。

因此:只有在确认 joinHints 中每一对连续立方体之间的关联都确实出现在某个立方体的 joins 中时,才设置 joinHints;还要遵循第一个盲点,检查两个方向。当存在多条合理路径时——例如有两条路径可以到达同一个字段,或事实表既能直接关联到某个维度,也能通过第三个立方体关联到它——不要默默地选一条。明确说明存在歧义,并在执行查询前请用户确认预期的关联关系或粒度——例如“每个订单项的收入”与“每个用户的收入”。设置 joinHints 后,也要在后续说明中交代所用路径及选择原因,以便用户发现错误假设。如果问题实际上不需要跨立方体查询,优先使用最简单的单立方体答案。

第 3 步:构建查询

Cube.js 查询应放在 body.query 中——注意嵌套结构;请求体应为 { "query": { … } },而不是查询对象本身。

字段结构说明
measuresstring[]要聚合的量,仅限 meta 中列出的量,例如 gold_order_items_enriched.count;如果其中没有 count,请参阅“统计行数”
dimensionsstring[]用于分组或细分的属性
segmentsstring[]来自 meta 的命名筛选条件,叠加在现有筛选条件之上
timeDimensions[{ dimension, granularity?, dateRange? }]见下文
filters见下文叶级条件,可嵌套在 and/or 组中
order[[member, dir], …] 或 { member: dir }两种结构均可——由 [member, "asc"|"desc"] 对组成的数组,或对象映射
joinHints[[cube, …], …]固定有歧义的连接路径——每个条目都是由两个或更多 cube 名称组成的有序路径,见步骤 2
limitnumber见下文“行数限制”
offsetnumber用于翻页,跳过 limit 条结果
timezoneIANA 字符串,例如 Europe/Amsterdam默认为 UTC
totalboolean同时返回匹配行的总数,不受 limit/offset 影响——适用于“(显示 842 条中的 10 条)”
ungroupedboolean返回未按维度聚合的原始行,以便检查单条记录而非汇总数据——仍受行数限制

此处未列出的键会原样转发给 Cube,因此,即使 API 未提及某项 Cube.js 功能,仍可使用。

timeDimensions[].granularity — 通常为 day、week、month、quarter、year 之一(Cube 也支持 hour/minute/second,以及维度定义的自定义粒度)。如果要返回整个范围的单个总计,而非时间序列,请完全省略此项。

timeDimensions[].dateRange — 相对日期字符串("last quarter"、"last 12 months"、"this month"),或一对明确的 ISO 日期,例如 ["2026-01-01", "2026-03-31"]。

同比/环比(“本月与上月对比”)— 每个范围分别运行一个查询,并为其设置各自的 dateRange,然后自行比较。避免使用 compareDateRange:端点会将 Cube 的响应当作普通结果读取,而对比查询并非普通结果,因此很可能会返回一个没有详细信息的 500 错误。

统计行数(“有多少笔交易”)— 仅当 meta 为该 cube 列出 count 度量时,才使用 <cube>.count。如果没有(measures 为空或不含 count),请查询该 cube 的主键维度——即 meta 标记为 primaryKey: true 的维度——并设置 limit: 0 和 total: true,然后根据 metadata.total 作答:

{ "query": { "dimensions": ["hubspot_deals.id"], "limit": 0, "total": true } }

total 统计的是所查询维度的不同值,因此只有主键才能给出行数;如果没有维度标记为 primaryKey,请说明无法可靠地统计行数。筛选条件、分段和 dateRange 均会应用于统计。data 会返回空值——这是预期行为,并不表示“没有匹配项”:请根据 metadata.total 用文字作答,并附上一行表格(例如 Deals | 141),不要使用图表。细分统计(“每个阶段各有多少笔”)需要 count 度量;如果没有,请说明该 cube 不包含此度量。

filters 是条件列表,每项为以下其中一种:

  • 条件:{ "member": "...", "operator": "...", "values": [...] }。 值通常是字符串(即使是数字或日期,["100"] 也可以正常使用),但也接受数字、布尔值和 null。运算符:equals、notEquals、in、notIn、contains、notContains、startsWith、notStartsWith、endsWith、notEndsWith、gt、gte、lt、lte、inDateRange、notInDateRange、onTheDate、beforeDate、beforeOrOnDate、afterDate、afterOrOnDate、measureFilter。set / notSet 用于检查是否存在,不接受 values。
  • 分组:{ "and": [...] } 或 { "or": [...] },其中每个条目仍为条件或分组(可以嵌套)。filters 数组中的顶层条目会隐式执行 AND 组合;问题需要“满足其中任一条件”而非“满足所有条件”时,请使用显式 or 分组——不支持在同一个 and/or 分组中混合维度条件和度量条件,请将它们分开。 这些是 Cube 自有的筛选器,位于 body.query.filters。端点会检查它们的结构(条件位置上如果是裸字符串或数字,会被拒绝并返回参数错误);成员名称和值由 Cube 检查,错误值会返回 Cube 的错误。不要将它们与步骤 1 中的 params.filter CEL 字符串混淆:前者筛选目录,这里筛选的是数据。
行数限制

limit 会传递给 Cube;如果查询未指定,Cube 会应用自己的默认值,并拒绝任何超过其配置上限的值。这些默认值是为数据仓库设计的,而非聊天窗口。由此带来两个后果:

  • 务必指定你实际需要的 limit。 不设 limit 的开放式查询可能会直接向对话中返回数千行。
  • 响应中的 metadata.limit 是权威值——它会报告 Cube 实际应用的限制值;当你说结果已截断时,应引用这个数字。
示例

最简单的情况:

{
  "resource": "cubes",
  "method": "query",
  "body": { "query": { "measures": ["gold_order_items_enriched.count"] } }
}

按类别取前 N 项:

{
  "resource": "cubes",
  "method": "query",
  "body": {
    "query": {
      "measures": ["gold_order_items_enriched.count"],
      "dimensions": ["gold_order_items_enriched.traffic_source"],
      "order": [["gold_order_items_enriched.count", "desc"]],
      "limit": 10
    }
  }
}

时间序列(最适合绘制图表的形式):

{
  "resource": "cubes",
  "method": "query",
  "body": {
    "query": {
      "measures": ["gold_order_items_enriched.revenue"],
      "timeDimensions": [
        {
          "dimension": "gold_order_items_enriched.order_date",
          "granularity": "month",
          "dateRange": "last 12 months"
        }
      ]
    }
  }
}

使用 OR 逻辑进行筛选(“已退款或测试订单”):

{
  "resource": "cubes",
  "method": "query",
  "body": {
    "query": {
      "measures": ["gold_order_items_enriched.count"],
      "filters": [
        {
          "or": [
            { "member": "gold_order_items_enriched.is_refunded", "operator": "equals", "values": ["true"] },
            { "member": "gold_order_items_enriched.is_test_order", "operator": "equals", "values": ["true"] }
          ]
        }
      ]
    }
  }
}

使用固定联接路径跨 Cube 查询(参见步骤 2):

{
  "resource": "cubes",
  "method": "query",
  "body": {
    "query": {
      "measures": ["gold_order_items_enriched.revenue"],
      "dimensions": ["gold_users_with_order_stats.country"],
      "joinHints": [["gold_order_items_enriched", "gold_users_with_order_stats"]]
    }
  }
}

经验法则:

  • 一个度量 + 一个维度 → 可按类别绘图。
  • 一个度量 + 一个带 granularity 的 timeDimensions → 可按时间绘图。
  • 多个度量或多个维度 → 仅用表格展示;不要尝试绘图。
  • 对于“前 N 项”问题,务必显式设置 limit(limit: N + 匹配的 order);对于开放式查询,绝不要依赖默认值。

步骤 4:运行查询并读取响应

响应为 { data, metadata }:

  • data——行数组,每一行都是一个扁平对象,其键为查询中使用的相同成员名称。
  • metadata.rowCount——返回的行数。
  • metadata.limit / metadata.offset——Cube 实际应用的限制值和偏移量。
  • metadata.total——仅当查询设置了 total: true 时才会出现。它是忽略限制值和偏移量后匹配的行数,因此,offset + rowCount < total 表示还有下一页。如果没有它,恰好填满的一页无法区分是还有后续行,还是已到最后一行——因此,只要你打算说“显示 N 条中的 10 条”或提供分页选项,就应设置 total: true。
  • metadata.lastRefreshTime——底层数据上次刷新的时间。如果用户询问数据有多新,值得提及。
  • metadata.annotation——Cube 对各成员的描述,按 measures / dimensions / segments / timeDimensions 分组,并以行中使用的相同全限定名称为键,至少包含 title、shortTitle 和 type。行中只包含值,因此人类可读的列标题和值类型来自这里——表格标题应使用 shortTitle,而不要打印原始成员名称;用 type 判断列是否为数字,以及是否应右对齐并设置格式。

如果调用出错,请直接展示错误消息。常见原因包括:成员名称拼写错误(重新检查 meta)、在非时间维度上使用了 dateRange / granularity,或 limit 超过 Cube 的上限。注意,以下情况不属于上述原因:错误的 joinHints 路径不会报错(步骤 2),因此,成功返回结果并不能确认路径正确。如果错误消息不够清楚,请调用 api_details({ resource: "cubes", operation: "query" }),并根据端点实际接受的内容检查查询结构。

步骤 5:在聊天中呈现结果

5a. 表格——始终使用

将返回的每一行都呈现为标准 Markdown 表格,而不是 ASCII 艺术图——表格可直接在聊天 UI 中原生渲染,无需手动计算列宽。表头应取自 metadata.annotation(shortTitle),而不是打印原始成员名称。使用 ---: 表头分隔符语法将数字列右对齐。数字使用千位分隔符;非整数四舍五入到小数点后 2 位。

| Traffic Source | Count |
|---|---:|
| Search | 142,318 |
| Organic | 88,204 |
| Email | 41,907 |
| Facebook | 22,015 |
| Display | 9,471 |
5b. 图表 — 数据形态适合时

如果查询结果恰好包含一个度量值和一个维度(类别或时间),还要创建一个工件,其中包含真实图表(类别细分用柱状图,时间序列用折线图或柱状图),使其以内嵌的实际图表形式呈现,而不是字符画。小型、自包含的 SVG 通常是最简单、最通用的选择——无需外部库或网络访问——但也可以使用最适合客户端的图表工件。标注坐标轴/类别并显示数值;配色方案应保持简洁。

如果当前客户端不支持工件,就跳过图表,只使用表格——不要退而求其次,用文本字符绘制图表。

以下情况应完全跳过图表:结果集为空;查询包含 2 个或更多度量值、2 个或更多维度;只有一个标量值(没有维度);或者所有度量值都为零或 null。对于单个标量答案,应在正文中写出数值,然后附上一行表格——不要绘制图表。

第 6 步:简要说明结果

渲染输出后,用通俗的英文写 1–3 句:说明测量了什么、数值最高/最低的分组,以及表格中明显的异常(例如缺少一个时间段、某个类别占总量的大部分)——不要推测数据中看不出的原因。准确建议用户可以接着提出的 一个 具体查询。

规则

  • 绝不要硬编码立方体或成员名称——务必先通过 meta 确认。
  • 即使问题提到了某个源系统(HubSpot、Zammad、Exact Online 等),也要根据语义模型回答聚合/分析问题——不要在任务中途切换到直接源连接器;直接源连接器适用于查询一个已知记录的具体信息,而不是对整个数据集计数(见第 1 步)。
  • 绝不要默默处理涉及多个立方体的模糊连接——应询问用户(见第 2 步)。
  • 绝不要把空的 joins 数组理解为“没有关系”:列表中只包含出向连接,因此也要检查另一个立方体(见第 2 步)。
  • 设置 joinHints 中的每个跳转前,都要对照已声明的连接进行确认。虚构的配对会被接受并得到回答,而不是被拒绝——查询成功并不能说明路径真实存在(见第 2 步)。
  • 搜索结果中有多个匹配项时,优先选择业务名称清晰的立方体,而不是内部/系统生成的立方体(见第 1 步)。
  • 始终渲染表格;只有数据形态适合时才渲染图表。
  • 始终有意设置 limit;端点会应用 Cube 自身的默认值,这个值远大于对话所需。
  • 如果计划报告“N/M”或提供下一页,务必设置 total: true。
  • 仅当 meta 列出了 <cube>.count 时才使用它计数;否则请参阅“计数行数”(第 3 步)。
  • 遇到“未找到”错误后,重新读取该立方体的 meta,只使用其中列出的成员。绝不要重试名称变体,也不要将维度放入 measures。
  • api_details 是端点所接受内容的权威依据——调用被拒绝时应查阅它,而不是猜测参数格式。
  • 这是一项只读的聊天内技能——不写入文件、不使用本地项目,也不使用 YAML。读取语义模型时只能使用 api_read;MCP 服务也提供 api_write 和 api_delete,但此技能绝不调用它们。
  • 如果 meta 未返回任何立方体,请告知用户其组织似乎尚未设置语义模型——不要猜测原因。

评论

支持 Markdown