【实践分享】25年到26年,我的NL2SQL智能体是如何迭代的

【实践分享】25年到26年,我的NL2SQL智能体是如何迭代的 前言 之前写过一篇《如何手搓一个RAG》,当时主要讲的是知识库问答。 引流: https://linux.do/t/topic/2187051 这篇讲另一个更痛苦的东西:NL2SQL。 先叠甲: 我不是底层模型开发,也不是论文选手,我是...
【实践分享】25年到26年,我的NL2SQL智能体是如何迭代的
实践分享】25年到26年,我的NL2SQL智能体是如何迭代的

实践分享】25年到26年,我的NL2SQL智能体是如何迭代的

前言

之前写过一篇《如何手搓一个RAG》,当时主要讲的是知识库问答。

引流:https://linux.do/t/topic/2187051

这篇讲另一个更痛苦的东西:NL2SQL。

先叠甲:

  1. 我不是底层模型开发,也不是论文选手,我是AI应用开发,现在人们叫AI应用落地工程师(有点想补一个黄豆流汗)。

  2. 这篇不讲特别高大上的理论,主要讲我自己从老项目到新项目的工程迭代。

  3. 里面很多东西可能现在看起来很土,很笨,很原始。

因为很多东西不是一开始就长成现在这个样子的。

现在大家都很懂智能体、tool call、workflow、planner、memory、MCP,但我第一次认真把这套东西串起来的时候,企业应用里还没有这么成熟的说法。那时候更多叫机器人、助手、问答系统,或者更朴素一点:大模型接口外面套一圈业务逻辑。

先说时间线

老项目是一个早期业务问答助手,这里就不写真实项目名了。

下面会附上老项目的git记录,其实实际时间比git更早,但有git记录就已经能说明老了。

老项目的Java服务模块最早能追到:

2025-08-04 11:00:49  早期数据问答基线上传

Python智能问答模块也是同一天开始:

2025-08-04 11:00:49  早期数据问答基线上传

然后到了2025-08-14,提交记录里已经出现了这种东西:

意图识别时校验问题中的数据表是否存在
生成SQL时判断问题中的字段是否存在
生成SQL提示词优化,增加自校验逻辑
SQL执行错误给出友好提示

再到2025-08-19,又有:

报表问答准确率提升:修改生成SQL提示词结构,采用少样本提示的标准写法

也就是说,至少在2025年8月,这个东西已经不是“我接个模型接口玩一下”了,而是在认真处理意图识别、表识别、字段校验、SQL生成、错误兜底这些问题。

新项目是2026年重做的一套业务智能体系统。

它的项目初始化是:

2026-04-20 14:45:08  项目初始化

数据问答正式落到新项目里,是:

2026-04-23 15:44:44  新增数据问答前后端能力并完成会话链路语义解析接口与数据库表落库

所以这篇讲的不是“我最近看agent火了,赶紧包装一个概念”。

更准确地说,是:

我在智能体概念还没有被企业应用完全标准化的时候,先用一种很土的方式把它做出来了;然后到2026年,我又把这套土办法重新工程化了一遍。

老数据问答:先把链路跑通

老数据问答主要看两个部分:

  1. Java服务模块

  2. 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生成。每次演示就好像上战场一样。

当时的思路很直接:

  1. 根据用户问题识别要查哪张表。

  2. 找到这张表的字段说明和相似问法。

  3. 把表结构、字段含义、few-shot样例、SQL生成规则一起塞给模型。

  4. 让模型返回SQL。

  5. 程序再把SQL抠出来执行。

这里的核心其实是few-shot。

例如“今年杭州新签约合同数量是多少”这种问题,对人来说很简单,对模型来说不一定简单。因为它要知道:

  1. “今年”对应哪个时间字段。

  2. “新签约”对应哪个状态或日期。

  3. “杭州”对应城市字段,而且可能还要处理“杭州市”“杭州分公司”这种说法。

  4. “数量”是count,不是把某个金额字段求和。

所以当时靠大量样例去教模型。

这条路是有效的。

但它有一个问题:越做越像补丁。

你发现“城市”识别不准,就加城市规则。

你发现“字段不存在”会乱生成,就加字段存在性校验。

你发现SQL报错太难看,就包装友好提示。

你发现用户想看图,就让模型再生成Echarts配置。

你发现“省、市、区县”容易混,就再写一个地名修正。

最后系统当然能跑,而且效果还可以。但你心里知道,这东西有点像用胶带把飞机粘起来。

飞是能飞。

但每次上线都要默念一句:千万别问奇怪问题。

老在哪里

这里我要强调一下,“老”不是贬义。

很多老办法在当时就是正确答案。

因为业务要结果,用户要能用,领导要演示,项目要交付。你不可能坐在那里说:等我设计一个完美语义层,半年后再说。

不现实。

老项目确实就是有很明显的时代痕迹。

第一,模型直接生成SQL

老链路里,模型承担的任务太重了。

它不仅要理解用户问题,还要理解表结构,还要选择字段,还要生成SQL,还要自己判断字段是否存在。

这就像让一个实习生同时当产品经理、数据库工程师、测试和客服。

他有时候很聪明,有时候也真的离谱。

最简单的例子:字段名。

你给他一张表,里面有“合同金额”“签约金额”“中标金额”“预算金额”。用户问“金额是多少”,他到底该用哪个?

如果业务口径稳定,模型可能猜对。

如果业务口径不稳定,模型猜对了也只是运气。

第二,提示词越来越长

为了让模型不犯错,我们会不断往提示词里加规则。

不能select *

日期要怎么转换。

城市要去后缀。

字段别名不能当查询条件的值。

枚举值模型不能自己发明。

SQL生成前后要判断字段是否存在。

SQL生成后再自检一下字段是否存在。

听起来很严谨,对吧?

但提示词不是法律。

提示词更像劝告。

你说了一百条规则,模型不一定真的每条都遵守。特别是表结构一长、样例一多、用户问题一复杂,它就开始挑自己记得住的部分执行。

这就是老链路让人很痛苦地方。

第三,解析靠字符串和正则

老项目里会从模型返回中提取JSON、提取SQL、提取Echarts配置。

这也是当时很常见的做法。

模型返回:

```sql
select ... from ... 
```

我们就用正则把他抠出来。

模型返回:

```json
{"sql": "select ..."} 
```

我们就转JSON。

问题是,模型有时候会多说一句“好的,以下是SQL”。

也有时候会少一个引号。

也有时候会把中文标点、代码块、换行混在一起。

然后你就开始写各种清洗逻辑。

写到最后,你都分不清自己是在做NL2SQL,还是在做大模型输出垃圾回收。

第四,安全边界后置

老链路也做了不少安全处理,比如权限校验、表名校验、SQL错误兜底、结果数量限制。

但整体上,它还是先让模型生成SQL,再在后面拦。

这个顺序在生产里会带来心理压力。

因为模型生成SQL这件事本身就是不稳定的。

你可以拦掉deleteupdatedrop,也可以限制只查某些表,但只要SQL是模型自由生成的,它就总有一些奇怪路径。

比如字段错了。

比如条件错了。

比如时间字段错了。

比如聚合口径错了。

这些不一定是安全事故,但一定是业务事故。

用户不一定知道SQL错了,他只会觉得你的机器人在胡言乱语。

第五,业务口径藏在提示词里

这是我后来最深的感受。

NL2SQL最难的不是SQL。

是业务口径。

“今年新增项目数”到底按创建时间、立项时间、签约时间,还是入库时间?

“中标金额”用预算金额、中标公告金额,还是合同签约金额?

“商机数量”算草稿、已发布、已中标,还是全部?

这些东西不应该藏在提示词里。

因为提示词不适合承载业务制度。

提示词适合表达任务,业务口径应该进配置、进表、进规则、进校验的结构。

这也是我从老项目走到新项目,最大的变化。

新数据问答:从让模型写SQL,到让模型填语义

新项目是一套面向具体业务场景的智能体系统。

这时候它已经不再是单点机器人,而是一个统一入口。

知识问答、数据问答、首页意图识别、知识库管理、快捷问题,都在一个体系里。

数据问答这块,新链路大概是这样:

用户提问
-> 会话上下文补全
-> 大模型解析结构化语义
-> 后端规则兜底与合并
-> 指标/维度/别名/口径匹配
-> 判断是否需要澄清或确认
-> 构建统一语义对象
-> 校验语义对象
-> 后端生成受控SQL
-> PreparedStatement执行
-> 构建指标卡片、表格、趋势、明细预览
-> 记录意图、语义、SQL、参数、结果行数
-> 流式返回前端

注意这里最关键的变化:

模型不再直接生成SQL。

模型做的是语义解析。

它只能基于后端给出的候选列表,选择指标、维度、过滤条件、时间规则、分析类型。

也就是说,模型从“写SQL的人”,变成了“帮用户填表的人”。

这一步非常关键。

四表语义层

新项目里数据问答有四张核心配置表:

  1. DQ_METRIC

  2. DQ_DIM

  3. DQ_ALIAS

  4. DQ_METRIC_VER

可以把它们理解成四层:

METRIC:用户要查什么指标
DIM:用户按什么过滤、按什么分组
ALIAS:用户自然语言怎么映射到系统编码
METRIC_VER:同一个指标有哪些业务口径

举个例子。

用户问:

近30天中标商机数量是多少?

老链路可能会让模型直接生成:

select count(*) from 某张表 where ...

新链路会先变成一个语义对象:

指标:商机数量
口径:中标
时间:近30天
分析类型:summary
过滤条件:无

然后后端再根据配置生成SQL。

这样做的好处是,SQL不再是模型凭空写出来的。

它是业务配置推导出来的。

指标和口径分开

老项目里,“项目数量”“新增项目数量”“中标项目数量”“已签约项目数量”这些东西,很多时候会混在提示词和样例里。

新项目里,我更倾向于拆开:

指标:项目数量
口径:新增/中标/已签约/履约中

这看起来只是表设计变化,但实际影响很大。

因为用户说“数量”的时候,他可能问的是同一个指标;用户说“新增”“中标”“已签约”的时候,他问的是不同业务口径。

如果把这些都交给模型猜,模型迟早会猜错。

如果放到METRIC_VER里,就能让口径变成可维护的配置。

用户问得不清楚时,系统还能反问。

这比胡乱给一个答案要好得多。

别名是NL2SQL的命门

我后来越来越觉得,NL2SQL不是“自然语言转SQL”。

更准确地说,它是:

自然语言里的业务说法,映射到数据库里的结构化对象。

这里最麻烦的是别名。

用户不会按数据库字段名说话。

用户会说:

中标
赢单
签了
集团客户
客户经理
近一月
这个月
本季度

这些东西都得落到系统能识别的编码。

所以新项目里有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

同时还有几条硬性边界:

  1. 只允许SELECT

  2. 拒绝DELETE/UPDATE/INSERT/MERGE/ALTER/DROP/TRUNCATE

  3. 必须有来源宽表。

  4. 必须有指标字段。

  5. 必须有时间字段和时间范围。

  6. 当前只支持单表宽表查询。

这就从根上改变了系统性质。

老项目是“模型生成SQL,程序尽量拦”。

新项目是“模型生成语义,程序决定SQL”。

一个是让模型开车,人在副驾驶踩刹车。

一个是人把轨道铺好,模型只负责把乘客放到正确车厢。

两代方案的本质区别

如果只看功能,两代数据问答都能做到:

  1. 用户自然语言提问。

  2. 系统查数据库。

  3. 返回总结。

  4. 必要时展示图表或表格。

但本质区别很大。

老方案:提示词工程驱动

老方案的核心资产是:

  1. 表结构说明。

  2. few-shot样例。

  3. SQL生成规则。

  4. 错误兜底逻辑。

  5. 一堆经验补丁。

它的优点是快。

一张表给出去,写一些样例,很快就能跑。

它的缺点也很明显:

  1. 表越多,提示词越膨胀。

  2. 字段越多,模型越容易选错。

  3. 口径越复杂,样例越难覆盖。

  4. 安全边界偏后置。

  5. 线上效果高度依赖模型稳定性。

所以老方案很适合从0到1。

它解决的是“有没有”。

新方案:语义层驱动

新方案的核心资产是:

  1. 指标配置。

  2. 维度配置。

  3. 别名配置。

  4. 口径配置。

  5. 后端SQL构建器。

  6. 可校验的意图、语义、SQL日志。

它的优点是稳。

因为大模型不再直接碰SQL。

它的缺点是慢。

不是运行慢,是建设慢。

你要先梳理指标、维度、别名、口径。你要问业务方:这个字段到底怎么算?这个“新增”到底按哪个日期?这个“中标”到底按哪个状态?

这时候你会发现,NL2SQL不是技术问题。

它会把业务系统里所有没讲清楚的口径问题,全部挖出来。

这个过程很痛苦,但这是好事。

因为以前这些问题也存在,只是藏在Excel、口头约定和老员工经验里。

现在你要让机器回答,就必须把它们写下来。

为什么说这是智能体迭代

有人可能会说:你这不就是NL2SQL么,为什么叫智能体?

我的理解是这样的:

如果只是:

用户问题 -> 大模型 -> SQL

那它确实只是NL2SQL。

但真实项目不是这样。

真实项目里要做:

上下文处理
意图识别
工具选择
权限判断
语义解析
SQL构建
SQL校验
数据库执行
结果解释
图表展示
日志校验
澄清确认
错误兜底

这些东西组合起来,就已经是一个面向业务目标的智能体链路了。

只不过老项目里,它是“土法智能体”。

新项目里,它开始变成“工程化智能体”。

老项目像这样:

模型很强,我写提示词让它尽量别乱来。

新项目像这样:

模型很强,但我只让它做适合它做的部分。

这是我的变化。

模型擅长理解自然语言、匹配候选、补全表达。

程序擅长执行规则、校验边界、生成稳定SQL。

让模型做模型擅长的事,让程序做程序擅长的事。

这就是我这一年最大的工程体感。

我踩过的几个坑

1.不要迷信“让模型自己想”

大模型很聪明,但生产系统不能靠聪明活着。

特别是数据问答。

知识问答答错了,用户还能去原文里看依据。

数据问答答错了,用户可能直接拿去汇报。

这就很吓人。

候选列表、配置边界、SQL模板、参数化执行,这些东西都要上。

2.不要把业务口径写死在提示词里

提示词里的规则很容易丢。

今天你改了一句提示词,明天结果变了,你很难解释到底变在哪里。

业务口径最好进表。

这样至少能回答:

  1. 这个指标叫什么。

  2. 它用哪张表。

  3. 它聚合哪个字段。

  4. 它默认带什么过滤条件。

  5. 它有哪些口径版本。

  6. 它支持哪些维度。

这才像一个能长期维护的系统。

3.不要觉得交互很丢人

很多时候,系统反问不是因为它笨,而是因为问题本身不完整。

用户说“金额”,但系统里有十种金额。

用户说“项目”,但系统里有建设项目、ICT项目、商机转项目、合同关联项目。

你硬答一个,看起来很流畅,其实是在赌。

赌赢了,用户夸你智能。

赌输了,用户说你乱讲。

4.训练样例还是有用,但位置变了

新方案不是不要样例。

样例依旧有用。

只是样例不应该承担全部。

老方案里,样例像发动机。

新方案里,样例更像校准器。

它用来补一些复杂问法、明细查询、模糊检索、TopN、范围比较,而不是让系统所有能力都靠样例撑着。

5.前端展示不是小事

NL2SQL的终点不是SQL。

甚至不是数据库结果。

终点是用户能不能看懂。

老项目里会让模型总结结果,也会生成Echarts。

新项目里更强调结构化返回:指标卡片、表格、趋势、明细预览。

用户问“今年有多少”,你给他一个单值就够了。

用户问“按城市分布”,你给他表格或柱状图。

用户问“近30天趋势”,你给他时间序列。

不要把所有结果都塞成一段大模型作文。

大模型作文看起来很像人,但业务人员要的是可核对、可复制、可追问。

总结

回头看这两代数据问答,我觉得自己的思路经历了三个阶段。

第一阶段:

让模型帮我写SQL。

这是最直觉的NL2SQL。

用户问一句,模型写SQL,程序执行,模型总结。

很爽,但爽完一塌糊涂。

第二阶段:

让模型在规则里写SQL。

加表结构、加样例、加字段校验、加权限、加错误兜底。

这时候系统已经能用了,但维护成本会越来越高。

第三阶段:

让模型只负责语义,SQL由系统生成。

指标、维度、别名、口径进配置。

模型做自然语言理解。

后端做校验和执行。

系统输出可审计结果。

这才是我现在更认可的方向。

当然,这不是说新方案完美。

它依旧有很多限制,比如当前更适合结构化聚合查询,更适合单表宽表,更依赖业务侧把指标维度梳理清楚。复杂关联查询、自由明细检索、长文本条件、跨域分析,后面还是要继续补。

但它至少从“能跑”走向了“能管”。

对企业应用来说,这个变化比炫技重要。

最后说一句很真实的话:

如果你现在想做NL2SQL,不要一上来就问模型选哪个。

先问自己:

  1. 你的业务指标清楚吗?

  2. 你的字段含义清楚吗?

  3. 你的时间口径清楚吗?

  4. 你的枚举值和别名清楚吗?

  5. 你的结果能校验吗?

这些问题没想清楚,大模型越聪明,系统可能越危险。

因为它会把一个不清楚的业务,包装成一个看起来很清楚的答案。

这才是NL2SQL最可怕的地方。

很菜,别骂。

这就是我从2025年的早期数据问答助手,到2026年的新版数据问答系统,关于数据问答和NL2SQL的一点实践复盘。

先发后改。

2 个帖子 - 2 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文