上一篇把 AI 的客户销售结果打开成了可筛选表格。下一步看起来像是继续增加一个指标,实际更容易暴露语义层的问题。
开发过程中,我们通常会给每个指标起一个唯一名称:
text
orderTotalIncludingTax
invoiceTotalIncludingTax
这样做是对的。模型、API、代码和审计都需要知道每个指标到底是什么,不能让两个不同的数字共用一个无法追踪的内部名称。
但客户不会按照 QueryModel 字段名提问。客户更可能说:
text
这个月销售额是多少?
最近卖了多少?
订单金额和开票的都看一下。
这不是客户表达得"不够技术",而是他们从自己的业务角色、页面和工作目标出发说话。财务、销售、仓库和老板都可能使用"销售额"这个词,但指向的业务事实并不一定相同。
用户问"销售额"时,系统到底应该返回订单金额,还是开票销售额?
这两个数字都来自业务库,也都能算出来,但它们不是同一个业务事实。
因此,真正要解决的不是要求客户记住唯一指标名,而是在用户表达和技术指标之间增加一层映射:
text
用户说法
→ 角色、页面、上下文和时间范围
→ 候选业务事实
→ 唯一指标名、公式、粒度和日期
→ 可解释的结果
唯一名称是给系统用的,不是给客户背的。
企业 AI Agent 不只是接一套语义层
语义层解决的是"数据是什么":
orderTotalIncludingTax代表什么。- 它来自订单还是发票。
- 使用哪个日期、粒度和计算公式。
- 哪些字段可以筛选、汇总和继续展开。
但 Agent 还要理解"谁在什么场景下问这个问题"。同一句"这个月销售额是多少",对不同提问者可能有不同的默认含义:
| 提问者 / 场景 | 更可能关心的事实 | 需要的上下文 |
|---|---|---|
| 财务人员打开开票报表 | 开票含税销售额 | 当前页面、财务口径、开票日期 |
| 销售人员查看待发货订单 | 订单含税金额 | 当前页面、订单状态、销售组织 |
| 仓库人员查看待履约任务 | 订单数量或待发货金额 | 当前任务、仓库、履约范围 |
| 管理者查看经营概览 | 经过企业确认的默认销售额,可能需要订单和开票并列 | 角色、组织、默认指标、统计期间 |
所以企业建立 AI Agent,至少要同时准备两类上下文:
text
语义上下文:指标、维度、公式、粒度、时间和可用操作
交互上下文:提问者、角色、组织、页面、当前任务、已选条件和历史对话
还要把"理解上下文"和"授权上下文"分开。用户是销售人员,可以帮助 Agent 理解"销售额"在当前销售页面里的默认含义;但能不能看到某个区域的数据,必须由业务系统或可信宿主传入的身份、组织和权限范围决定,不能让模型在自然语言里自己声明或扩大权限。
当前设计目标可以表达为一个完整闭环:
text
提问者与业务场景
→ Agent 上下文
→ 语义模型中的候选指标
→ 唯一指标、权限范围和查询计划
→ 可解释结果
只有语义层,没有提问者和场景上下文,系统最多知道"有哪些数字可以查";它还不知道当前用户想用哪个数字解决什么问题,也不知道结果应该落在哪个业务范围内。反过来,只有用户画像而没有语义层,Agent 也只能猜指标,不能稳定计算和回放。
这也是为什么"把数据库接给 Agent"与"建立企业 AI 分析能力"不是一回事:前者提供数据入口,后者还要把业务事实、用户上下文、权限边界和可验证结果接成一个闭环。
先看结果
我用同一个 WideWorldImporters OLTP 数据库,选择 2016-01-01(含)到 2016-06-01(不含)这个样例事实期,分别查询订单和发票:
| 事实口径 | 日期字段 | 明细行数 | 未税金额 | 含税金额 | 税额 | 数量 |
|---|---|---|---|---|---|---|
| 订单金额 | Sales.Orders.OrderDate |
29,902 | 23,395,792.75 | 26,848,038.92 | 3,452,246.17 | 1,291,028 |
| 开票销售额 | Sales.Invoices.InvoiceDate |
29,458 | 22,633,175.55 | 25,971,029.11 | 3,337,853.56 | 1,241,304 |
如果页面、API 和 AI 都只把这两行显示成"销售额",用户看到的就不是一个统一指标,而是两个没有解释的数字。技术内部可以使用两个唯一名称,但用户界面还必须说明它们分别代表订单还是发票。
这次的结论不是"订单金额错了"或者"发票销售额才是真实销售额"。结论是:这两个指标都合法,但必须在模型和入口中明确区分。
这次只接了哪些事实
订单口径来自:
Sales.Orders:订单头和OrderDate。Sales.OrderLines:数量、单价和税率。
开票口径来自:
Sales.Invoices:发票头和InvoiceDate。Sales.InvoiceLines:数量、单价、税额、含税金额和利润。
两个口径的日期也不同:订单按下单日期分组,发票按开票日期分组。即使筛选条件都是"2016 年 1 月到 5 月",它们筛选的业务事件也不同。
订单含税金额不是一条简单的乘法
发票明细已经有 ExtendedPrice,可以直接作为含税金额;订单明细没有一个与之等价的已存储总额,所以本次订单 QM 按订单行计算:
text
订单未税行金额 = 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-01 | 5,293,047.93 | 5,103,948.25 | 189,099.68 |
| 2016-02 | 4,704,477.81 | 4,596,534.78 | 107,943.03 |
| 2016-03 | 5,516,385.76 | 5,330,250.56 | 186,135.20 |
| 2016-04 | 5,437,764.20 | 5,236,062.81 | 201,701.39 |
| 2016-05 | 5,896,363.22 | 5,704,232.71 | 192,130.51 |
所以不能给订单金额乘一个固定比例,就把它当成开票销售额。差异可能来自订单与发票不是同一个事件、日期不同、数量不同以及业务过程中的取消、拆单、部分开票或其他状态;这批样例期没有贷项发票行,只能说明本次数据没有提供该种样本,不能说明模型已经覆盖所有企业流程。
客户说"销售额"时,系统要做的是翻译
不能把技术侧的唯一命名直接变成用户侧的提问要求。更合理的做法是保留唯一的内部指标名,同时为用户表达建立别名、提问者上下文和澄清规则。
| 用户表达 | 可用上下文 | 内部落点 | 系统行为 |
|---|---|---|---|
| 订单金额、下单金额 | 订单列表、待发货场景 | wwi_oltp_order_lines.orderTotalIncludingTax + orderDate |
直接回答,并显示"订单含税金额" |
| 开票销售额、发票销售额 | 发票、财务或开票报表 | wwi_oltp_invoice_lines.totalIncludingTax + invoiceDate |
直接回答,并显示"开票含税销售额" |
| 这个月销售额 | 当前页面、用户角色、组织、企业默认口径 | 由上下文选择订单或发票指标 | 回答时说明采用的口径,并提供切换入口 |
| 订单和开票的都看一下 | 用户明确要求两个事实 | 两个 QM 并列查询 | 并列展示,不能把两个金额直接相加 |
| 没有上下文的"销售额" | 无法判断业务事实 | 候选指标集合 | 采用已确认默认值,或先追问,不能悄悄猜测 |
这里的"追问"也不是唯一方案。如果企业已经确认财务页面的默认销售额就是开票含税销售额,系统可以直接回答,但必须把解释带出来:
text
这里按开票日期统计含税销售额,不是订单含税金额。
如果你要看下单口径,我也可以切换到订单含税金额。
反过来,如果用户问"接下来要发货的订单金额",页面和动作上下文已经提供了足够信息,系统就不应该因为默认销售模型叫 sales 而把问题路由到发票模型。
这也是"两个都需要"和"两个指标相加"的区别:前者是并列查看两个业务事实,后者会制造一个没有业务定义的新数字。
统一语义不是让所有数字相等
这次没有把订单和发票合并成一个叫 sales 的大模型,也没有为了让两个结果相等而修改历史数据。
统一语义真正要做的是把下面几件事固定下来,同时让用户不必先掌握这些技术术语:
- 事实是什么:订单还是发票。
- 时间是什么:下单日期还是开票日期。
- 粒度是什么:订单行还是发票行。
- 金额怎么算:来源字段、税额和舍入发生在哪里。
- 技术名称是什么:例如
orderTotalIncludingTax和invoiceTotalIncludingTax。 - 用户怎么说:别名、自然语言问法、角色、组织、页面和当前任务上下文。
- 权限范围是什么:由可信宿主传入的身份、组织和数据范围,不由模型从问题中猜测。
- 谁来确认:企业业务负责人,而不是模型根据字段名称临时决定。
这样,页面、查询 API 和 AI 才能使用同一套已确认的定义。定义不同的指标可以同时存在,但不能被包装成同一个答案。
这次还没有宣称什么
- 当前订单 QM 是为了证明双口径的最小模型,不是完整订单分析产品。
- Runtime 0.1.15 对订单计算聚合列仍未回显完整的
dataType元数据;数值查询已通过,但这部分不能写成 Viewer 类型兼容已经完成。 - 当前验证使用本地开发/测试 Runtime 和只读账号,不代表生产认证、RBAC、审计和并发能力已经完成。
- 本文重点验证模型、公式和查询口径;自然语言到两个模型的路由、提问者上下文注入和业务页面仍需要结合企业真实场景继续验证,不能把"AI 已经能从一句销售额稳定选对模型"写成完成能力。
你们系统里的"销售额"通常由哪个角色、哪个页面和哪个业务动作来定义?如果只把唯一名称写在模型里,却没有建立用户表达与指标之间的映射,AI 只会更快地返回一个技术上合法、但未必符合使用者意图的数字。