副标题:收到一句“这个结果不对”,先把原问题、指标、权限和 SQL 现场找回来
前文:业务系统有表格,BI 有看板,AI 问数还需要做什么?
AI 问数一旦接到业务里,迟早会遇到一句话:
这个结果不对。
很多团队的第一反应,是改提示词,或者再给模型补一段“销售额必须使用某某字段”的说明。
但结果“不对”,不一定是模型没理解。用户可能想看订单金额,系统返回的却是开票金额;权限条件可能已经生效,用户只能看到自己有权访问的客户;也可能只是数据还没刷新,或者明细和汇总根本不在同一个粒度上。
没有先复现原问题就改提示词,下一次即使数字变了,我们也不知道改对了什么,更不知道其他用户会不会跟着变。
所以我遇到这类反馈时,第一步不是继续调提示词,而是把这次结果是怎么来的逐层捋一遍:
用户表达与上下文
→ 指标和业务口径
→ QM / TM 模型
→ DSL 查询条件
→ 权限与数据范围
→ 物理来源与查询执行
→ 结果汇总与页面展示
先别急着给提示词加戏
这里不讨论“AI 会不会答错”这种大问题。我更关心的是,业务说“结果不对”以后,团队能不能把当时那次查询找回来,知道该改指标、权限、数据还是页面。
我通常会把下面几样东西留下来:
- 原问题和提问者上下文,之后还能复现;
- 这次用的指标、模型、条件和权限范围,别人能核对;
- 能分清是业务口径、数据、权限、查询还是展示出了问题;
- 改完以后有几条反例可以重跑,知道有没有误伤其他场景。
这和“把提示词改得更长”不是一回事。提示词能帮模型理解用户在说什么,却不能替代指标定义、权限执行、数据质量检查和查询引擎。
我会先这样跟 Codex 说
实际排查时,我不会一开始就写很长的故障单,也不会直接让 Codex 改模型。先把想查什么、暂时不能改什么说清楚就够了:
这个销售额结果看起来不对,先别改提示词。
帮我把这次查询用的模型、DSL、权限范围和 SQL 找出来,
再和基准结果比一下,看看问题到底出在哪。
如果它继续追问,我再补充:
先只复现和定位,不改模型、不改数据。
能拿到的证据都列出来,拿不到历史执行轨迹就明确说没有。
我希望它先找证据,不是先猜原因。原始 DSL、身份上下文或模型版本缺了,就把缺口写出来,不能拿一次新查询冒充刚才那次结果。接入真实用户时,也只传受控的授权上下文,不要把 Token 或 Cookie 贴进对话。
同一句“销售额不对”,背后可能不是同一种问题
我会先做一个粗分:
| 表面现象 | 先检查什么 | 更可能由谁处理 |
|---|---|---|
| 用户说“销售额”,结果和预期不一样 | 订单、发票、回款等指标口径 | 业务负责人和指标建模人员 |
| 同一问题换一种问法,结果发生变化 | 用户表达、别名和上下文映射 | Agent 路由和语义描述 |
| 同一个问题,不同用户结果不同 | Authorization、组织和数据范围 | 权限与宿主系统 |
| 明细加总和汇总金额对不上 | 数据粒度、JOIN、重复聚合 | TM / QM、DSL 和数据实施 |
| 生成 SQL 与当前模型定义一致,但数据仍然不符合预期 | 来源表、状态、刷新时间和快照 | 数据实施和业务负责人 |
| 重新查询后金额改变 | 模型版本、数据变化或时间窗口 | 版本、数据和查询记录 |
| 接口结果正确,页面数字不一样 | 舍入、分页、前端汇总和格式化 | 前端组件和展示层 |
先把问题放对位置,后面的修复动作才不会跑偏。
先从最容易混淆的“销售额”说起
前文已经提过,业务口中的“销售额”,技术上可能对应不同的指标:
orderTotalIncludingTax
invoiceTotalIncludingTax
在当前 WWI 样例基线中,统计 2016-01-01(含)至 2016-06-01(不含)的数据,订单和发票本身就不是同一个数字:
| 事实 | 含税金额 | 明细行数 | 数量 |
|---|---|---|---|
| 订单 | 26,848,038.92 | 29,902 | 1,291,028 |
| 发票 | 25,971,029.11 | 29,458 | 1,241,304 |
如果用户想要订单含税金额,AI 按当前页面默认口径回答了开票含税金额,那么计算本身可能没错,错的是口径。
这时要改的是指标映射或上下文,不是再告诉模型一句“销售额算准一点”。我会继续确认:
- 当前用户属于财务、销售还是运营角色;
- 用户所在页面是订单、待发货还是发票页面;
- 企业是否已经配置默认口径;
- 这次问题是要看订单和开票两个指标,还是只要其中一个。
如果两个口径都合法,就并列展示并写清名称,不要让模型把两个数字相加,凭空造出一个没有业务定义的新“销售额”。
先把当时的查询现场留下来
只记一句“用户觉得不对”不够。能拿到的现场信息,最好都留下引用或摘要:
{
"question": "昨天的销售额是多少?",
"actorContext": {
"user": "<opaque-user>",
"organization": "<opaque-organization>",
"page": "invoice-list",
"selectedFilters": ["<redacted>"]
},
"model": "wwi_oltp_invoice_lines",
"payload": "<normalized-query-payload>",
"permissionContextRef": "<controlled-audit-reference>",
"permissionScopeSummary": "<redacted-policy-scope>",
"modelRevision": "<revision>",
"dataCutoff": "<observed-cutoff>",
"resultReference": "<result-reference>"
}
这里保存的是复现所需的引用和摘要,不是把 Cookie、Authorization 或客户明细直接写进日志。权限部分只记录受控的内部审计引用或脱敏后的权限决策摘要,不保存 Token、Authorization,也不保存由凭证派生的指纹;查询参数和业务数据同样按现有脱敏规则处理。
如果当时没有保存模型版本、权限摘要或结果引用,就明确写“缺少什么”。事后重新查一次,只能说明现在查出来什么,不能把它说成当时的真实结果。
我会按这几层查
1. 用户到底问了什么
先看原始问法和上下文,而不是只看模型最后整理出的结构化问题。
用户:昨天的销售额是多少?
这句话还缺几个关键信息:
- 销售额是订单、发票还是回款;
- “昨天”使用哪个时区和哪个业务日期;
- 用户要看总额、客户排行还是单据明细;
- 当前页面或历史对话是否已经提供了默认口径。
如果页面上下文已经确认了发票口径,可以直接回答,同时把口径写出来;如果上下文不足,就先澄清,不要让模型自己挑一个字段。
2. QM 和指标有没有选错
在 WWI 发票明细模型里,链路可以直接写出来:
QM: wwi_oltp_invoice_lines
→ measure: totalIncludingTax
→ date: invoiceDate
→ aggregation: SUM
如果用户实际要的是订单事实,却被映射到这个发票 QM,问题在指标选择;如果用户已经明确说“开票含税销售额”,但模型却选择了订单 QM,问题在自然语言路由或模型边界。
只看最后的数字,很难判断到底选了哪个指标,还是得回到模型定义。dataset.explain_query 的 DEFINITION 模式适合回答:
这个字段是什么?
它属于哪个 QM / TM?
默认日期和聚合方式是什么?
物理来源有没有明确声明?
DEFINITION 读取当前定义,不编译查询,也不返回业务结果。
3. DSL 有没有把范围改掉
指标选对了,这次查询仍可能有问题。把本次 DSL 展开,看具体条件:
{
"columns": [
"invoiceDate$calendarMonthLabel",
"sum(totalIncludingTax) as totalIncludingTax"
],
"groupBy": [
"invoiceDate$calendarMonthLabel"
],
"slice": [
{
"field": "invoiceDate$id",
"op": "[)",
"value": ["2016-01-01", "2016-06-01"]
}
]
}
我会重点看:
- 日期字段是否是发票日期,而不是订单日期;
- 时间范围是否为左闭右开,是否多算或少算了一天;
- 分组粒度是否符合用户要求;
- 是否存在隐藏的状态条件或默认过滤;
- 查询是否在下钻时换成了另一张事实表。
用户追问“这次查询现在会生成什么 SQL”时,可以使用 RECOMPILED。它会按当前模型版本、当前请求和当前授权上下文重新编译,并返回规范化条件、权限来源、查询规划和 SQL 证据。
4. 权限有没有把数据范围收窄
权限问题很容易被说成“AI 少算了”。但同一个 DSL,换一个身份,本来就可能得到不同的数据范围。
例如:
用户条件:invoiceDate 在 2016-01-01 到 2016-06-01
模型权限:customer.salesTerritory = Southeast
解释结果里,最好把两者标成不同来源:
USER_SLICE
MODEL_PERMISSION
用户输入的筛选条件可以由用户修改,模型权限条件则由可信授权上下文决定。不能让用户在自然语言里说“我是管理员”,也不能因为一次回答结果看起来少,就直接移除权限条件。
以 Southeast 区域经理为例,解释结果里的逻辑 SQL 可以表示为:
-- 为了聚焦权限,下面只展示合计,省略原 DSL 的月份分组
SELECT SUM(total_including_tax) AS total_amount
FROM wwi_oltp_invoice_lines
WHERE invoice_date >= '2016-01-01'
AND invoice_date < '2016-06-01'
AND sales_territory = 'Southeast' -- MODEL_PERMISSION
这里的 sales_territory 条件不是用户在 DSL 中填写的筛选,而是由可信身份上下文注入的模型权限。总部身份不会增加这条区域条件;跨区域支持人员则会得到 IN (...)。上面是便于阅读的逻辑 SQL,真实 Runtime 可能会展开客户维度关联并使用实际表别名,不能把示例文本当成完整的物理执行 SQL。
如果只有某个用户或某个组织遇到问题,我会先查身份、组织、授权和权限模型,而不是先调提示词。
5. 来源、状态和刷新时间有没有对齐
在当前 WWI 发票模型里,totalIncludingTax 可以继续往下追:
QM.totalIncludingTax
→ TM.totalIncludingTax
→ SUM(total_including_tax)
→ Sales.InvoiceLines.ExtendedPrice
QM 到 TM 的关系来自模型结构,聚合方式来自度量定义,物理来源则由 viewSql 和 sourceRefs 等声明说明。
字段选对了也不代表结果一定对。来源数据可能还没刷新,取消、退货、贷项的状态条件也可能没对齐;这种情况下,生成 SQL 和当前模型定义一致,结果仍然可能和业务预期不同。
这时还要把下面几件事对上:
- 数据统计截止时间;
- 来源表或同步表的刷新时间;
- 取消、退货、贷项和草稿是否排除;
- OLTP 明细、汇总表和 BI 预聚合的粒度;
- 业务人员认可的核对基线。
6. JOIN、聚合和执行路径有没有把行数放大
明细加总和汇总对不上时,常见原因不是 LLM,而是中途把粒度弄变了:
- 发票行与发票头关联后,一对多关系被重复展开;
- 先聚合再关联和先关联再聚合产生了不同结果;
- 客户、商品或区域维度的 JOIN 条件不完整;
- 预聚合和原始表使用了不同的过滤边界。
这时要检查 TM / QM 的关联定义、DSL 的分组和过滤,以及查询引擎最后采用的规划。
如果只是使用 RECOMPILED,得到的是当前状态下的重新编译结果。它可以帮助定位当前模型会如何生成 SQL,但不能证明刚才那次请求当时实际走过了哪条 JDBC 或缓存路径。
7. 页面有没有把结果改掉
有时接口结果没问题,页面展示出来却不一样:
- 前端把每页小计当成了总计;
- 金额在前端再次四舍五入;
- 分组表格隐藏了部分行;
- 图表使用了另一组筛选条件;
- BI 看板的刷新时间早于业务系统。
所以“生成 SQL 和当前模型定义一致”只说明了查询链路的一部分。错误记录还要带上页面筛选、请求参数、返回摘要和展示规则,免得把展示问题当成数据问题。
什么时候让 Codex 展开解释
普通问数没必要每次都输出 QM、TM 和 SQL。用户问“上个月哪个客户销售额最高”时,先给结果;用户追问“为什么是这个数”时,再把证据展开。
前文《当 AI 输出销售额时,如何让它解释这个数字是怎么算出来的?》已经讲过 QM、TM、过滤条件和 SQL 的逐层解释。放到错误排查里,更重要的是把调用边界说清楚:
大致是这样:
普通问题
→ query execute
→ 返回业务结果
解释性追问
→ dataset.explain_query
→ 返回生成路径和解释证据
要复核当前请求时,可以从 CLI 调用,也可以在 MCP 场景调用同一个 dataset.explain_query:
foggy-runtime \
--base-url http://127.0.0.1:18066 \
--namespace wwi-oltp-sales \
query explain wwi_oltp_invoice_lines \
--payload <query-payload.json> \
--depth detailed \
--include-sql \
--include-physical-names
带上 payload 后会进入 RECOMPILED 模式:按当前模型 revision、当前请求和当前授权上下文,重新推导一遍查询路径。它不改已有的 query execute、models describe 工具协议,也不返回业务结果行,更不能拿来绕过模型权限、namespace 隔离和查询限制。
排查时要分清三类证据:
| 能力 | 可以说明什么 | 不能说明什么 |
|---|---|---|
DEFINITION | 当前字段、指标、QM → TM 与声明的物理来源 | 不编译本次查询,不返回业务结果 |
RECOMPILED | 当前 DSL、权限来源、查询规划和当前会生成的 SQL | 不能证明历史请求实际执行了这条路径 |
EXECUTED_TRACE | 历史请求当时使用的模型、权限、SQL、缓存或 JDBC 路径 | 当前版本暂不支持 |
所以当前 RECOMPILED 响应会带上这个限制:
RECOMPILED_NOT_EXECUTED_TRACE
用户问“刚才返回的结果为什么是这个数”时,如果系统没保存当时的执行 trace,只能这样说:
我可以按当前模型和权限重新解释这条查询,
但不能证明它就是刚才那次请求实际执行过的全部过程。
解释工具给的是当前定义和重新编译证据;历史运行轨迹是另一种能力,不能混为一谈。
回放结束,给业务方看什么
不要把一大段 SQL 原样甩给业务方。把每一层的判断写成能复核的结论就够了。下面是一份简化示例,展示的是写法,不是系统规定的固定格式:
| 检查项 | 回放结果 | 初步判断 |
|---|---|---|
| 原始问题 | 2016 年前 5 个月的含税销售额 | 需要确认是订单还是发票 |
| QM / 指标 | wwi_oltp_invoice_lines / totalIncludingTax | 当前选择的是发票明细口径 |
| DSL 日期 | invoiceDate$id,[2016-01-01, 2016-06-01) | 左闭右开,边界明确 |
| 授权范围 | 区域经理:Southeast | 不是全公司的数据范围 |
| SQL 条件 | sales_territory = 'Southeast' | 由 MODEL_PERMISSION 注入 |
| 数据截止 | 2016-05-31 | 没有覆盖截止日之后的数据 |
如果用户拿全公司的财务报表来对比,而这次请求来自 Southeast 区域经理,“数字少了”首先说明权限范围不同;如果用户明确要看全公司,却被路由到了发票指标,才是指标或上下文问题。前面这些都对上了,再去查查询执行或底层数据。
查到原因以后,改哪里
原因找到了,修复位置也要跟着变:
| 根因 | 正确的修复位置 | 不应该怎么做 |
|---|---|---|
| 用户表达不清 | 澄清规则、别名、上下文映射 | 让模型随机猜默认指标 |
| 指标口径错误 | 业务指标决策、QM / TM 定义 | 只在提示词里写一遍公式 |
| DSL 边界错误 | 查询模型和 DSL 生成 / 校验 | 直接拼接一条特殊 SQL |
| 权限范围错误 | 可信身份、组织和权限规则 | 允许用户用自然语言扩大范围 |
| 数据来源或刷新错误 | 数据实施、同步链路和状态规则 | 修改 AI 回答掩盖数据问题 |
| JOIN / 聚合错误 | TM、QM、查询规划和回归基线 | 让模型在结果上“修正数字” |
| 页面展示错误 | 前端组件、分页和格式化逻辑 | 把 UI 差异归因到底层数据 |
提示词负责帮助模型理解用户表达、选择调用路径;权限执行、金额修正、数据清洗和历史结果还原,不该塞进提示词里。
改完别只再问一遍原问题
修完以后,只重跑原问题还不够。我会补几条反例:
1. 用户明确问订单含税销售额时,是否不会落到发票指标?
2. 用户明确问开票含税销售额时,日期是否使用 invoiceDate?
3. 两个不同授权上下文是否仍然只能看到各自范围?
4. 诊断记录是否只保存权限决策摘要或受控审计引用,而不保存 Authorization、Token 及其派生指纹?
5. 左闭右开的日期范围是否不会多算边界日期?
6. 客户、商品和明细三种粒度回加后是否保持一致?
7. 数据未刷新或状态不完整时,回答是否提示统计截止时间?
8. 用户追问“这个数怎么算”时,是否进入 explain,而不是修改普通查询协议?
如果问题来自 Agent 的周期任务,还要保存任务定义版本、统计窗口、指标口径、接收人,以及受控的权限审计引用或权限决策摘要。任务不能每天重新猜一遍“昨天的销售额”是什么意思,也不能把调用凭证写进任务记录。
写在最后
AI 查错以后,最容易走弯路的动作,是还没弄清问题在哪一层,就先去改提示词。
实际顺序并不复杂:先把现场留下来,再重跑原查询;然后分清口径、权限、数据、模型、执行和展示;最后把修复放回对应的责任层,用几条反例重新跑一遍。
语义层不只是帮 AI 找字段,也要让团队看得出这个结果为什么可以这样算;查询解释工具也不只是吐一段 SQL,而是把 QM、TM、权限、规划和物理来源整理成能核对的证据。
证据也有边界:当前 RECOMPILED 只能说明“现在会怎么生成”;没有 EXECUTED_TRACE,就不能说自己还原了刚才真实执行的全部过程。
遇到“结果不对”时,你会先改提示词,还是先把指标、权限和 SQL 现场翻出来?