跳转到主要内容

category

Francisco Ingham和Jon Luo是领导SQL集成变革的两名社区成员。我们真的很高兴能写这篇博客文章,让他们复习他们学到的所有技巧和窍门。我们更高兴地宣布,我们将与他们进行一个小时的网络研讨会,讨论这些知识并提出其他相关问题。本次网络研讨会将于3月22日举行-请在以下链接注册:

LangChain库有多个SQL链,甚至还有一个SQL代理,旨在使与存储在SQL中的数据的交互尽可能简单。以下是一些相关链接:

介绍

企业的大部分数据传统上存储在SQL数据库中。随着那里存储了大量有价值的数据,使查询和理解那里存在的数据变得容易的商业智能(BI)工具越来越受欢迎。但是,如果您可以用自然语言与SQL数据库进行交互呢?有了今天的LLM,这是可能的。LLM对SQL有一定的理解,并且能够很好地编写它。然而,有几个问题使这成为一项不平凡的任务。

问题

所以LLM可以编写SQL——还需要什么?

不幸的是,有几件事。

目前存在的主要问题是幻觉。LLM可以编写SQL,但它们通常倾向于组成表、组成字段,并且通常只编写SQL,如果对数据库执行,这些SQL实际上是无效的。因此,我们面临的一大挑战是如何在现实中建立LLM,使其生成有效的SQL。

解决这个问题的主要想法(我们将在下面详细介绍)是向LLM提供关于数据库中实际存在的内容的知识,并告诉它编写与此一致的SQL查询。然而,这会遇到第二个问题——上下文窗口长度。LLM有一些上下文窗口,限制了它们可以操作的文本量。这是相关的,因为SQL数据库通常包含大量信息。因此,如果我们天真地把所有的数据都传递给LLM,那么我们很可能会遇到这个问题。

第三个问题是一个更基本的问题:有时LLM会搞砸。它所写的SQL可能由于任何原因而不正确,也可能是正确的,但只是返回了一个意外的结果。那我们该怎么办?我们放弃了吗?

(高级)解决方案

当思考如何解决这些问题时,思考我们作为人类如何解决这些事情是很有启发性的。如果我们能够复制我们将要采取的解决这些问题的步骤,我们也可以帮助LLM做到这一点。因此,让我们思考一下,如果数据分析师被要求回答BI问题,他们会怎么做。

当数据分析师查询SQL数据库时,他们通常会做一些事情来帮助他们做出正确的查询。例如,他们通常事先进行一个示例查询,以了解数据的外观。他们可以查看表的模式,甚至可以查看某些行。这可以被认为是数据分析师学习数据的外观,以便在未来编写SQL查询时,它以实际存在的数据为基础。数据分析师通常也不会同时查看所有数据(或数千行),他们可能会将任何探索性查询限制在前K行,或者查看摘要统计数据。这可以为如何绕过上下文窗口限制提供一些提示。最后,如果数据分析师遇到错误,他们不会放弃——他们会从错误中吸取教训并编写新的查询。

我们将在下面单独的一节中讨论这些解决方案中的每一个。

描述您的数据库

为了给LLM提供足够的信息,使其能够为给定的数据库生成合理的查询,我们需要在提示中有效地描述数据库。这可以包括描述表结构、数据外观的示例,甚至是数据库良好查询的示例。以下示例来自奇努克数据库。

描述模式

在旧版本的LangChain中,我们只提供了表名、列及其类型:

Table 'Track' has columns: TrackId (INTEGER), Name (NVARCHAR(200)), AlbumId (INTEGER), MediaTypeId (INTEGER), GenreId (INTEGER), Composer (NVARCHAR(220)), Milliseconds (INTEGER), Bytes (INTEGER), UnitPrice (NUMERIC(10, 2))

Rajkumar等人进行了一项研究,评估了OpenAI Codex在各种不同提示结构下的文本到SQL性能。当使用CREATETABLE命令提示Codex时,它们获得了最佳性能,这些命令包括列名、列类型、列引用和键。对于“轨迹”表,如下所示:

CREATE TABLE "Track" (
"TrackId" INTEGER NOT NULL,
"Name" NVARCHAR(200) NOT NULL,
"AlbumId" INTEGER,
"MediaTypeId" INTEGER NOT NULL,
"GenreId" INTEGER,
"Composer" NVARCHAR(220),
"Milliseconds" INTEGER NOT NULL,
"Bytes" INTEGER,
"UnitPrice" NUMERIC(10, 2) NOT NULL,
PRIMARY KEY ("TrackId"),
FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"),
FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"),
FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId")
)

描述数据

我们可以通过额外提供数据外观的示例来进一步提高LLM创建最佳查询的能力。例如,如果我们在Track表中搜索作曲家,那么了解Composer列是否由全名、缩写名或两者组成,甚至可能由其他表示形式组成,将非常有用。Rajkumar等人发现,在CREATE TABLE描述之后的SELECT语句中提供示例行可以带来一致的性能改进。有趣的是,他们发现提供3行是最佳的,提供更多的数据库内容甚至会降低性能。

我们采用了他们论文中的最佳实践结果作为默认设置。总之,我们在提示中的数据库描述如下所示:

db = SQLDatabase.from_uri(
	"sqlite:///../../../../notebooks/Chinook.db",
	include_tables=['Track'], # including only one table for illustration
	sample_rows_in_table_info=3
)
print(db.table_info)

哪些输出:

CREATE TABLE "Track" (
"TrackId" INTEGER NOT NULL,
"Name" NVARCHAR(200) NOT NULL,
"AlbumId" INTEGER,
"MediaTypeId" INTEGER NOT NULL,
"GenreId" INTEGER,
"Composer" NVARCHAR(220),
"Milliseconds" INTEGER NOT NULL,
"Bytes" INTEGER,
"UnitPrice" NUMERIC(10, 2) NOT NULL,
PRIMARY KEY ("TrackId"),
FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"),
FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"),
FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId")
)
SELECT * FROM 'Track' LIMIT 3;
TrackId	Name	AlbumId	MediaTypeId	GenreId	Composer	Milliseconds	Bytes	UnitPrice
1	For Those About To Rock (We Salute You)	1	1	1	Angus Young, Malcolm Young, Brian Johnson	343719	11170334	0.99
2	Balls to the Wall	2	2	1	None	342562	5510424	0.99
3	Fast As a Shark	3	2	1	F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman	230619	3990994	0.99

使用自定义表信息

尽管LangChain可以方便地自动组装模式和样本行描述,但在少数情况下,最好用手工制作的描述覆盖自动信息。例如,如果您知道表的前几行没有信息,那么最好手动提供示例行,为LLM提供更多信息。例如,在“音轨”表中,有时多个作曲家用斜线而不是逗号分隔。这首先出现在表的第111行,远远超出了我们的3行限制。我们可以提供此自定义信息,以便示例行包含此新信息。下面是一个在实践中这样做的例子。

也可以使用自定义描述来限制LLM可见的表的列。应用于Track表的这两种用法的示例可能如下所示:

CREATE TABLE "Track" (
"TrackId" INTEGER NOT NULL,
"Name" NVARCHAR(200) NOT NULL,
"Composer" NVARCHAR(220),
PRIMARY KEY ("TrackId"),
)
SELECT * FROM 'Track' LIMIT 4;
TrackId	Name	Composer
1	For Those About To Rock (We Salute You)	Angus Young, Malcolm Young, Brian Johnson
2	Balls to the Wall	None
3	Fast As a Shark	F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman
4	Money	Berry Gordy, Jr./Janie Bradford

如果您有不希望发送到API的敏感数据,可以使用此功能来提供模拟数据,而不是实际的数据库。

限制输出的大小

当我们在链或代理中使用LLM进行查询时,查询的结果将用作另一个LLM的输入。如果查询结果太大,这将使模型的输入大小最大化。因此,明智地限制查询输出的大小通常是一种很好的做法。我们可以通过指示LLM使用尽可能少的列并限制返回的行数来实现这一点。

正如我们在下面的例子中所看到的那样,如果我们要求提供每个国家的总销售额列表,而没有指定国家的数量,那么查询的上限将为10。您可以使用top_k参数来管理此限制。

agent_executor.run("List the total sales per country. Which country's customers spent the most?")

 

…
Action Input: SELECT c.Country, SUM(i.Total) AS TotalSales FROM Invoice i INNER JOIN Customer c ON i.CustomerId = c.CustomerId GROUP BY c.Country ORDER BY TotalSales DESC LIMIT 10
Observation: [('USA', 523.0600000000003), ('Canada', 303.9599999999999), ('France', 195.09999999999994), ('Brazil', 190.09999999999997), ('Germany', 156.48), ('United Kingdom', 112.85999999999999), ('Czech Republic', 90.24000000000001), ('Portugal', 77.23999999999998), ('India', 75.25999999999999), ('Chile', 46.62)]
…

正在检查语法

如果LLM生成的查询在语法上被破坏,我们会发现在运行链或代理时会得到回溯。如果我们想将其用于生产目的,这是非常有问题的。我们如何帮助LLM更正查询?如果我们自己犯了错误,我们可以完全复制我们会做的事情。我们将原始查询和回溯日志一起发送到LLM,并要求它通过准确地了解出了什么问题来纠正错误。这个概念的灵感来自于这篇博客文章,在那里你可以找到更详细的解释。

在下面的文档示例中,您可以看到模型试图查询一个不存在的列,当它发现查询错误时,它会立即使用query_checker_sql_db工具进行更正:

观察:错误:(sqlite3.OperationalError)没有这样的列:Track.ArtistId

SQL: SELECT Artist.Name, SUM(InvoiceLine.Quantity) AS TotalQuantity FROM Artist INNER JOIN Track ON Artist.ArtistId = Track.ArtistId INNER JOIN InvoiceLine ON Track.TrackId = InvoiceLine.TrackId GROUP BY Artist.Name ORDER BY TotalQuantity DESC LIMIT 3

(此错误的背景位于:https://sqlalche.me/e/14/e3q8)

我想:在执行查询之前,我应该仔细检查一下。

操作:query_checker_sql_db

操作输入:SELECT Artist.Name, SUM(InvoiceLine.Quantity) AS TotalQuantity FROM Artist INNER JOIN Track ON Artist.ArtistId = Track.ArtistId INNER JOIN InvoiceLine ON Track.TrackId = InvoiceLine.TrackId GROUP BY Artist.Name ORDER BY TotalQuantity DESC LIMIT 3

观察结果:

SELECT Artist.Name, SUM(InvoiceLine.Quantity) AS TotalQuantity
FROM Artist
INNER JOIN Track ON Artist.ArtistId = Track.ArtistId
INNER JOIN InvoiceLine ON Track.TrackId = InvoiceLine.TrackId
GROUP BY Artist.Name
ORDER BY TotalQuantity DESC
LIMIT 3;

我想:我现在知道了最后的答案。

操作:query_sql_db

操作输入:SELECT Artist.Name, SUM(InvoiceLine.Quantity) AS TotalQuantity FROM Artist INNER JOIN Album ON Artist.ArtistId = Album.ArtistId INNER JOIN Track ON Album.AlbumId = Track.AlbumId INNER JOIN InvoiceLine ON Track.TrackId = InvoiceLine.TrackId GROUP BY Artist.Name ORDER BY TotalQuantity DESC LIMIT 3

未来工作

正如您所知,该领域进展迅速,我们正在共同寻找实现最佳LLM-SQL交互的最佳方法。以下是未来的积压工作:

很少有镜头示例

Rajkumar等人还发现,Codex的SQL生成准确性在基准测试中得到了提高,只需少量的镜头学习,其中问题查询示例被附加到提示中(见图2)。

使用子查询

一些用户发现,告诉代理将问题分解为多个子查询,包括对每个子查询的作用的评论,有助于代理获得正确的答案。在子查询中进行思考会迫使代理按逻辑步骤进行思考,从而降低查询中出现结构错误的概率。这类似于在非sql问题的提示中添加CoT类型的短语,如“逐步思考这个问题”。

如果你想帮助实施其中任何一个,或者有其他你觉得有用的最佳实践,请在Discord的#sql频道的讨论中分享你的想法,或者直接尝试公关!

 

文章链接