删库跑路,一个所有程序员可能都幻想过的场景,不过想想后果还是很严重的,不过不要怕,现在我们可以利用大模型来删库,啊不,操作数据库,这就是传说中的Text2Sql。
什么是Text2Sql
Text2Sql就是将自然语言的问题转化为结构化的sql查询语句,可以让用户更直观的和数据库进行交互。其实Text2Sql不是什么特别新的东西,在大模型出来之前,也是有类似的尝试的
早期阶段主要依赖于人工编写的模板规则,用模板匹配自然语言和sql语句之间的关系,后面机器学习兴起之后,开始采用序列到序列模型这样的机器学习方法,来学习自然语言到SQL之间的映射关系。最后就是现在,能够借助大语言模型的强大语言理解和代码生成能力,再加上提示词工程、微调这样的方法,进一步提升了Text2Sql的性能。
LLM下的Text2Sql
在大语言模型的加持下,Text2Sql主要有以下几个步骤
- 自然语言理解:分析用户输入的自然语言问题,理解意图和语义
- 模式链接:将问题中的实体与数据库中的表和列进行链接
- SQL生成:根据语义和模式链接结果,生成对应的SQL查询语句
- SQL执行:在数据库上执行SQL语句,将结果返回给用户
前面几篇文章我们介绍了LangChain的使用,下面我们就用LangChain来简单实现一个Text2sql
Text2Sql的实现
SQLDatabaseTookit使用
SQLDatabaseTookit
是LangChain提供的一个agent,可以轻松帮助我们实现Text2Sql
导入相关依赖
python
from langchain_community.agent_toolkits.sql.base import create_sql_agent
from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
from langchain_community.utilities import SQLDatabase
from dotenv import load_dotenv
load_dotenv('../.env')
配置数据库
python
db_user = "user"
db_password = "pwd"
db_host = "localhost:3306"
db_name = "db_name"
db = SQLDatabase.from_uri(f"mysql+pymysql://{db_user}:{db_password}@{db_host}/{db_name}")
实例化agent
python
api_key = os.environ.get('DASHSCOPE_API_KEY')
llm = ChatOpenAI(
temperature=0.01,
model="qwen-max",
openai_api_base = "https://dashscope.aliyuncs.com/compatible-mode/v1",
openai_api_key = api_key
)
# 需要设置llm
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
# SQL智能体:给它目标,它自己会进行规划,最终把结果给你
agent_executor = create_sql_agent(
llm=llm,
toolkit=toolkit,
verbose=True
)
提问
python
agent_executor.run("找出英雄攻击力最高的前5个英雄")
输出:
makefile
> Entering new SQL Agent Executor chain...
Action: sql_db_list_tables
Action Input: address, asset_grades, bank, car_sales, customers, dept, employee, form, height_grades, hero_score, heros, orders, person, player, player_score, student, team, team_score, test_work, trips, user, users, using, weather从表名来看,`heros` 表可能包含了英雄的信息。我需要查看 `heros` 表的结构来确定是否有攻击力(attack)相关的字段。
Action: sql_db_schema
Action Input: heros
CREATE TABLE heros (
id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
hp_max FLOAT,
hp_growth FLOAT,
hp_start FLOAT,
mp_max FLOAT,
mp_growth FLOAT,
mp_start FLOAT,
attack_max FLOAT,
attack_growth FLOAT,
attack_start FLOAT,
defense_max FLOAT,
defense_growth FLOAT,
defense_start FLOAT,
hp_5s_max FLOAT,
hp_5s_growth FLOAT,
hp_5s_start FLOAT,
mp_5s_max FLOAT,
mp_5s_growth FLOAT,
mp_5s_start FLOAT,
attack_speed_max FLOAT,
attack_range VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
role_main VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
role_assist VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
birthdate DATE,
PRIMARY KEY (id)
)DEFAULT CHARSET=utf8mb3 ENGINE=InnoDB
/*
3 rows from heros table:
id name hp_max hp_growth hp_start mp_max mp_growth mp_start attack_max attack_growth attack_start defense_max defense_growth defense_start hp_5s_max hp_5s_growth hp_5s_start mp_5s_max mp_5s_growth mp_5s_start attack_speed_max attack_range role_main role_assist birthdate
10000 夏侯惇 7350.0 288.8 3307.0 1746.0 94.0 430.0 321.0 11.57 159.0 397.0 21.14 101.0 98.0 3.357 51.0 37.0 1.571 15.0 0.0 近战 坦克 战士 2016-07-19
10001 钟无艳 7000.0 275.0 3150.0 1760.0 95.0 430.0 318.0 11.0 164.0 409.0 22.07 100.0 92.0 3.143 48.0 37.0 1.571 15.0 0.0 近战 战士 坦克 2021-05-01
10002 钟 8341.0 329.4 3450.0 100.0 0.0 100.0 301.0 10.57 153.0 504.0 27.07 125.0 115.0 4.143 57.0 5.0 0.0 5.0 0.0 近战 坦克 辅助 2012-05-04
*/`heros` 表中确实有 `attack_max` 字段,这应该代表英雄的最大攻击力。为了找出攻击力最高的前5个英雄,我需要根据 `attack_max` 字段降序排列,并限制结果为前5条记录。同时,我还应该选择 `name` 和 `attack_max` 两个字段来展示英雄的名字和他们的最大攻击力。
Action: sql_db_query_checker
Action Input: SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5```sql
SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5
```查询语句看起来是正确的,接下来我将执行这个查询。
Action: sql_db_query
Action Input: SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5[('阿轲', 427.0), ('孙尚香', 411.0), ('百里守约', 410.0), ('虞姬', 407.0), ('黄忠', 403.0)]I now know the final answer.
Final Answer: 攻击力最高的前5个英雄及其攻击力如下:
1. 阿轲 - 427.0
2. 孙尚香 - 411.0
3. 百里守约 - 410.0
4. 虞姬 - 407.0
5. 黄忠 - 403.0
> Finished chain.
根据以上的输出,我们可以看到agent一共做了这几步操作:
- 查询有哪些表
sql
Action: sql_db_list_tables
Action Input: address, asset_grades, bank, car_sales, customers, dept, employee, form, height_grades, hero_score, heros, orders, person, player, player_score, student, team, team_score, test_work, trips, user, users, using, weather
- 分析哪张表式需要查询的表
sql
从表名来看,`heros` 表可能包含了英雄的信息。我需要查看 `heros` 表的结构来确定是否有攻击力(attack)相关的字段。
Action: sql_db_schema
Action Input: heros
CREATE TABLE heros (
id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
hp_max FLOAT,
hp_growth FLOAT,
hp_start FLOAT,
mp_max FLOAT,
mp_growth FLOAT,
mp_start FLOAT,
attack_max FLOAT,
attack_growth FLOAT,
attack_start FLOAT,
defense_max FLOAT,
defense_growth FLOAT,
defense_start FLOAT,
hp_5s_max FLOAT,
hp_5s_growth FLOAT,
hp_5s_start FLOAT,
mp_5s_max FLOAT,
mp_5s_growth FLOAT,
mp_5s_start FLOAT,
attack_speed_max FLOAT,
attack_range VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
role_main VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
role_assist VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
birthdate DATE,
PRIMARY KEY (id)
)DEFAULT CHARSET=utf8mb3 ENGINE=InnoDB
- 查看表里的数据,并分析需要用的字段
yaml
/*
3 rows from heros table:
id name hp_max hp_growth hp_start mp_max mp_growth mp_start attack_max attack_growth attack_start defense_max defense_growth defense_start hp_5s_max hp_5s_growth hp_5s_start mp_5s_max mp_5s_growth mp_5s_start attack_speed_max attack_range role_main role_assist birthdate
10000 夏侯惇 7350.0 288.8 3307.0 1746.0 94.0 430.0 321.0 11.57 159.0 397.0 21.14 101.0 98.0 3.357 51.0 37.0 1.571 15.0 0.0 近战 坦克 战士 2016-07-19
10001 钟无艳 7000.0 275.0 3150.0 1760.0 95.0 430.0 318.0 11.0 164.0 409.0 22.07 100.0 92.0 3.143 48.0 37.0 1.571 15.0 0.0 近战 战士 坦克 2021-05-01
10002 钟 8341.0 329.4 3450.0 100.0 0.0 100.0 301.0 10.57 153.0 504.0 27.07 125.0 115.0 4.143 57.0 5.0 0.0 5.0 0.0 近战 坦克 辅助 2012-05-04
*/`heros` 表中确实有 `attack_max` 字段,这应该代表英雄的最大攻击力。为了找出攻击力最高的前5个英雄,我需要根据 `attack_max` 字段降序排列,并限制结果为前5条记录。同时,我还应该选择 `name` 和 `attack_max` 两个字段来展示英雄的名字和他们的最大攻击力。
- 根据提问编写sql并执行查询,最终返回结果
sql
Action: sql_db_query_checker
Action Input: SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5```sql
SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5
```查询语句看起来是正确的,接下来我将执行这个查询。
Action: sql_db_query
Action Input: SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5[('阿轲', 427.0), ('孙尚香', 411.0), ('百里守约', 410.0), ('虞姬', 407.0), ('黄忠', 403.0)]I now know the final answer.
Final Answer: 攻击力最高的前5个英雄及其攻击力如下:
1. 阿轲 - 427.0
2. 孙尚香 - 411.0
3. 百里守约 - 410.0
4. 虞姬 - 407.0
5. 黄忠 - 403.0
如果是一个程序员从一个陌生的数据库里查询数据,基本上也就是这个流程了,虽然这个sql不是很复杂,但是可以看到agent已经具备通过自然语言来查询数据的能力了。
下面我们通过保险场景,来看一下,Text2Sql的实战能力如何
保险场景实战
数据库信息
首先来看一下我们要用的数据表

-
客户信息表(CustomerInfo):客户ID(CustomerID)、客户姓名(Name)、性别(Gender)、出生日期(DateOfBirth)、身份证号码(IDNumber)、联系地址(Address)、联系电话(PhoneNumber)、电子邮件地址(EmailAddress)、婚姻状况(MaritalStatus)、职业(Occupation)、健康状况(HealthStatus)、客户注册日期(RegistrationDate)、客户类型(CustomerType)、客户来源(SourceOfCustomer)、客户状态(CustomerStatus)
-
保单信息表(PolicyInfo):保单号(PolicyNumber)、客户ID(CustomerID)、保险产品ID(ProductID)、保单状态(PolicyStatus)、受益人(Beneficiary)、受益人关系(Relationship)、投保日期(PolicyStartDate)、终止日期(PolicyEndDate)、保费支付状态(PremiumPaymentStatus)、保费支付日期(PaymentDate)、保费支付方式(PaymentMethod)、代理人ID(AgentID)
-
理赔信息表(ClaimInfo):理赔号(ClaimNumber)、保单号(PolicyNumber)、理赔日期(ClaimDate)、理赔类型(ClaimType)、理赔金额(ClaimAmount)、理赔状态(ClaimStatus)、理赔描述(ClaimDescription)、受益人ID(BeneficiaryID)、医疗记录(MedicalRecords)、事故报告(AccidentReport)、审核人(ClaimHandler)、审核日期(ReviewDate)、支付方式(PaymentMethod)、支付日期(PaymentDate)、拒赔原因(DenialReason)
-
受益人信息表(BeneficiaryInfo):受益人ID(BeneficiaryID)、姓名(Name)、性别(Gender)、出生日期(DateOfBirth)、国籍(Nationality)、联系地址(Address)、电话号码(PhoneNumber)、电子邮件(EmailAddress)
-
代理人信息表(AgentInfo):代理人ID(AgentID)、姓名(Name)、性别(Gender)、出生日期(DateOfBirth)、联系地址(Address)、电话号码(PhoneNumber)、电子邮件(EmailAddress)、证书号码(CertificateNumber)、执照发放日期(LicenseIssueDate)、执照到期日期(LicenseExpirationDate)、佣金结构(CommissionStructure)
-
保险产品信息表(ProductInfo):产品ID(ProductID)、产品名称(ProductName)、产品类型(ProductType)、保险金额范围(CoverageRange)、保险期限(CoverageTerm)、保费(Premium)、缴费频率(PaymentFrequency)、产品特性(ProductFeatures)、投保年龄限制(AgeLimit)、保费计算方式(PremiumCalculation)、理赔流程(ClaimsProcess)、投保要求(UnderwritingRequirements)、销售区域(SalesRegion)、产品状态(ProductStatus)
-
保险公司内部员工表(EmployeeInfo):员工ID(EmployeeID)、姓名(Name)、性别(Gender)、出生日期(DateOfBirth)、联系地址(Address)、电话号码(PhoneNumber)、电子邮件(EmailAddress)、入职日期(HireDate)、职位(Position)、部门(Department)、工资(Salary)、工作地点(Location)、上级主管(Supervisor)、员工类型(EmployeeType)、员工状态(EmployeeStatus)
编写agent
python
import os
from dotenv import load_dotenv
load_dotenv('.env')
from langchain_community.agent_toolkits.sql.base import create_sql_agent
from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
from langchain_community.utilities import SQLDatabase
from langchain_community.chat_models import ChatOpenAI
db_user = "user"
db_password = "pwd"
db_host = "localhost:3306"
db_name = "db_name"
db = SQLDatabase.from_uri(f"mysql+pymysql://{db_user}:{db_password}@{db_host}/{db_name}")
# 从环境变量获取 dashscope 的 API Key
api_key = os.environ.get('DASHSCOPE_API_KEY')
llm = ChatOpenAI(
temperature=0.01,
model="deepseek-v3",
openai_api_base = "https://dashscope.aliyuncs.com/compatible-mode/v1",
openai_api_key = api_key
)
# 需要设置llm
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
agent_executor = create_sql_agent(
llm=llm,
toolkit=toolkit,
verbose=True
)
要查询的信息
以下是我们的agent要完成的任务,让我们看看执行效果如何:
-
获取所有客户的姓名和联系电话。
-
生成的sql:
sqlSELECT Name, PhoneNumber FROM customerinfo LIMIT 10;
-
查询结果 1. 欧颖 - 14708198484 2. 李辉 - 15182875235 3. 李璐 - 18604878643 4. 张玉 - 18266615576 5. 张云 - 15783496041 6. 周玉兰 - 18944485428 7. 徐波 - 18537535161 8. 季丹丹 - 18556735783 9. 张杰 - 13648915552 10. 张杰 - 13302970653
-
验证
-
-
找出所有已婚客户的保单。
- 生成的sql:
sqlSELECT p.PolicyNumber, c.Name, c.MaritalStatus FROM policyinfo p JOIN customerinfo c ON CAST(p.CustomerID AS UNSIGNED) = c.CustomerID WHERE c.MaritalStatus = '已婚' LIMIT 10
-
查询结果
目前数据库中没有已婚客户的保单记录。
-
验证
这个结果对吗?我们去数据库里做一下验证,agent用了连表查询,我们换一种方式,用子查询来做:
sqlselect PolicyNumber from policyinfo where CustomerID IN (select CustomerID from customerinfo where MaritalStatus='已婚');
可以看到,结果是对的
-
查询所有未支付保费的保单号和客户姓名。
-
生成的sql:
sqlSELECT p.PolicyNumber, c.Name FROM policyinfo p JOIN customerinfo c ON p.CustomerID = c.CustomerID WHERE p.PremiumPaymentStatus = '未支付' LIMIT 10
sql看起来没有什么问题
-
查询结果
目前数据库中没有未支付的保单号和客户姓名
这个结果好像不太对,数据库里明明是有大批量的未支付的保单
让我们来查找一下原因: 查看数据库发现policyinfo表的CustomerID字段和customerinfo表的CustomerID字段是对不上的
所以是这里的信息没有关联上导致没有查询到数据,而不是sql的问题
-
根据上面这三个任务,我们可以看到,agent生成sql的能力还是可以的,但是如果数据库的数据有问题就无能为力了,不过这就不是代码层面的问题了,所以综合效果还是可以。
但是,还存在一个问题,就是上面的代码都是依靠框架完成的,如果我需要更灵活的配置,该怎么办?这个我们可以借助RAG来完成,后面的文章会详细介绍。就这样,bye!!!