跳到正文
LangChain:Blog·· 2023-03-13精选AI 评分60

LangChain 发布 LLM 与 SQL 结合的实践指南

LLMs and SQL

AI 导读

LangChain 与社区成员 Francisco Ingham、Jon Luo 发布 LLM 与 SQL 结合的实践指南,并宣布 3 月 22 日举行一小时网络研讨会。

推荐理由

LangChain 团队系统整理了让 LLM 生成有效 SQL 的三类问题与对应解法,方法可直接迁移到自己的 Text-to-SQL 场景。

正文 · AI 翻译

Francisco Ingham 和 Jon Luo 是两位社区成员,他们正在引领 SQL 集成方面的变革。我们非常兴奋能与他们一起撰写这篇博客文章,回顾他们在这一过程中学到的所有技巧和诀窍。我们更兴奋地宣布,我们将与他们一起举办一场长达一小时的网络研讨会,讨论这些经验并解答其他相关问题。本次网络研讨会将于 3 月 22 日举行——请通过以下链接报名:

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 提供足够的信息,使其能够为给定数据库生成合理的查询,我们需要在提示中有效地描述数据库。这可以包括描述表结构、数据样子的示例,甚至该数据库的优秀查询示例。下面的示例来自 Chinook 数据库。

描述模式

在旧版本的 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 在多种不同提示结构下的 Text-to-SQL 性能。当使用 CREATE TABLE 命令提示 Codex 时,他们获得了最佳性能,这些命令包括列名、类型、列引用和键。对于 Track 表,这看起来像:

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 提供更多信息的示例行。举例来说,在 `Track` 表中,有时多个作曲家之间用斜杠而不是逗号分隔。这首次出现在表的第 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 生成的查询在语法上有问题,我们会在运行链或代理时看到 traceback。如果我们想将其用于生产目的,这会非常麻烦。我们如何帮助 LLM 修正查询?我们可以完全复制自己犯错时会采取的做法。我们将原始查询连同 traceback 日志发送给 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 最佳交互的最优方式。以下是未来的待办事项:

少样本示例

Rajkumar 等人还发现,在基准测试中,通过少样本学习(即在提示中附加问题-查询示例,见图 2),Codex 的 SQL 生成准确率有所提高。

使用子查询

一些用户发现,让智能体将问题分解为多个子查询,并附上每个子查询作用的注释,有助于智能体得出正确答案。用子查询思考会迫使智能体按逻辑步骤思考,从而降低查询中出现结构性错误的概率。这类似于在非 SQL 问题的提示中加入“逐步思考这个问题”之类的 CoT 短语。

如果你想帮助实现其中任何一项,或者有其他你认为有帮助的最佳实践,请在 Discord 的 #sql 频道讨论中分享你的想法,或者直接尝试提交一个 PR!

来源:LangChain:Blog · langchain.com