veriself
面向 AI agent 的类型化指标执行层。 LLM 不能写 SQL,只能组合已注册的指标——每个数字都带口径版本、粒度、血缘与审计头。
30 秒演示
下面的输出是实跑的,不是手写的:本机
veriself synth && veriself demo原文,仅删除整行 (删除处标…)与行尾空格。面板偏宽是因为输出被重定向时宽度固定为 120 列 (见interfaces/render.py的_NON_TTY_WIDTH)。
真实用户在 MCP 客户端里说的是一句自然语言("我最近睡眠债有多严重?")。把它翻成下面这个
结构化查询对象是客户端的事——veriself 只接受这个对象,没有任何参数能传原生语句。
所以"LLM 写出错误 SQL"这个失败模式在这里根本不存在。
下面这条命令的前提是先跑过 veriself synth(生成 3 年确定性合成数据,输出见折叠区):
$ veriself demo
── 步骤 1/4 · 建库 · 加载契约 · 物化指标 ────────
┌─ 步骤 1 完成 ────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 契约数量 18 │
│ 建表函数 veriself.warehouse.loader.ensure_schema │
│ 契约写入维度表 18 │
│ 物化指标 18 │
│ 物化行数 14408 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
── 步骤 2/4 · 正常查询:subject.sleep_debt_7d(结构化对象) ────────
提交给 veriself 的查询对象:
{
"metrics": [
"subject.sleep_debt_7d"
],
"dimensions": [
"date.weekday"
],
"filters": {
"date.between": [
"2026-09-01",
"2026-09-30"
]
},
"grain": "day",
"limit": 30
}
date.day date.weekday subject.sleep_debt_7d
─────────────────────────────────────────────────
2026-09-01 Tuesday 3.03
2026-09-02 Wednesday 3.62
2026-09-03 Thursday 3.28
2026-09-04 Friday 3.43
2026-09-05 Saturday 4.24
2026-09-06 Sunday 4.67
…
2026-09-25 Friday 5.44
共 30 行,仅显示前 25 行(--json 可拿全量)
┌─ 审计头 audit(契约第 4 节) ────────────────────────────────────────────────────────────────────────────────────────┐
│ 查询角色 owner │
│ 指标版本 subject.sleep_debt_7d = 2 │
│ 契约哈希 subject.sleep_debt_7d = sha256:9d32e8f0fa6dfed3 │
│ RLS 改写 owner_only │
│ 强制校验 registered → dimensions → grain → ast_join_path → rls │
│ as-of 口径 2026-10-01 │
│ 查询时间 2026-10-07T17:08:30+08:00 │
│ row_versioning valid_to IS NULL AND metric_version = contract.version │
│ actor_role owner │
…
── 步骤 3/4 · 拒绝演示:越界请求必须失败(这才是卖点) ────────
┌─ ❌ 拒绝 ────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 请求已被指标契约拒绝。 │
│ │
│ reason: dimension_not_allowed: 维度 'date.hour' 不在指标 'subject.sleep_debt_7d' 的 allowed_ │
│ dimensions ['date.weekday', 'date.day_of_week', 'date.month', 'date.quarter'] 中 │
│ 触发的校验: dimension_not_allowed │
│ 强制校验链: registered → dimensions → grain → ast_join_path → rls │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
…
提示:这次拒绝也写进了审计(veriself audit 可查)。
这个 demo 的重点不是"它答对了",而是"它无法答错":第二个请求里 LLM 想越界,架构不允许它越界——拒绝发生在生成 SQL 之前(拒绝原因是 dimension_not_allowed,指向契约里的白名单),并且这次拒绝同样写进审计。
其余命令的完整输出(实跑原文,未删改)
veriself synth
对象 行数
──────────────────────────
dim_date 1,004
dim_subject 2
dim_source 4
dim_context 4
fact_observation 106,424
fact_event 5,882
latent_daily 1,004
obs_daily 10,040
obs_intraday 144,576
┌─ veriself synth 完成 ────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 数仓 <仓库根> │
│ 种子 20261001 │
│ 日期范围 2024-01-01 → 2026-09-30 │
│ 建表 已确保 schema │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
veriself query --json:契约第 4 节的 data + audit
命令:veriself query -m subject.sleep_debt_7d --filters '{"date.between": ["2026-09-28", "2026-09-30"]}' --role owner --json
{
"data": [
{
"date.day": "2026-09-28",
"subject.sleep_debt_7d": 6.970000000000001
},
{
"date.day": "2026-09-29",
"subject.sleep_debt_7d": 7.15
},
{
"date.day": "2026-09-30",
"subject.sleep_debt_7d": 6.720000000000001
}
],
"audit": {
"metric_versions": {
"subject.sleep_debt_7d": 2
},
"contract_hashes": {
"subject.sleep_debt_7d": "sha256:9d32e8f0fa6dfed3"
},
"compiled_sql": "SELECT \"d\".\"date\" AS \"date.day\", ANY_VALUE(CASE WHEN \"f\".\"metric_id\" = ? THEN \"f\".\"value\" END) AS \"subject.sleep_debt_7d\" FROM \"fact_metric_value\" AS f INNER JOIN \"dim_date\" AS d ON \"d\".\"date_key\" = \"f\".\"date_key\" WHERE \"f\".\"valid_to\" IS NULL AND (\"f\".\"metric_id\" = ? AND \"f\".\"metric_version\" = ?) AND \"f\".\"subject_id\" = ? AND \"d\".\"date\" >= CAST(? AS DATE) AND \"d\".\"date\" <= CAST(? AS DATE) GROUP BY 1 ORDER BY \"date.day\" ASC LIMIT ?",
"rls_applied": [
"owner_only"
],
"enforced_checks": [
"registered",
"dimensions",
"grain",
"ast_join_path",
"rls"
],
"as_of_definition": "2026-10-01",
"queried_at": "2026-10-07T17:08:30+08:00",
"row_versioning": "valid_to IS NULL AND metric_version = contract.version",
"actor_role": "owner"
}
}
为什么存在
每个 text-to-SQL 工具都在优化同一个问题:SQL 跑通了吗? 但近期的研究表明,在可执行的查询里,仍有 73%–99% 与提问者本意存在静默的语义分歧, 而且只对齐指标口径还不够——访问策略也必须被强制执行。
我们优化的是另一件事:让"返回一个无人能解释的数字"成为不可能。 口径与权限由代码强制,而不是在 prompt 里请求。
这不是什么
- 不是 text-to-SQL 工具。 LLM 在这里从不写 SQL,只能组合已注册的指标—— 因此不存在"幻觉出一个数字"的攻击面。
- 不是语义格式。 口径是 YAML,并将保持与 Open Semantic Interchange / Apache Ossie 兼容。我们不在格式上竞争。
- 不是 BI 仪表盘。 不提供拖拽式看板,图表是下游的事。
- 不是记忆框架。 不做检索或 embedding。我们做的是记忆框架跳过的那部分:版本、粒度、as-of、血缘、审计。
- 还不是领域无关的。 执行引擎(contract → compile → enforce → audit)本身与领域无关,
但物理模型映射目前硬接在"个人/纵向数据"这个形态上(日粒度、
(date_key, subject_id)骨架、 声明的 JOIN 路径仍是模块级常量)。泛化成domains/*.yml描述符是 路线图 v0.2 第 5 项——在此之前,换一个领域意味着改引擎代码。 - 无遥测、无云、无账号。 一个文件,local-first。
商业版缺失的那一半
治理能力在这些项目里都被放进了商业版:
| 项目 | 被管控的部分在哪 |
|---|---|
| Cube Core | RBAC 与多租户 → 商业版 |
| WrenAI | 行/列级安全 → 商业版 |
| dbt Semantic Layer | 指标查询 → dbt Cloud Team/Enterprise |
| DataHub | 指标值查询:不支持 |
于是我们把缺的那一半开源做出来:强制校验层。
五条强制校验
每条查询按顺序通过以下五条;失败是拒绝,不是警告。
| # | 校验 | 失败时 |
|---|---|---|
| 1 | registered — 指标存在且未弃用 |
拒绝:unknown_metric: / deprecated_metric: |
| 2 | dimensions — 请求的维度/过滤器在契约白名单内 |
拒绝:dimension_not_allowed: / filter_not_allowed: |
| 3 | grain — 日粒度指标不能以更细的粒度查询 |
拒绝:grain_not_compatible: |
| 4 | ast_join_path — SQL 用 sqlglot 解析;只允许声明的表、声明的 JOIN 路径,禁止子查询/UNION/CTE 逃逸,禁止 SELECT * |
拒绝:ast_violation: |
| 5 | rls — 按角色注入行级策略(owner_only / aggregate_min5 / no_pii) |
改写 SQL,并写入审计头 |
执行前另有基于 EXPLAIN 的扫描量预检。
架构
CLI (veriself) MCP server ← LLM 仅有的两个入口
│ │
└─────┬──────┘ 只接受结构化查询对象,绝不接受 SQL 字符串
▼
semantic/ 契约 → 校验 → 编译(sqlglot)→ 强制校验 → 审计
▼
warehouse/ DuckDB:dim_*(SCD2)→ fact_* → 物化指标值
▼
synth/ 确定性合成主体数据(含植入的潜在结构)
完整接口契约:docs/00-接口契约.md
指标清单:docs/01-指标清单.md
与业界术语的对应关系
如果你见过 dbt MetricFlow、Cube 或 LookML:这里没有为改而改地重命名概念—— 这张表只是翻译器(字段名已冻结,见接口契约):
| veriself | dbt MetricFlow / 业界 | 说明 |
|---|---|---|
semantic_models/*.yml |
semantic_models.yml |
物理表之上的逻辑列:通道 → 日粒度列、事件表达式、SCD2 维度列 |
metrics/*.yml |
metrics.yml |
一个文件一个指标,带版本,每个指标有冻结的维度/过滤器白名单 |
formula_sql |
type_params.expr(派生指标) |
只有派生指标允许 SQL;这里被限制为日粒度标量表达式 |
bucket: {agg: ...} |
指标的 type_params.window + time_granularity |
非日粒度的桶内聚合(声明式,不写 OVER() 样板) |
agg(顶层) |
measure agg |
跨粒度上卷:请求粒度比指标粒度粗时如何聚合 |
direction |
polarity / improves_when(Avo) |
higher_better / lower_better / neutral |
lineage.sources / upstream_metrics |
语义模型的 measure + 指标依赖 | formula_sql 里仅有的两种引用形式 |
rls_policy |
data-mesh 策略 / 行级安全 | 治理写在指标口径里,不在旁挂目录里 |
contract_hash + metric_version |
dbt 节点唯一 ID + 工件版本 | 每个数字把口径版本与哈希带进审计头 |
快速开始
git clone <this repo> && cd veriself
uv sync --extra dev # uv.lock 是依赖的唯一事实来源
veriself init # 建 DuckDB schema、加载指标契约
veriself synth # 生成 3 年确定性合成数据
veriself metrics list # 查看 18 个已注册指标
veriself query --metrics subject.sleep_debt_7d --filters '{"date.last_n_days": 30}' --role owner
veriself reject subject.focus_skore # 看拒绝原因与相近建议
veriself audit --limit 5 # 每次查询都留痕
不想激活环境时,把 veriself 换成 uv run veriself。
MCP(stdio)可用于任何 MCP 客户端:
// claude_desktop_config.json
{ "mcpServers": { "veriself": { "command": "veriself", "args": ["mcp"] } } }
关于列式存储
我本职工作里的其中一个数仓是 MySQL InnoDB,我反对过把它换成列式引擎。本项目把它的每一条约束都反过来了:
| 本职工作 | 本项目 | |
|---|---|---|
| 查询形态 | 点查 | 跨多年、多维度聚合 |
| 表形态 | 窄 EAV | 宽星型模型 |
| 写入模式 | 删+插(毁掉历史) | 追加 + SCD2(历史就是功能) |
| 部署 | MySQL | 一个本地文件,秒级重建 |
同样的问题,不同的约束,不同的答案——这就是这里用 DuckDB 的原因。
现状
v0.1.0 —— 强制校验层与合成数据集是必须做对的部分;Web UI 有意推迟。
验证结果(本机实测):
| 项 | 结果 |
|---|---|
| 测试 | uv run python -m pytest tests → 373 passed |
| 端到端红队验收 | uv run python -m pytest tests/test_e2e_redteam.py → 22 passed(真实链路,非 mock) |
| 合成数据 | 1,004 天 · fact_observation 106,424 行 · fact_event 5,882 行 |
| 指标物化 | 18 个指标 · 14,408 行 · 重算幂等(无变化时零写入,不累积历史) |
| 植入效应可检出 | 睡眠 ≤6h 的次日专注度 44.14 vs ≥7.5h 的 50.06(Welch t=−5.29, p=2.3e−06) |
| 内部自洽 | 独立重算"近 7 日睡眠债" vs 物化结果:最大绝对误差 0.0 |
| 双时间轴 | 同一 (metric, subject, date_key) 在当前版本内多行有效 = 0;值变化才留痕 |
| 周内排序 | date.day_of_week 按星期序(1→7);date.weekday 是名字,按它排序是字母序 |
逐文件用例明细、改动后必须满足的验收条件、以及各模块的边界,见 AGENTS.md。
已知限制:
dim_context与事实表没有连接键,因此dim_context.is_travel维度/过滤器暂不可用(已在契约里禁止放进白名单,列为 v0.2 工作项)。*_7d滚动指标的agg=mean:day→month 上卷得到的是"滚动值的均值",不等于月度均值。逐日展示不受影响。metrics/history/里的历史口径版本只用于 as-of 演示与文档,不进dim_metric(该表主键只有metric_id)。- 容器化尚未交付:v0.1 没有
Dockerfile/compose(记在docs/03-路线图.md)。 - 同一指标可同时存在多个有效版本:物化只作用于声明的
metric_version,升级口径不会关闭旧版本的 有效行。因此读取必须带metric_version过滤——semantic已如此实现, 但裸查fact_metric_value会同时读到多个版本。
许可证
Apache-2.0 — 见 LICENSE。
Metadata
Release files for veriself 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| veriself-0.1.0.tar.gz | 342.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| veriself-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 507.0 kB
Release files / veriself-0.1.0.tar.gz
| Download URL | veriself-0.1.0.tar.gz |
|---|---|
| Size | 342.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ae293a65a0f93fc7e2a75fdcb58e4150eb6afb6e6ea248bfed16568b53b16c7d
|
|
BLAKE2b-256 checksum How to use checksums |
844db46e8f8a5705ec2ae506eec806374936c3664ef2fa0800eb9d5351c1e523
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / veriself-0.1.0-py3-none-any.whl
| Download URL | veriself-0.1.0-py3-none-any.whl |
|---|---|
| Size | 164.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7ea335c6c7a8eba58a835438e0b706a17be36a1b8ee95e62b6850310a39f075c
|
|
BLAKE2b-256 checksum How to use checksums |
9d478e14c833ebec855f0f0ed8e6a0a96c8bd686d763b3de39b02adf06af7b84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|