LangChain 库有多个 SQL 链,甚至还有一个 SQL 代理,本文旨在尽可能轻松地与存储在 SQL 中的数据进行交互。以下是一些相关链接:

介绍

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

问题所在

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

不幸的是,有几件事。

存在的主要问题是幻觉。LLM可以编写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性能。当使用命令(包括列名称、列类型、列引用和键)提示 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创建最佳查询的能力。例如,如果我们在表中搜索作曲家,那么知道该列是否由全名、缩写名称、两者甚至其他表示形式组成将非常有用。Rajkumar 等人发现,在描述之后的语句中提供示例行会导致一致的性能改进。有趣的是,他们发现提供 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

Observation: Error: (sqlite3.OperationalError) no such column: 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]
(Background on this error at: https://sqlalche.me/e/14/e3q8)
Thought: I should double check my query before executing it.
Action: query_checker_sql_db
Action Input: 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
Observation:
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;
Thought: I now know the final answer.
Action: query_sql_db
Action Input: 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交互的最佳方法。以下是未来的积压工作:

Few-shot示例

Rajkumar 等人还发现,Codex 的 SQL 生成准确性在具有少量样本学习的基准测试中有所提高,其中问题查询示例附加到提示中(参见图 2)。

使用子查询

一些用户发现,告诉代理将问题分解为多个子查询,包括对每个子查询的注释,有助于代理获得正确的答案。在子查询中进行思考会强制代理以逻辑步骤进行思考,从而降低在查询中犯结构错误的可能性。这类似于在提示中添加 CoT 类型短语,例如对于非 sql 问题“逐步思考此问题”。