产品概念
Trove 是数据决策智能体:问数 · 分析 · 决策 · 行动,一条对话链路。 这一页先画出这条链路的全貌,再回到「问数」这一段最核心的概念—— 它不把问题直接翻译成 SQL,而是先判断这个问题能不能答、按谁的口径答,然后才生成。 理解这套概念之后,后面所有页面的设计取舍都会变得理所当然。
一条链路:问数 · 分析 · 决策 · 行动
问数回答「是多少」;分析回答「为什么变了」;决策回答「该不该警觉」;行动把一次触发 冻结成一份等人拍板的提案,并在事后验收效果。四段不必全配齐—— 不声明判定规则、不配行动模板,问数与分析照样工作——但四段共用同一条纪律: 数字由确定性引擎算出来,模型只写解读,每个数字都带着可复算的证据。
flowchart LR
Q["问数 · 是多少
语义编译 + 生成与校验"]:::gen --> A["分析 · 为什么变了
确定性分解 + 噪声带"]:::exec
A --> D["决策 · 该不该警觉
判定规则 · 零 LLM"]:::gate
D --> ACT["行动 · 接下来做什么
提案 → 审批 → 外送"]:::act
ACT -. "效果验收 · 同一套噪声带" .-> D
classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px;
classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px;
classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px;
classDef act fill:#8b5cf614,stroke:#8b5cf699,stroke-width:1.5px;
linkStyle 3 stroke:#8b5cf6,stroke-width:1.5px
分析:把「为什么」确定性地拆开
问「为什么……」「什么导致……」时不走生成 SQL,而是确定性分解:先按贡献最大的维度把总变化 拆开(贡献分解 / 比率 shift-share),再沿指标表达式下钻成驱动器树。每个数字都由可复算的 查询算出来、进证据,模型只把结果写成一句人话。「变了多少才算变了」由稳健 z 与历史同粒度块的 噪声带回答——超出噪声带 / 落在带内 / 判不了,三态如实;判不了绝不冒充「没变化」。 器械细节见 分解数学与驱动器树 与 统计与噪声带,界面读法见 归因分析卡。
decompose.py:36 contribution decompose.py:71 shift_share expr_tree.py:91 metric_components
决策:阈值规则说语义模型的语言
阈值规则不用另学一套 DSL:它直接用语义模型自己的词汇声明——指标、维度、过滤、时间窗, 与语义查询是同一个形状;声明一次,每次求值都按它执行,判定过程零 LLM。 「变了多少才算警觉」同样交给季节噪声带:超出才触发、在带内不打扰、判不了如实报。 每条判定都留着它当时判的 SQL 与原始结果行;规则文件每次改动都是一次 git 提交, 判定史按规则版本分桶回评;条件满足时还能升级因果对照、在假想数字上做 what-if 回放, 没声明规则的地方由主动扫描先找异常、把候选落成待确认草稿。详见 判定规则 与 判定内核。
service.py:334 DecisionService rules.py:183 DecisionRule expr.py:70 VARIABLES
行动:停在提案,写回不存在
判定触发不会只发一条通知了事:它冻结成一份行动提案——载荷在创建那一刻定死, 之后谁批准,外送的就是那一份;批准之前,任何东西都不会离开系统。 批准后外送走 webhook / IM,回执逐条落库;行动之后还会用与判定同一套噪声带复测 「数字动了没有」——超出 / 无变化 / 判不了,三态如实。系统自身没有写回业务库的通道, 这是构造上的保证,不是配置开关。安全架构见 行动安全架构,提案生命周期见 行动与审批。
propose.py:213 build_proposal service.py:155 ActionService
四段各有哪些功能块、哪些事刻意不做,见 功能架构。 下面回到「问数」这一段——它是四段里唯一把答案「生成」出来的, 也最需要先把可答边界讲清楚。
语义模型:可答边界的来源
每个数据源有一份 semantics.yml,它不是「提示词素材」,而是这个数据源的
契约:哪些指标存在、每个指标怎么算、哪些字段是维度、表之间怎么关联。
文件格式遵循 Apache OSSIE 的 core spec——顶层是 semantic_model,
下面挂 datasets 与 metrics,指标表达式按方言分别给出:
ossie_format.py:40 dialects ossie_format.py:76 semantic_model
这里有一个容易忽略但很关键的机制:一个指标属于哪些数据集,是算出来的,不是声明的。 解析器拿指标表达式去过一遍 SQL 语法树,凡是被表名限定的列,其表名若在已声明的数据集里, 就记一笔归属:
ossie.py:84 _dataset_refs ossie.py:171 parse_ossie
为什么值得这么做:如果归属靠人手写,它就会和表达式本身分叉——改了公式忘了改归属,
系统就会在一个错误的表集上算这个指标。从表达式反推则不可能分叉,而且反推不出来
(表达式里全是裸列、没有任何表限定)时它会报错而不是猜。
这也解释了为什么指标表达式必须写成表限定形式:loans.amount,不是 amount。
语义模型的真源是 KB 里的 semantics.yml(由 /kb init 起草、人审确认)。配置目录下的语义文件只作补充,不覆盖它——两个真源等于没有真源。
可答边界:不是「全有全无」
有了语义模型,就能在生成之前判断一个问题的命运。编译器的判定结果有三种,而不是两种:
| 结果 | 含义 | 下游动作 |
|---|---|---|
| 编译成功 | 计划被完整翻译成 SQL,可解析部分全部权威化 | 直接执行,生成通道基本不参与 |
| 软 MISS | 词表/取值/口径未声明,但结构是清楚的 | 产出骨架,交给生成通道补齐,回答照常交付 |
| 硬 MISS | 结构性问题:会产出错误的行集 | 拒绝并反问,引导扩展语义模型 |
flowchart LR
Q["查询计划"]:::gen --> C{"语义编译器"}:::gen
C -- "全部命中" --> H["编译成功
权威 SQL 直接执行"]:::exec
C -- "软 MISS:词表 / 取值 / 口径缺口" --> S["PartialCompile 骨架
join / 过滤 / 分组被钉住"]:::gen
S --> G["生成通道补齐未解析部分"]:::gen
G --> V{"骨架保真校验"}:::exec
V -- "保住了" --> E["执行交付"]:::exec
V -- "丢了 join / 过滤" --> X["不放行,回滚重试"]:::err
C -- "硬 MISS:结构性问题" --> R["refuse:拒绝 + 起草扩展草案"]:::gate
R -- "确认后重答" --> Q
classDef gen fill:#6366f114,stroke:#6366f199,stroke-width:1.5px;
classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px;
classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px;
classDef err fill:#dc262614,stroke:#dc262699,stroke-width:1.5px;
linkStyle 8 stroke:#8b5cf6,stroke-width:1.5px
compiler.py:1973 SemanticCompiler compiler.py:2917 compile_detailed compiler.py:1361 CompileMiss compiler.py:1374 PartialCompile
哪些算硬 MISS
分界线只有一条:放行它,会不会算出一个看起来对、其实错的数。会,就是硬 MISS。 典型的是这四类结构性问题:
| reason | 为什么放行会出错 |
|---|---|
| fan_out | 一对多 join 把被聚合的行复制了多份,聚合结果被放大 |
| ambiguous_join_path | 两张表之间有多条关联路径,选错一条就是另一个口径 |
| unreachable_table | 问题涉及的表不在本次匹配集里,生成会引用未覆盖的表 |
| derived_cycle | 派生指标的定义成环/超深/悬空,展开它没有意义 |
compiler.py:1437 HARD_MISS_REASONS compiler.py:1456 SOFT_MISS_REASONS compiler.py:1396 MISS_REASONS
判定函数有一个刻意的默认值:遇到不认识的 reason 一律按硬处理。 宁可多拒绝一次、让人来确认,也不要把一个没见过的结构性问题当软问题放过去。
分级逃生梯
拒绝是最后手段,不是第一反应。Trove 的边界是分级的:能救的救,救不了的才拦。
软 MISS:产出骨架,而不是停摆
软 MISS 意味着「结构我们懂了,只是某个词没登记」。这时编译器不会整体放弃, 而是把能权威化的部分(join 路径、过滤条件、分组)编译成骨架固化下来, 未解析的组件留给生成通道按计划文本补齐。
关键在于执行前还有一道骨架保真校验:生成通道补完的那条 SQL,必须仍然保留骨架里的 join 边、WHERE 条件与分组宽度。少一个 join、丢一个过滤,校验不通过,SQL 不放行。
所以软 MISS 不是「降级成裸表生成」,是「权威部分被钉住,剩下的填空不许改动已钉住的部分」。
execute_sql.py:192 skeleton_preserved compiler.py:4346 skeleton_preserved contract.py:160 PlanContract
硬 MISS:拒绝 + 反问 + 扩展模型
硬 MISS 走的是反问扩展这条路——拒绝不是终点,是引导用户把语义模型补全:
- 路由短路到
refuse节点,携带结构化的分因(是 fan-out?还是 join 二义?)。 - LLM 依据分因草拟一份最小扩展草案——要补什么字段、什么指标、什么枚举值。
- 草案先做冲突检测,再作为
pending草稿写入semantic_drafts.yml,不碰semantics.yml。 - 管理端确认后应用到
semantics.yml并刷新镜像。 - 用户的原始问题重新作答——这一次它落在边界之内了。
refuse.py:1051 make_refuse semantic_draft.py:229 draft_refusal_extension manage.py:1029 SemanticManager
「管理端确认」在主路径之外还有一条自动档:如果草案补的字段能在物理 catalog 里直接找到、 或者补的指标能通过机械的聚合验证,系统会直接落库,不再等人点确认。这条路的边界写死在代码里, 认不出的仍然回到人工那一档。
两条快径,别混为一谈
系统里有两种「不调用模型直接出 SQL」的机制。它们经常被当成同一件事,但触发条件、 依据、风险模型完全不同:
| 模板快径 | KB 精确命中 | |
|---|---|---|
| 依据 | examples.yml 里 kind='template' 的条目 |
examples.yml 里语义等价的问答对 |
| 触发 | 问题与模板四重硬约束全部满足 | 词重叠 ≥ 0.95 |
| 产出 | 把参数填进模板 SQL | 直接复用示例 SQL |
| 兜底 | 任一约束不满足即放弃 | 修正轮禁用(避免复读错误答案) |
模板快径过四重约束,全部满足才命中——任一不满足即放弃,回到正常链路:
| 约束 | 具体是什么 | 卡掉的是什么 |
|---|---|---|
| SQL 形状合格 | 单表 FROM;无 JOIN / 子查询 / CTE / GROUP / ORDER / LIMIT;至多一个聚合函数;无 WHERE col > 0 这类占位比较 |
形状一复杂就没法安全填参的模板;阈值 0 是结构占位,不是数据锚定 |
| 表锚定一致 | 模板的表(复数归一后)要在 schema linking 给出的 matched_tables 里 |
「问题里提了一嘴、但 linker 判定无关」的表——问题提及通道只在 matched 多表(外键邻居扩展)时才启用 |
| 聚合意图词 | 问题里出现该聚合函数的说法(AVG → 平均 / average) | 问题在问别的事、模板碰巧是个聚合 |
| 有家族证据 | date_range / aggregate / enum_filter / bare_count 四个家族各自的匹配器要过 | 形状对、词也对,但条件对不上(比如裸 COUNT 模板遇到还有剩余 token 的问题) |
另有两道守卫,命中任一即放弃,而且放弃得比约束更主动:
| 守卫 | 为什么必须放弃 |
|---|---|
| 问题带分组或排序意图(per / each / 按…分 / 每个 / 各地区 / top N / desc) | 形状约束已经排除了带 GROUP/ORDER 的模板,任何模板都表达不了这个语义;命中只会静默丢掉分组/排序,产出一个错误的全局聚合 |
| 问题带年份或日期字面量,而模板 SQL 没有 WHERE | 无过滤的模板会把时间条件整个丢掉(「1996 年发放的贷款平均金额」不该命中「AVG(amount) FROM loan」);date_range 家族自会接手这类问题 |
它是零 LLM 零网络的纯函数,所以能在毫秒级给出答案,而且答案天然可解释—— 命中了哪条模板,是查得出来的。
fast_match.py:444 make_fast_match fast_match.py:143 template_sql_shape_ok graphs.py:174 KB_EXACT_OVERLAP
模板快径只用 kind='template' 的条目——这是 KB 防作弊约束的直接体现:
模板是 /kb init 的确定性产物,人往里塞的东西不会成为快径依据。
知识库:五个文件,三种角色
KB 不是一堆配置,是按「谁写、谁能改」分工的五个文件:
| 文件 | 内容 | 谁来写 |
|---|---|---|
| schema_notes.yml | 表/列的语义注释 | /kb init 起草,人可改 |
| semantics.yml | 语义模型(数据集 + 指标) | /kb init 起草,人可改 |
| examples.yml | 问答对与模板 | /kb init + /kb learn,人审 |
| rules.yml | 生成时必须遵守的规则文本 | 纯人工,没有写入器 |
| lessons.yml | 教训(失败模式与规避) | 蒸馏路径懒创建,两档确认 |
service.py:3 human-editable init_pipeline.py:438 INIT_FILES
两个值得记住的细节。其一:/kb init 只产出三个文件
(schema_notes.yml、semantics.yml、examples.yml),
lessons.yml 是后续反馈路径首次写入时才创建的——刚初始化完的 KB 里没有它,是正常的。
其二:rules.yml 是唯一没有程序化写入器的文件,它只能由人编写,
这意味着它承载的是组织口径而不是系统推断。
同一目录下还住着两类文件,它们不算上面这五份:decisions.yml
(判定规则,纯人工编写、没有生成器,只有编辑器的写回,见
判定规则)与
semantic_drafts.yml(硬 MISS 反问产出的待确认扩展草稿,
确认后才并入 semantics.yml)。
把它们并进「知识」那一栏会混淆「已确认的事实」与「待确认的变更」。
manage.py:1129 create_draft decisions.py:648 put_decisions
人工确认门禁:pending 不进检索
凡是系统自动生成的内容——好评问答、修正记录、蒸馏教训、自动回写的样例、LLM 起草的技能草稿——
统一以 pending 落盘。读侧有明确的过滤器:pending 的示例与未确认的教训,
根本不参与检索,等于不存在;技能同理——未确认的草稿既不进节点提示词,
也不进按需加载工具(下一节展开)。
service.py:617 pending 草稿不参与检索 service.py:1735 confirmed service.py:1980 pending
「自动回写 → 自动注入」是个很诱人的闭环,但它会让 KB 被自己的输出污染:一次错误的修正变成教训, 下次生成照着这条教训走,再产出一个错误答案、再蒸馏出一条教训。几轮之后没人说得清 KB 里哪些是真的。
所以确认门禁不是流程负担,是防污染的必要代价。记忆子系统里唯一豁免的是「情节记忆」—— 它记录的是「当时发生了什么」这类客观事实,不是可复用的知识。
技能:四档投放,两道断言闸
跨数据源的方法论——组织怎么拆比率、哪类题先查什么——以 Skills 的形式沉淀, 与数据源事实分开:事实进 KB,方法论进 SKILL.md。技能同样是自动内容, 走上面那道确认门;确认之后落进四档之一——前两档是给模型读的,后两档不是:
| 档 | 给谁用 | 怎么落地 |
|---|---|---|
| required | 模型 | 正文全文注入匹配节点的系统提示 |
| available | 模型 | 只把描述广告出去,正文由 load_skill 工具按需取 |
| validator | 引擎 | 结果域的断言清单——在 validate 节点拦在交付前 |
| guard | 引擎 | SQL 域的断言清单——在 execute_sql 节点拦在执行前 |
后两档的正文从不进模型,它们是给引擎执行的声明式断言。以 guard 为例:
管理员把「SQL 不许长什么样」写成闭集变量上的条件(select_star、
join_count、has_time_predicate 等九个 0/1 或计数变量,
域外标识符在写入时就报错),在授权门之后、执行之前零 LLM 判定——明确违反即拦,
按声明的目标回滚到生成侧收窄;判不了绝不拦:缺失基线或类型不匹配记为
「判不了」,不会把否定式断言翻成假阳性。这与判定规则是同一套三值纪律——
把「不知道」与「否」分开。
治理动作也是颗粒的:一条已确认的技能可以单独停用(status: disabled),
停用是暂停投递,不是撤回确认,恢复走「启用」;停用后四个消费面同步断开——
节点提示词注入、load_skill 的广告与按名直取、validator / guard 的执行、
信封枚举与试跑语料。
资产装上之前还有一道关:先在本机语料上做装前试跑(装 / 不装双态消融)
与影响面回放(逐题四桶差分)——零 LLM,报告直喂回归门。
service.py:96 _TIERS guards.py:55 GUARD_VARIABLES execute_sql.py:62 ORG_GUARD_TAG service.py:731 disable impact.py:881 run_impact
四档各自的写法、guard / validator 的完整变量表与判定例,见 Skills;信封、试跑与影响面的治理流程见 扩展与治理。