行业需求
NL2SQL任务旨在将自然语言自动转换为SQL语句,成熟的NL2SQL功能可以赋能业务人员易如反掌的数据洞察能力,让大量缺乏深入SQL训练的业务人员能够充分理解、加工、使用数据。但这类应用在落地环节却总会遇到困难,这种困难常表现为“捉襟见肘的数据样本”与“高不可及的确定性要求”之间的矛盾,要么是海量组织数据用以训练模型的成本畸高,要么是即便依此训练出来的模型在很多场景下准确率也很不稳定,出现的结果让人啼笑皆非。NL2SQL是AI应用研究的热点,惯常的技术路线是通过海量的训练样本打造通用的SQL生成能力。以业界著名的数据集WikiSQL和Spider为例,前者包括80645个标注样本,后者则包含了10181个问题和5693个SQL样本,在这两个静态数据集上,最高的准确率均已超过90%。这样貌似出色的成绩对于落地NL2SQL却意义不大,因为真实场景下绝大多数用户并没有办法提供大量的匹配的数据样本,同时也缺乏训练海量数据样本的知识、经验以及计算能力;而且即便整体达到90%的准确率,在特定用户的自身的特定使用场景中,常常发现准确率却会低到50%以下。
因此我们可以看到,NL2SQL在行业应用中真正需求可以简要概括为:用户往往只能提供少量真实样本(几条到几十条),但目的是要让特定的使用场景实现接近100%的NL2SQL技术能力,且运行和维护成本要足够低(用足够小的模型和计算资源,也很忌惮供应商持续提供大量昂贵的专业服务才能勉强维护)。
本文的工作
ChainTuning(链式微调)是我们应对落地需求所给出的解决方案。此方案中,将COT(Chain of Thought)理念应用于大模型的适配器(Adapter),构建出CoT-A(Chain of Thought Adapter,思维链适配器)为微量种子情况下的NL2SQL任务带来杰出的推理确定性。同时,ChainTuning包含模型的自演化能力,可以通过多轮链式迭代自动补充样本、调优CoT-A和各模型的效果。ChainTuning在7~8B的语言模型基础上,只需要微量的种子模式作为样本,通过自动样本生成、CoT-A构建、InC(Intent Clarifier)的构建、自动演化,能在种子模式上实现90%-100%的准确率(种子不纳入训练集),并且对与种子相关的提问也具备良好的泛化性,该方法为NL2SQL在微量业务样本场景的落地提供了切实可行的途径。后面的篇幅,将从ChainTuning的总体流程、训练样本的构建、CoT-A的实现、InC的实现、模型自演化、实验效果几个角度展开讲解。ChainTuning可以支持包含 “聚合操作”的SQL、含UNION的SQL、含nested(嵌套结构)的SQL、多表SQL的生成,本文将以包含“聚合操作”的单表SQL为例展开后续小节,因为实现该类SQL的生成已涵盖本文最为核心的思想。UNION、nested、多表类型的SQL由于包含过多的工程化细节,将在以后单独撰文介绍。后文中提及大模型部分,如无特殊说明,均基于Llama3-8B-Instruct。用于实现NL2SQL的微量的种子模式,在后文中也被称为“业务Seed”。
ChainTuning的总体流程
ChainTuning所解决的问题是在微量种子模式的场景下,构建高精度的NL2SQL模型。图1展示了ChainTuning的全流程。该流程起始于“收集业务Seed”,在这个步骤中,可以提供几条、十几条、几十条不等的业务相关训练样本,每条这样的样本包含“问题”和“SQL”两个部分,后文中把这样的样本称为“业务Seed”。在运行第一轮时,所有的“业务Seed”将被标定为本轮演化的Seed,每一轮演化迭代将以增强模型对本轮 “业务Seed”的回答能力为目标。在明确本轮演化的Seed之后,进入样本生成阶段,这一阶段将生成两类训练样本:“CoT-A训练样本”和“InC训练样本”。样本生成完毕后,新生成的样本将与过去几轮演化过程中生成的历史样本进行合并,合并后的数据集将用于本轮演化中的模型训练。在模型训练环节,使用合并后的样本训练CoT-A(作用为SQL生成器)和InC(作用为用户意图澄清)。完成训练后,将使用所有“业务Seed”测试NL2SQL的效果(通过输入Seed中的问题,最后得到输出的SQL),以评估本轮模型的训练成绩。如果评估效果达标,则终止ChainTuning的流程,反之将筛选出效果不佳的业务Seed,他们将被作为下一轮演化的Seed,而后开始下一轮演化迭代,直至SQL生成效果达到设定的标准。
图1 ChainTuning总体工作流程
自动样本生成
1. CoT-A的训练样本生成
首先,展示一条真实的业务Seed,包括业务问题和对应的SQL:
问题:
以客户端mac聚合,统计相关的ipv4包数、ipv6包数、包字节数、包总数,过滤条件为探针地址10.6.11.144、网卡为nic0
SQL:
SELECT MACNumToString(`@client_mac`) as mac,
sum(if(`@ether_type` = 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)) as ipv4_pkt_cnt,
sum(if(`@ether_type` != 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)) as ipv6_pkt_cnt,
sum(`@pkt_len_c2s` + `@pkt_len_s2c`) as pkt_len,
sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as pkt_cnt
FROM npm_conv.rollup_main
WHERE sp_addr in ('10.6.11.144')
AND nic in ('nic0')
GROUP BY mac
从上述Seed样本可以看到该样本涉及`@client_mac`、`@ether_type`、`@pkt_cnt_s2c`等多个列,在其他一些例子中可能涉及更多的列、复杂的计算方法、函数的组合应用等。为各种数据源提供高度复杂的SQL生成的能力是NL2SQL应用价值的体现,但也是挑战。先说一个首先能想到的问题:是否需要向模型“介绍”数据源的表结构、函数信息、指标算法等业务知识?这里我们认为“不必”。原因很直观,哪怕是一个真实的面向SQL表的业务分析人员,也未必需要“背诵”上述信息,他只需要“用到哪、学到哪”,ChainTuning也采用这种理念。那么,万一有些高频问题涉及种子模式以外列、函数、算法如何应对?很简单,那就加一个“业务Seed”。
CoT-A的训练样本生成,本质上是仿写与“业务Seed”具有相关模式的样本。在这一步,我们通过Few-shot让大模型来完成仿写。仿写主要存在三个问题:1)仿写的SQL如何能高概率语法准确(提高仿写效率);2)如何与Seed模式既相似又不同;3)如何在覆盖Seed模式的同时,又获得良好的泛化性。下面给出三个问题的解答:
1)采用优先生成SQL、再生成问题的数据生成顺序,有利于提高SQL生成的准确性。采用这种策略,是一种经验主义的做法,但确实提高了SQL语法的准确率。直觉上很容易理解,毕竟照着一个Seed给定的正确SQL仿写一个相似但不同的SQL,还是很可能写对语法的,如果根据模型生成的“提问”再写个SQL,能否写对就很不确定,毕竟模型“提问”的思维如此发散。无独有偶,开源项目Lamini中也采用这种顺序进行样本的生成;
2)同时生成多个SQL、采用动态的Prompt模板。“同时生成多个SQL”是通过提示词让大模型一次生成两句相似但又不同的SQL,极大概率可以一次得到两个不同的SQL,这一做法参考了Lamini中的样本生成部分。但本文作者认为仅仅这样,多样性依然不够,因此引入了动态Prompt模板:准备多条修改原始SQL的“指令”(a. Change the select alias; b. Change the Filter Condition c. Modify the arguments of function d. … e. … f. …等),在构建Prompt时,从多条“指令”中随机取出并排列这些方法、加入Prompt,由此构建出不同的Prompt。大语言模型是一种基于前缀预测后缀的模型,不同的输入序列必将为输出带来多样性;
3)同时构建“单Seed”、“多Seed”两种Prompt。“单Seed”Prompt中仅仅包含一个“业务Seed”作为参考,生成数据时采用2)中的随机选取“指令”方法增加生成样本的多样性,但这些样本终归是和Prompt中单一的“业务Seed”较为相似的。“多Seed”Prompt是指从所有“业务Seed”中随机选择多个组合成Prompt,要求模型参考这些Seed写出相关SQL,这时,所产生的SQL将参考Prompt中的所有Seed,这类样本有助于提高模型的泛化性。另一方面,假设当总业务Seed数为10个时,如果随机选择3条并进行排列,则可以产生多达720种不同的Prompt,大幅增加了生成样本的多样性,随着总Seed的增多Prompt的多样性将呈“指数级”增强。
“单Seed”提示词示例:
Write two different queries that are similar to the given example for model training.
Example:
Question:<Seed的问题部分>
Query: <Seed的sql部分>
Please do the every operation below to modify the example: //a-f是从集合中随机采样获得
a)Change the condition in WHERE clause.
b)For the LIMIT clause, adjust the limit number, remove the LIMIT clause, or add a LIMIT clause.
c)For the ORDER BY clause, adjust the fields, remove the ORDER BY clause, or add a ORDER BY clause.
d)Make adjustments to the fields in the SELECT statement: remove fields, apply aliases, or change the order of fields.
e)If a GROUP BY clause exists, modify the field in the GROUP BY clause.
f)Use timeSlice to aggregrate by different time granularity.
Ensure the accuracy of the sql syntax.
Never write the fields are not existed in the example.
Never use functions that are not mentioned in the example, except for standard SQL functions like SUM, AVG, etc.
Format the queries as a JSON object, i.e.
{ "explanation": str, "sql_query_1" : str, "sql_query_2": str }.
First write an explanation of why you decided to write these new queries in about 3-5 sentences, then write valid SQL queries for each of the 2 new queries.
“多Seed”提示词示例:
Write two different queries that are similar to the given examples for model training.
Examples: //下面三个样本为随机采样获得
Question:<Seed1的问题部分>
Query: <Seed1的sql部分>
Question:<Seed2的问题部分>
Query: <Seed2的sql部分>
Question:<Seed3的问题部分>
Query: <Seed3的sql部分>
Ensure the accuracy of the sql syntax.
Never write the fields are not existed in the example.
Never use functions that are not mentioned in the example, except for standard SQL functions like SUM, AVG, etc.
Format the queries as a JSON object, i.e.
{ "explanation": str, "sql_query_1" : str, "sql_query_2": str }.
First write an explanation of why you decided to write these new queries in about 3-5 sentences, then write valid SQL queries for each of the 2 new queries.
根据SQL生成问题的提示词示例:
Queries, and questions that they are used to answer: //样本为Seed中随机采样获得
Example-Query: <Seed1的问题部分>
Example-Question: <Seed1的sql部分>
Example-Query: <Seed2的问题部分>
Example-Question: <Seed2的sql部分>
Example-Query: <Seed3的问题部分>
Example-Question: <Seed3的sql部分>
Now consider the following query.
Query: <生成的SQL>
Write a question for the query, the question must be in style of the the Example-Question.
Format your response as a JSON object, i.e.
{ "explanation": str, "question": str }.
First write an explanation in about 3-5 sentences, then write the question for the query, the question must be in style of the Example-Question.
上面所给出的是CoT-A训练样本的生成过程,事实上,最终输入CoT-A的样本的“问题”部分还需要改写成更为严谨、形式化一些的表述,本文将此操作称为“意图澄清”,下一节即介绍用于训练“意图澄清”功能的模型的样本生成。
2. InC训练样本的生成
InC(Intent Clarifier,意图澄清器)的训练样本为从“口语化的问题表述”到“相对详尽、形式化的描述文本”的句子对。这些样本也采用大模型生成。
上文曾给出一个业务Seed的示例,其中问题部分如下:
以客户端mac聚合,统计相关的ipv4包数、ipv6包数、包字节数、包总数,过滤条件为探针地址10.6.11.144、网卡为nic0
这样的提问,似乎还是把SQL的筛选条件、输出列描述得非常清晰的,但用户每次都会输入这样的“易于理解”的提问吗?答案是否定的,更多的用户提问可能是下面这样的:
流量超过20M 的探针有哪些?
最近一周探针10.6.11.144 上各捕获口的流量变化情况。
本周10.7.255.5:80 与 10.7.109.214 之间的网络通讯是否正常?
以最后一行的问题为例,对应的SQL为:
select sum(`@pkt_len_c2s`) / 1024 / 1024 / 1024 as gigabytes_c2s,
sum(`@pkt_len_s2c`) / 1024 / 1024 / 1024 as gigabytes_s2c,
sum(`@conn_open_cnt`) as estb_conn,
sum(`@attach_succ`) / sum(`@attach_succ` + `@attach_fail`) as estb_succ_rate,
sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as packets,
sum(`@tcp_segment_lost_c2s` + `@tcp_segment_lost_s2c` + `@capture_lost_c2s` + `@capture_lost_s2c` ) / sum(`@tcp_segment_lost_c2s` + `@tcp_segment_lost_s2c` + `@capture_lost_c2s` + `@capture_lost_s2c` + `@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as packets_loss_rate,
sum(`@retransmission_c2s` + `@retransmission_s2c`) / sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as retransmission_rate,
(sum(`@network_delay_sum_c2s`) / sum(`@network_delay_cnt_c2s`) + sum(`@network_delay_sum_s2c`) / sum(`@network_delay_cnt_s2c`)) / 1000000 as network_delay_ms
from npm_conv.rollup_main
where `@server_ip` = '10.7.255.5'
and `@server_port` = 80
and `@client_ip` = '10.7.109.214'
order by retransmission_rate desc,
packets_loss_rate desc,
network_delay_ms desc
以上SQL对应的问题为“本周 10.7.255.5:80 与 10.7.109.214 之间的网络通讯是否正常?”,从问题中基本很难确定SQL应该输出哪些列、如何制定过滤条件。如果以这样的问题和SQL构建样本进行训练,纵有如“多头自注意力机制”般强大的网络结构加持,效果也是欠佳的。ChainTuning在此处引入了“InC”,以将用户输入的问题翻译为更为清晰、形式化的描述。真正用于训练CoT-A的数据的问题部分都是经过InC澄清;在实现NL2SQL时,所有的输入问题也都通过InC“澄清”后才进入CoT-A生成SQL。
在InC的样本生成方面,也是通过构建包含few-shot的Prompt让大模型辅助生成。通过few-shot例子,模型将根据SQL反推出与few-shot中例子风格接近的“严谨、形式化”的提问,与原始“口语化”提问共同构成训练样本。下面给出用于生成InC样本的few-shot的例子:
Query:SELECT MACNumToString(`@client_mac`) as mac,
sum(if(`@ether_type` = 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)) as ipv4_pkt_cnt, sum(if(`@ether_type` != 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)) as ipv6_pkt_cnt, sum(`@pkt_len_c2s` + `@pkt_len_s2c`) as pkt_len, sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as pkt_cnt
FROM npm_conv.rollup_main
WHERE sp_addr in ('10.6.11.144') AND nic in ('nic0')
GROUP BY mac
Question:筛选条件:探针地址为10.6.11.144,nic为nic0;聚合条件:mac(客户端);聚合筛选条件:不限;输出列:mac(客户端)、ipv4包数、ipv6包数、包长;排序方法:不限;返回条数:不限。
例子中,问题部分是较为形式化的,是大模型仿写的目标风格,SQL中的每个列、筛选条件、聚合条件、排序方法、返回数量均可以在此种风格的提问中找到对应的部分。“InC”完成从“口语风格”到“形式化风格”的转换,输入输出均为自然语言,这种功能通过微调大语言模型可以稳定实现。下述为一段用于InC的样本输出部分的Prompt:
Below are the Standard-Examples of queries and the corresponding questions
Standard-Examples:
Query:SELECT MACNumToString(`@client_mac`) as mac, sum(if(`@ether_type` = 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)) as ipv4_pkt_cnt, sum(if(`@ether_type` != 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)) as ipv6_pkt_cnt, sum(`@pkt_len_c2s` + `@pkt_len_s2c`) as pkt_len, sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as pkt_cnt FROM npm_conv.rollup_main WHERE sp_addr in ('10.6.11.144') AND nic in ('nic0') GROUP BY mac
Question:筛选条件:探针地址为10.6.11.144,nic为nic0;聚合条件:mac(客户端);聚合筛选条件:不限;输出列:mac(客户端)、ipv4包数、ipv6包数、包长;排序方法:不限;返回条数:不限。
Now consider the following query and original-question.
Query: select sum(`@pkt_len_c2s`) / 1024 / 1024 / 1024 as gigabytes_c2s, sum(`@pkt_len_s2c`) / 1024 / 1024 / 1024 as gigabytes_s2c, sum(`@conn_open_cnt`) as estb_conn, sum(`@attach_succ`) / sum(`@attach_succ` + `@attach_fail`) as estb_succ_rate, sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as packets, sum(`@tcp_segment_lost_c2s` + `@tcp_segment_lost_s2c` + `@capture_lost_c2s` + `@capture_lost_s2c`) / sum(`@tcp_segment_lost_c2s` + `@tcp_segment_lost_s2c` + `@capture_lost_c2s` + `@capture_lost_s2c` + `@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as packets_loss_rate, sum(`@retransmission_c2s` + `@retransmission_s2c`) / sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as retransmission_rate, (sum(`@network_delay_sum_c2s`) / sum(`@network_delay_cnt_c2s`) + sum(`@network_delay_sum_s2c`) / sum(`@network_delay_cnt_s2c`)) / 1000000 as network_delay_ms\nfrom npm_conv.rollup_main\nwhere `@server_ip` = '10.7.255.5' and `@server_port` = 80 and `@client_ip` = '10.7.109.214' order by retransmission_rate desc, packets_loss_rate desc, network_delay_ms desc
Original-Question: 本周 10.7.255.5:80 与 10.7.109.214 之间的网络通讯是否正常? // 原始提问,“意图翻译器”的样本的输入部分
Rewrite the original-question in the style of Standard-Examples, must describe all output fields.
Clarify the server/client information, such as 客户端ip, 服务端site, 客户port and so on.
Format your response as a JSON object, i.e.
{{ "explanation": str, "question": str }}.
First write an explanation in about 3-5 sentences, then write a one sentence question in style of Standard-Examples, must describe all output fields.
模型输出的Json中question部分即为原始问题的“澄清”后结果。以下是一些“原始问题”与“澄清问题”的数据样本:
{"ORIGINAL_QUESTION": "统计最近 00:1e:67:5b:79:57 与 00:1e:67:9b:60:0f 之间的主要网络指标趋势。", "REWRITE_AS": "筛选条件:客户端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14',服务端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14';聚合条件:客户端mac和服务端mac;聚合筛选条件:不限;输出列:客户端mac、服务端mac、C2S方向的Gigabytes、S2C方向的Gigabytes、建立连接数、连接成功率、总包数、包丢失率、重传率、网络延迟(毫秒);排序方法:不限;返回条数:不限。"}
{"ORIGINAL_QUESTION": "统计最近 00:1e:67:5b:79:57 与 00:1e:67:9b:60:0f 之间的主要网络指标趋势。", "REWRITE_AS": "筛选条件:客户mac为00:1E:67:5B:79:57和00:1E:67:9B:60:0f之间的mac地址;聚合条件:客户mac、服务mac;聚合筛选条件:不限;输出列:客户mac、服务mac、客户端传输GBytes、服务端传输GBytes、建立连接数、附件成功率、总包数、包损失率、网络延迟ms;排序方法:不限;返回条数:不限。"}
{"ORIGINAL_QUESTION": "统计最近 00:1e:67:5b:79:57 与 00:1e:67:9b:60:0f 之间的主要网络指标趋势。", "REWRITE_AS": "筛选条件:客户端mac为00:1E:67:5B:79:57或00:1E:67:9B:60:0F,服务端mac为00:1E:67:5B:79:57或00:1E:67:9B:60:0F;聚合条件:客户端mac、服务端mac;聚合筛选条件:不限;输出列:客户端mac、服务端mac、C2S方向的Gigabyte、S2C方向的Gigabyte、建立连接数、建立连接成功率、总包数、包损失率、重传率、网络延迟毫秒;排序方法:不限;返回条数:不限。"}
示例中original_question部分为InC训练的输入,REWRITE_AS是InC训练的输出。在生成InC的样本时,输入的SQL、问题有两个来源,一个是“业务Seed”,另一个是已生成的“CoT-A训练样本”。同一个原始问题可以生成多个“澄清样本”,这样有助于提高InC的泛化性。
CoT-A的实现
通过使用“CoT-A的训练样本生成”一节中介绍的SQL样本数据生成方法,可以产生很多与种子模式相似但又存在差异的样本,样本中的“问题”部分通过InC可以澄清成更为直观的表述风格。接下来介绍如何通过这些生成的样本,基于大语言模型,实现CoT-A。训练完毕的CoT-A可以在仅提供微量业务Seed的情况下达到90%~100%的SQL生成准确率,语法准确率达到95%以上。
使用大语言模型生成SQL,最直接的训练思路是输入问题、输出对应的SQL。但由于现实落地时往往样本严重不足,这导致使用“直接训练”所实现的NL2SQL在SQL的语法准确性、语义准确性方面均存在欠缺。重温上文给出过的一个业务Seed例子,这个例子中已经加入了InC处理(意图澄清)后的问题:
原始问题:统计最近 00:1e:67:5b:79:57 与 00:1e:67:9b:60:0f 之间的主要网络指标趋势。
意图澄清:筛选条件:客户端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14',服务端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14';聚合条件:客户端mac和服务端mac;聚合筛选条件:不限;输出列:客户端mac、服务端mac、C2S方向的Gigabytes、S2C方向的Gigabytes、建立连接数、连接成功率、总包数、包丢失率、重传率、网络延迟(毫秒);排序方法:不限;返回条数:不限。
查询:
select MACNumToString(`@client_mac`) as client_mac,
MACNumToString(`@server_mac`) as server_mac,
sum(`@pkt_len_c2s`) / 1024 / 1024 / 1024 as gigabytes_c2s, sum(`@pkt_len_s2c`) / 1024 / 1024 / 1024 as gigabytes_s2c,
sum(`@conn_open_cnt`) as estb_conn,
sum(`@attach_succ`) / sum(`@attach_succ` + `@attach_fail`) as estb_succ_rate,
sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as packets,
sum(`@tcp_segment_lost_c2s` + `@tcp_segment_lost_s2c` + `@capture_lost_c2s` + `@capture_lost_s2c` ) / sum( `@tcp_segment_lost_c2s` + `@tcp_segment_lost_s2c` + `@capture_lost_c2s` + `@capture_lost_s2c` + `@pkt_cnt_c2s` + `@pkt_cnt_s2c` ) as packets_loss_rate,
sum(`@retransmission_c2s` + `@retransmission_s2c`) / sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as retransmission_rate, ( sum(`@network_delay_sum_c2s`) / sum(`@network_delay_cnt_c2s`) + sum(`@network_delay_sum_s2c`) / sum(`@network_delay_cnt_s2c`) ) / 1000000 as network_delay_ms
from npm_conv.rollup_main
where MACNumToString(`@client_mac`) in ('00:1E:67:5B:79:5D', '00:1E:67:9B:60:14') and MACNumToString(`@server_mac`) in ('00:1E:67:5B:79:5D', '00:1E:67:9B:60:14')
group by client_mac, server_mac
原始问题在澄清后,可以形成和预期输出查询列之间的对应关系。但是,要确保整个SQL能输出正确的语法和预期语义,所需解决的问题还很多,对于上例,必须要解决:输出查询的select子句中的指标算法和函数用法、where子句中的筛选条件、group子句中聚合列与select中的对应关系。对于存在Order子句、Limit子句的SQL,则需要关注更多的细节。大语言模型在输出长文本(长SQL)时,也许能在总体结构上相对正确,但以目前的大模型技术,对于细节的精确把控依然存在问题。
当面对复杂、冗长、逻辑严谨的输出需求时,COT(Chain of Thought)是首先能被想到的技巧。COT通过引导模型逐步展示它的思考过程,能显著提高模型的表现,减少生成错误答案的概率,适合于复杂的文本生成任务。COT确实相对好用,但面对业务复杂、严谨、冗长的SQL生成场景,依然显得不足。本文提出CoT-A(Chain of Thought Adapter)用于解决该问题,这里的Adapter是指大模型微调技术中的适配器。CoT-A由一组适配器构成,其中的每个Adapter对应COT中的一个环节,通过将每个环节具体化为一个Adapter,CoT-A可以提供准确率、稳定性远高于COT的长序列输出能力。而且由于每个Adapter负责的内容更少,因此所需的训练样本更少,支持的局部复杂性更高,尤其适合解决“指标算法”、“函数使用”等问题。
当生成SQL时,CoT-A将一句SQL的生成转化为多个Adapter的序贯调用,以生成高质量的SQL。上面展示的SQL生成例子,在CoT-A中被拆成多个步骤,为了直观理解,下面给出CoT-A中的每个步骤的SubAdapter的输入与输出:
CoT-SubAdapter of Select Clause // 用于生成Select子句的Adapter
输入:问题:筛选条件:客户端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14' …
输出:
{“Select_Clause”: “MACNumToString(`@client_mac`) as client_mac, MACNumToString(`@server_mac`) as server_mac, …”}
CoT-SubAdapter of Where Clause// 用于生成Where子句的Adapter
输入:问题:筛选条件:客户端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14' …
Select Clause:
MACNumToString(`@client_mac`) as client_mac, MACNumToString(`@server_mac`) as server_mac, …
输出: {“Where_Clause”: “MACNumToString(`@client_mac`) in ('00:1E:67:5B:79:5D', '00:1E:67:9B:60:14') and MACNumToString(`@server_mac`) in ('00:1E:67:5B:79:5D', '00:1E:67:9B:60:14')”}
CoT-SubAdapter of Group Clause// 用于生成Group子句的Adapter
输入:问题:筛选条件:客户端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14' …
Select Clause:
MACNumToString(`@client_mac`) as client_mac, MACNumToString(`@server_mac`) as server_mac, …
输出: {“Group_Clause”: “client_mac, server_mac”, “Having_Clause”: “no”}
CoT-SubAdapter of Order Limit Clause// 用于生成Order、Limit子句的Adapter
输入:问题:筛选条件:客户端mac为'00:1E:67:5B:79:5D'或'00:1E:67:9B:60:14' …
Select Clause:
MACNumToString(`@client_mac`) as client_mac, MACNumToString(`@server_mac`) as server_mac, …
输出: {“Order_Clause”: “no”, “Limit”: “no”}
从上面可以看出,整个SQL的生成被拆分为四个步骤:Select内容生成、Where内容生成、Group/Having内容生成、Order/Limit内容生成。其中Select子句生成的输入依赖为“问题”,因为select之后输出哪些列、计算哪些指标只和问题相关;Where子句、Group/Having内容、Order/Limit内容的生成输入为“问题”和已生成的Select子句,因为这三个部分的SQL内容均与“问题”和select子句中的相关。上面的内容中未提及from部分,因为前文已说明:为了简化问题,将本文的所有SQL假定为针对同样的一个表的,但如果在实践时需要多表,增加对应的SubAdapter即可以实现。
CoT-A在训练时,将每个样本也拆分成支持每个步骤的CoT-SubAdapter的训练样本。CoT-A在输出时,将这些CoT-SubAdapter序贯调用以产生SQL。
InC的实现
InC(Intent Clarifier,意图澄清器)的作用是将原始用户输入翻译为更为严谨、形式化的表达。InC的样本生成过程在“InC训练样本的生成”一节中已经阐述,InC的训练过程就是以样本中的输入、输出部分训练一个基于大模型的Adapter。值得注意的是,InC的样本不必拘泥于“业务Seed”,任何与目标数据源相关的SQL均可以用于产生样本、训练InC,以达到更好的泛化性。InC的泛化性对整个ChainTuning的泛化性至关重要。
至此CoT-A与InC的实现均已介绍完毕,下图描述了从问题到SQL生成的完整过程:
图2 ChainTuning的SQL生成过程
模型的自演化
当CoT-A和InC均训练完毕,就可以使用“所有”的业务Seed来测试生成的效果。
生成的效果本文从两个角度衡量:Syntax Accuracy(语法准确率)和Semantic Accuracy(语义准确率)。生成的SQL如果语法正确,则在语法准确率上得分,只有查询结果中的包含业务Seed被执行后的“关键列”,才认为在语义准确率上得分。“关键列”可以由业务Seed的提供方给出。当语义准确率和语法准确率达到预设标准时,ChainTuning停止模型迭代。如果未达到停止迭代的标准,对于某个Seed的问题,无论其语法或者语义不正确,该Seed均被认为是不达标业务Seed,所有不达标业务Seed被集中在一起作为下一轮演化的Seed。在下一轮中将针对不达标的Seed生成新的训练样本,在合并历史样本后训练新的CoT-A和InC,以达到模型能力的自动演化。目前系统采用的训练策略为“样本积累”情况下的模型重新微调,事实上也可以采取在已有Adapter上进行新、旧样本的迭代训练策略。
实验效果
1. 实验方法
由于本文关注的是微量Seed下的高确定性NL2SQL生成,所以WikiSQL、Spider等标准数据集并不适合本文的预设落地场景,接下来通过实测、对比来检验ChainTuning的效果。
ChainTuning在针对5~15个业务Seed(包含聚合、Union、nested多种类型)的场景下,准确率可以达到93%~100%不等。下面选择GPT-o1为比较对象,对15个Seed在ChainTuning与GPT-o1中的SQL生成效果进行对比。实验设计如下:
1) 对比实验中要求生成的SQL是在Hive数据库中的方言,根据前期测试,我们发现GPT-o1具备Hive-SQL的知识;
2) 所有例子的SQL查询时间范围已经另外选定,未在例子中体现;
3) ChainTuning在15个Seed上进行5轮迭代训练;
4) GPT-o1在实验中,首先通过多轮对话告知表结构、各类指标(建连成功率、延迟时间、总包数、比特率、重传率等)在该表上的计算方法、函数的用法;
5) 通过向ChainTuning和GPT-o1发送Seed中的问题,查看生成的SQL的执行结果,审核SQL逻辑。
对于实验结果,统计SQL的Syntax Accuracy和Semantic Accuracy指标。其中Syntax Accuracy主要关注生成SQL的语法准确性,Semantic Accuracy是指在语法准确的前提下,是否准确回答了目标查询意图。参与实验的业务Seed共15个,先给出实验的统计结果,限于篇幅,再取出两个Seed的生成效果进行对比。
2. 与GPT-o1的统计对比
表1 ChainTuning与GPT-o1效果对比
Syntax Accuracy | Semantic Accuracy | |
ChainTuning | 100% (15/15) | 93.3% (14/15) |
GPT-o1 | 93.3% (14/15) | 66.7% (10/15) |
在Syntax Accuracy(语法准确率)环节,ChainTuning全部正确,GPT-o1在输出一个较长的SQL时,错把单引号写成斜引号,总体来说,表现也相当出色。在Semantic Accuracy环节,ChainTuning出现了一处错误,它在Where条件中加入了一个错误的IP过滤条件,虽然这处过滤条件未造成输出结果的差异,但这只是个巧合。分析错误的原因,这应当是由于Seed偏少导致的训练数据丰富度不够,让模型在这个过滤条件上产生了幻觉。GPT-o1在Semantic Accuracy环节则出现诸多问题:仅考虑某个IP作为服务端时的统计值,未考虑作为客户端的情况;对于探针与nic一对多的关系理解不足,聚合条件中缺少探针地址(sp_addr);对于在一些情况下关注的业务指标不了解。所有这些错误都让GPT-o1在检索结果上确实出现了错误。
3. 与GPT-o1的生成示例对比
1)示例对比1
问题:与服务器 10.3.255.98 通讯的 IP 有哪些?建连成功率如何?
ChainTuning:正确。
GPT-o1:错误。原因:未考虑10.3.255.98作为client_ip时的情况。
问题:按捕获口统计网络延迟情况
// ChainTuning生成:
select `@client_ip` as client_ip,
`@server_ip` as server_ip,
sum(`@attach_succ`) / sum(`@attach_succ` + `@attach_fail`) as estb_succ_rate
from npm_conv.rollup_main
where `@client_ip` = '10.3.255.98'
or `@server_ip` = '10.3.255.98'
group by client_ip,
server_ip
order by estb_succ_rate DESC
// GPT-o1生成:
SELECT
`@client_ip`,
sum(`@attach_succ`) / sum(`@attach_succ` + `@attach_fail`) as estb_succ_rate
FROM
npm_conv.rollup_main
WHERE
`@server_ip` = '10.3.255.98'
GROUP BY
`@client_ip`;
2)示例对比2
问题:00:1E:67:9B:60:0E上IPv4和IPv6流量的分布情况如何?
ChainTuning:正确。
GPT-o1:错误。原因:在生成复杂的长SQL时,错误地使用了斜引号(`)。
问题:site4的网络延迟情况
// ChainTuning生成:
select sum(
if(
`@ether_type` = 'IPv4',
`@pkt_cnt_c2s` + `@pkt_cnt_s2c`,
0
)
) as ipv4_sum_pkt_cnt,
sum(
if(
`@ether_type` = 'IPv6',
`@pkt_cnt_c2s` + `@pkt_cnt_s2c`,
0
)
) as ipv6_sum_pkt_cnt,
sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as sum_pkt_cnt,
ipv4_sum_pkt_cnt / sum_pkt_cnt as ipv4_sum_pkt_cnt_rate,
ipv6_sum_pkt_cnt / sum_pkt_cnt as ipv6_sum_pkt_cnt_rate
from npm_conv.rollup_main
where MACNumToString(`@server_mac`) in ('00:1E:67:9B:60:0E')
or MACNumToString(`@client_mac`) in ('00:1E:67:9B:60:0E')
// GPT-o1生成:
SELECT sum( if(`@ether_type` = 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)
) as ipv4_sum_pkt_cnt,
sum( if(`@ether_type` = 'IPv6', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)
) as ipv6_sum_pkt_cnt,
sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`) as sum_pkt_cnt,
(sum( if(`@ether_type` = 'IPv4', `@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)
) / sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`)) as ipv4_sum_pkt_cnt_rate,
( sum( if(`@ether_type` = 'IPv6',`@pkt_cnt_c2s` + `@pkt_cnt_s2c`, 0)
) / sum(`@pkt_cnt_c2s` + `@pkt_cnt_s2c`)
) as ipv6_sum_pkt_cnt_rate
FROM npm_conv.rollup_main
WHERE MACNumToString(`@client_mac`) = '00:1E:67:9B:60:0E'
OR MACNumToString(`@server_mac`) = '00:1E:67:9B:60:0E` //此处最后错用斜引号( ` )
4、实验结论
实验表明,基于少量Seed,ChainTuning就可以达到极高的准确率,尤其是在语法准确率方面的表现极为稳定。但由于Seed无法保证样本的丰富度,在预防幻觉方面该方法仍然有改进余地,接下来,可以使用自动化的方法根据表结构生成更为丰富的SQL及问题,以提高训练数据的丰富度。总体而言,ChainTuning运用COT思想构建Adapter、实现了自演化的大模型微调,为NL2SQL在行业应用的落地带来了一种具备高度确定性、可行性的方案。
参考文献
【1】Jason Wei, Xuezhi Wang. Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. arXiv.2201.11903.
【2】Zhuosheng Zhang, Aston Zhang. Multimodal Chain-of-Thought Reasoning in Language Models. Transactions on Machine Learning, arXiv.2302.00923
【3】WikiSQL:https://github.com/salesforce/WikiSQL
【4】Spider:https://yale-lily.github.io/spider
【5】Xuezhi Wang, Jason Wei. Self-Consistency Improves Chain of Thought Reasoning in Language Models. ICLR 2023.
【6】Lamini:https://github.com/lamini-ai/lamini
【7】Yifei Li, Zeqi Lin. Making Large Language Models Better Reasoners with Step-Aware Verifier. arXiv:2206.02336.
【8】Pranab Sahoo. A Systematic Survey of Prompt Engineering in Large Language Models: Techniques and Applications. arXiv:2402.07927
【9】Lei Huang, Weijiang Yu. A Survey on Hallucination in Large Language Models: Principles, Taxonomy, Challenges, and Open Questions. arXiv:2311.05232
【10】Xinyu Liu, Shuyu Shen. A Survey of NL2SQL with Large Language Models: Where are we, and where are we going? arXiv:2408.05109
【11】Zihui Gu, Ju Fan. Interleaving Pre-Trained Language Models and Large Language Models for Zero-Shot NL2SQL Generation. arXiv:2306.08891
【12】Ziwei Xu, Sanjay Jain. Hallucination is Inevitable: An Innate Limitation of Large Language Models. arXiv:2401.11817