AI 查错了,先别急着改提示词

17 阅读1分钟

副标题:收到一句“这个结果不对”,先把原问题、指标、权限和 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.9229,9021,291,028
发票25,971,029.1129,4581,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 现场翻出来?