一、Text2SQL 是啥?比赛又是咋回事?
先说说 Text2SQL
简单来说,Text2SQL(或者叫 NL2SQL)就是把我们平时说的话(自然语言)自动翻译成数据库能懂的查询语言(SQL)。这样一来,就算你完全不懂 SQL,也能直接问数据库问题,拿到你想要的数据。这技术能:
-
降低数据查询门槛:让业务同学、运营人员也能轻松自己查数据。 -
提高工作效率:数据分析师不用再吭哧吭哧写那么多 SQL 了。 -
让数据更普及:公司里更多人能方便地用上数据。 -
改善 BI 工具体验:在报表平台里提问查数,更自然。
总之,商业价值挺大的。
我参加的比赛
我参加的是“2024 金融行业・大模型挑战赛”。这个比赛由清华大学基础模型研究中心主办,智谱 AI 提供支持,聚焦大模型在金融领域的应用。
比赛方提供了挺“豪华”的金融数据集:77 个数据表,3000 多个字段,涵盖 A 股、港股、美股、基金、财务报表等等。
题目也分了难度:初级(简单查个数)、中级(带点统计分析)、高级(复杂的金融分析),挺全面地考察我们用大模型处理金融数据的能力。
感兴趣可以去官网看看:(2024金融行业·大模型挑战赛) https://competitions.zhipuai.cn/matchDetail?id=120241202000000003
二、Text2SQL 难在哪?(都是痛点)
搞 Text2SQL 并不容易,我总结了几个主要的难点:
2.1 找对表和字段太难(DB召回难)
这是第一道坎。用户问个问题,模型得先弄明白该去哪些表里、找哪些字段才能回答。
-
找少了:缺胳膊少腿,没法生成正确的 SQL。 -
找多了:信息太杂,模型容易“看花眼”,选错字段。 -
数据库太大:几十上百个表,关系错综复杂,理清它们本身就很难。 -
术语对不上:用户说的话和数据库里的专业名词 spesso (often) 不是一回事。
比如,用户问“最近三年盈利能力最强的科技公司”,模型得知道去哪找“科技公司”的定义,怎么关联“财务数据”,还得理解啥叫“盈利能力”,涉及哪些字段,怎么计算。
2.2 听懂人话不容易(用户提问理解难)
用户的提问方式千奇百怪,而且往往不够精确:
-
省略信息:问“去年营收多少?”,没说哪个公司;问“对比一下?”,没说跟谁比,比什么。时间、排序、范围经常省略。 -
术语模糊:“表现好”可以指股价涨得多、营收增长快,也可能是别的指标。 -
指代不清:多轮对话里,“这些公司”、“它的竞争对手”到底指谁? -
追问太短:“那增长率呢?”单独看完全不知道问啥,必须结合上下文。 -
条件变化:聊着聊着,用户可能会改主意,模型得知道哪些条件要更新,哪些保留。
用户很少一次就把需求说明白,都是边问边想,这对模型的理解力是巨大考验。
2.3 复杂问题不好解(复杂问题求解难)
特别是金融领域,很多问题不是一条简单 SQL 能搞定的:
-
计算复杂:比如“计算连续三年净利润增长的公司的平均股价”,得分好几步。 -
逻辑嵌套深:各种 WHERE,GROUP BY,HAVING嵌套,还得用聚合函数。 -
时间序列分析:同比增长、环比变化、算 N 日平均线等,需要跟历史数据比。 -
多表关联路径多:从 A 表到 C 表,可以通过 B 表,也可以通过 D 表,选哪条路? -
中间结果难处理:第一步查出来的结果,怎么在第二步 SQL 里用?
这类问题需要模型有规划能力,把大问题拆成小步骤去执行。
2.4 领域知识门槛高(领域知识与业务理解难)
光懂技术不行,还得懂业务:
-
指标计算有讲究:PE、PEG、ROE 这些金融指标有特定公式,不是数据库字段直接相加。 -
行业术语得翻译:“高成长”、“价值洼地”这些概念怎么对应到数据查询? -
数据结构藏着业务逻辑:财务报表里的资产负债表、利润表、现金流量表之间怎么关联? -
业务规则影响 SQL:比如交易日怎么判断?涨跌停限制怎么处理?财报啥时候发布? -
术语有多义性:同一个词在不同场景下意思可能不同。
缺了领域知识,就算 SQL 语法没错,结果也可能完全没意义。
2.5 系统又慢又不稳(系统效率与鲁棒性难)
要把 Text2SQL 做成好用的系统,工程挑战也不小:
-
大模型太“贵”:每次调用都要消耗 Token,上下文长了成本就高。 -
复杂 SQL 跑得慢:数据库大了,一个复杂查询可能半天没响应。 -
模型老犯错:生成的 SQL 可能语法不对,或者逻辑有问题,需要自我修复。 -
用户输入“不讲武德”:各种奇怪的问法、边缘情况都得能处理。 -
模型产生“幻觉”:瞎编不存在的表或字段,或者给出错误答案。
一个靠谱的系统,不仅要准,还得快、稳、省钱,能扛住各种压力。
三、我们是怎么解决的?(一些思路和方案)
面对上面这些难点,我们在比赛中尝试了一些解决方案,效果还不错:
3.1 应对 DB 召回难:两套组合拳
怎么让模型在茫茫多的表和字段里找到对的?我们试了两种方法:
3.1.1 思路一:让 LLM 层层筛选(多级召回)
这就像“缩小包围圈”,让 LLM 分几步走:
-
第一步:根据问题,先选出最可能相关的几个数据库(如果有多库)。 -
第二步:在选中的库里,再挑出相关的数据表。 -
第三步:最后,从这些表里选出需要的字段。
-
好处:避免信息爆炸(不用一次把所有 schema 给 LLM),省 Token,思路比较清晰。 -
坏处:对 LLM 理解能力要求高(得懂业务),错误会累积(第一步错了后面全完),得多调几次 LLM,慢。
3.1.2 思路二:LLM 假扮用户提问 + 向量检索(推荐!)
我们发现,直接把表名、字段名和它们的描述(比如 revenue_growth_rate: 营收增长率)做成向量,然后去检索,效果很差。
灵光一闪的创新点:让 LLM 帮忙“编”问题!
-
让 LLM 给每个字段写“模拟问题”:想象一下用户可能会怎么问到这个字段?
-
“哪些公司增长最快?” -
“业绩提升明显的企业有哪些?” -
“销售额上涨幅度大的公司?” -
“营收增速如何?”
-
比如 revenue_growth_rate这个字段,用户可能问: -
我们让 LLM 自动生成很多这类问题。
-
向量化这些“模拟问题”:注意,是向量化这些问题,而不是原始的字段描述。
-
用户提问时,用向量检索:用户的真实问题进来后,我们去匹配最相似的“模拟问题”,就能找到背后对应的字段了。
聪明的召回结果整合
直接按字段分数排序选 top N 个字段?效果不好。因为一个模拟问题可能对应多个字段,而且有些表可能整体相关性很高,但单个字段分数不算最高。
我们的做法是:
-
表级别聚合:把每个字段的得分,加权算到它所属的表上。 -
两层筛选:先选出得分最高的 Top N 个表,再从这几个表里,分别选出得分最高的 Top M 个字段。 -
平衡与控制:这样既保证了相关表的覆盖度,又控制了总字段数量,不会太多也不会太少。
对比一下
-
方案一(多级召回)得调两次 LLM 才确定表,而且可能把表里所有字段都选进来,信息还是太多。 -
方案二(向量检索)一次召回就能拿到比较精准的候选表和字段列表,效率高,字段数量也更可控。
两种方案都需要“最后确认”:无论用哪种方法召回,最后都需要再让 LLM 从候选表和字段里,最终确认哪些是真正回答问题所必需的。这一步很关键,因为无关字段太多会严重干扰 LLM 生成 SQL。我们的向量检索方案因为前期召回更准,最后这一步的压力也小很多。
3.2 应对用户提问理解难:简单粗暴有时更好
多轮对话里,用户经常问得很简洁,比如前面问了 A 公司的营收,接着问“那增长率呢?”。
方案一:让 LLM 重写问题(尝试过,效果一般)
我最初的想法是:
-
把历史对话和用户的新问题一起喂给 LLM。 -
让 LLM 生成一个包含了所有上下文信息、能独立理解的“完整问题”。
比如,把“那增长率呢?”,根据上下文改写成类似“A 公司的营收增长率是多少?”。
但是,这个方法有坑!
-
LLM 有可能在重写时理解错,歪曲用户的原意。 -
一旦问题理解错了,后面生成的 SQL 肯定也是错的,就像导航一开始就设错了目的地。
方案二:直接拼接上下文(推荐!)
后来发现,一个更简单直接的方法效果反而更好:
-
把历史的几轮问答,原封不动地拼在用户新问题的前面。 -
不做任何改写,直接把“聊天记录”丢给 LLM。 -
让 LLM 自己从完整的对话历史中去理解用户当前问题的意图。
实践证明:
-
避免了“问题重写”引入的误解风险。 -
信息是原汁原味的,最全。 -
减少了中间处理环节,降低了出错概率。
这个经验告诉我们:处理多轮对话理解,有时保持信息的完整性比做过多“聪明”的加工更靠谱。
3.3 应对复杂问题:让 AI “边想边做”(迭代思考框架)
遇到那种一步到位的 SQL 很难写出来、甚至写不出来的复杂问题,怎么办?我用了一个简单但很有效的迭代思考框架。
构建“思考 → 执行 → 再思考”的循环
就像我们人解决复杂问题一样,我让 AI:
-
先思考(Think):分析用户问题,判断我现在需要哪些信息?第一步该查什么? -
写个 SQL(Write SQL):不用想着一步登天,先写个 SQL 查一部分需要的数据。 -
执行 SQL 看结果(Execute SQL & Observe):看看查回来的数据是啥。 -
根据结果再思考(Think Again):好了,现在我知道了这些信息,下一步还需要什么?再写个 SQL 去查?还是信息够了,可以回答用户了?
这个过程可以循环几次,直到 AI 认为信息足够回答用户问题为止。
让 AI 把思考过程“说出来”
关键是让模型在每一步都明确地输出它的思考过程,比如:
-
"根据用户问题和已知信息,我现在需要查询 X 表的 Y 字段,来获取 Z 信息。" -
"上一步查询结果是 A,但这还不够,我还需要结合 B 表的 C 字段,下一步准备执行…" -
"信息已经足够,我现在可以整合结果回答用户了。"
通过这种方式,AI 能更好地拆解复杂任务,一步步逼近答案。
为什么这方法有效?
-
化繁为简:把一个大难题拆成一连串小问题。 -
带纠错功能:每一步都能看到中间结果,如果发现有问题(比如查出来是空的,或者结果不对),可以及时调整策略。 -
逻辑更清晰:每一步的思考和 SQL 都很明确,方便调试和优化。 -
模拟人类思维:更符合我们解决问题的直觉。
对于需要多表连接、复杂计算、多重条件嵌套的问题,这个“边想边做”的方法特别管用。
3.4 应对领域知识难:给模型“喂”知识
模型不懂金融术语和业务规则怎么办?硬塞肯定不行,得讲究方法:
3.4.1 通用术语解释放 Prompt 里(少而精)
一些最常用、最基础的业务术语,可以直接加到 System Prompt 里。
-
原则:只加通用且必要的。比如解释一下什么是“交易日”。 -
注意:别加太多! 无关的解释不仅浪费 Token,还可能分散模型注意力,让它忽略了核心指令。实测发现,Prompt 里塞太多东西,效果反而会变差。
3.4.2 给数据库字段加“注释”(按需加载)
这是个好方法:在定义数据库 Schema 时,就给重要的字段加上详细的业务含义解释。
-
比如 PE_TTM字段,注释可以写:“滚动市盈率,衡量股价相对于每股收益的倍数,常用于估值”。 -
当这个字段被前面的召回步骤选中后,我们自动把它的注释补充到 Prompt 里。 -
好处:针对性强,不浪费 Token,只在需要时提供相关知识。
3.4.3 用 SQL 样例做 Few-shot(最有效!)
这招效果最好:提前准备一些典型问题的 SQL 样例。当用户提出类似问题时,动态地把最相似的几个 SQL 样例(问题+SQL)塞到 Prompt 里,作为示例(Few-shot)给模型看。
为什么有效?
-
光用文字解释“你应该怎么思考怎么关联表”,模型很难学会。 -
但给它看一个具体的、相似问题的正确 SQL,模型就很容易“照猫画虎”,举一反三。
通过这几种方法组合,就能比较好地解决领域知识不足的问题了。
3.5 应对效率和鲁棒性难:省钱还得能扛事
怎么让系统跑得快点、稳点、省点钱?
3.5.1 省 Token 的召回策略
前面提到的 DB 召回方案其实已经在省 Token 了:
-
多级召回:避免一次性加载所有 Schema。 -
向量检索:更精准地筛选少量相关表和字段给 LLM。
这两个都是为了在保证效果的前提下,尽量减少喂给大模型的信息量,从而降低 Token 消耗。
3.5.2 提高稳定性的“安全带”
在上面提到的“迭代思考”框架里,我们加了几道保险:
-
设置最大迭代次数:防止模型陷入死循环(比如因为模型能力不足、数据有问题、或者召回不全导致一直没法得到最终结果),迭代超过一定次数就强制终止。这个很重要,因为总有意外情况。 -
SQL 执行结果必须反馈:无论 SQL 执行成功、失败还是超时,都要把完整的执行信息(包括错误信息)反馈给 LLM。这样模型就有机会知道自己上一步 SQL 写的有问题,从而进行自我纠错。这对鲁棒性至关重要。 -
检测“复读机”行为并熔断:我们发现 LLM 有时会“卡壳”,连续几次生成完全一样的、错误的 SQL。我们加了个检测机制,如果发现模型连续重复犯同一个错误,就直接熔断,不再继续尝试,避免无效消耗。
这些机制就像安全带,保证了系统在遇到各种异常时,不会彻底崩溃。
4. 代码也开源了!
为了方便大家交流学习,我把这次比赛用到的核心代码框架整理开源了。
代码仓库
GitHub 链接:(FinGLM2-semi-final)
https://github.com/Jinglever/FinGLM2-semi-final
代码结构

系统架构


