LangChain教程 - 基于SQL数据的问答系统教程

前提条件

本文假定你已经熟悉以下概念:

  • Chaining runnables(可运行的链)
  • Chat models(聊天模型)
  • Tools(工具)
  • Agents(代理)

在本教程中,我们将使用Python来创建一个可以对SQL数据库中的表格数据进行自然语言问答的系统。我们将通过链(Chain)和代理(Agent)来实现这个系统。

数据库设置

首先,我们需要设置一个SQLite数据库来存储数据,并创建表和插入一些初始数据。

python 复制代码
from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db")

# 执行建表语句
create_table_sql = '''
CREATE TABLE Employee (
    EmployeeId INTEGER NOT NULL,
    LastName NVARCHAR(20) NOT NULL,
    FirstName NVARCHAR(20) NOT NULL,
    Title NVARCHAR(30),
    ReportsTo INTEGER,
    BirthDate DATETIME,
    HireDate DATETIME,
    Address NVARCHAR(70),
    City NVARCHAR(40),
    State NVARCHAR(40),
    Country NVARCHAR(40),
    PostalCode NVARCHAR(10),
    Phone NVARCHAR(24),
    Fax NVARCHAR(24),
    Email NVARCHAR(60),
    CONSTRAINT PK_Employee PRIMARY KEY (EmployeeId),
    FOREIGN KEY (ReportsTo) REFERENCES Employee (EmployeeId) 
        ON DELETE NO ACTION ON UPDATE NO ACTION
);
'''

db.run(create_table_sql)

# 插入初始化数据
insert_data_sql = '''
INSERT INTO [Employee] ([EmployeeId], [LastName], [FirstName], [Title], [ReportsTo], [BirthDate], [HireDate], [Address], [City], [State], [Country], [PostalCode], [Phone], [Fax], [Email]) VALUES
    (1, 'Adams', 'Andrew', 'General Manager', NULL, '1962-02-18', '2002-08-14', '11120 Jasper Ave NW', 'Edmonton', 'AB', 'Canada', 'T5K 2N1', '+1 (780) 428-9482', '+1 (780) 428-3457', 'andrew@chinookcorp.com'),
    (2, 'Edwards', 'Nancy', 'Sales Manager', 1, '1958-12-08', '2002-05-01', '825 8 Ave SW', 'Calgary', 'AB', 'Canada', 'T2P 2T3', '+1 (403) 262-3443', '+1 (403) 262-3322', 'nancy@chinookcorp.com'),
    (3, 'Peacock', 'Jane', 'Sales Support Agent', 2, '1973-08-29', '2002-04-01', '1111 6 Ave SW', 'Calgary', 'AB', 'Canada', 'T2P 5M5', '+1 (403) 262-3443', '+1 (403) 262-6712', 'jane@chinookcorp.com'),
    (4, 'Park', 'Margaret', 'Sales Support Agent', 2, '1947-09-19', '2003-05-03', '683 10 Street SW', 'Calgary', 'AB', 'Canada', 'T2P 5G3', '+1 (403) 263-4423', '+1 (403) 263-4289', 'margaret@chinookcorp.com'),
    (5, 'Johnson', 'Steve', 'Sales Support Agent', 2, '1965-03-03', '2003-10-17', '7727B 41 Ave', 'Calgary', 'AB', 'Canada', 'T3B 1Y7', '1 (780) 836-9987', '1 (780) 836-9543', 'steve@chinookcorp.com'),
    (6, 'Mitchell', 'Michael', 'IT Manager', 1, '1973-07-01', '2003-10-17', '5827 Bowness Road NW', 'Calgary', 'AB', 'Canada', 'T3B 0C5', '+1 (403) 246-9887', '+1 (403) 246-9899', 'michael@chinookcorp.com'),
    (7, 'King', 'Robert', 'IT Staff', 6, '1970-05-29', '2004-01-02', '590 Columbia Boulevard West', 'Lethbridge', 'AB', 'Canada', 'T1K 5N8', '+1 (403) 456-9986', '+1 (403) 456-8485', 'robert@chinookcorp.com'),
    (8, 'Callahan', 'Laura', 'IT Staff', 6, '1968-01-09', '2004-03-04', '923 7 ST NW', 'Lethbridge', 'AB', 'Canada', 'T1H 1Y8', '+1 (403) 467-3351', '+1 (403) 467-8772', 'laura@chinookcorp.com');
'''

db.run(insert_data_sql)

通过上面的代码,我们创建了Employee表并插入了一些初始员工数据。通过下面代码测试:

python 复制代码
from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db")
print(db.dialect)
print(db.get_usable_table_names())
db.run("SELECT * FROM Artist LIMIT 10;")

使用链来实现

链支持那些步骤明确且可预测的应用场景。在这个场景下,我们可以创建一个简单的链,来执行以下操作:

  1. 将问题转换为SQL查询 :我们可以利用LangChain的create_sql_query_chain方法将用户问题转换为SQL查询。
  2. 执行查询:通过SQL查询数据库获取结果。
  3. 根据结果回答问题:生成用户问题的最终回答。

将问题转换为SQL查询

首先,我们导入OpenAI模型并创建SQL查询链:

python 复制代码
from langchain_openai import ChatOpenAI
from langchain.chains import create_sql_query_chain

llm = ChatOpenAI(model="gpt-4o-mini")
chain = create_sql_query_chain(llm, db)

response = chain.invoke({"question": "How many employees are there"})
print(response)

在这个步骤中,用户问题被转换成了SQL查询:

sql 复制代码
SQLQuery: SELECT COUNT("EmployeeId") AS "TotalEmployees" FROM "Employee";

执行SQL查询

生成SQL查询后,我们需要执行它,并返回结果:

python 复制代码
db.run(response[9:])

使用链(Chains)实现问答系统

接下来我们将构建一个链来处理SQL查询并返回结果。该链的步骤如下:

  1. 将用户问题转换为SQL查询
  2. 执行查询
  3. 使用查询结果回答问题

创建并执行SQL查询链

我们使用LangChain提供的create_sql_query_chain来将用户问题转换为SQL查询。

python 复制代码
from operator import itemgetter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool

# 定义用于回答问题的模板
answer_prompt = PromptTemplate.from_template(
    """Given the following user question, corresponding SQL query, and SQL result, answer the user question.

Question: {question}
SQL Query: {query}
SQL Result: {result}
Answer: """
)

# 将问题转换为SQL查询
write_query = create_sql_query_chain(llm, db)
execute_query = QuerySQLDataBaseTool(db=db)

# 清理查询语句结果
def clean_query(query_result):
    return query_result.get("query", "").replace("SQLQuery:", "").strip()

# 使用 assign + itemgetter 进行数据流转
chain = (
    RunnablePassthrough.assign(query=write_query).assign(
        result=lambda x: execute_query.invoke({"query": clean_query(x)})
    )
    | answer_prompt
    | llm
    | StrOutputParser()
)

# 执行链并回答问题
chain.invoke({"question": "How many employees are there"})

在上面的代码中,我们首先定义了问题模板answer_prompt,并通过create_sql_query_chain将用户的问题转换为SQL查询。然后我们使用QuerySQLDataBaseTool执行查询并通过链(Chain)将查询结果返回给用户。

使用代理(Agents)实现问答系统

代理提供了比链更灵活的方式,允许多次查询数据库,并根据需要生成答案。

初始化SQL代理

我们可以通过SQLDatabaseToolkit来创建工具集,并结合LangChain的create_react_agent代理系统来处理SQL查询。

python 复制代码
from langchain_community.agent_toolkits import SQLDatabaseToolkit
from langchain_core.messages import SystemMessage

toolkit = SQLDatabaseToolkit(db=db, llm=llm)
tools = toolkit.get_tools()

# 定义代理系统提示信息
SQL_PREFIX = """You are an agent designed to interact with a SQL database.
Given an input question, create a syntactically correct SQLite query to run, then look at the results of the query and return the answer."""

system_message = SystemMessage(content=SQL_PREFIX)

from langgraph.prebuilt import create_react_agent
agent_executor = create_react_agent(llm, tools, state_modifier=system_message)

使用代理执行查询

接下来我们使用代理来执行更复杂的SQL查询并返回结果:

python 复制代码
from langchain_core.messages import HumanMessage

for s in agent_executor.stream(
    {"messages": [HumanMessage(content="Who is the oldest employee?")]}
):
    print(s)

通过上述代码,代理会自动分析数据库中的表结构,生成正确的SQL查询,最终回答问题。比如在这个例子中,代理会根据Employee表中的出生日期找到年龄最大的员工。

完整代码实例

python 复制代码
# 安装必要的库
# %pip install langchain langchain-community langchain-openai faiss-cpu sqlite3

import os
import getpass
from langchain_community.utilities import SQLDatabase
from langchain_openai import ChatOpenAI
from langchain.chains import create_sql_query_chain
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
from langchain_community.agent_toolkits import SQLDatabaseToolkit
from langchain_core.messages import SystemMessage
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import HumanMessage

# 设置 API 密钥
os.environ["OPENAI_API_KEY"] = getpass.getpass("请输入您的 OpenAI API 密钥: ")

# 初始化数据库并插入数据
db = SQLDatabase.from_uri("sqlite:///Chinook.db")

# 检查表是否已经存在
table_check_sql = '''
SELECT name FROM sqlite_master WHERE type='table' AND name='Employee';
'''
table_exists = db.run(table_check_sql)

# 如果表不存在,则创建表并插入数据
if not table_exists:

    # 执行建表语句
    create_table_sql = '''
    CREATE TABLE Employee (
        EmployeeId INTEGER NOT NULL,
        LastName NVARCHAR(20) NOT NULL,
        FirstName NVARCHAR(20) NOT NULL,
        Title NVARCHAR(30),
        ReportsTo INTEGER,
        BirthDate DATETIME,
        HireDate DATETIME,
        Address NVARCHAR(70),
        City NVARCHAR(40),
        State NVARCHAR(40),
        Country NVARCHAR(40),
        PostalCode NVARCHAR(10),
        Phone NVARCHAR(24),
        Fax NVARCHAR(24),
        Email NVARCHAR(60),
        CONSTRAINT PK_Employee PRIMARY KEY (EmployeeId),
        FOREIGN KEY (ReportsTo) REFERENCES Employee (EmployeeId) 
            ON DELETE NO ACTION ON UPDATE NO ACTION
    );
    '''

    db.run(create_table_sql)

    # 插入初始化数据
    insert_data_sql = '''
    INSERT INTO [Employee] ([EmployeeId], [LastName], [FirstName], [Title], [ReportsTo], [BirthDate], [HireDate], [Address], [City], [State], [Country], [PostalCode], [Phone], [Fax], [Email]) VALUES
        (1, 'Adams', 'Andrew', 'General Manager', NULL, '1962-02-18', '2002-08-14', '11120 Jasper Ave NW', 'Edmonton', 'AB', 'Canada', 'T5K 2N1', '+1 (780) 428-9482', '+1 (780) 428-3457', 'andrew@chinookcorp.com'),
        (2, 'Edwards', 'Nancy', 'Sales Manager', 1, '1958-12-08', '2002-05-01', '825 8 Ave SW', 'Calgary', 'AB', 'Canada', 'T2P 2T3', '+1 (403) 262-3443', '+1 (403) 262-3322', 'nancy@chinookcorp.com'),
        (3, 'Peacock', 'Jane', 'Sales Support Agent', 2, '1973-08-29', '2002-04-01', '1111 6 Ave SW', 'Calgary', 'AB', 'Canada', 'T2P 5M5', '+1 (403) 262-3443', '+1 (403) 262-6712', 'jane@chinookcorp.com'),
        (4, 'Park', 'Margaret', 'Sales Support Agent', 2, '1947-09-19', '2003-05-03', '683 10 Street SW', 'Calgary', 'AB', 'Canada', 'T2P 5G3', '+1 (403) 263-4423', '+1 (403) 263-4289', 'margaret@chinookcorp.com'),
        (5, 'Johnson', 'Steve', 'Sales Support Agent', 2, '1965-03-03', '2003-10-17', '7727B 41 Ave', 'Calgary', 'AB', 'Canada', 'T3B 1Y7', '1 (780) 836-9987', '1 (780) 836-9543', 'steve@chinookcorp.com'),
        (6, 'Mitchell', 'Michael', 'IT Manager', 1, '1973-07-01', '2003-10-17', '5827 Bowness Road NW', 'Calgary', 'AB', 'Canada', 'T3B 0C5', '+1 (403) 246-9887', '+1 (403) 246-9899', 'michael@chinookcorp.com'),
        (7, 'King', 'Robert', 'IT Staff', 6, '1970-05-29', '2004-01-02', '590 Columbia Boulevard West', 'Lethbridge', 'AB', 'Canada', 'T1K 5N8', '+1 (403) 456-9986', '+1 (403) 456-8485', 'robert@chinookcorp.com'),
        (8, 'Callahan', 'Laura', 'IT Staff', 6, '1968-01-09', '2004-03-04', '923 7 ST NW', 'Lethbridge', 'AB', 'Canada', 'T1H 1Y8', '+1 (403) 467-3351', '+1 (403) 467-8772', 'laura@chinookcorp.com');
    '''

    db.run(insert_data_sql)

# 初始化OpenAI LLM模型
llm = ChatOpenAI(model="gpt-4o-mini")

# ------------------------
# 使用链(Chains)实现问答系统
# ------------------------

# 定义用于回答问题的模板
answer_prompt = PromptTemplate.from_template(
    """Given the following user question, corresponding SQL query, and SQL result, answer the user question.

Question: {question}
SQL Query: {query}
SQL Result: {result}
Answer: """
)

# 将问题转换为SQL查询
write_query = create_sql_query_chain(llm, db)
execute_query = QuerySQLDataBaseTool(db=db)

# 清理查询语句结果
def clean_query(query_result):
    return query_result.get("query", "").replace("SQLQuery:", "").strip()

# 使用 assign + itemgetter 进行数据流转
chain = (
    RunnablePassthrough.assign(query=write_query).assign(
        result=lambda x: execute_query.invoke({"query": clean_query(x)})
    )
    | answer_prompt
    | llm
    | StrOutputParser()
)

# 执行链并回答问题
print(chain.invoke({"question": "How many employees are there"}))

# ------------------------
# 使用代理(Agents)实现问答系统
# ------------------------

# 初始化SQL代理工具包
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
tools = toolkit.get_tools()

# 定义代理系统提示信息
SQL_PREFIX = """You are an agent designed to interact with a SQL database.
Given an input question, create a syntactically correct SQLite query to run, then look at the results of the query and return the answer."""
system_message = SystemMessage(content=SQL_PREFIX)

# 创建代理
agent_executor = create_react_agent(llm, tools, state_modifier=system_message)

# 使用代理执行查询并回答问题
for s in agent_executor.stream(
    {"messages": [HumanMessage(content="Who is the oldest employee?")]}
):
    print(s)

总结

在本教程中,我们演示了如何通过链(Chains)和代理(Agents)实现基于SQL数据的问答系统。链适用于步骤明确、可预测的场景,而代理适合处理更加复杂和灵活的查询需求。在实际使用中,务必保证数据库的安全性,避免潜在的风险。

系列文章索引
LangChain教程 - 系列文章

相关推荐
NiNg_1_23414 分钟前
SQL 基础语法
sql
高垚淼21 分钟前
如何构建智能应用:深入探索Langchain的强大功能与应用潜力
人工智能·python·langchain
ChinaZ.AI2 小时前
ComfyUI 速度更快,显存占用更低的图像反推模型Florence2PromptGen,效果媲美JoyCaption,还支持Flux训练打标
人工智能·stable diffusion·aigc·flux·comfyui·florence
DisonTangor4 小时前
利用 Llama-3.1-Nemotron-51B 推进精度-效率前沿的发展
人工智能·aigc·llama
元宇宙时间4 小时前
美国林氏集团宣布全面进军Web3领域
sql
今心上5 小时前
SQL 查询语句的顺序详解
数据库·sql
AI-入门6 小时前
AI大模型:是走向新的巅峰还是陷入发展的僵局?
数据库·人工智能·缓存·langchain·prompt·agi
RisingWave中文开源社区7 小时前
重大升级!开源分布式 SQL 数据库|RisingWave 2.0 发布!
数据库·sql·开源
失心疯_20237 小时前
MySQL_插入、更新和删除数据
数据库·sql·mysql·关系型数据库·update·mysql教程·inset into
文心快码 Baidu Comate7 小时前
新一代的程序员如何培养自己的核心竞争力?(一)
人工智能·程序员·ai编程·文心快码·智能编程助手