LLM在组织内部应用的一类重要场景就是利用LLM的NL2SQL能力,简化用户对数据库的访问。本文主要介绍如何使用LLM生成SQL语句,不涉及到如何训练提升LLM的SQL生成能力。
开启正文之前,我们先明确一下这类功能在组织内服务的目标群体。我们将服务目标定位为没有太多IT技术背景的业务及运营人员。这些人访问数据库的需求,伴随着业务的发展,会超过特定服务软件提供的功能边界。服务软件的开发周期很难适配应用需求的迫切性。另外,熟练的使用服务软件获取使用者期望的信息也需要一定的学习成本。为此,利用LLM的SQL生成能力实时获取期望的数据库信息就成为了此类需求非常好的解决方案。这里,我们还要排除掉一部分IT运维用户的需求。因为这些用户可能会有需求让LLM帮忙生成出非常复杂的SQL语句以方便运维。但是为了让LLM能够生成对应的SQL语句,需要输入的文本描述会非常复杂,且需要对数据表的结构及关系有一些了解。这对于一般用户来说都太有难度,所以我们先排除掉这类需求。
很多时候,用户会问我们LLM生成SQL语句的准确率问题。这个问题比较难回答。因为如果我用训练集训练一个LLM,并用测试集测试,比较好回答此类问题。但今天我们讲的是一个利用LLM进行工程落地的场景,不涉及训练优化问题。所以,这个基础的NL2SQL的准确率问题是由选中的LLM模型保障的,而我们会通过一些设计原则优化LLM提示来提升这种SQL生成的准确率问题,从而确保用户的使用体验。当然,当我们在最后评价这个NL2SQL应用的效果时,可以用一个预先准备好的测试集对系统进行测试,确认系统的准确率是否满足使用要求。
在网络上我们能够看到很多用来训练模型NL2SQL能力的训练数据,这些数据给出的数据结构描述往往都比较简单。这可能会误导一些希望利用LLM生成SQL的使用者的工程方案。想让LLM通过数据库表的字段名或简单的注释就能了解该如何生成正确的SQL语句。其实,这是远远不够的。在这种场景下,笔者觉得使用者应该把LLM当成一个拥有丰富数据库经验的DBA来看待。如果希望他能正确的帮到你,你应该尽量的描述数据表结构的细节。毕竟,你希望DBA在没看到数据表的数据之前就能正确生成SQL语句。你不描述清楚点,DBA肯定是做不到的。同理,LLM也是做不到的。尽量不要期待LLM在此有一种与你心意相通的能力,它总能猜到你想要的。 当然这种问题在我们人类世界里也经常会遇到。在人与人沟通时,我们经常会潜意识的认为对方是知道一些“显而易见”的信息的。因为,这些显而易见的信息是行业或组织内都清楚的信息,属于任何沟通场景下的“背景信息”。这经常会被沟通者无意识的忽略,从而会导致沟通不畅或引起误会。有经验的沟通者,通常会在沟通前对齐一些“背景信息”以确保沟通的顺畅。但这显然是会浪费一些时间的,这也就导致在中国的商业环境里,甲方通常是愿意与有行业经验的乙方合作,因为无需对齐“背景信息”,可以大大提高沟通效率。当然,这也是新入行者比较难于获得甲方青睐的原因之一。
对于LLM来说,由于它没有什么行业经验,所以我们需要尽可能的给予它恰当的“背景信息”,才能使它更好的工作。所谓恰当,不是越多越好,因为太多的信息会消耗掉LLM的可接受上下文大小,同时也可能会造成LLM信息理解混乱。因此,笔者从实践中总结了以下几条数据库“背景信息”整理原则,用于提高LLM生成SQL语句的正确率:
尽量全面的描述表的功能
-
以业务视角描述表的功能含义,表的描述文字尽量与常用的业务数据契合。比如:这是一张学生信息表。
-
包含表中拥有的核心字段信息。如:包括,学生的学号,姓名,年龄,性别,班级ID...。这些信息可以有效帮助LLM判断回答问题时,是否要检索这张表。
-
描述表的关联性,如果当前表与其它表有关联性,需要描述表的关联性。比如:本表通过班级ID与班级表相关,班级表的ID字段是班级ID的外键。这种描述方便LLM了解表之间的结构。
尽量全面的描述字段的功能
-
以业务视角描述字段的功能含义,字段的描述文字也应尽量与常用的业务数据契合。比如:学生的名字。
-
数值类型的字段,如有计量单位,应写出计量单位。比如:千米/小时,万辆等。
-
枚举类型的字段,即当前字段的取值是有限的。需要视LLM的能力来判断是需要列出全部值还是只列出部分值。比如:字段是一个“表示行政区县的字段”,但这种描述不太容易让LLM准确理解字段的含义,可以继续加入下面的描述“其值包括:海淀区,昌平区,东城区等”;如果字段业务性特别强,LLM难于确认提取出的问题要素与那个人字段相关,那么LLM就无法正常转换为SQL。此时,需要将全部可能的取值都列出来,帮助LLM判断如何选取字段。此时,那些针对行业应用的LLM会拥有一定的优势。就是它可以了解字段的业务含义,知道可能的取值,因此不必列出所有的取值情况。
-
时间字段,一定要描述时间在表中的存储格式。对于SQL检索来说,时间字段的格式转换是最频繁的,也是最容易出错的。因此告诉LLM时间字段的格式,可以帮助LLM提高生成SQL语句的正确率。
-
如果可以,尽量描述出数据值的格式,增强LLM对字段含义的理解。如:学生的住址信息,格式:XXX省XXX市XXX(区/县)XXX(街道/村)XXX小区等。
-
如果字段是外键字段,需要描述与其它表的关系。
适当放弃意义等价的字段
有时数据表设计时会留存意义等价的字段。比如:在学生表中有个存放学生所在行政区县的字段area,同时也有一个区县对应行政编号的字段area_code。二者在业务意义上是等价的,如果在性能上没有特别的检索要求。那么,可以忽略掉area_code字段。因为很少有人会使用行政编码查找区域内的学生。
放弃业务上无用的字段
对于有些数据表,数据字段做了一些冗余性的设计。这些字段对业务没有太多的帮助。检索时,也基本不会用到。在提供表的背景信息时,可以考虑删除掉这些字段,不进行描述。如:设计中留了经纬度字段,但实际应用中,并没有相关数据,可以忽略掉这些信息。
以上是一些基于工作实践总结出的数据库“背景信息”整理原则。主打一个该详细,详细;该简略,简略。核心一个原则,“让LLM能够清晰理解数据库的样子”。细心的读者应该已经发现,上述原则中我们对表描述时,会涉及到部分字段的描述。在字段的描述中,我们又进一步详细的进行了描述。这样的描述方式会增大对LLM上下文的占用,感觉有些冗余。其实不然,这里我们给出的原则有一个配套的用法,用于面对拥有几十上百张表的应用场景。很显然,一次丢这么多张表及其结构描述给LLM让其根据问题生成SQL语句,极有可能超出其上下文限制。那么在碰到此类场景时,我们可以将这个目标拆解为两步。第一步,我们只为LLM提供表的描述信息,让LLM根据表的“背景信息”选出回答问题需要用到哪几张表,并输出表名字;第二步,将对应表名的表连同其结构描述一起交给LLM,由其根据输入的信息生成对应的SQL语句。通过这样的两步操作,可以有效的解决占用LLM上下文过大的问题。
以上的原则只是给出了一些思想方法,具体描述相关信息以及生成SQL时,还会受到LLM的型号,版本等的限制。需要做一些适当的修改。希望能够给与笔者一样,在LLM应用路上的朋友一点帮助和启发。
以下是交给LLM的数据库“背景信息”的格式:
{
"tables": [
{
"name": "表名称",
"note": "表描述",
"columns": [
{
"name": "列名称",
"type": "字段类型",
"note": "列描述"
},
{...col-n...}
]
},
{...tab-n...}
}