这是园区数字化落地系列的技术第六篇。前五篇分别讲四层架构与数据建模、四端协同的事件驱动架构、指标口径中心、读时建模与多源异构接入、NL2SQL 的落地链路。第五篇把一句人话怎么变成 SQL 讲完了,这一篇接着往后看,SQL 跑完之后,平台怎么把结果变成业务方敢拍板的东西。
第五篇末尾那张踩坑表里有一行,叫「结果不可核对」,写的是只给一个数,业务方问哪来的答不上。这一行不是随手写的。我们上线头两个月,被老板问住的次数,八成出在「数给你了,但你说不清它怎么来的」。本篇就拆这件事,看平台怎么把一次查询变成四样东西,让每条回答都带证据链。
一、只给结论的产品为什么没人敢用
问数平台最容易做成黑盒。用户问一句,屏幕弹一个数,完事。这个数对不对,全凭信任。
园区里信不过黑盒。财务拿到「本月收缴率 92%」,第一反应不是高兴,是问「按哪个口径,含不含历史欠费」。运营拿到「空置房源 37 间」,会去翻房源表核对楼栋分布。只要答不上来源,这个数就不算数,还不如当初让人自己查。
我们后来把验收标准定死,一次问答必须同时产出四样,缺一样业务方就拒收。这一条比模型准不准还靠前。
二、一次问答的四件套产物
这四样分别是结论、图表、明细、来源。
结论是一句话的答案,比如「本月收缴率 92%,较上月升 3 个点」。它必须能被图表和明细支撑,不能单独出现。
图表是结论的可视化,折线看趋势,柱状看对比,饼图看占比。图表的数据点要能点开,不能只是装饰。
明细是支撑结论的原始行,业务方想深挖就能下载到每一笔账单、每一间房。业务方要核对原始账,就得靠它。没有明细,结论再漂亮也没法验证。
来源标注这条结论从哪个库、哪张表、哪个字段、哪个口径版本、什么时间窗口算出来的。来源说不清,前三项都站不住。
四件套里,明细和来源最容易被偷懒省掉,却恰恰是业务方最看重的。我们强制在服务端塑形,不依赖前端兜。
三、产物怎么从 SQL 长出来
第五篇的 L4 叫执行回填,本篇把它拆细。一次查询从 SQL 到四件套,走四步。
第一步,只读账号执行 SQL,拿到原始结果集。这一步不碰原业务库写权限,纯粹读。
第二步,结果塑形。把结果集按意图拆成聚合层和明细层,聚合层喂给结论和图表,明细层落一张临时导出表。
第三步,口径挂接。从口径中心拉出这条指标的定义、计算 SQL、数据时间戳、版本号,贴到来源标签上。
第四步,三级下钻装配。结论卡、图表、明细入口、来源标签组装成一个回答页,前端各取所需。
塑形这一步我们踩过坑。最初让前端自己决定哪些字段做图、哪些做表,结果不同端展示不一致,同一个数在网页和手机上长的两样。后来把塑形逻辑收归服务端,前端只负责渲染,一致性才稳住。
四、支撑四件套的数据模型
四件套不能只在内存里拼一下就返回,得落库,不然没法审计、没法回溯、没法版本化。我们建了三张表。
sql
-- 产物元数据, 每次问答的产物登记, 四件套各挂一项 CREATE TABLE answer_artifact ( answer_id VARCHAR(64) NOT NULL COMMENT '一次问答的编号', kind VARCHAR(16) NOT NULL COMMENT 'conclusion/chart/detail/source', payload MEDIUMTEXT NOT NULL COMMENT '结论文本 或 图表配置 或 明细表名 或 来源 JSON', metric_code VARCHAR(64) COMMENT '关联的指标编码', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (answer_id, kind), INDEX idx_metric (metric_code) ) COMMENT='问答产物'; -- 口径血缘, 每个指标标注取数路径与版本 CREATE TABLE metric_lineage ( metric_code VARCHAR(64) NOT NULL COMMENT '指标编码, 如 collection_rate', version VARCHAR(16) NOT NULL COMMENT '口径版本, 如 v202610', catalog_path VARCHAR(128) NOT NULL COMMENT 'catalog 路径, 如 park/room/bill', calc_sql TEXT NOT NULL COMMENT '该版本的计算 SQL', data_as_of DATETIME NOT NULL COMMENT '数据时间戳, 取数快照点', owner VARCHAR(32) NOT NULL COMMENT '口径负责人', PRIMARY KEY (metric_code, version) ) COMMENT='指标口径血缘'; -- 明细导出任务, 支撑下载的原始行 CREATE TABLE detail_export_job ( job_id VARCHAR(64) NOT NULL PRIMARY KEY, answer_id VARCHAR(64) NOT NULL COMMENT '所属问答', src_sql TEXT NOT NULL COMMENT '取明细的原始 SQL, 含权限裁剪', row_count INT COMMENT '导出行数', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待跑 1完成 2失败', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) COMMENT='明细导出任务';三张表各管一段,answer_artifact记产物,metric_lineage记口径来历,detail_export_job记明细出处。业务方质疑时,拿 answer_id 一查,四件套和来源全在。
五、口径来源怎么标注
来源标签是业务方最在意的,我们把它做成结构化字段,而不是一句话描述。每个指标在metric_lineage里登记 catalog 路径、计算 SQL、数据时间戳、版本。
查询时,平台先取指标的当前生效版本,再把血缘拼进来源标签。
sql
-- 取指标的当前生效口径, 拼进来源标签 SELECT l.catalog_path, l.calc_sql, l.data_as_of, CONCAT(l.metric_code, '@', l.version) AS lineage_tag FROM metric_lineage l WHERE l.metric_code = 'collection_rate' AND l.version = ( SELECT MAX(version) FROM metric_lineage WHERE metric_code = 'collection_rate' );来源标签返回到前端长这样,业务方一眼看到数从哪张表、哪个版本、什么时点算的。
json
{ "metric": "collection_rate", "lineage_tag": "collection_rate@v202610", "catalog_path": "park/bill/collection", "data_as_of": "2026-10-22 23:59:59", "calc": "实收金额 / 应收金额, 含本期及历史欠费" }数据时间戳这道关很关键。园区口径常按月结,十月二十二号问的数,应当快照到九月三十号的账期,而不是实时库里还在变的流水。data_as_of就是把这点钉死,避免同一个人早晚问出两个收缴率。
六、明细下钻的取数
结论和图表是聚合层,明细是原始层。下钻时不能再去重算一遍聚合 SQL,那样慢且易错。我们的做法是聚合层和明细层共用同一条带权限的底层 SQL,聚合做 GROUP BY,明细直接 SELECT 原始行。
sql
-- 底层带权限的取数 SQL, 聚合与明细共用 SELECT b.building_name, bil.tenant_id, bil.bill_no, bil.receivable, bil.received, bil.due_date FROM fact_bill bil JOIN dim_building b ON bil.building_id = b.building_id WHERE bil.period_month = '2026-09' AND bil.building_id IN ( SELECT building_id FROM user_visible_building WHERE user_id = 'u_1024' ); -- 结论与图表, 在底层结果上聚合 SELECT building_name, SUM(received) / NULLIF(SUM(receivable), 0) AS rate FROM ( /* 上面的底层 SQL */ ) t GROUP BY building_name; -- 明细, 直接取底层原始行, 落导出任务 INSERT INTO detail_export_job (job_id, answer_id, src_sql, status) VALUES ('dj_8841', 'ans_5520', '/* 底层 SQL */', 0);这样聚合和明细永远对得上,不会出现图显示 92% 但下载的明细加起来是 88% 的尴尬。明细落任务表后异步跑,前端拿 job_id 轮询,大表也不卡界面。
七、口径版本怎么治理
口径会变。九月收缴率含历史欠费,十月财务改主意要剔除非本期账单,口径就漂移了。漂移不可怕,怕的是悄悄变,昨天答的和今天答的用的不是一套算法,业务方却不知道。
我们把口径当成带版本的配置管,变更要走灰度、通知、回滚、审计四步。
yaml
metrics: - code: collection_rate versions: - tag: v202609 calc_sql: | SELECT SUM(received)/SUM(receivable) FROM fact_bill WHERE period_month='2026-09' data_as_of: "2026-09-30 23:59:59" owner: 财务-王磊 status: archived - tag: v202610 calc_sql: | SELECT SUM(received)/SUM(receivable) FROM fact_bill WHERE period_month='2026-09' AND is_current_period=1 data_as_of: "2026-10-22 23:59:59" owner: 财务-王磊 status: active rollout: gray_ratio: 0.2 # 先放 20% 查询命中新口径 notify: [财务群, 运营群] # 变更群内播报 rollback: v202609 # 出问题一键回退 audit: true # 每次口径命中留审计记录灰度让新口径先在小流量验证,通知让相关方知情,回滚保底,审计兜底。口径变更走配置,某个工程师改条 SQL 就自动生效的事不会再发生。历史回答挂的是当时的版本号,回头查九月的数,拉 v202609,不会被十月的改动污染。
八、权限与审计
可核对的前提是可查,可查的前提是权限守得住。平台用专用只读账号连库,不等于能翻任意一张表。我们在三个层面收口。
账号层,给问数平台单独开一个只读账号,权限范围由 DBA 配,原系统读写账号绝不共用。
对象层,可查对象走白名单。语义层只登记允许被问的表和字段,没登记的表,生成层根本看不到,更不会进 SQL。
审计层,每次查询落一条记录,谁问的、问了什么、跑了哪条 SQL、命中哪个可见范围、返回多少行、耗时多少,全记下来。
json
{ "account": "rd_askbot_ro", "privilege": "read_only", "visible_objects": ["dim_room", "dim_building", "fact_lease", "fact_bill"], "query_log": { "user_id": "u_1024", "question": "本月各楼栋空置房源数", "final_sql": "SELECT ... AND building_id IN (SELECT building_id FROM user_visible_building WHERE user_id='u_1024')", "row_scope": "building=1,3,7", "result_rows": 42, "elapsed_ms": 318, "asked_at": "2026-10-22 14:05:11" } }权限这道锁宁可多一道不嫌烦。我们试过只靠提示词约束模型别查敏感字段,十次里有两次还是查了。改成账号层只读、对象层白名单、召回层剔除三层同时收口,敏感字段从一开始就进不来。
九、工程踩坑表
按踩到的频率排,长这样。
| 坑 | 现象 | 解法 |
|---|---|---|
| 只给结论 | 弹一个数,业务方问来源答不上 | 强制四件套,结论图表明细来源一个不少 |
| 明细缺位 | 图显示 92%,下载明细对不上 | 聚合与明细共用同一条带权限底层 SQL |
| 口径漂移 | 同日问同指标前后两版算法 | 口径带版本号,变更走灰度回滚审计 |
| 数据时点乱 | 早晚问出两个收缴率 | 口径钉死 data_as_of 快照点 |
| 前端各做各 | 网页和手机展示不一致 | 塑形逻辑收归服务端,前端只渲染 |
| 敏感字段泄露 | 提示词约束,模型偶尔还查 | 账号只读+对象白名单+召回剔除三层收口 |
| 明细卡界面 | 大表直接查,界面转圈 | 明细落导出任务异步跑,前端轮询 |
| 来源一句话 | 来源写成自然语言,不可机读 | 来源结构化,catalog 路径+SQL+时间戳+版本 |
| 口径改动无痕 | 工程师改条 SQL 就生效 | 口径当配置管,变更群内播报留痕 |
| 历史污染 | 查九月的数被十月改动影响 | 历史回答挂当时版本号,回溯不受影响 |
十、关于本文涉及的两套系统
这套可核对工程不是凭空长出来的,它的输入和运行环境靠两套系统配合。
- 智慧园区系统,四端一体的运营平台,负责把业务在线办完、过程留痕、数据结构化沉淀。房源、租客、合同、账单都从这里出,是问数平台的唯一数据源。没有它,语义层就没有可问的对象,明细和来源也无从谈起。
- 数智安全问数平台(鲁联数智提供),在园区系统之上加一个问数入口。只读接入,不改原业务表结构,NL2SQL 把人话翻成 SQL,本篇讲的四件套塑形、口径血缘、明细下钻、版本治理,是它把数变成证据链的实打实工程。
一句话,智慧园区系统管发生,问数平台管看懂,可核对是看懂这件事能不能让人服气的那道关。