自然语言问数说起来很轻巧,落地时往往会撞上三堵墙:指标语义、用户身份、稳定分析链路。去年我们把内部“问数助手”灰度上线时,业务同学随口问了一句“本月华东区业绩怎么样”,我当时就意识到,这句话背后的每一步都不能靠模型临时发挥。业务方觉得只是一个提问,对我这边来说却要把过去几个月埋下的雷挨个拆一遍。
这三件事其实是有依赖顺序的:先明确指标语义,再叠加用户身份,最后才谈链路稳定性。语义错了,数就错了;权限漏了,数就泄露了;链路不稳,再准的数也等于没有。这篇文章想把我在一线趟出来的经验记录下来,给准备做或者正在做自然语言问数的数据平台、AI应用团队一个参考。不聊大模型原理,只讲能直接落地的工程约束。
1. 一句“本月华东区业绩怎么样”,背后要拆开的三层含义
一个看似普通的问题,放到自然语言问数系统里,至少要过三道关卡才能真正变成一条可执行的SQL。
第一关是语义意图。用户说“业绩”,他指的是订单金额、回款金额、确认收入,还是毛利?说“华东区”,是按法人组织划分的华东大区,还是按客户收货地址归属的华东地理区域?“本月”是自然月1号到今天,还是财务口径的账期月度?这些在人的日常交流里靠上下文默认补全,但在系统里没有人替你默认,必须有一套指标语义层先把概念钉死。
第二关是身份作用域。同一个指标“销售额”,集团总裁能看全国所有法人公司的汇总,华东大区总监只能看华东组织树下的数据,一线销售只能看自己名下客户的数据。自然语言问数系统同一个模型、同一套指标定义处理同一个问题,但因为请求者的用户身份不同,最后执行出来的查询必须带上不同的强制过滤条件。这一步不是“可以不做”的优化项,而是“不做就会出事”的合规底线。
第三关是分析链路的稳定性。语义懂了,身份确定了,SQL也拼好了,还要考虑执行为什么慢、结果为什么不对、高峰期为什么不响应、凌晨指标口径更新后用户的旧问题为什么还返回老答案。自然语言问数本质上是把原来分析师通过看板、报表、SQL取数的工作,压缩成一句对话,这个压缩过程必须有工程链路的确定性兜底,否则模型的每次随机波动都会直接变成业务面前的“错误数据”。
拿“本月华东区业绩怎么样”这句话拆开看:
| 用户语言 | 可能的指标口径 | 身份影响 | 链路注意事项 |
|---|---|---|---|
| 本月 | 自然月至今 / 账期月 / 排除未支付订单 | 无 | 时间参数需与业务日历对齐 |
| 华东区 | 组织维度 / 地理维度 / 收货区域 | 行级权限必须注入 | 维度归属可能随组织调整变化 |
| 业绩 | 销售额 / 净收入 / GMV / 回款 | 部分指标可能对当前身份不可见 | 指标口径变更要有版本管理 |
这三层含义不是三个独立模块,而是层层递进的关系。语义层决定“该算什么”,身份层决定“能看哪些”,链路层决定“怎么稳定地算出来”。如果团队只把自然语言问数理解成“把中文转成SQL”,大概率会在第一层就翻车。
2. 指标语义的工程化:把“销售额”从一句话变成可计算的协议
自然语言问数最大的陷阱是让模型直接面对一张物理表去生成SQL。一旦用户说出来的概念和表的字段名不一样,比如表里叫gmv,业务叫“流水”,模型就会靠猜。猜对一次是运气,猜错十次才是常态。正确做法是在物理表之上先建指标语义层,把“用户口中的指标”翻译成“有确定计算逻辑的协议”。
2.1 指标注册表:一切语义的起点
指标注册表是语义层的地基。它不是一张Excel表,而是一套结构化定义,每条指标至少要包含:指标名称、别名、计算公式、可下钻维度、时间粒度、负责人、口径版本。实际项目中我用类似这样的定义:
metric: net_sales name: 净销售额 aliases: [销售额, 净收入, 销售净额, NET_SALES] formula: SUM(sales_amount) - SUM(discount_amount) - SUM(refund_amount) time_granularity: day dimensions: [region, channel, customer_id, product_id] owner: finance_center version: 2024.03 signals: [finance_approved]有了注册表,用户说“净收入”或“销售额”时,系统能自动聚合到同一个指标ID上。关键是这个注册表必须成为所有下游查询的唯一入口,不允许业务方跳过注册表直接写SQL,否则语义就散了。
2.2 同义词、别名与“用户口中同一个词,系统里不是一个指标”
真实业务里,同样一个词在不同团队嘴里经常代表不同指标。“销售额”有时候指订单金额,有时候指扣除退款后的实际收入;“客单价”有时候是GMV除以订单数,有时候是支付金额除以支付人数。这些问题不可能靠让模型“理解上下文”来解决,因为上下文本身没有定义硬边界。
我的做法是在指标注册表里预设别名和冲突策略。当同一个自然语言词命中多个指标时,系统不猜,而是直接向用户展示候选口径:“你说的‘销售额’是指订单口径销售额,还是回款口径销售额?”这一步虽然多了一次交互,但能避免把业务最敏感的取数口径问题留给概率。
2.3 时间表达、聚合粒度与口径版本
自然语言里的时间表达非常灵活:“最近七天”“上月”“今年以来”“去年同期”“双十一当天”。这些表达必须被识别成确定的时间区间,而且要和业务日历对齐。最典型的一个坑是用自然月替代财务月,财务同学问“本月数据”时,系统给出的是1号到今天的支出,而财务口径的账期可能是每月26号到下月25号。时间语义的偏移会直接污染所有趋势类问题。
指标口径版本化也是必须做的。业务负责人隔三差五调整指标定义,比如把“活跃用户”从“登录过就算”改成“有核心行为才算”。如果指标定义一变,线上所有相关查询必须能感知新口径并缓存失效。否则业务方在上个月和这个月问同一个问题,得到的口径不一致,就会被质疑系统数据是错的。
我在实际项目里会给每个指标定义生成一个语义指纹,包含公式、时间粒度、维度集合和版本号。这个指纹进入下游生成的SQL注释中,也进入缓存键中。任何指标定义变更,都会让旧缓存自动失配。
3. 用户身份不是“登录态”,而是查询改写的第一道关卡
很多团队做自然语言问数时,把用户身份当成一个“可有可无的参数”,只在最后拼SQL时拼一个user_id条件。这是典型的后置思维,往往会在权限上漏出大问题。用户身份必须作为查询改写的第一道关卡,在生成SQL之前就决定“哪些数据可以看”。
3.1 身份解析要先于查询生成
查询生成前要拿到完整的用户身份上下文:用户ID、所属组织树、角色、被授予的指标范围、被限制的数据维度。这些信息组合后形成一个作用域,所有查询生成逻辑都必须在这个作用域内运行。
举个例子,一个华东大区销售负责人问“全国TOP10客户是哪些”,这句话里没有出现地区限制。如果系统严格按照字面意思生成全量SQL,就是一次越权。正确做法是身份层把“全国”改写为“当前用户可见范围内的全部地区”,也就是只能查华东组织树下的客户。这不是提示词工程能解决的问题,而是必须在查询改写时强制注入的作用域规则。
3.2 行级权限、指标级权限如何嵌入查询改写
行级权限和指标级权限要分开处理。行级权限通常转化为SQL里的强制过滤条件,比如WHERE org_id IN ('OU_EAST_01','OU_EAST_02');指标级权限则决定用户是否能看到某个指标,比如普通销售看不到“毛利率”这样的财务指标。
最危险的场景是让大模型自己理解权限规则。模型没有稳定的“边界感”,它可能在一个查询里记住权限,在另一个相似查询里就忘了。因此我的做法是把权限规则固化成模板片段,在查询生成阶段作为不可被模型修改的约束拼接进最终SQL。用户问“毛利率最高的三个城市”,如果身份上下文里没有毛利率权限,系统直接拦截并提示“你没有该指标权限”,绝不会尝试生成一条碰运气的SQL。
SELECT region, SUM(sales_amount) AS net_sales FROM daily_sales_fact WHERE stat_date BETWEEN :start_date AND :end_date AND org_id IN (:visible_org_ids) -- 身份作用域强制注入,不可被LLM覆盖 GROUP BY region ORDER BY net_sales DESC LIMIT 10;3.3 同一个人换了一家子公司,同一个问题为什么答案不同
用户身份不是静态标签,身份和组织关系会变化。一个销售从华东调到华南,他再问“我负责的客户销售额”,系统必须立刻感知到组织归属变化,并同步调整可见客户范围。如果系统把身份画像缓存得太久,用户换组织后仍能看到旧数据,这就不再是功能问题,而是数据合规事故。
所以身份上下文要区分“稳定属性和动态属性”。用户ID、工号是稳定属性;组织树路径、角色、可见范围是动态属性。动态属性必须实时拉取或设置极短缓存。我的经验是:宁可牺牲一点性能,也不要让动态权限属性被缓存超过5分钟。
4. 稳定分析链路:一次线上故障带出来的四个环节
做自然语言问数,最怕的不是模型答错,而是系统不稳定。上午还能回答的问题,下午因为指标口径变更就报错;普通月份查询很快,月底结算日所有查询全部超时;某个人发起一个“全量明细导出”就把分析库IO打满。这些问题都属于分析链路的工程范畴,和模型质量无关,但会直接决定系统能不能上线。
4.1 四个环节:接受、理解、执行、反馈
我们最终把分析链路拆成四个环节,每个环节都有独立的健康检查。
请求接收环节负责把自然语言问题标准化,包括去掉口语化噪音、统一全角半角、识别问题中的业务实体;语义解析环节把问题映射到指标注册表、维度表和位置标识;查询执行环节把语义解析结果和身份作用域合并后生成可执行查询;结果反馈环节对返回的数值做基本校验,比如总额非负、行数不超过预期、与看板同口径数据偏差在可接受范围内。
其中最容易挂掉的其实是执行层。因为自然语言问数会催生很多“临时、大跨度、跨层级”的查询,这些查询以前分析师会分步拆开做,现在用户一句话就想让系统算出来,对底层计算引擎和查询改写策略的压力是几何级增长的。我们在执行层做了一套查询预算机制,单次查询能扫描的数据量、执行超时时间、返回行数都有硬上限,超出后自动走汇总表或提示用户缩小范围。
4.2 那天晚上的故障:权限缓存没刷新
一次真实的故障排查过程值得完整写下来,它能代表这类系统最常见的问题。
当晚十一点,业务负责人在群里反馈,用问数助手查“本季度各区域销售额对比”,发现华东区的数据比其他看板多了将近一倍。第一反应是语义口径问题,优先检查指标注册表,结果净销售额定义没有变化。然后检查执行SQL,发现FROM表没错,指标公式没错,但WHERE条件里缺少了渠道类型过滤。业务看板只统计自营渠道,而这次查询因为自然语言里没有提到“自营渠道”,系统就漏掉了这个强制条件。
顺着这条线继续排查,发现更严重的问题:查询执行时身份作用域中的组织过滤条件丢失了,导致能看到多个法人公司的数据。当时立刻从旁路查询验证,确认是权限缓存把用户旧的可见组织列表返回给新查询生成器。根因在于缓存键设计得不够精细,只用了用户ID和指标ID,没有加入组织树版本。比如用户从华东调到华南后,组织树版本变了,但缓存键没变,导致旧权限继续命中缓存。
修复方案有两层:立刻清掉该用户所有缓存,并把缓存键改为“用户ID+组织树版本+指标版本”的组合;随后对权限相关的查询,默认不做长时缓存,只允许5秒内的短缓存。这次故障之后,我把身份作用域指纹纳入所有查询缓存键,杜绝了权限维度上的缓存复用。
4.3 链路稳定性的三根支柱:超时、哨兵与结果校验
超时是分析链路的“刹车片”。每个环节必须有独立的超时参数,语义解析最多2秒,查询执行最多20秒,超过就返回优化建议而不是让用户无限等待。尤其要避免一个用户的问题把后端线程池占住,让其他所有用户一起卡死。我们用了单独的线程池做问数查询,池子满了就直接排队,并在响应里告知“当前请求较多”。
哨兵机制负责发现数据异常。我们会在查询响应前自动跑一个简单的一致性校验:当同一个指标在问数系统返回的值和官方看板同口径值差距超过5%时,系统拒绝直接返回结果,提示“当前结果存在口径偏差,请稍后重试或联系数据管理员”。这个机制会拦截很多口径漂移问题。
结果校验也不能只看数值,还要看返回字段数量。曾经一次模型升级后,用户问“各部门人数”,系统返回了明细员工名单,虽然SQL语法正确,但显然不是用户要的聚合结果。我们后来增加了返回结构的校验规则:用户问题里如果包含“多少人、多少金额、占比、排名”等聚合词,查询只能返回聚合结果,不能返回明细行。
5. 我踩过的三个坑和现在的做法
前面讲的是设计原则,这一节说几个具体踩过的坑,给正在搭自然语言问数系统的同学当个参考。
5.1 语义缓存导致的权限越权事故
第一坑就是缓存粒度太粗。最初为了让系统响应快,我们对“问题标准化+指标ID”做了缓存,命中后直接返回历史SQL和结果。结果一次销售组织架构调整后,一个销售总监还是能查到调整前的部门明细。排查原因时发现,他的查询命中了一个旧的语义缓存,缓存里的SQL生成时还没有加入新的组织过滤规则。
现在的做法是缓存键必须包含三层指纹:语义指纹、身份作用域指纹、指标口径版本指纹。其中身份作用域指纹用用户角色、组织树路径、可见指标集合生成一个哈希值,任何权限变化都会导致哈希变化,从而避开旧缓存。权限敏感查询干脆不缓存,或者只做5秒短缓存。
5.2 把大模型的“造句能力”当成了“计算能力”
第二个坑是最普遍的认知误区。一开始我们也是用大模型直接生成SQL,效果看起来不错,直到遇到“上个月退货率”这样的问题。模型生成的SQL看起来逻辑完整,但“退货率”的分母应该是“已支付订单数”,模型却用“全部订单数”做了分母,结果偏差很大。问题不在于模型不会写SQL,而在于模型不具备业务口径的确定性。
后来我们变成了混合架构:大模型负责理解自然语言,输出结构化的指标和维度标签;真正生成SQL时,走指标注册表映射和受控查询模板,计算逻辑全部来自语义层,不允许模型自由发挥计算公式。这样改完之后,口径准确性提升非常明显。大模型的角色从“决策者”变成了“翻译官”。
5.3 指标口径改了,下游没感知
第三个坑是指标口径变更后的联动失效。业务方把“销售额”从包含退款调整为扣除退款,我们更新了指标定义,但用户还缓存着旧查询。结果同一时间范围、同一个问题,问数助手给出的数据和BI看板完全对不上,业务方第一反应就是“平台数据不准”。
现在指标口径的变更会触发语义版本更新,版本变化会让所有依赖该指标的缓存自动失效。同时我们会在系统里保留一个“口径变更记录”,用户问同一个问题时如果发现结果比昨天有明显变化,可以一键查看“该指标近期是否有口径调整记录”,把数据差异变成可解释的业务调整,而不是让用户怀疑系统出错。
我在实际项目里深刻体会到,自然语言问数最终拼的不是模型聪明程度,而是工程约束是否到位。语义层钉得越死,身份层管得越严,链路层守得越稳,模型才能在一个可控边界里正常发挥。一开始不要试图做一个“什么都能答”的全能问数机器人,先圈定一小部分核心指标,把口径吃透、把权限堵死、把链路跑稳,再慢慢扩范围,这条路走起来反而更快。