微信扫码
添加专属顾问
我要投稿
探索自然语言查询数据库的新方式,深入了解Text-to-SQL技术及其应用场景。 核心内容: 1. Text-to-SQL技术定义及其重要性 2. Text-to-SQL的应用场景与挑战 3. Text-to-SQL技术发展阶段及实现方法
在各个企业数据量暴涨的现在,Text-to-SQL越来越重要了,所以今天就来聊聊Text-to-SQL。
Text-to-SQL是一种将自然语言查询转换为数据库查询的技术。它可以让用户通过自然语言来查询数据库,而不需要编写复杂的SQL语句。
我将Text-to-SQL的应用场景按照以下两个维度进行划分:
这里说的复杂除了是说SQL本身的复杂程度,比如要求生成的SQL语句中需要包含多个表的关联,多个子查询,或者需要包含复杂的聚合操作等。也体现在数据的复杂上面,数据库本身有大量的表,每个表有大量的字段,每个字段有大量的数据。这两点复杂性给Text-to-SQL带来了很大的挑战。
准确性则很好理解,就是能正确反映Text的内容,不要出错。
我们期望的Text-to-SQL肯定是右上角这种,既能生成复杂的SQL,又能保证准确性。但是目前来说,受限于知识工程的匮乏以及模型的能力,目前还没有一个完美的Text-to-SQL的解决方案。
左上角的区域是类似辅助SQL开发的场景,这种场景下,我们期望的是能生成一个SQL的草稿,然后我们再根据这个草稿进行修改,从而生成一个准确的SQL。由于有Human in the loop,所以这种场景下,Text-to-SQL的准确性要求相对较低,但是生成SQL的复杂程度要求较高,需要能快速出一版大概能用的SQL。
右下角的区域则是偏业务人员基于已经过滤得比较干净的数据,进行自助的报表查询、数据查询、数据分析的场景。由于业务人员没有读写SQL的技能,所以这种场景下,Text-to-SQL的准确性要求较高,但是目前的技术没法做到同时保证生成SQL的复杂程度和准确性。所以只能退而求其次,让业务人员先使用右下角区域的Text-to-SQL了。
Text-to-SQL的发展过程可以分为以下几个阶段:
得益于LLM的强大语言理解能力和生成能力,Text-to-SQL已经迎来了爆发式增长。通过下面的两个Text-to-SQL最著名的评测数据集,我们可以看到,基于LLM的Text-to-SQL已经完全碾压了所有其他的方法。
我们先来看一个最简单的Text-to-SQL的实现。
这个实现很简单,就是简单地把用户的查询和数据库中的DDL一起加到Prompt中,让LLM来生成一段SQL。加入一点点CoT的技巧,这个实现在Bird上面可以达到55%左右的执行准确率。现在排行榜上最好的结果是77.1%左右,说明我们还有很大的提升空间。
执行准确率的意思是,生成的SQL语句在执行后能返回正确结果的比例。
想要提升Text-to-SQL的性能,我们得先分析一下Text-to-SQL的难点在哪里。基于我这段时间的调研,我将Text-to-SQL的难点总结为了以下5个挑战。每个挑战又对问题进行了拆解。
针对这些问题,我梳理了8大应对方案,最终将8大应对方案映射到了3类能力范围:模型能力、架构设计、知识工程,如下图所示:
下面来依次分析这些挑战。
应对这三个问题的方案是 问题澄清
,依赖于多轮对话的能力。最简单的方法就是把Text-to-SQL作为函数调用给到大语言模型,然后让大语言模型收集到足够且明确的信息后,再执行Text-to-SQL。这么做的一个坏处就是,加重了LLM的“知识负载”,我尝试过让gpt-4o来进行这样的函数调用,效果不是很好,单独用一个prompt让gpt-4o来识别歧义的时候,它能识别出来;但是加上函数调用后,就没法识别了,直接就调用了Text-to-SQL。所以现阶段如果想要效果做得好,还是得拆分任务。
应对这两个问题的方案是 上下文的理解
。其实LLM本身已经具备了这部分能力,唯一需要做的就是当上下文特别长的时候,我们可以通过总结的方式来减少上下文的长度,辅助LLM处理更长的上下文。
应对这个问题的方案是 推理增强
,具体的细节比较复杂,会在后面一节中单独讲解。
应对这个问题的方案是 SchemaLinking
,将用户的问题中的实体与数据库中的值进行关联。具体的应对方案比较复杂,会在后面一节中单独讲解。
应对这个问题的方案是 检索增强
。我们需要梳理出所有问题中可能出现的业务术语,然后都用自然语言描述清楚术语具体是什么。最常见的就是别名了,可能用户说的是 Ax
,然后 Ax
的别名是 A
,数据库中存的也是 A
,那么这条业务数据就很关键了。
实现方法则很简单(当然要做得比较好,需要花大量的时间):混合检索一上,Rerank一加,视场景复杂度也可以加上知识图谱检索。网上讲RAG的内容已经很多了,就不展开讲了。
应对方案也是 SchemaLinking
,后面展开讲。
应对方案有两个:一个是 SchemaLinking
,另一个是 划分工作区
。 SchemaLinking
会在后面展开讲,这里讲一下 划分工作区
的做法。
这个思想来自Uber的QueryGPT1,我们需要给用户一个设置他自己的工作区的机会。这个思想很朴素:就是作为一个用户,我其实并不关心企业的整个数据库,我管好我的一亩三分田就好了。所以用户可以自己划定工作区,只关注一部分表。某个用户可能身兼多职,那就按不同的职位划分不同的工作区就可以了。一个团队也可以划分团队的工作区。这样在生成SQL的时候,就只需要关注工作区中的表,大大降低了LLM的“知识负载”。
工作区的划分本身也不是一个很简单的事情,因为技术表和业务词汇中间可能还是有一些差距的,业务人员可能不知道如果选择表作为工作区,这个时候数仓建模的优势就体现出来了。业务域、数据域、业务过程的建模是高度业务化的,业务人员也能很容易得看懂,自然就更好选择自己工作区需要的表了。
应对方案是 ReviseAgent
。
大语言模型其实是有修复这种错误的能力的,因为执行报错的时候数据库会给出错误信息,LLM能根据错误信息进行SQL的修改,就跟我们人写复杂SQL的时候也不是一次写对,会根据语法错误来修改SQL是一样的。
所以可以引入一个agent,把生成的SQL、SQL的执行结果或者报错信息都输入给模型,让模型去判断是否需要去修改SQL,还是说现在的SQL就OK了,当然也需要去设置一下最大的重试次数。这样Agent就能不停地去修正SQL,直到它觉得这个SQL是OK的了。
应对方案有两种,一种是 添加示例
,另外一种就是 微调模型
。
添加示例
微调模型
应对这三个问题的方案都是 模型微调
和 推理增强
。
应对这些问题的方案都是 模型微调
和 添加示例
。
我这边想提的一点是,虽然上面的问题可以通过这两个方案来解决,但是目前来看也只能解决一部分,模型的能力还是没有那么强,新出的BIRD-CRITIC榜单上,最强的模型目前也才38.5%的准确性。
所以我们需要通过工程手段,来尽量避免掉这些复杂性。join太多?那就看看是不是某些表是可以合并的,用数仓的维度建模可以降低join的复杂性。某些计算逻辑很复杂?那就看看是不是能提前计算一下。
回顾一下推理增强能解决的问题:
大模型本身具备了一定的推理能力,我们要做的就是引导出模型进行推理即可。加上了CoT(Chain of Thought)之后,推理能力就能显著提升。这里我用自己做得一个微调小实验来证明这一点。
我做的微调实验是关于英雄联盟的,所以需要先给大家一个背景知识,英雄联盟是一款游戏,游戏中有很多英雄,而每个英雄都有被动技能。英雄除了比较官方的名字之外,我们在玩的时候都会喊他的外号:比如官方名为“复仇焰魂·布兰德”的英雄,我们一般会叫他“火男”。
我先测试了一下XiYanSQL-QwenCoder-32B2对于火男这个概念是否了解,结果如下:
Q:英雄联盟中火男叫什么,全称是什么A:英雄联盟中的火男(莫甘娜)的被动技能名为“燃烧之魂”。这个技能的效果是,莫甘娜的技能会对敌人造成额外的魔法伤害,并且在敌人死亡时,她会恢复一定量的生命值。这个被动技能使得莫甘娜在团战中能够持续输出伤害并保持自身的生存能力。
模型对于火男这个概念存在幻觉,认为他是莫甘娜了。
然后我就通过lora的微调,给模型添加了“火男相关”的知识,然后再次测试,结果如下:
Q:英雄联盟中的火男的被动是什么。A:火男的被动技能是“炽热之焰”。
然后我们让其写SQL,用火男的被动技能名称来查询技能描述,这需要模型知道火男的被动技能是“炽热之焰”,然后把“炽热之焰”作为查询条件。
Q:CREATE TABLE skill_description (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT NOT NULL
);
根据ddl生成sql:帮我查一下英雄联盟火男的被动技能的描述,通过技能的名字来查
A:SELECT description FROM skill_description WHERE name = '火男的被动技能';
可以看到,模型在填写name的时候,没有足够的背景知识知道这里应该填写“炽热之焰”,这是因为我的训练语料中,只说了火男的被动技能是“炽热之焰”,但是这种转SQL的场景并没有特别交代,而且训练的数据量也不大,没法改变模型的固有思维。 那就再限制一下模型,只允许他填写真实的技能名字:
Q:...
根据ddl生成sql:帮我查一下英雄联盟火男的被动技能的描述,通过技能的真实名字来查,别用占位符
A:SELECT description FROM skill_description WHERE name = '火之意志';
结果还是不对,在转SQL的数据分布下,模型还是不知道要填写“炽热之焰”。
我们再来试试看CoT,模型已经能知道火男的被动技能是“炽热之焰”了,如果他在输出SQL前先把这个知识给补全,那是不是就能填对了。
Q:...
根据ddl生成sql:帮我查一下英雄联盟火男的被动技能的描述,你先思考一下火男被动的名字是什么,然后通过技能的名字来查。
A:在英雄联盟中,火男的被动技能名称是“炽热之焰”。因此,我们可以使用这个名称来查询技能描述表中的描述信息。
SELECT description FROM skill_description WHERE name = '炽热之焰';
这次模型就能正确地填对了,这个小实验证明了CoT在推理能力上的有效性。如今大火的模型DeepSeek-R1其实也是基于这个最基本的思想进行的。让模型能在生成的时候更多地思考,从而提升推理能力;不同是R1模型是通过训练模型的方式,并且从结果层面进行了正确答案的对齐,确保思考的正确性概率更大,最终效果更好。
既然提到了CoT,那不得不提一下Self Consistency这个方法。这个方法的思路是:通过给模型设置较高的Temperature,让模型在生成的时候有更多的随机性,从而生成更多的可能的答案,然后从这些答案中选择结果数量最多的答案。这个方法经过大量的实践,在多个数据集上都能显著提升效果。在Text-to-SQL领域自然也是适用的。这里的门道就是,模型本身是有随机性的,如果它只有80%,而不是100%的概率能生成出一个对的SQL,那么,为了每次都让他能生成正确的SQL,我们让他生成10次,即使偶尔错了2/3次,那还是有7/8次是正确的,最终投票选出来的,就一定会是一个正确的SQL了。看一下Spider数据集的排行榜,可以发现第二名就用了这个方法。
直接使用Self Consistency还是会出问题的。在Bird数据集上,我们就不太见到Self Consistency的方法了,原因其实有以下两点:
所以针对这两点,都有了相应的改进策略。
回顾一下Schema Linking能解决的问题:
先讲一下 SchemaLinking
是什么吧? 看一下上面这个例子,将自然语言中的实体、关键词和数据库中的表、列、值关联起来,就是 SchemaLinking
。
这个例子里,针对“查询在shenzhen购买了产品A的客户id”这个输入, shenzhen
会link到数据库 location
表中的 深圳市
的值, 购买
则会去link到 orders
表,然后 产品A
的话会link到 product
表里面的 A product
的值。 客户id
则会link到 orders
表的 customer_id
列,注意,link到最底层的值的时候,我们同时会把它的列以及它的表也都给取回,丰富上下文信息。最后还需要关注的是表与表之间的link,这个现在通常的做法就是默认把主键和外键都带上。
针对上面提到的三个问题, SchemaLinking
是这么来解决的:
比如说在这个例子上面,用户查询中,深圳其实就用了一个拼音 shenzhen
,而数据库中存的是 深圳市
。
对于这种情况的,如果不去匹配数据库里具体的值,那么模型就会用 shenzhen
的拼音去做where的过滤条件了,所以就需要将用户查询中的错别字同义词,link到表里面的具体值。
客户id
需要被映射到 order
表的 customer_id
列, 购买
需要被映射到 orders
表。
我们需要对这些表和列的进行筛选过滤,也就是说我们需要通过用户的查询输入,找到跟用户查询最相关的表和列,然后只把这些表和列输入给模型,让它去生成SQL,不然的话大量的表和列信息太多,甚至都没办法输入到模型里面去,即使能都放进去,同时输入的token数量过多,模型的性能是会显著下降的。
最后就是表/列可能隐含了一些特殊的业务知识,而模型对于这些知识是不清楚的,那需要去取到这些表/列对应描述,帮助模型来理解表和列的具体含义。不把所有的表描述和列描述都放到prompt中,还是因为放太多会导致模型的性能下降。
我们来详细展开讲一下实现。
首先是值的link问题:目前主流的做法是使用文本匹配和语义相似性。文本匹配更适合那些没有语义信息的数据,比如名字、电话、域名等。而语义相似度适配的范围就很广了,性别、职业、爱好等,都可以使用语义相似度来匹配。
值得一提的是文本匹配主要还是英文的做法,因为英文中的字母错拼的概率更大,一个单词中出现一两个字母的错误,其实还是很容易匹配的。但是中文环境下,其实错误更多发生在两种情况(注:暂时不考虑使用五笔输入法的小部分情况),一个是同音字,一个是拼错了键盘邻近的字母,原本可能就2、3个字的内容,直接按中文做匹配,召回率会非常低。所以中文不太能直接用字来进行数据库值的查找,转而使用拼音匹配能增大召回率。具体的实现方法的话,可以参考CHASE SQL3中的做法。先对拼音进行ngram划分,再使用MinHash+LSH来召回一批结果,最后通过编辑距离来进行更精确地筛选。
而语义相似性的话大家应该就很熟悉了,选个好用的Embedding模型(中文推荐xiaobu-embedding-v2,好用,成本也低),然后通过向量数据库来存储和检索,最后用rerank模型来精排一遍,选出最合适的值。
这里值的召回还需要提一点,就是值都是会和具体的表和列相关的,所以在查询的时候有两种查询方式,一种是直接查询值,一种是加上表和列再来查询值,具体要在什么时候用什么方法,就需要看用户的输入信息了。如果用户的输入信息中包含了列的信息,那么,同时用列加值查询效果就会更好。比如用户的输入是“查询业务类型是A的产品”,那么我们在知道列是 业务类型
的情况下,再从 业务类型
的列中进行值的召回,效果就会更好。如果要通过表和列进行过滤,就需要注意向量数据库索引类型的选择,向量数据库正在往向量索引和标量索引两类索引发展了,标量索引用于先过滤缩小筛选范围,向量索引用于向量匹配。
另外一个要解决的就是,拿什么去做检索和匹配,我们不能直接拿着用户的问题去检索和匹配,因为用户的问题中往往包含太多的信息,我们想要的是用户的问题中包含的一些关键词,比如 shenzhen
, AProduct
, 客户id
,这些关键词才是我们想要去匹配的。关键词的提取可以交给LLM,现在LLM对于提取关键词已经很厉害了,只需要给个few-shot的prompt,就能控制模型提取关键词的方式了。我之前在一个场景下,需要LLM给我们提取具体的量词,比如当用户问“xxx有几户”的时候,这个几户会需要进行相应的检索,一开始模型是不会提取 几户
作为关键词的,后面我在few-shot中加了一个示例,模型就能很好地提取出 几户
作为关键词了。
值选择完之后就可以开始选列了,选列主要有几步:
选表也是一样的逻辑:
这个的话,还是通过 检索增强
来实现。值得一提的是,某些特定的计算逻辑可以直接在描述里写出SQL来帮助模型减负,比如某个状态需要通过判断枚举值等于A或B,那就直接用SQL写出来: CASE WHEN...THEN1Else0asXXX状态
。当然,这种计算最好是通过工程手段直接用ETL处理好!
将上面提到的内容合起来,就可以得到下面这张架构图:
如果能把这里面所有的内容都实现了的话,针对具体场景做到90%
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-10-14
2024-06-20
2025-02-04
2024-10-09
2024-06-14
2024-06-16
2024-06-14
2024-05-31
2024-07-24
2025-02-09
2025-03-24
2025-03-11
2025-03-10
2025-03-10
2025-02-28
2025-02-25
2025-02-22
2025-02-22