自然语言问数Agent与NL2SQL落地方案:权限、准确率与评测实践
做数据这行快十年了我最大的感受是团队的价值不在于能写多复杂的SQL而在于让业务人员少等几天。过去一年我主导了一个自然语言问数Agent项目目标很朴素——让业务人员用大白话提问比如上周华东区哪家门店销售额环比下降最多系统自动翻译成SQL把结果和图表一起返回来实现业务人员自主完成数据分析。这篇文章我把从架构设计到落地踩坑的完整过程做个梳理包括Agent的语义解析链路、数据权限隔离方案、准确率优化的真实经验以及怎么搭评测集让系统越用越准。如果你也在纠结怎么让数据团队从无穷无尽的取数工单里解放出来这篇应该能陪你走一程。1. 业务取数这件事到底卡在哪个环节1.1 工单制取数的三座大山很多公司今天的取数流程还是这样业务发消息给数据团队帮我看一下最近用户怎么涨得这么快数据同学先反反复复确认口径——你说的用户是注册用户还是活跃用户时间范围是自然周还是自然月要不要看新老客一个简单的描述性查询来回三四轮是家常便饭。第一座大山是沟通成本。业务同学说销售额背后可能藏着多种口径含退款吗含未支付订单吗是开票金额还是实付金额如果公司没有一套强制统一的口径字典同样的词在不同部门理解完全不同。第二座大山是响应速度。哪怕是一条取上周各品类复购率的中等查询排进需求队列也可能要两三天。业务人员等得起但业务决策等不起。很多时候他们等不到结果就自己拍脑袋了或者拿着已经过期的数据在会议上争得面红耳赤。第三座大山是数据团队自身的低水平重复。我见过有些数据开发一周做几十个取数工单其中百分之六七十是相似查询的不同变体。这种工作长期做下来团队没有时间建设指标体系也没有精力优化数据质量反而离业务越来越远。1.2 传统BI工具为什么解决不了这个问题有人会说公司不是上了BI吗让业务自己拖拽报表不就行了。现实是拖拽式分析有认知门槛要让业务理解什么是维度、什么是度量、怎么写筛选条件还要学会正常表和多维分析模型的区别。大多数业务同学在自助BI上完成的第一个复杂分析通常是把Excel数据导入后做个透视表。固定看板能解决问题吗能解决一半。看板提前定义好指标和维度对于重复性监控足够但业务问题往往是非结构化的临时问题——昨天促销活动的转化率比平时高多少武汉仓哪些SKU在途超过三天了。这些问题在创建看板时根本没想到过最终还是要回到提工单这条路。自然语言问数Agent的价值恰恰在这它把写SQL的门槛去掉让业务用母语提问同时把数据团队从翻译官角色中解放出来集中精力去治理指标口径、优化底层数据模型。它不是取代BI看板而是补齐了灵活取数这个最常用的空白地带。2. Agent内部是怎么把人话变成SQL的2.1 整体流程意图识别、语义映射、语句生成、执行解释我会把整个Agent拆成四层来看每一层都有明确的输入输出边界。第一层是交互层。负责接收用户的自然语言输入展示结果表格或图表支持多轮追问。这里最重要的是把问题-生成的SQL-执行结果完整记录下来形成可审计的链路后面优化靠的就是这些历史数据。第二层是语义理解层。要做三件事意图分类、实体抽取、条件归一化。意图分类用来判断用户是想看汇总、对比、趋势、明细还是占比实体抽取是把华东区上周销售金额这些业务词提取出来条件归一化则负责把近7天从10号到16号最近一周统一换算成SQL里的日期区间。第三层是语句生成层也就是NL2SQL的核心。根据语义理解结果结合数据字典和业务口径定义生成可执行SQL。这里我会让大模型先生成逻辑查询计划——先明确查哪个表、聚合什么指标、按什么维度分组、过滤条件是什么——再翻译成具体的SQL方言。先有逻辑再有语法能避免很多错误。第四层是执行与解释层。执行前要拼接权限过滤条件、加上LIMIT限制执行后把SQL结果解释成自然语言摘要比如华东区共有6家门店销售额最高的前三名分别是……。2.2 意图识别不用太复杂先分清楚六类意图大模型出现之后意图识别已经不推荐用单独的意图分类模型了直接用提示词让大模型分类更省事。我实践中把问法归成六类汇总查询上个月总销售额是多少趋势分析最近12个月每月销售额变化对比排行各区域销售额排名占比构成各品类销售额占总销售额的比例明细查询列出上周退货订单明细异常分析哪些门店销售额环比下降超过20%分类结果决定SQL的骨架。比如占比构成类大模型通常会生成带子查询的SQL用总销售额做分母趋势分析类通常用GROUP BY配合时间维度。这里给一个用大模型做意图分类的简单示例Python风格代码示意即可import json system_prompt 你是数据分析助手。请判断用户问题的意图类型只输出JSON {intent: total|trend|rank|proportion|detail|anomaly, reason: 判断依据} 规则 - total仅询问单一汇总数值 - trend包含时间序列、变化趋势 - rank包含排名、前N、TOP - proportion包含占比、比例、份额 - detail需要查看明细记录 - anomaly包含同比、环比、下降、超过等异常条件 def detect_intent(user_query): result llm_chat(system_prompt, user_query) return json.loads(result)[intent]如果你用的是支持工具调用的模型可以直接让模型输出结构化字段比先输出一段话再解析更稳定。2.3 实体抽取与NL2SQL一个真实问题的拆解我拿一个业务真实会问的问题来走一遍各地区门店上周销售额排名前五有哪些语义理解层应该抽出指标销售额映射到字段fact_order_sum.sales_amount时间条件上周归一化为 [2025-04-07, 2025-04-13]以周一为一周起点维度地区、门店粒度不按天只看汇总排序销售额降序取前5范围无额外过滤在此基础上逻辑查询计划大致是SELECT region, store_name, SUM(sales_amount) FROM fact_order_sum WHERE order_date BETWEEN 2025-04-07 AND 2025-04-13 AND region IS NOT NULL GROUP BY region, store_name ORDER BY SUM(sales_amount) DESC LIMIT 5这里最容易出错的是上周的边界。业务以为的周一系统可能认为是周日所以我的做法是在语义理解层就先用一套业务时间词典包含本周、上周、近7天、本月累计、上个月等换算成绝对日期再将绝对日期传给SQL生成层而不是让大模型在生成SQL时自己算相对时间。这一步能砍掉很多时间口径错误。另一件很关键的事是不要一上来就生成完整SQL先生成结构化的查询参数。这样即使后面SQL生成失败也方便做可解释的兜底比如告诉用户我理解您想看销售额但没找到地区维度您是否需要改为各省份3. 数据权限与安全在开放和可控之间走钢丝3.1 权限问题在自然语言问数里被放大了十倍平时写SQL查数能接触数据库的人就那么几个。一旦把自然语言问数Agent开放给全员每个业务人员都可能点击执行此时如果你不加控制就相当于让全公司的人都拥有了一条直连数仓的SQL通道。一个低风险操作是业务人员本来只想看自己团队的销售结果因为Agent生成的SQL没有带WHERE team_id把全公司数据看了个遍。这种事只要发生一次项目就会被叫停。所以在Agent的设计里数据权限不是后置的功能而是从第一天就必须进架构的强制环节。3.2 两层权限如何落到SQL上我采用的方案是两层权限模型字段权限和行级权限。字段权限用于控制哪些人能看到哪些列。比如销售中心的人可以看销售额、毛利但看不到成本价HR人员可以看人员编制但看不到绩效明细。这个映射关系维护在一张权限配置表里Agent在生成SQL时会把SELECT列表中的每个字段与当前用户的字段权限比对越权的字段直接剔除或脱敏。行级权限用查询改写的方式实现。每个用户登录时先从用户中心获取他所属的组织机构编码和层级然后在生成的SQL上强制追加过滤条件。举个例子-- 用户会话中解析出的权限条件 WHERE org_code IN (A01, A02, A03) -- 来自用户中心 -- 改写后的SQL SELECT region, store_name, SUM(sales_amount) FROM fact_order_sum WHERE order_date BETWEEN 2025-04-07 AND 2025-04-13 AND org_code IN (A01, A02, A03) GROUP BY region, store_name ORDER BY SUM(sales_amount) DESC LIMIT 5这里有个细节权限条件的注入必须放在SQL生成层之后、执行层之前作为一个独立的中间模块不能依赖大模型自觉。大模型的任务只是生成业务查询SQL权限改写由代码强制拼接这样无论模型怎么变化权限都不会被绕过。3.3 脱敏与不可查字段的兜底行级权限管住了哪些人能看哪些行但敏感字段仍然需要处理。比如用户表里可能有手机号、身份证号就算用户只能看自己部门的数据也不该把完整手机号展示出来。我的做法是给每个字段打一个敏感级别标签普通字段正常查询敏感字段查询后动态脱敏例如手机号显示138****1234禁止字段不允许出现在SELECT和WHERE中禁止字段主要在WHERE上做拦截。业务人员如果问查一下身份证号为110101…的用户订单这就是典型的个人信息查询应该拦截。怎么识别一是权限配置表里标记该字段不可作为筛选条件二是语义理解层增加规则涉及个人隐私实体时默认拒答并提示用户说明合法用途。3.4 别让一条SQL把数据库打爆自然语言问数的场景非常容易产生无意的全表扫描。用户说各个地区平均客单价是多少Agent可能会生成不带时间限制的GROUP BY然后扫全表。线上的表动辄上亿行一次这样的查询就能把在线服务拖垮。熔断机制我按三个维度做了兜底LIMIT强制上限任何查询都会自动追加LIMIT明细分页一次最多返回200行汇总结果最多返回5000行。扫描行数预估执行前利用目标表的元数据估算扫描行数超过阈值比如2000万行则要求用户补充筛选条件或者路由到离线查询引擎。超时终止查询超过30秒未返回直接取消并提示用户缩小时间范围或提高粒度。这一套下来试运行阶段基本没再出现查询拖垮数据库的事故。记住宁可让用户多等一秒也不能让一条失败的查询影响整个线上数据服务。4. 准确率与信任感那些真正难缠的坑4.1 第一大坑同一种意思一百种问法业务同学说上个月卖了多少钱另一个同学说上月营收怎么样还有人说3月整体收入多少——这些在系统里可能都是同一个指标但表达差异极大。只靠大模型的语义理解能力经常会在营收和收入之间犹豫更别说方言化表达前场业绩商场业绩这种内部黑话。我们的解法是两层同义词池。第一层是全局同义词配置比如营收收入销售收入实际销售额第二层是从历史查询记录里挖掘用户独有的说法。每周跑一次聚类把用户查询过的自然语言表达按执行成功的指标ID聚合提取高频未命中词作为待补充同义词。这样系统用得越久对老业务黑话的识别能力越强。4.2 第二大坑指标名可以一样口径必须唯一同义词解决了叫法不同但还有更隐蔽的同一个名字两个定义。比如财务口径的销售额剔除退货运营口径的销售额包含全额支付但含退款中和已退款订单。如果数据库里两个字段都叫sales_amount大模型根本不知道该用哪个。对付这个问题没有捷径只能建设指标注册表。它是一张包含指标唯一ID、业务名称、字段来源、聚合方式、时间口径、过滤条件的表。NL2SQL生成时先用语义匹配找到指标ID再根据ID去查字段和口径定义而不是直接让大模型自由写GROUP BY和SUM。我的指标注册表示意指标ID业务名称字段表达式聚合方式默认过滤M001销售额财务口径SUM(fact_order_sum.sales_net)SUMrefund_flag 0M002销售额运营口径SUM(fact_order_sum.sales_gross)SUM无M003客单价SUM(fact_order_sum.sales_net) / COUNT(DISTINCT order_id)表达式refund_flag 0当用户说销售额而上下文没有指明口径时Agent应该主动追问一次您是指财务口径不含退款还是运营口径含退款这虽然多了一步交互但能避免大量错误结果反而提升了信任感。4.3 多轮对话中的指代问题单轮查询跑通之后用户很快就会开始多轮追问。最典型的场景是用户问完华东区销售额怎么样紧接着说那按品类看一下呢。这时候Agent必须知道那指的是华东区按品类看要保留之前的区域筛选条件把维度从区域切换为品类。实现多轮继承的关键在于维护上下文槽位。每一轮解析结果以JSON形式沉淀为槽位包含指标、维度、时间条件、过滤条件。新问题先做指代消解看是否有那这它分别也这类指代词有则从上一轮槽位里继承未提及的字段。我一般会给大模型一个结构化的上下文记忆而不是直接把原始对话历史全丢进去。原始历史信息太多容易引入噪音结构化槽位信息更干净。这里可以这样组织上下文{current_slots: {metric: 销售额, dims: [区域], time: natural_month_202504, filters: []}}当用户说那按品类看一下吧模型就会基于current_slots把dims改成[品类]区域条件保留为filters。这一步需要结合业务理解不是简单的replace那么简单。4.4 宁可说不知道也不要编一个SQL大模型NL2SQL最大的风险是生成看起来正确、实际完全不符合业务语义的SQL。比如用户问复购率模型可能真去数据库里找一个reorder_rate字段但这个字段其实是配送及时率只是名字碰巧相似。为了减少这种幻觉我在提示词里明确要求模型遵循未知即拒答原则当数据字典中找不到完全匹配指标或维度时必须返回不确定请求不能随便猜测同名字段。同时在执行层增加结果合理性校验——比如查询结果为NULL或为负值时给出提示对销售额这种指标来说负值基本意味着取数口径或者SQL逻辑有问题。对确实查不到的情况Agent会返回当前数据字典中没有找到指标复购率请尝试使用以下近似指标老客回购率复购人数。这比硬着头皮给一个错数强一百倍。业务人员宁可得到没有的反馈也不能默默接受一个错误数字并在周会上说出来。4.5 复杂指标能用预计算就不要现场算在实践里我发现凡是复杂的业务比率例如复购率、客单价、件单价、同店同比让大模型现场生成SQL失败率非常高。原因在于这些指标通常需要多表关联、子查询、还要对时间周期做特殊处理大模型一旦分拆错表间关联粒度数字就会离正确值很远。我的建议是团队数据建模的时候把高频复杂指标预计算成汇总表或增加字段。Agent只查询预计算结果而不是尝试现场计算。你可能会说这样不够灵活但换来的是稳定性和准确性。对于少数临时复杂分析可以让经验丰富的数据分析师把逻辑模板配到指标库里而不是指望业务人员的自然语言能生成正确的多层嵌套SQL。5. 评测与迭代让Agent从能跑到好用的方法论5.1 用真实业务问法建评测集自然语言问数Agent上线后最棘手的问题就是一个提示词改完不知道哪里会变坏。模型输出的不确定性决定了这种系统必须持续回归测试。建评测集是第一步。我们没有请算法工程师凭空造句而是翻了一个季度的取数工单筛选出200多条真实业务问法覆盖前面说的六类意图同时尽量包含不同地区的黑话表达。每条问法都配上期望输出参照SQL、期望返回的指标ID列表、期望维度粒度、期望时间范围。我建议每条至少配两套正解因为一个问题往往有多种正确的SQL写法。评测文本和SQL分别存两个文件用脚本批量跑Agent对比SQL的字段使用、时间条件和返回结果。字段和条件可以精确比对但SQL语法层面允许差异所以我的工具会自动解析AST去比较查询语义而不是直接比字符串否则误杀率太高。5.2 错误分级先把致命错误和小瑕疵分开跑完评测集后会得到一堆错误如果不分级就排不出优先级。我把错误粗略分为三级。第一级是致命错误指标识别错误、口径出错、权限遗漏。例如用户问销售额返回了成本字段。这种错误必须立刻修因为它会忽悠业务做出错误决策。第二级是较大错误时间范围偏差、维度选择不对。比如用户问上周解析成了上上周。这种问题一般调整时间词典和归一化逻辑就能解决。第三级是小瑕疵排序方向不一致、返回字段顺序不一致、图表类型不合预期。这种不阻塞使用但也要记录持续优化提示词。每次发版前至少要保证致命错误为零较大错误率低于5%整体准确率达到90%以上才有信心推到生产环境。5.3 线上反馈闭环让用户教你改评测集解决的是已知问题回归但总有评测集覆盖不到的新问法。所以从上线第一天起我就在对话界面上加了反馈按钮用户可以对结果点准确/不准确并输入正确预期。这个数据放在一个反馈表里每天自动汇总每周人工看一下Top20错误。反馈闭环里最有价值的是用户提交的纠正SQL。当用户说你应该查的是M002这个指标这相当于直接给了我们一个标注样本。把这类数据补充进评测集模型的薄弱点会越来越清楚评测集本身也会慢慢从200条涨到2000条。5.4 别忽视执行层的性能与稳定性准确性搞到一定程度性能就开始影响体验。最初上线时一次请求平均耗时在10秒以上用户会觉得比提工单还慢。后来分两方面优化一是把意图识别和实体抽取改为并行调用二是把指标表和同义词表预先加载到向量库语义检索的耗时从2秒降到200毫秒。真正复杂的NL2SQL调用仍然需要几秒。为了不让用户干等我的交互层做了流式输出先把我理解到您要查销售额按门店分组时间上周展示出来再逐步展示SQL最后显示结果。这样做有两个好处用户能早期判断Agent理解是否偏差如果理解不对可以马上打断不用等SQL执行完才发现。5.5 试用一个季度后的实际效果参考在这里我不写具体数字只说趋势我们在一个模拟零售项目中试运行了三个月自然语言问数的月查询量从第一周的仅几十条到第三个月稳定在日均百条以上日活用户数占到业务人员总数的四成。SQL生成成功率从首月的不到六成提升到第二个月的八成再到第三个月接近九成。更明显的是数据团队的取数工单量减少了约一半剩下的一半多是临时深度分析和口径争议恰好是数据团队应该腾出手来真正创造价值的活。需要注意的是这个效果依赖一个前提——权项模型和指标字典必须前置搭好。如果底层数据表没有清晰的元数据项目落地周期至少会翻倍很多分析也不会真正沉淀下来。说一千道一万自然语言问数Agent不是一个纯算法项目它更像一个系统工程。大模型负责理解人的意图数据团队负责规则、口径、权限和评测这些确定性的部分。把确定性的基础打牢把不确定性的表达交给模型这个组合在业务侧是真的能跑起来的。最后再分享一个小技巧上线之前先用两周时间让数据团队内部每天轮流扮演智能助手去回答真实业务问题把最容易答错的问题和正确解法整理成提示词补充材料。这个办法成本极低但往往比找算法专家调一个月模型更见效。等真正上线的时候你手里已经握着一份来自一线「人肉评测」的宝贵样本了。