客户说“销售额”,系统怎样落到订单含税金额或开票含税销售额?

0 阅读1分钟

上一篇把 AI 的客户销售结果打开成了可筛选表格。下一步看起来像是继续增加一个指标,实际更容易暴露语义层的问题。

开发过程中,我们通常会给每个指标起一个唯一名称:

orderTotalIncludingTax
invoiceTotalIncludingTax

这样做是对的。模型、API、代码和审计都需要知道每个指标到底是什么,不能让两个不同的数字共用一个无法追踪的内部名称。

但客户不会按照 QueryModel 字段名提问。客户更可能说:

这个月销售额是多少?
最近卖了多少?
订单金额和开票的都看一下。

这不是客户表达得“不够技术”,而是他们从自己的业务角色、页面和工作目标出发说话。财务、销售、仓库和老板都可能使用“销售额”这个词,但指向的业务事实并不一定相同。

用户问“销售额”时,系统到底应该返回订单金额,还是开票销售额?

这两个数字都来自业务库,也都能算出来,但它们不是同一个业务事实。

因此,真正要解决的不是要求客户记住唯一指标名,而是在用户表达和技术指标之间增加一层映射:

用户说法
  → 角色、页面、上下文和时间范围
  → 候选业务事实
  → 唯一指标名、公式、粒度和日期
  → 可解释的结果

唯一名称是给系统用的,不是给客户背的。

企业 AI Agent 不只是接一套语义层

语义层解决的是“数据是什么”:

  • orderTotalIncludingTax 代表什么。
  • 它来自订单还是发票。
  • 使用哪个日期、粒度和计算公式。
  • 哪些字段可以筛选、汇总和继续展开。

但 Agent 还要理解“谁在什么场景下问这个问题”。同一句“这个月销售额是多少”,对不同提问者可能有不同的默认含义:

提问者 / 场景更可能关心的事实需要的上下文
财务人员打开开票报表开票含税销售额当前页面、财务口径、开票日期
销售人员查看待发货订单订单含税金额当前页面、订单状态、销售组织
仓库人员查看待履约任务订单数量或待发货金额当前任务、仓库、履约范围
管理者查看经营概览经过企业确认的默认销售额,可能需要订单和开票并列角色、组织、默认指标、统计期间

所以企业建立 AI Agent,至少要同时准备两类上下文:

语义上下文:指标、维度、公式、粒度、时间和可用操作
交互上下文:提问者、角色、组织、页面、当前任务、已选条件和历史对话

还要把“理解上下文”和“授权上下文”分开。用户是销售人员,可以帮助 Agent 理解“销售额”在当前销售页面里的默认含义;但能不能看到某个区域的数据,必须由业务系统或可信宿主传入的身份、组织和权限范围决定,不能让模型在自然语言里自己声明或扩大权限。

当前设计目标可以表达为一个完整闭环:

提问者与业务场景
  → Agent 上下文
  → 语义模型中的候选指标
  → 唯一指标、权限范围和查询计划
  → 可解释结果

只有语义层,没有提问者和场景上下文,系统最多知道“有哪些数字可以查”;它还不知道当前用户想用哪个数字解决什么问题,也不知道结果应该落在哪个业务范围内。反过来,只有用户画像而没有语义层,Agent 也只能猜指标,不能稳定计算和回放。

这也是为什么“把数据库接给 Agent”与“建立企业 AI 分析能力”不是一回事:前者提供数据入口,后者还要把业务事实、用户上下文、权限边界和可验证结果接成一个闭环。

先看结果

我用同一个 WideWorldImporters OLTP 数据库,选择 2016-01-01(含)到 2016-06-01(不含)这个样例事实期,分别查询订单和发票:

事实口径日期字段明细行数未税金额含税金额税额数量
订单金额Sales.Orders.OrderDate29,90223,395,792.7526,848,038.923,452,246.171,291,028
开票销售额Sales.Invoices.InvoiceDate29,45822,633,175.5525,971,029.113,337,853.561,241,304

如果页面、API 和 AI 都只把这两行显示成“销售额”,用户看到的就不是一个统一指标,而是两个没有解释的数字。技术内部可以使用两个唯一名称,但用户界面还必须说明它们分别代表订单还是发票。

这次的结论不是“订单金额错了”或者“发票销售额才是真实销售额”。结论是:这两个指标都合法,但必须在模型和入口中明确区分。

这次只接了哪些事实

订单口径来自:

  • Sales.Orders:订单头和 OrderDate
  • Sales.OrderLines:数量、单价和税率。

开票口径来自:

  • Sales.Invoices:发票头和 InvoiceDate
  • Sales.InvoiceLines:数量、单价、税额、含税金额和利润。

两个口径的日期也不同:订单按下单日期分组,发票按开票日期分组。即使筛选条件都是“2016 年 1 月到 5 月”,它们筛选的业务事件也不同。

订单含税金额不是一条简单的乘法

发票明细已经有 ExtendedPrice,可以直接作为含税金额;订单明细没有一个与之等价的已存储总额,所以本次订单 QM 按订单行计算:

订单未税行金额 = ROUND(Quantity × UnitPrice, 2)
订单税额       = ROUND(Quantity × UnitPrice × TaxRate / 100, 2)
订单含税行金额 = ROUND(Quantity × UnitPrice × (1 + TaxRate / 100), 2)

逐行舍入后再汇总,订单含税金额是 26,848,038.92

如果先把所有订单行的未舍入金额汇总,再统一计算税额,会得到 26,848,035.9025,少了 3.0175。金额精度不是最后展示时再处理的格式问题,而是指标定义的一部分。

两个 QueryModel,两个业务事实

本次增加了一个最小的 wwi_oltp_order_lines QueryModel,现有 wwi_oltp_invoice_lines 继续作为默认开票销售模型。

订单模型明确了:

  • 默认日期是 orderDate
  • 默认金额是逐行舍入后的 orderTotalIncludingTax
  • 它只用于订单金额问题和与发票销售额的显式对照。

发票模型明确了:

  • 默认日期是 invoiceDate
  • 默认销售额是 totalIncludingTax,来源为 Sales.InvoiceLines.ExtendedPrice
  • 它还保留未税金额、税额、利润和明细行金额。

两个模型可以使用相同的 Runtime Query API。对系统内部来说,模型和指标名称必须唯一;对用户入口来说,则要允许多个自然语言说法映射到同一个指标,也要允许同一个词在不同上下文中映射到不同指标。

月度差异也不是固定比例

把两个 QM 按各自日期字段分组,可以看到每个月的差异都不一样:

月份订单含税金额开票销售额订单 - 开票
2016-015,293,047.935,103,948.25189,099.68
2016-024,704,477.814,596,534.78107,943.03
2016-035,516,385.765,330,250.56186,135.20
2016-045,437,764.205,236,062.81201,701.39
2016-055,896,363.225,704,232.71192,130.51

所以不能给订单金额乘一个固定比例,就把它当成开票销售额。差异可能来自订单与发票不是同一个事件、日期不同、数量不同以及业务过程中的取消、拆单、部分开票或其他状态;这批样例期没有贷项发票行,只能说明本次数据没有提供该种样本,不能说明模型已经覆盖所有企业流程。

客户说“销售额”时,系统要做的是翻译

不能把技术侧的唯一命名直接变成用户侧的提问要求。更合理的做法是保留唯一的内部指标名,同时为用户表达建立别名、提问者上下文和澄清规则。

用户表达可用上下文内部落点系统行为
订单金额、下单金额订单列表、待发货场景wwi_oltp_order_lines.orderTotalIncludingTax + orderDate直接回答,并显示“订单含税金额”
开票销售额、发票销售额发票、财务或开票报表wwi_oltp_invoice_lines.totalIncludingTax + invoiceDate直接回答,并显示“开票含税销售额”
这个月销售额当前页面、用户角色、组织、企业默认口径由上下文选择订单或发票指标回答时说明采用的口径,并提供切换入口
订单和开票的都看一下用户明确要求两个事实两个 QM 并列查询并列展示,不能把两个金额直接相加
没有上下文的“销售额”无法判断业务事实候选指标集合采用已确认默认值,或先追问,不能悄悄猜测

这里的“追问”也不是唯一方案。如果企业已经确认财务页面的默认销售额就是开票含税销售额,系统可以直接回答,但必须把解释带出来:

这里按开票日期统计含税销售额,不是订单含税金额。
如果你要看下单口径,我也可以切换到订单含税金额。

反过来,如果用户问“接下来要发货的订单金额”,页面和动作上下文已经提供了足够信息,系统就不应该因为默认销售模型叫 sales 而把问题路由到发票模型。

这也是“两个都需要”和“两个指标相加”的区别:前者是并列查看两个业务事实,后者会制造一个没有业务定义的新数字。

统一语义不是让所有数字相等

这次没有把订单和发票合并成一个叫 sales 的大模型,也没有为了让两个结果相等而修改历史数据。

统一语义真正要做的是把下面几件事固定下来,同时让用户不必先掌握这些技术术语:

  • 事实是什么:订单还是发票。
  • 时间是什么:下单日期还是开票日期。
  • 粒度是什么:订单行还是发票行。
  • 金额怎么算:来源字段、税额和舍入发生在哪里。
  • 技术名称是什么:例如 orderTotalIncludingTaxinvoiceTotalIncludingTax
  • 用户怎么说:别名、自然语言问法、角色、组织、页面和当前任务上下文。
  • 权限范围是什么:由可信宿主传入的身份、组织和数据范围,不由模型从问题中猜测。
  • 谁来确认:企业业务负责人,而不是模型根据字段名称临时决定。

这样,页面、查询 API 和 AI 才能使用同一套已确认的定义。定义不同的指标可以同时存在,但不能被包装成同一个答案。

这次还没有宣称什么

  • 当前订单 QM 是为了证明双口径的最小模型,不是完整订单分析产品。
  • Runtime 0.1.15 对订单计算聚合列仍未回显完整的 dataType 元数据;数值查询已通过,但这部分不能写成 Viewer 类型兼容已经完成。
  • 当前验证使用本地开发/测试 Runtime 和只读账号,不代表生产认证、RBAC、审计和并发能力已经完成。
  • 本文重点验证模型、公式和查询口径;自然语言到两个模型的路由、提问者上下文注入和业务页面仍需要结合企业真实场景继续验证,不能把“AI 已经能从一句销售额稳定选对模型”写成完成能力。

你们系统里的“销售额”通常由哪个角色、哪个页面和哪个业务动作来定义?如果只把唯一名称写在模型里,却没有建立用户表达与指标之间的映射,AI 只会更快地返回一个技术上合法、但未必符合使用者意图的数字。