使用生成式 AI 查询大型 BI 表:我们的发现
我们评估了不同的基于 LLM 的文本到 SQL 方法
2023 年 11 月 29 日在拥有大量表格数据的组织中,数据分析师的工作是理解这些数据,通过提取、转换并围绕它构建故事。分析师访问数据的主要工具是SQL。鉴于大型语言模型(LLM)令人印象深刻的能力,人们自然会想知道AI是否能帮助我们将信息需求转化为格式正确的SQL查询。
诚然,大多数LLM可以根据自然语言输入输出至少一些SQL查询。但它们能否在实际环境中处理大型、多表数据库?换句话说,它们能胜任数据分析师的工作吗?deepset的一个小型团队着手回答了这个问题。在三个月的时间里,我们尝试找到在真实数据集上生成SQL查询的最佳方法。
他们的发现概括起来就是:商业智能(BI)很难,评估BI用例也很难。声称高准确率的现有方法可能并没有讲述全部故事,而GPT-4是生成SQL查询的最佳模型之一。如果您想了解更多关于我们的发现——并找出我们评估过的方法中哪种表现最好——那么这篇文章就是为您准备的。
我们还发布了我们的基准测试:用于提示和评估LLM的代码,以及我们为这个项目创建的数据集。希望它们能对您有所帮助:)!
文本转SQL与表格问答不同
在deepset,我们长期以来一直对使用语言模型根据表格数据回答问题这一任务感兴趣。最近,我们写了关于我们与空中客车合作的项目,该项目涉及从飞行员手册中检索表格并从中提取答案——有时甚至使用简单的聚合函数,例如查找列的最大值或平均值。
然而,BI用例的操作规模完全不同。像SQL这样的查询语言可以对更大的表执行更强大的操作;例如,它可以跨多个表进行查询,连接它们,并根据复杂标准重新排序它们。为了区分这个问题与更简单(且大部分已解决)的表格问答任务,我们称之为“商业智能问答”(BIQA)。它也经常被称为“文本转SQL”。
BI用例很难,即使对人类来说也是如此。这是因为自然语言固有的歧义性,当我们试图将其映射到SQL这样的形式化语言时,这种歧义性变得尤为明显。因此,自然语言中的一个问题在SQL中可以有多种解释。比方说,我们想知道有多少开发者在日常工作中会使用Python。当我们将此表示为SQL查询时,我们必须明确:谁确切地被算作开发者?我们如何定义“日常”?
高质量数据的高标准可以大大简化歧义性问题:恰当命名的列、详尽的Schema描述和逻辑组织的表格都有助于创建更容易管理(对人类和机器而言)的表格数据库。
项目
我们项目的目标是找到将自然语言文本转换为有效SQL查询的最佳解决方案,这些查询能为用户产生正确的结果。生成的查询将与结果一起返回,以便具有一定SQL知识的用户可以验证其正确性。
以自然语言查询大型数据库的能力将为人们节省大量时间。LLM方法也意味着您可以输入拼写错误,使用英语以外的语言进行查询,甚至可以将SQL功能集成到更复杂的工作流程中——例如,它可以用作代理的工具。
现有方法
当我们开始时,似乎有很多方法可供选择。在过去的几个月里,许多团队试图利用LLM的自然语言理解(NLU)能力来生成SQL查询。
但当我们更仔细地查看他们的结果时,我们发现它们有所欠缺:通常,表现最好的模型只能解决一个非常具体的问题,而无法推广到其他BI用例。这也意味着模型在不同的基准测试中表现截然不同。我们决定创建自己的数据集和基准测试,因为我们想要的数据点能最好地反映常见的、实际的用例——并确保评估集由LLM之前未见过的数据组成。
数据集
对于我们的数据集,我们使用了最新的StackOverflow开发者调查。每年,该平台都会就编程语言和其他工具的使用情况对开发者和广大社区成员进行民意调查。调查结果以交互式图表的形式展示在他们的网站上。此外,StackOverflow还发布了全部的调查问题和答案数据集。为了构建我们的数据集,我们逆向工程了StackOverflow的SQL查询,最终得到了大约120对查询-答案。
为了使数据库更接近真实的生产环境,我们规范化了数据库Schema。例如,我们将允许一次包含多个值的列拆分成单独的、相关的表。每个数据点包含一个自然语言查询和一个对应的标签,即查询的SQL等价物。
文本转SQL评估的困境
在深入研究现有方法时,我们还发现,即使它们报告了高准确率的结果,这些结果也常常基于有疑问的评估方法。由于上述自然语言的歧义性,文本转SQL的评估仍然是一个未解决的问题,没有标准化的方法。我们认为,它至少总是需要手动部分。这是一个令人不快的真相,研究团队经常对此含糊其辞——也许是因为他们不想谈论他们只使用了极小的评估数据集来弥补耗时的手动评估结果的事实。
在尝试了几种方法后,我们采用了半自动化的评估程序。我们的评估脚本预处理了LLM的输出,将每个结果分类为正确(如果完全匹配我们评估集中的真实数据)、不正确(如果结果为None)或需要手动评估。然后,我们手动处理最后一组结果,以验证结果是否确实不正确,或者模型是否只是选择了另一个仍然产生相同结果的SQL查询。
显然,这种方法非常耗时。因此,随着时间的推移,我们在基准测试数据集中添加了几种已接受的查询。这使我们能够更快地迭代,因为我们在手动评估上花费的时间更少。
总结我们的方法
我们尝试了许多不同的方法,使我们能够探索LLM的完整NLP流水线:代理、检索增强、微调等。在这里我们总结了我们的结果。在本节末尾,我们提供了所有方法及其性能的概述。
Schema感知提示
方法:在我们的基线方法中,我们将数据库的Schema包含在LLM的提示中。Schema描述了数据库的布局:表名和描述,以及列名。这种方法为LLM提供了大量的上下文。
我们还尝试将数据库中的行包含在提示中,以帮助模型更好地解析模糊的列名,并为它提供更多关于给定单元格期望的信息。例如,在下面的示例中,我们可以看到HTML/CSS在“Languages”表中被视为一种语言。这样,模型就理解了两者总是组合在一起的,并可以相应地调整其对数据库的查询。
观察:Schema感知提示会产生非常长的提示。我们的数据库有66个表,包含每个表的详细信息导致提示大约有3000个token。这不仅昂贵,而且意味着模型必须一次处理更多信息,这可能会对其输出产生负面影响。Schema感知提示和GPT-4的简单组合使我们的准确率达到了48%左右。模型错误范围从无效的SQL查询(不匹配实际Schema)到更微妙的错误(SQL查询返回的结果不是问题的正确答案)。在提示中包含示例行确实提高了性能。
代理驱动的数据库导航
方法:代理是一个LLM,可以在给定一组工具的情况下自主行动。例如,它可以在将输出返回给用户之前迭代另一个LLM的输出。在我们的方法中,我们指示模型使用越来越复杂的SQL查询来导航数据库。这意味着我们不必自己提供数据库Schema,而是可以让模型根据需要自行找出。事实上,我们要求代理通过检索数据库中所有表的信息来开始探索。从那里,它必须自己找出其余的策略。
观察:代理方法允许LLM修复无效查询。一个好处是查询始终是有效的SQL。以下是代理驱动方法的一个示例(屏幕截图后有对各个步骤的分解):
# 问题:有多少受访者按年龄(百分比)将自己视为Stack Overflow社区的“绝对”或“某种程度”的成员?
{“query”: “PRAGMA table_info(Responses);”, “show_user”: false}
# 首先,代理获取“Responses”表的有关信息。
{“query”: “SELECT DISTINCT SOComm FROM Responses;”, “show_user”: false}
# 它发现需要查看SOComm列,特别是其中的唯一值。
{“query”: “SELECT DISTINCT Age FROM Responses;”, “show_user”: false}
# 它还检查了“Age”列。
{“query”: “SELECT Age, COUNT(*) as Total, SUM(CASE WHEN SOComm IN (‘Yes, definitely’, ‘Yes, somewhat’) THEN 1 ELSE 0 END) as PartOfSOCommunity FROM Responses GROUP BY Age;”, “show_user”: false}
# 执行必要的操作,但仍然show_user: false。
{“query”: “SELECT Age, ROUND((SUM(CASE WHEN SOComm IN (‘Yes, definitely’, ‘Yes, somewhat’) THEN 1 ELSE 0 END) * 100.0) / COUNT(*), 2) as Percentage FROM Responses GROUP BY Age;”, “show_user”: true}
# 计算百分比并设置show_user: true以结束生成。
虽然这种方法最初允许我们输入较短的提示,但代理的迭代方法意味着历史数据会随着代理采取的每一步而传递:提示不断增长。因此,我们面临着与大型提示相同的问题,即模型难以管理,并且API调用成本越来越高。代理方法的黑箱性质也存在一个问题,有时会导致比Schema感知提示更难理解的错误。
包含原始Schema描述
方法:在这种方法中,我们不仅提供了数据库生成的Schema,还提供了StackOverflow关于列含义的描述。我们在任何表Schema描述之前,将这些原始描述添加到提示的前面。这些手写描述包含有价值的信息,例如用于获取答案的问题。因此,预计它们将澄清数据库Schema中的任何歧义。例如,在下面的屏幕截图中,我们看到YearsCode列定义为“包括任何教育”的编程经验年限。
观察:为每列添加描述使提示更长(约7k token)。它将准确率提高了10个百分点,达到58%。
包含列级别描述和唯一值
方法:在此方法中,我们试图通过包含每列的描述作为Schema的一部分来丰富LLM可用的列级别信息。我们还包含了最多20个列可能包含的唯一值以及描述。这导致查询大约为6.5k token。
观察:这进一步提高了结果,将准确率提高到65%。
包含少样本示例和自定义指令
方法:在分析了模型遇到的一些问题后,我们尝试修改提示并添加少样本示例来缓解错误。LLM的一个常见错误是重复计数出现在多个行中的人员。相反,我们希望它在COUNT()函数中添加DISTINCT子句,以删除相同数据的重复出现。
因此,我们将以下指令添加到提示中:
此外,对于响应的百分比计算,请使用“responses”主表(考虑null值)或关联表的唯一值。而不是关联表的全部计数。
此外,我们还包含了一些示例,向LLM展示期望的行为。
可以争辩说,这超出了数据库或域的提示工程范围,接近于对评估集的过度拟合。尽管如此,它证明了LLM对这些变化的响应能力。
观察:模型对指令和示例做出了响应,并且大部分停止了观察到的错误。这使得准确率提高到70%。
检索增强
方法:模型不需要知道数据库中的所有表来回答问题。例如,评估集中的大多数问题都可以使用66个表中的五个或更少来回答。检索模块可以检索正确的表,这将缩短提示并帮助模型只关注相关信息。然而,值得注意的是,当时没有针对将自然语言查询匹配到表Schema这一特定任务进行调整的检索器。我们使用了deepset/all-mpnet-base-v2,我们在之前的表检索任务中看到它表现良好。
观察:检索组件效果不佳。嵌入模型根本无法为每个查询选择相关表。下面的图表绘制了检索到的表数(x轴)与召回率(y轴,即正确检索的表占总数的百分比)。它显示,即使增加了top_k值,检索组件也未能识别出所有相关的表。当然,这会导致LLM一开始看不到生成其SQL查询的正确上下文。
列级别检索(即单独嵌入列,并在列值较高时返回整个表)比表级别检索表现更好。这可能是因为将查询与单个列进行匹配更容易,因为包含许多与查询无关的列的表可能会淹没来自单个列的信号。此外,我们处理的设置中,一个具有大量列的特定表(“Responses”)几乎总是应该被检索。
我们还尝试了完美检索(只传递相关表的Schema),以确定基于检索方法的上限。令我们惊讶的是,它的表现不如传递所有表(60% vs. 65.8%),因为LLM犯的错误更多。
RAG方法表现不佳的部分原因是数据集不平衡,其中表的列数差异很大。我们可以尝试改进它(例如,切换到更好的检索模型或重新调整设置),但鉴于即使是完美检索在此数据集上的表现也不佳,这项探索最好在不同的数据集/设置上进行。
其他模型
方法:除了GPT-4,我们还尝试了许多开源模型:Starcoderbase、New Hope、Codegen 2.5、sqlcoder模型等。
观察:其中,最近发布的sqlcoder-34b-alpha(8bit加载)给出了最有希望的结果(35.8%),但仍不及GPT-4。我们遇到的主要问题是它经常会产生不存在的列,例如work_exp、response_op_sys_professional_use。
微调现有OSS模型
方法:我们可以使用一个较小的开源LLM,并在一个合适的数据集上对其进行微调。为此,我们在EC2实例上使用资源高效的QLoRA方法和Spider数据集的子集对多个模型进行了微调。这些模型包括Llama 2 70B,特别是当时所有可用的代码相关模型(例如,Starcoderbase、New Hope、Codegen 2.5)。
观察:不幸的是,我们所有的OSS方法都失败了,很可能是因为我们用于微调的数据集不适合这项任务。Spider数据集是学术性的,并没有真正捕捉到我们感兴趣的实际业务用例(并且我们的评估数据集相当准确地反映了这些用例)。性能得分相应较低,约为10%。
改进的Schema+评估结果
从我们对模型预测和错误模式的定性分析来看,我们注意到许多表或列名很难与原始问题对应起来。当LLM除了Schema之外还获得了列的描述时,其性能有所提高,这似乎证实了这一点。
因此,最初的任务就像给一个新来的业务分析师一个混乱的数据库,里面有几十个表和晦涩难懂的Schema,没有任何文档或帮助,然后期望他们能回答我们的问题。我们觉得我们没有给模型一个公平的机会来完成这项工作。
良好的数据卫生对于BI至关重要,因此我们决定坐下来修改Schema,以一种更易于理解的方式构建表,并更改列名使其具有自解释性。例如,我们将LearnCode列重命名为LearningToCodeMethods,这大大降低了原始列名的歧义性。
本文中报告的所有评估结果均基于新的Schema,这显著提高了各种方法的整体性能。
| 方法 | 总体正确率 (%) |
|---|---|
| 代理驱动 | 40.8 |
| Schema感知 | 48.3 |
| Schema感知 + 原始描述 | 58.3 |
| Schema感知 + 列级别描述 + 唯一值20 | 65.8 |
| Schema感知 + 完美检索 + 唯一值20 | 60.0 |
| Schema感知 + 列级别描述 + 唯一值20 + 少样本 | 70.0 |
| Schema感知 + 完美检索 (sqlcoder-34b-alpha @ 8bit) | 35.8 |
挑战与经验
创建一个通用的、由LLM驱动的SQL商业智能解决方案仍然是一个尚未完全解决的问题——这使得进一步探索它变得更加有趣。主要挑战与以下两个因素有关:
数据集创建和评估非常耗时
为评估文本转SQL方法创建数据集比例如提取式问答要困难得多。本质上,您必须为每个数据点编写一段代码(SQL查询)。在查询可以放入数据集之前,需要对其进行测试和调试。
不仅仅是数据集的创建——评估本身也很耗时,因为正如我们之前描述的,它无法完全自动化。我们目前进行的文本转SQL评估并不是真正可扩展的。需要准确评估其方法的项目必须考虑到这会花费大量时间。
语言和数据库的歧义性
如我们所见,自然语言查询可能非常模糊,难以将其翻译成SQL查询。另一方面,从数据库中提取信息的正确方法通常不止一种,而我们的评估数据集可能没有考虑到所有这些方法。这让我们回到了这样一个事实:我们需要在每次运行后至少部分地手动评估模型的结果。
在这里发挥作用的另一个因素是数据库本身也可能含糊不清——如果Schema设计得不是特别好,那么情况会更糟。这就是为什么需要一个熟悉数据库的人参与进来——这将帮助您更好地评估生成的查询。您甚至可以使用模型的输出来作为对命名实践的一种检查——例如,如果一个模型一直在滥用一个列,这可能是一个信号,表明是时候重命名它或更新其描述了。
演示
除了我们的数据集和基准测试,我们还发布了一个演示,展示我们表现最佳的文本转SQL方法,与同事、朋友以及现在的您——我们的读者分享。它旨在验证我们的准确率结果是否代表了真实世界场景。我们发现用户反馈与我们的结果非常接近。该演示托管在deepset的企业平台deepset Cloud上,该平台提供开箱即用的用户界面。它看起来是这样的:
让我们尝试重现一个来自开发者调查的问题
经验丰富的开发者与新手计划在未来如何使用AI工具?请提供数字。
结果是一个表格,后面跟着生成的查询本身。让我们先看看查询。
请注意,LLM已经决定自己定义谁是“经验丰富的开发者”,即编码超过五年的人。这是模型如何补偿我们自然语言查询的歧义性的一个例子。以下是我们得到的结果表的快照:
我们现在可以通过将表格复制到csv文件(或其他结构化文件格式)来重新使用此生成的内容。我们甚至可以使用pandas和matplotlib等Python库进行可视化。
现在我们可以看到,尽管新手群体(由LLM定义)比经验丰富的开发者群体大得多,但两个群体对AI工具的期望似乎非常相似。
这只是我们文本转SQL解决方案功能的一瞥。请随意试用演示并给我们反馈!
结论
高管希望通过AI即时获取任何见解或仪表板而无需经过业务分析师的梦想,在短期内不会实现。我们测试过的方法仍然太脆弱,无法盲目信任它们的结果来做关键业务决策。
然而,就像Copilot提高开发人员生产力一样,BI的AI助手可以显著提高任何业务分析师的生产力。他们可以利用LLM生成初始查询,审查生成的SQL和输出,只有在必要时进行编辑。仅仅通过我们自己使用该工具,我们确信它将以与代码助手影响开发人员类似的方式提升分析师的生产力。
要创建一个通用的SQL模型非常困难——大多数成功案例都涉及非常具体的用例。目前,您只能为特定用例创建具有更好微调数据集的专用模型。但请记住,创建数据集非常耗时。
稳健的评估同时允许您快速前进,这对于取得进步至关重要。如果您的组织已经实施了良好的数据管理实践,那么您在文本转SQL方面很可能会取得更大的成功。
想超越AI炒作,在LLM能发挥真正价值的地方使用它们?请查看我们在GitHub上的Haystack repo,并加入我们的Discord!
