删库跑路,一个所有程序员可能都幻想过的场景,不过想想后果还是很严重的,不过不要怕,现在我们可以利用大模型来删库,啊不,操作数据库,这就是传说中的Text2Sql。
什么是Text2Sql
Text2Sql就是将自然语言的问题转化为结构化的sql查询语句,可以让用户更直观的和数据库进行交互。其实Text2Sql不是什么特别新的东西,在大模型出来之前,也是有类似的尝试的
早期阶段主要依赖于人工编写的模板规则,用模板匹配自然语言和sql语句之间的关系,后面机器学习兴起之后,开始采用序列到序列模型这样的机器学习方法,来学习自然语言到SQL之间的映射关系。最后就是现在,能够借助大语言模型的强大语言理解和代码生成能力,再加上提示词工程、微调这样的方法,进一步提升了Text2Sql的性能。
LLM下的Text2Sql
在大语言模型的加持下,Text2Sql主要有以下几个步骤
- 自然语言理解:分析用户输入的自然语言问题,理解意图和语义模式链接:将问题中的实体与数据库中的表和列进行链接SQL生成:根据语义和模式链接结果,生成对应的SQL查询语句SQL执行:在数据库上执行SQL语句,将结果返回给用户
前面几篇文章我们介绍了LangChain的使用,下面我们就用LangChain来简单实现一个Text2sql
Text2Sql的实现
SQLDatabaseTookit使用
SQLDatabaseTookit
是LangChain提供的一个agent,可以轻松帮助我们实现Text2Sql
导入相关依赖
from langchain_community.agent_toolkits.sql.base import create_sql_agentfrom langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkitfrom langchain_community.utilities import SQLDatabasefrom dotenv import load_dotenvload_dotenv('../.env')
配置数据库
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
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)# 需要设置llmtoolkit = SQLDatabaseToolkit(db=db, llm=llm)# SQL智能体:给它目标,它自己会进行规划,最终把结果给你agent_executor = create_sql_agent( llm=llm, toolkit=toolkit, verbose=True)
提问
agent_executor.run("找出英雄攻击力最高的前5个英雄")
输出:
> Entering new SQL Agent Executor chain...Action: sql_db_list_tablesAction 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_schemaAction Input: herosCREATE 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:idnamehp_maxhp_growthhp_startmp_maxmp_growthmp_startattack_maxattack_growthattack_startdefense_maxdefense_growthdefense_starthp_5s_maxhp_5s_growthhp_5s_startmp_5s_maxmp_5s_growthmp_5s_startattack_speed_maxattack_rangerole_mainrole_assistbirthdate10000夏侯惇7350.0288.83307.01746.094.0430.0321.011.57159.0397.021.14101.098.03.35751.037.01.57115.00.0近战坦克战士2016-07-1910001钟无艳7000.0275.03150.01760.095.0430.0318.011.0164.0409.022.07100.092.03.14348.037.01.57115.00.0近战战士坦克2021-05-0110002钟8341.0329.43450.0100.00.0100.0301.010.57153.0504.027.07125.0115.04.14357.05.00.05.00.0近战坦克辅助2012-05-04*/`heros` 表中确实有 `attack_max` 字段,这应该代表英雄的最大攻击力。为了找出攻击力最高的前5个英雄,我需要根据 `attack_max` 字段降序排列,并限制结果为前5条记录。同时,我还应该选择 `name` 和 `attack_max` 两个字段来展示英雄的名字和他们的最大攻击力。Action: sql_db_query_checkerAction Input: SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5```sqlSELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5```查询语句看起来是正确的,接下来我将执行这个查询。Action: sql_db_queryAction 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.02. 孙尚香 - 411.03. 百里守约 - 410.04. 虞姬 - 407.05. 黄忠 - 403.0> Finished chain.
根据以上的输出,我们可以看到agent一共做了这几步操作:
- 查询有哪些表
Action: sql_db_list_tablesAction 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
2. 分析哪张表式需要查询的表
从表名来看,`heros` 表可能包含了英雄的信息。我需要查看 `heros` 表的结构来确定是否有攻击力(attack)相关的字段。Action: sql_db_schemaAction Input: herosCREATE 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. 查看表里的数据,并分析需要用的字段
/*3 rows from heros table:idnamehp_maxhp_growthhp_startmp_maxmp_growthmp_startattack_maxattack_growthattack_startdefense_maxdefense_growthdefense_starthp_5s_maxhp_5s_growthhp_5s_startmp_5s_maxmp_5s_growthmp_5s_startattack_speed_maxattack_rangerole_mainrole_assistbirthdate10000夏侯惇7350.0288.83307.01746.094.0430.0321.011.57159.0397.021.14101.098.03.35751.037.01.57115.00.0近战坦克战士2016-07-1910001钟无艳7000.0275.03150.01760.095.0430.0318.011.0164.0409.022.07100.092.03.14348.037.01.57115.00.0近战战士坦克2021-05-0110002钟8341.0329.43450.0100.00.0100.0301.010.57153.0504.027.07125.0115.04.14357.05.00.05.00.0近战坦克辅助2012-05-04*/`heros` 表中确实有 `attack_max` 字段,这应该代表英雄的最大攻击力。为了找出攻击力最高的前5个英雄,我需要根据 `attack_max` 字段降序排列,并限制结果为前5条记录。同时,我还应该选择 `name` 和 `attack_max` 两个字段来展示英雄的名字和他们的最大攻击力。
4. 根据提问编写sql并执行查询,最终返回结果
Action: sql_db_query_checkerAction Input: SELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5```sqlSELECT name, attack_max FROM heros ORDER BY attack_max DESC LIMIT 5```查询语句看起来是正确的,接下来我将执行这个查询。Action: sql_db_queryAction 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.02. 孙尚香 - 411.03. 百里守约 - 410.04. 虞姬 - 407.05. 黄忠 - 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
import osfrom dotenv import load_dotenvload_dotenv('.env')from langchain_community.agent_toolkits.sql.base import create_sql_agentfrom langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkitfrom langchain_community.utilities import SQLDatabasefrom langchain_community.chat_models import ChatOpenAIdb_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 Keyapi_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)# 需要设置llmtoolkit = SQLDatabaseToolkit(db=db, llm=llm)agent_executor = create_sql_agent( llm=llm, toolkit=toolkit, verbose=True)
要查询的信息
以下是我们的agent要完成的任务,让我们看看执行效果如何:
获取所有客户的姓名和联系电话。
生成的sql:
SELECT Name, PhoneNumber FROM customerinfo LIMIT 10;
查询结果1. 欧颖 - 147081984842. 李辉 - 151828752353. 李璐 - 186048786434. 张玉 - 182666155765. 张云 - 157834960416. 周玉兰 - 189444854287. 徐波 - 185375351618. 季丹丹 - 185567357839. 张杰 - 1364891555210. 张杰 - 13302970653
验证
找出所有已婚客户的保单。
- 生成的sql:
SELECT 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用了连表查询,我们换一种方式,用子查询来做:
select PolicyNumber from policyinfo where CustomerID IN (select CustomerID from customerinfo where MaritalStatus='已婚');
可以看到,结果是对的
查询所有未支付保费的保单号和客户姓名。
生成的sql:
SELECT 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!!!