【实践分享】25年到26年,我的NL2SQL智能体是如何迭代的
前言
之前写过一篇《如何手搓一个RAG》,当时主要讲的是知识库问答。
引流:https://linux.do/t/topic/2187051
这篇讲另一个更痛苦的东西:NL2SQL。
先叠甲:
我不是底层模型开发,也不是论文选手,我是AI应用开发,现在人们叫AI应用落地工程师(有点想补一个黄豆流汗)。
这篇不讲特别高大上的理论,主要讲我自己从老项目到新项目的工程迭代。
里面很多东西可能现在看起来很土,很笨,很原始。
因为很多东西不是一开始就长成现在这个样子的。
现在大家都很懂智能体、tool call、workflow、planner、memory、MCP,但我第一次认真把这套东西串起来的时候,企业应用里还没有这么成熟的说法。那时候更多叫机器人、助手、问答系统,或者更朴素一点:大模型接口外面套一圈业务逻辑。
先说时间线
老项目是一个早期业务问答助手,这里就不写真实项目名了。
本来我只打算从2025年那套数据问答开始写,后来又去翻了一下更老的业务仓库,发现这条线其实还能往前追。
最早能看到的是2024年6月:
2024-06-25 19:05:18 合同类AI问答,前端原型开发
到2024年7月,提交记录里已经不是单纯页面了,而是开始出现后端接口、聊天记录、报表模式这些东西:
2024-07-10 15:28:47 后端接口开发
2024-07-11 16:15:58 聊天记录相关接口进入工程
2024-07-12 15:51:57 报表模式开发
这段现在回头看还挺有意思的。
那时候我不会用“智能体”这个词描述它,也不会说什么workflow、tool call、planner。更直白一点,它就是业务系统里的一个AI问答入口:有聊天窗口,有问题配置,有聊天记录,有后端接口,也开始尝试把回答结果和报表展示接起来。
到了2025年,它才慢慢进入更像“能交付、能运维”的阶段:
2025-04-01 17:46:24 现场优化,准备上线
2025-05-08 11:28:43 图表绘制优化
2025-05-14/15 对话记录保存
2025-07~10 数据权限、数据同步、报表和使用统计持续优化
再往后,才是我这篇真正想讲的NL2SQL数据问答链路。
老数据问答的Java服务模块最早能追到:
2025-08-04 11:00:49 早期数据问答基线上传
Python智能问答模块也是同一天开始:
2025-08-04 11:00:49 早期数据问答基线上传
然后到了2025-08-14,提交记录里已经出现了这种东西:
意图识别时校验问题中的数据表是否存在
生成SQL时判断问题中的字段是否存在
生成SQL提示词优化,增加自校验逻辑
SQL执行错误给出友好提示
再到2025-08-19,又有:
报表问答准确率提升:修改生成SQL提示词结构,采用少样本提示的标准写法
所以按git记录看,这条线大概分成两截:2024年是业务问答入口和报表问答的雏形,2025年8月开始,才更明确地往NL2SQL工程链路上走。
新项目是2026年重做的一套业务智能体系统。
它的项目初始化是:
2026-04-20 14:45:08 项目初始化
数据问答正式落到新项目里,是:
2026-04-23 15:44:44 新增数据问答前后端能力并完成会话链路语义解析接口与数据库表落库
所以这篇讲的不是“我最近看agent火了,赶紧包装一个概念”。
说难听点,当时它就是一个失败的项目,一个失败的数据问答机器人。
但它在我心里的地位很重要,因为这是我自信满满到失败,一塌糊涂的一个项目。
2026年,终于有机会再做一遍。感谢客户的信任。
这里,我将其工程化改造,并把经历留下。
老数据问答:先把链路跑通
老数据问答主要看两个部分:
Java服务模块
Python智能问答模块
虽然用户入口、会话记录、前后端接口在Java应用里,但真正干NL2SQL脏活累活的是Python侧的问答模块。
当时的链路大概是这样:
用户提问
-> 问题重写
-> 意图识别
-> 判断要查哪张表
-> 权限校验
-> 拼表结构和few-shot样例
-> 让大模型生成SQL
-> 从模型返回里抠出SQL
-> 执行SQL
-> 让大模型解释查询结果
-> 如果用户要图表,再让大模型生成Echarts配置
-> 流式返回给前端
现在看起来,这不就是一个智能体么?
有意图识别,有工具选择,有工具执行,有结果加工,有流式输出,甚至还有图表工具。
但当时我不会这么说。
我只会说:这是数据问答。
或者说得更土一点:这是一个会查数据库的机器人。
问题重写
老链路里第一步就是问题重写。
这个东西在知识问答里重要,在NL2SQL里更重要。
因为用户不会每次都把问题说完整。
比如:
第一轮:今年杭州有多少商机?
第二轮:按行业分一下
如果第二轮直接拿去生成SQL,大模型可能知道“按行业分一下”是什么意思,也可能不知道。它不知道的时候,就开始表演了。
所谓表演,就是一本正经地胡说八道。
所以老项目里先把历史问题压缩成当前独立问题。这个思路没错,到现在也没错。
只是当时实现比较朴素:把最近几轮对话塞进提示词,让模型判断要不要改写。能用,但依赖模型稳定性。
意图识别
老项目里有一个很典型的东西:函数式意图识别。
它会让模型返回类似这样的结构:
{
"function_call": {
"name": "search_sql",
"arguments": {
"table": "项目表",
"char": "柱状图"
}
}
}
这其实已经很接近现在大家说的tool call了。
只不过那个时候不是标准工具协议,也不是框架自动帮你做。就是自己写提示词,自己解析JSON,自己判断name是什么,自己决定下一步走哪里。
有人会问了,这不就是手搓function calling么?
是的。
很土,但能跑。
SQL生成
老链路真正刺激的地方是SQL生成。每次演示就好像上战场一样。
当时的思路很直接:
根据用户问题识别要查哪张表。
找到这张表的字段说明和相似问法。
把表结构、字段含义、few-shot样例、SQL生成规则一起塞给模型。
让模型返回SQL。
程序再把SQL抠出来执行。
当时最能救命的是few-shot。
例如“今年杭州新签约合同数量是多少”这种问题,对人来说很简单,对模型来说不一定简单。因为它要知道:
“今年”对应哪个时间字段。
“新签约”对应哪个状态或日期。
“杭州”对应城市字段,而且可能还要处理“杭州市”“杭州分公司”这种说法。
“数量”是count,不是把某个金额字段求和。
所以当时靠大量样例去教模型。
这条路是有效的。
但它有一个问题:越做越像补丁。
你发现“城市”识别不准,就加城市规则。
你发现“字段不存在”会乱生成,就加字段存在性校验。
你发现SQL报错太难看,就包装友好提示。
你发现用户想看图,就让模型再生成Echarts配置。
你发现“省、市、区县”容易混,就再写一个地名修正。
系统当然能跑,而且效果还可以。但你心里知道,这东西有点像用胶带把飞机粘起来。
飞是能飞。
但每次上线都要默念一句:千万别问奇怪问题。
老在哪里
这里我要强调一下,“老”不是贬义。
很多老办法在当时就是正确答案。
因为业务要结果,用户要能用,领导要演示,项目要交付。你不可能坐在那里说:等我设计一个完美语义层,半年后再说。
不现实。
老项目确实就是有很明显的时代痕迹。
第一,模型直接生成SQL
老链路里,模型承担的任务太重了。
它不仅要理解用户问题,还要理解表结构,还要选择字段,还要生成SQL,还要自己判断字段是否存在。
这就像让一个实习生同时当产品经理、数据库工程师、测试和客服。
他有时候很聪明,有时候也真的离谱。
最简单的例子:字段名。
你给他一张表,里面有“合同金额”“签约金额”“中标金额”“预算金额”。用户问“金额是多少”,他到底该用哪个?
如果业务口径稳定,模型可能猜对。
如果业务口径不稳定,模型猜对了也只是运气。
第二,提示词越来越长
为了让模型不犯错,我们会不断往提示词里加规则。
不能select *。
日期要怎么转换。
城市要去后缀。
字段别名不能当查询条件的值。
枚举值模型不能自己发明。
SQL生成前后要判断字段是否存在。
SQL生成后再自检一下字段是否存在。
听起来很严谨,对吧?
但提示词不是法律。
提示词更像劝告。
你说了一百条规则,模型不一定真的每条都遵守。特别是表结构一长、样例一多、用户问题一复杂,它就开始挑自己记得住的部分执行。
这就是老链路让人很痛苦地方。
第三,解析靠字符串和正则
老项目里会从模型返回中提取JSON、提取SQL、提取Echarts配置。
这也是当时很常见的做法。
模型返回:
```sql
select ... from ...
```
我们就用正则把他抠出来。
模型返回:
```json
{"sql": "select ..."}
```
我们就转JSON。
问题是,模型有时候会多说一句“好的,以下是SQL”。
也有时候会少一个引号。
也有时候会把中文标点、代码块、换行混在一起。
然后你就开始写各种清洗逻辑。
写到最后,你都分不清自己是在做NL2SQL,还是在做大模型输出垃圾回收。
第四,安全边界后置
老链路也做了不少安全处理,比如权限校验、表名校验、SQL错误兜底、结果数量限制。
但整体上,它还是先让模型生成SQL,再在后面拦。
这个顺序在生产里会带来心理压力。
因为模型生成SQL这件事本身就是不稳定的。
你可以拦掉delete、update、drop,也可以限制只查某些表,但只要SQL是模型自由生成的,它就总有一些奇怪路径。
比如字段错了。
比如条件错了。
比如时间字段错了。
比如聚合口径错了。
这些不一定是安全事故,但一定是业务事故。
用户不一定知道SQL错了,他只会觉得你的机器人在胡言乱语。
第五,业务口径藏在提示词里
这是我后来最深的感受。
NL2SQL最难的不是SQL。
是业务口径。
“今年新增项目数”到底按创建时间、立项时间、签约时间,还是入库时间?
“中标金额”用预算金额、中标公告金额,还是合同签约金额?
“商机数量”算草稿、已发布、已中标,还是全部?
这些东西不应该藏在提示词里。
因为提示词不适合承载业务制度。
提示词适合表达任务,业务口径应该进配置、进表、进规则、进校验的结构。
这也是我从老项目走到新项目,最大的变化。
新数据问答:从让模型写SQL,到让模型填语义
新项目是一套面向具体业务场景的智能体系统。
这时候它已经不再是单点机器人,而是一个统一入口。
知识问答、数据问答、首页意图识别、知识库管理、快捷问题,都在一个体系里。
数据问答这块,新链路大概是这样:
用户提问
-> 会话上下文补全
-> 大模型解析结构化语义
-> 后端规则兜底与合并
-> 指标/维度/别名/口径匹配
-> 判断是否需要澄清或确认
-> 构建统一语义对象
-> 校验语义对象
-> 后端生成受控SQL
-> PreparedStatement执行
-> 构建指标卡片、表格、趋势、明细预览
-> 记录意图、语义、SQL、参数、结果行数
-> 流式返回前端
这里我后来想明白了一件事:
模型不再直接生成SQL。
模型做的是语义解析。
它只能基于后端给出的候选列表,选择指标、维度、过滤条件、时间规则、分析类型。
到这里,模型就不再像一个“写SQL的人”,更像是在帮用户把查询条件填进一张受控表单。
这个转变看起来很小,但对我来说差不多是从睡不着到睡得着(客户压力我的,每天晚上10点钟给我打电话,诶呀,我们这个数据问答这么垃圾,晚上我都睡不着觉啊)。
四表语义层
新项目里数据问答绕不开四张配置表:
DQ_METRIC
DQ_DIM
DQ_ALIAS
DQ_METRIC_VER
它们不是为了看起来专业才拆出来的,而是我后来发现,不拆就会乱。
我当时给它们分的工大概是:
METRIC:用户要查什么指标
DIM:用户按什么过滤、按什么分组
ALIAS:用户自然语言怎么映射到系统编码
METRIC_VER:同一个指标有哪些业务口径
举个例子。
用户问:
近30天中标商机数量是多少?
老链路可能会让模型直接生成:
select count(*) from 某张表 where ...
新链路会先变成一个语义对象:
指标:商机数量
口径:中标
时间:近30天
分析类型:summary
过滤条件:无
然后后端再根据配置生成SQL。
这样做的好处是,SQL不再是模型凭空写出来的。
它是业务配置推导出来的。
指标和口径分开
老项目里,“项目数量”“新增项目数量”“中标项目数量”“已签约项目数量”这些东西,很多时候会混在提示词和样例里。
新项目里,我更倾向于拆开:
指标:项目数量
口径:新增/中标/已签约/履约中
这看起来只是表设计变化,但实际影响很大。
因为用户说“数量”的时候,他可能问的是同一个指标;用户说“新增”“中标”“已签约”的时候,他问的是不同业务口径。
如果把这些都交给模型猜,模型迟早会猜错。
如果放到METRIC_VER里,就能让口径变成可维护的配置。
用户问得不清楚时,系统还能反问。
这比胡乱给一个答案要好得多。
别名是NL2SQL的命门
我后来越来越觉得,NL2SQL不是“自然语言转SQL”。
后来我觉得它更像是在干这个活:
自然语言里的业务说法,映射到数据库里的结构化对象。
这里最麻烦的是别名。
用户不会按数据库字段名说话。
用户会说:
中标
赢单(其实就是中标)
签了(其实也是中标)
集团客户(其实就是客户)
客户经理
新增(大模型总以为是数据的create时间,其实是项目的中标时间)
有了(这个最离谱,这个也是中标)
这些东西都得落到系统能识别的编码。
所以新项目里有DQ_ALIAS。
这东西不是锦上添花,少了它就很难用。
如果别名配得差,用户就会觉得机器人笨。
如果别名配得好,用户会觉得系统“懂业务”。
但其实不是模型突然变聪明了,是我们把业务语言铺好了。
时间必须有硬性规则
NL2SQL里时间非常容易翻车。
“今年”“本月”“近30天”“2024年”“2024年1月到3月”,看起来都是时间,但实际处理方式完全不一样。
老项目里很多时间规则是靠提示词和样例。
新项目里,我更愿意让后端规则优先。
比如代码里会先解析显式日期:
2024年
2024-01-01到2024-03-31
再处理“今年”“本月”“近30天”这种相对时间。
而且新链路会要求必须有时间范围,不允许直接扫全量。
有人会说:这是不是太保守了?
是的。
但数据问答不是闲聊。你扫全量,那完蛋,到时候追责第一个找你。
交互和确认
老链路更像“一问一答”。
用户问,系统尽量答。
答不了,就报错或者提示重新描述。
新链路里,我加入了交互和确认。
比如用户问:
今年金额是多少?
系统不应该硬猜“金额”是预算金额、中标金额、合同金额还是投资金额。
而是与用户产生交互,并提问:
你想查询的是预算金额、中标金额,还是合同金额?
这就是从“机器人努力回答”变成“系统控制风险”。
我以前也会觉得,反问是不是显得机器人不够聪明?
后来发现不是。
真正成熟的系统,应该知道什么时候不能瞎答。
受控SQL
新项目里SQL是后端构建的。
它基于语义对象拼:
select
from
where
group by
order by
而且参数走PreparedStatement。
同时还有几条硬性边界:
只允许SELECT。
拒绝DELETE/UPDATE/INSERT/MERGE/ALTER/DROP/TRUNCATE。
必须有来源宽表。
必须有指标字段。
必须有时间字段和时间范围。
当前只支持单表宽表查询。
这就从根上改变了系统性质。
老项目是“模型生成SQL,程序尽量拦”。
新项目是“模型生成语义,程序决定SQL”。
一个是让模型开车,人在副驾驶踩刹车。
一个是人把轨道铺好,模型只负责把乘客放到正确车厢。
两代方案到底差在哪
如果只看演示,两代东西好像差不多。
用户问一句,系统查数据库,然后返回一段话,最好还能带个表格或者图。
但自己做过就知道,这里面差太多了。
老方案靠什么?
靠表结构、靠样例、靠提示词、靠一堆“上次这里翻车了所以这次补个规则”的经验。
这个办法真的快。
今天把表结构整理出来,明天补几个样例,后天就能拉着人演示。用户一问,模型给个SQL,数据库返回结果,页面流式输出。
完美。
问题是,后面每多一张表,就多一堆麻烦。
字段越多,模型越容易选错;口径越多,样例越多;业务越复杂,提示词越长。你明知道里面有问题,但它又确实能跑,于是就会陷入一种很微妙的状态:不改不放心,改了也不放心。
所以你只能这样:
每新增一张表,就新增20个SQL;用户每次说口径不对,你就把用户的问题和对应的SQL原模原样补到样例里去。
它一开始甚至显得很笨。
你得先把指标配进去,把维度配进去,把别名配进去,把口径配进去。
你还得问业务方:你这个“新增”到底按哪个日期?你这个“金额”到底是哪种金额?你这个“中标”到底以哪个字段为准?
这时候你会发现,NL2SQL最烦人的地方根本不是SQL。
是业务本来就没讲清楚。
以前这些东西藏在Excel、口头约定、老员工经验里。人查数的时候会自动脑补,系统不会。你要让机器回答,就得把这些东西写下来。
所以新方案慢,很慢,有时候甚至有点折磨。
但它有一个好处:出事的时候能查。
这个指标从哪张表来,用哪个字段聚合,默认带了什么过滤条件,为什么识别成这个口径,SQL怎么拼出来的,都能往回追。
老方案像是在先把火救下来。
新方案则是承认这东西以后要长期活着,所以先把骨架搭出来。
后来才发现它确实像智能体
有人可能会说:你这不就是NL2SQL么,为什么叫智能体?
如果只是:
用户问题 -> 大模型 -> SQL
那它确实只是NL2SQL。
但真实项目不是这样。
真实项目里要做:
上下文处理
意图识别
工具选择
权限判断
语义解析
SQL构建
SQL校验
数据库执行
结果解释
图表展示
日志校验
澄清确认
错误兜底
这些东西堆在一起,其实就已经不是单纯的NL2SQL了。
只是老项目里,我当时没这个词,也不会包装它。它就是一套土办法:把用户问题接收,拆一拆,查一查,错了兜一下,尽量给人一个能看的结果。
所以我一直叫他数据问答机器人,智能问答机器人。
到了新项目里,这套东西开始从“能跑”往“可控、可追踪、能维护”走。
老项目像这样:
模型很强,我写提示词让它尽量别乱来。
新项目像这样:
模型很强,但我只让它做适合它做的部分。
这个变化对我影响挺大。
我以前总想让模型多干一点活,后来发现不能这么贪心。模型一多干活,就开始自由发挥。能写死在程序里的,能放进配置里的,能让后端校验掉的,就不要交给它猜。
后面才反应过来的几件事
1.模型自己想,真的很爽,也真的很吓人
大模型很聪明,但生产系统不能靠聪明活着。
特别是数据问答。
知识问答答错了,用户还能去原文里看依据。
数据问答答错了,用户可能直接拿去汇报。
没事就皆大欢喜,出问题就吃投诉。
候选列表、配置边界、SQL模板、参数化执行,这些东西听起来很不AI,但该上还是得上。
2.口径藏在提示词里,迟早会反噬
提示词里的规则很容易丢。
今天你改了一句提示词,明天结果变了,你很难解释到底变在哪里。
业务口径要沉淀到表里,至少以后追问题的时候能说清楚:这个指标叫什么,用哪张表,聚合哪个字段,默认带了什么过滤条件,有哪些口径版本,支持哪些维度。
能回答这些,系统才有点长期维护的样子。
3.该反问就反问
很多时候,系统反问不是因为它笨,而是因为问题本身不完整。
用户说“金额”,但系统里有十种金额。
用户说“项目”,但系统里有建设项目、商机转项目、合同关联项目。
你硬答一个借个,看起来很流畅,其实是在赌。
赌赢了,用户夸你智能。
赌输了,用户说你乱讲。
所以一定要让模型再次向用户确认:
你要的到底是什么项目,是建设项目、商机转项目还是合同关联项目?
4.样例不是没用,但是不能全靠样例
新方案不是不要样例。
样例依旧有用。
只是样例不应该承担全部。
老方案里,样例像发动机。
新方案里,样例更像校准器。
它用来补一些复杂问法、明细查询、模糊检索、TopN、范围比较,而不是让系统所有能力都靠样例撑着。
5.页面怎么展示也会影响信任感
NL2SQL的终点不是SQL。
甚至不是数据库结果。
终点是用户能不能看懂。
老项目里会让模型解释查询结果,也会生成Echarts。
新项目里更强调结构化返回:指标卡片、表格、趋势、明细预览。
用户问“今年有多少”,你给他一个单值就够了。
用户问“按城市分布”,你给他表格或柱状图。
用户问“近30天趋势”,你给他时间序列。
别把所有结果都塞成一段大模型作文。
大模型作文看起来很吊,但业务人员要的是可核对、可复制、可追问。
写到最后
写到这里,其实我最想说的是,我以前对NL2SQL的理解太片面了。
一开始会觉得,用户问一句,模型写SQL,程序执行,再让模型把结果解释一下,这不就完了吗?
真做起来才发现,爽是真的爽,糟心也是真的糟心。
后来我开始往提示词里加规则,加表结构,加样例,加字段校验,加权限,加错误兜底。系统确实越来越能跑,但每次补丁打上去,都心里默念,这辈子不会再加了。
但是你不知道下一个奇怪问题会从哪里冒出来,也不知道某个提示词改动会不会把另一个场景带崩。
再后来我才慢慢接受一件事:模型不要直接碰SQL。它更适合做自然语言理解,帮我从候选指标、维度、别名、口径里选东西;真正的校验、拼SQL、执行、审计,还是放回程序和配置里。
当然,这不是说新方案完美。
它依旧有很多限制,比如当前更适合结构化聚合查询,更适合单表宽表,更依赖业务侧把指标维度梳理清楚。复杂关联查询、自由明细检索、长文本条件、跨域分析,后面还是要继续补。
但它至少从“能跑”走向了“能管”。
对企业应用来说,这个变化比炫技重要。
如果现在再有人问我怎么做NL2SQL,我可能不会先问他模型选哪个。
我会先问:业务指标清楚了吗?字段含义清楚了吗?时间口径清楚了吗?枚举值和别名清楚了吗?结果出错以后能不能往回查?
这些问题没想清楚,大模型越聪明,系统反而可能越危险。
因为它会把一个不清楚的业务,包装成一个看起来很清楚的答案。
这才是NL2SQL最让我后怕的地方。
很菜,别骂。
这就是我从2025年的早期数据问答助手,到2026年的新版数据问答系统,关于数据问答和NL2SQL的一点实践复盘。
一修,美化了一下语句,记录一下。
二修,我是老资历,请问你是?给老资历跪下!