关于 semantic-model
通过 RevOS MCP 服务中的组织实时语义模型回答业务问题。该 Skill 说明如何发现可查询字段、构建 Cube 查询,并处理关联路径与结果限制。
主要功能
- 01发现 Cube 及可查询成员
- 02构建聚合与记录查询
- 03应用筛选条件、细分和时间范围
- 04组合 Cube 前核实关联路径
- 05处理行数统计和结果限制
适用场景
- 01汇总指定日期范围内的销售数据
- 02按维度拆分指标
- 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": { … } },而不是查询对象本身。
| 字段 | 结构 | 说明 |
|---|---|---|
measures | string[] | 要聚合的量,仅限 meta 中列出的量,例如 gold_order_items_enriched.count;如果其中没有 count,请参阅“统计行数” |
dimensions | string[] | 用于分组或细分的属性 |
segments | string[] | 来自 meta 的命名筛选条件,叠加在现有筛选条件之上 |
timeDimensions | [{ dimension, granularity?, dateRange? }] | 见下文 |
filters | 见下文 | 叶级条件,可嵌套在 and/or 组中 |
order | [[member, dir], …] 或 { member: dir } | 两种结构均可——由 [member, "asc"|"desc"] 对组成的数组,或对象映射 |
joinHints | [[cube, …], …] | 固定有歧义的连接路径——每个条目都是由两个或更多 cube 名称组成的有序路径,见步骤 2 |
limit | number | 见下文“行数限制” |
offset | number | 用于翻页,跳过 limit 条结果 |
timezone | IANA 字符串,例如 Europe/Amsterdam | 默认为 UTC |
total | boolean | 同时返回匹配行的总数,不受 limit/offset 影响——适用于“(显示 842 条中的 10 条)” |
ungrouped | boolean | 返回未按维度聚合的原始行,以便检查单条记录而非汇总数据——仍受行数限制 |
此处未列出的键会原样转发给 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.filterCEL 字符串混淆:前者筛选目录,这里筛选的是数据。
行数限制
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未返回任何立方体,请告知用户其组织似乎尚未设置语义模型——不要猜测原因。