构建语义层与指标引擎:让 BI 数据真正可信

一、为什么我们需要语义层?
做数据平台最怕什么?不是数据存不下来,而是大家算出来的数对不上。
同一个"活跃用户数",产品看的是登录,运营看的是交互,财务看的是付费。三份报表摆在一起,数字能差出 30%。管理层拿着这些数据做决策,心里肯定犯嘀咕。
更麻烦的是计算逻辑散落在各处:SQL 脚本、Python Notebook、Excel 公式、BI 工具里的计算字段......版本乱成一团。一旦业务规则变了(比如"活跃"的定义改了),得把相关报表翻个底朝天手动改,漏一个就是一个坑。
我们做这个 AI 增强的 BI 平台,核心就是想通过**语义层(Semantic Layer)**把指标定义收归统一。顺便利用大模型做自然语言查询和异常归因,让 BI 工具别只做个"报表生成器",能真正帮人分析数据。
二、语义层架构:把业务逻辑和数据物理层解耦
2.1 分层设计
语义层的作用很简单:在原始数据和业务应用之间,垫一层统一的指标定义。
这层主要管四样东西:
- 维度(Dimension):看数据的角度,比如时间、区域。决定了数据怎么切分。
- 度量(Measure):能聚合的数字,比如 GMV、订单量。定义了是用 SUM 还是 COUNT。
- 指标(Metric):具体的业务公式,比如"客单价=GMV/订单量"。这是业务方直接关心的。
- 关系(Relation):表怎么连,JOIN 条件是什么,基数怎么算。
2.2 指标也要讲版本
指标定义不是刻在石头上的。业务变了,公式就得改,但历史数据还得按旧口径算。
所以语义层里,每个指标都得维护一个版本链。查数据的时候,要么指定查哪个版本,要么默认用最新的。这样既保证了历史可比性,又支持了业务演进。
三、代码实现:语义层与智能计算引擎
python
"""AI增强BI平台:语义层与智能指标计算引擎"""
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional
class AggregationType(Enum):
"""聚合类型"""
SUM = "sum"
COUNT = "count"
COUNT_DISTINCT = "count_distinct"
AVG = "avg"
MAX = "max"
MIN = "min"
MEDIAN = "median"
class MetricType(Enum):
"""指标类型"""
ATOMIC = "atomic" # 原子指标,直接聚合
DERIVED = "derived" # 衍生指标,基于其他指标计算
WINDOW = "window" # 窗口指标,涉及时间窗口计算
@dataclass
class Dimension:
"""维度定义"""
name: str
display_name: str
sql_expression: str # 对应的SQL表达式
data_type: str # string / int / date
hierarchy: Optional[list[str]] = None # 层级维度,如 [省, 市, 区]
@dataclass
class Measure:
"""度量定义"""
name: str
display_name: str
sql_expression: str
aggregation: AggregationType
table_name: str
description: str = ""
@dataclass
class MetricVersion:
"""指标版本"""
version: int
formula: str # 计算公式
description: str # 口径说明
created_at: str = ""
is_active: bool = True
@dataclass
class Metric:
"""指标定义"""
name: str
display_name: str
metric_type: MetricType
versions: list[MetricVersion] = field(default_factory=list)
dimensions: list[str] = field(default_factory=list) # 可用维度
owner: str = "" # 指标负责人
tags: list[str] = field(default_factory=list)
@property
def active_version(self) -> Optional[MetricVersion]:
"""获取当前生效的版本"""
active = [v for v in self.versions if v.is_active]
return active[-1] if active else None
class SemanticLayer:
"""语义层引擎"""
def __init__(self):
self.dimensions: dict[str, Dimension] = {}
self.measures: dict[str, Measure] = {}
self.metrics: dict[str, Metric] = {}
def register_dimension(self, dim: Dimension) -> None:
"""注册维度"""
self.dimensions[dim.name] = dim
def register_measure(self, measure: Measure) -> None:
"""注册度量"""
self.measures[measure.name] = measure
def register_metric(self, metric: Metric) -> None:
"""注册指标"""
if not metric.versions:
raise ValueError(f"指标 {metric.name} 必须至少有一个版本")
self.metrics[metric.name] = metric
def update_metric_formula(
self, metric_name: str, new_formula: str, description: str,
) -> int:
"""更新指标公式,创建新版本"""
metric = self.metrics.get(metric_name)
if not metric:
raise KeyError(f"指标 {metric_name} 不存在")
# 将旧版本标记为非活跃
for v in metric.versions:
v.is_active = False
# 创建新版本
new_version = len(metric.versions) + 1
metric.versions.append(MetricVersion(
version=new_version,
formula=new_formula,
description=description,
is_active=True,
))
return new_version
def compile_metric_to_sql(
self, metric_name: str,
dimensions: Optional[list[str]] = None,
filters: Optional[dict[str, Any]] = None,
version: Optional[int] = None,
) -> str:
"""将指标编译为SQL查询"""
metric = self.metrics.get(metric_name)
if not metric:
raise KeyError(f"指标 {metric_name} 不存在")
# 确定使用的版本
if version:
ver = next(
(v for v in metric.versions if v.version == version), None
)
if not ver:
raise ValueError(f"版本 {version} 不存在")
else:
ver = metric.active_version
# 解析公式中的度量引用,替换为SQL表达式
formula = ver.formula
for measure_name, measure in self.measures.items():
pattern = r'\b' + measure_name + r'\b'
agg_sql = f"{measure.aggregation.value}({measure.sql_expression})"
formula = re.sub(pattern, agg_sql, formula)
# 构建SELECT子句
select_parts = []
dim_exprs = []
if dimensions:
for dim_name in dimensions:
dim = self.dimensions.get(dim_name)
if dim:
select_parts.append(
f"{dim.sql_expression} AS {dim_name}"
)
dim_exprs.append(dim.sql_expression)
select_parts.append(f"{formula} AS {metric_name}")
# 构建FROM和JOIN子句(简化版,实际需要关系推导)
involved_tables = set()
if dimensions:
for dim_name in dimensions:
dim = self.dimensions.get(dim_name)
if dim and '.' in dim.sql_expression:
involved_tables.add(dim.sql_expression.split('.')[0])
for measure_name in re.findall(r'\b\w+\b', ver.formula):
m = self.measures.get(measure_name)
if m:
involved_tables.add(m.table_name)
from_clause = ", ".join(involved_tables)
# 构建WHERE子句
where_parts = []
if filters:
for key, value in filters.items():
dim = self.dimensions.get(key)
if dim:
if isinstance(value, list):
vals = "','".join(str(v) for v in value)
where_parts.append(
f"{dim.sql_expression} IN ('{vals}')"
)
else:
where_parts.append(
f"{dim.sql_expression} = '{value}'"
)
# 组装SQL
sql = f"SELECT {', '.join(select_parts)} FROM {from_clause}"
if dim_exprs:
sql += f" GROUP BY {', '.join(dim_exprs)}"
if where_parts:
sql += f" WHERE {' AND '.join(where_parts)}"
return sql
def get_metric_lineage(self, metric_name: str) -> dict:
"""获取指标的血缘关系"""
metric = self.metrics.get(metric_name)
if not metric:
return {}
ver = metric.active_version
# 从公式中提取依赖的度量和指标
deps = {
"metric": metric_name,
"formula": ver.formula,
"depends_on_measures": [],
"depends_on_tables": [],
}
for measure_name in re.findall(r'\b\w+\b', ver.formula):
m = self.measures.get(measure_name)
if m:
deps["depends_on_measures"].append(measure_name)
deps["depends_on_tables"].append(m.table_name)
return deps
class AIAnomalyAttributor:
"""AI驱动的指标异常归因"""
def __init__(self, llm_client):
self.llm_client = llm_client
def attribute_anomaly(
self, metric_name: str, anomaly_info: dict,
semantic_layer: SemanticLayer,
) -> dict:
"""对指标异常进行归因分析"""
lineage = semantic_layer.get_metric_lineage(metric_name)
metric = semantic_layer.metrics[metric_name]
ver = metric.active_version
# 构建归因分析Prompt
prompt = f"""指标 {metric_name} 出现异常,请进行归因分析。
异常信息:
- 指标名称:{metric.display_name}
- 当前值:{anomaly_info.get('current_value')}
- 预期值:{anomaly_info.get('expected_value')}
- 偏差幅度:{anomaly_info.get('deviation_pct')}%
- 时间段:{anomaly_info.get('time_range')}
指标定义:
- 计算公式:{ver.formula}
- 口径说明:{ver.description}
- 依赖度量:{lineage.get('depends_on_measures', [])}
- 依赖表:{lineage.get('depends_on_tables', [])}
请从以下维度分析可能的原因:
1. 上游度量变化:哪些依赖度量发生了显著变化
2. 维度拆解:哪个维度的细分数据贡献了最大偏差
3. 数据质量:是否存在数据缺失或重复
4. 业务事件:是否有促销、活动等业务因素
返回JSON格式的归因结果。"""
# 调用LLM进行归因分析
response = self.llm_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)
return {
"metric": metric_name,
"attribution": response.choices[0].message.content,
}
四、工程上的几个坑
4.1 灵活性与性能的博弈
语义层本质是把业务公式翻译成 SQL。这中间涉及公式解析、表关系推导、JOIN 路径选择。对于复杂指标(比如带窗口函数、子查询的),生成的 SQL 往往不如手写优化得好,查询性能可能会掉 20%-50%。
怎么折中?
高频查的指标,直接预编译成物化视图;低频的保持动态编译。另外得留个后门,允许对特定指标手动指定优化后的 SQL 模板,别让语义层卡住性能。
4.2 版本管理的复杂度
指标之间往往有依赖:指标 C 依赖 B,B 依赖 A。底层指标口径一变,上面全得跟着变。如果每个指标都独立版本化,组合爆炸会让版本矩阵变成噩梦。
实际做法:
把相关指标打包成"指标组",同组同步更新。跨组依赖通过接口约定,别直接在公式里硬引用。
4.3 什么时候别用语义层
别为了技术而技术,以下情况建议慎重:
- 报表少于 20 个:口径问题还不明显,维护语义层的成本高于收益。
- 查询高度定制:每个报表的 SQL 都是特化的,通用抽象覆盖不了。
- 实时流式计算:语义层主要面向批处理 SQL,对流式窗口的支持目前还比较弱。
五、总结
这套方案的核心价值,是把散落在 SQL、Notebook 和 Excel 里的指标定义收拢到一处。版本管理让变更可追溯,血缘分析能帮你快速定位影响范围,AI 归因则把排查异常从"人工逐层下钻"变成了"智能定位"。
落地路线建议:
- 先挑核心业务指标(GMV、DAU、转化率)建语义层,验证编译准确性和性能。
- 再慢慢扩大覆盖范围,加上版本管理和血缘追踪。
- 最后接入 AI 归因,实现从"看数据"到"理解数据"的转变。
语义层不是要替代 SQL,而是让指标的定义、计算和使用有章可循。数据可信了,决策才能踏实。