本地大模型编程实战(22)用langchain实现基于SQL数据构建问答系统(1)

news2025/5/13 13:27:41

使 LLM(大语言模型) 系统能够查询结构化数据与非结构化文本数据在性质上可能不同。后者通常生成可在向量数据库中搜索的文本,而结构化数据的方法通常是让 LLM 编写和执行 DSL(例如 SQL)中的查询。
我们将演练在使用基于 langchain 链 ,在结构化数据库 SQlite 中的数据上创建问答系统的基本方法,该系统建立以后,我们用自然语言询问有关数据库中数据的问题并返回自然语言答案。
后面我们将基于 智能体(Agent) 实现类似功能,两者之间的主要区别在于:智能体可以根据需要多次循环查询数据库以回答问题

实现上述功能需要以下步骤:

  • 将问题转换为 DSL 查询:模型将用户输入转换为 SQL 查询;
  • 执行 SQL 查询;
  • 回答问题:模型使用查询结果响应用户输入。
    SQL智能体

使用 qwen2.5deepseek 以及 llama3.1 做实验。

准备

在正式开始撸代码之前,需要准备一下编程环境。

  1. 计算机
    本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:

    • CPU: Intel i5-8400 2.80GHz
    • 内存: 16GB
  2. Visual Studio Code 和 venv
    这是很受欢迎的开发工具,相关文章的代码可以在 Visual Studio Code 中开发和调试。 我们用 pythonvenv 创建虚拟环境, 详见:
    在Visual Studio Code中配置venv。

  3. Ollama
    Ollama 平台上部署本地大模型非常方便,基于此平台,我们可以让 langchain 使用 llama3.1qwen2.5deepseek 等各种本地大模型。详见:
    在langchian中使用本地部署的llama3.1大模型 。

准备 SQLite 数据库

SQLite 是一个轻量级、嵌入式的关系型数据库管理系统,不需要独立的服务器进程,所有数据存储在单一文件中。它支持大部分 SQL 标准,适用于移动应用、本地存储和小型项目。

我们将使用 Chinook 数据库做练习,数据库文件放在本文最后提及的代码仓库中的 assert 文件夹,名为:Chinook.db 。
下图是该数据库的结构:
SQLite数据结构

点击 sqlitestudio 可以下载 SQLite 的可视化管理工具,Sample Databases for SQLite 详细介绍了该数据库的情况。

  • 创建数据库实例
from langchain_community.utilities import SQLDatabase
db = SQLDatabase.from_uri(f"sqlite:///{db_file_path}")
  • 测试数据库
print(db.dialect)
print(db.get_usable_table_names())
print(db.run("SELECT * FROM Artist LIMIT 1;"))
sqlite
['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']
[(1, 'AC/DC')]

输出上述内容说明 SQLite 可以正常工作。

将问题转换为 SQL

langchain 中,可以使用 create_sql_query_chain 轻松的将问题转化为 SQL ,并且通过 db.run 方法执行SQL,基于这两个方法,我们创建了下面的方法用于将问题转化为SQL并执行:

def execute_query(llm_model_name,question: str):
    """把问题转换为SQL语句并执行"""
    
    llm = ChatOllama(model=llm_model_name,temperature=0, verbose=True)
    chain = create_sql_query_chain(llm, db)
    #print(chain.get_prompts()[0].pretty_print())

    # 转化问题为SQL
    response = chain.invoke({"question": question})
    print(f'response SQL is:\n{response}')

    # 执行SQL
    result = db.run(response)
    print(f'result is:\n{result}')

我们问几个问题,把使用三个大模型做一下简单测试,看看效果。

问题1:“How many Employees are there?”

  • llama3.1
response SQL is:
SELECT COUNT(*) FROM Employee;
result is:
[(8,)]

llama3.1 生成了正确的SQL并返回了正确的结果。
完美!

  • qwen2.5
To find out how many employees there are, you can use the following SQL query:\n\n
```sql\nSELECT COUNT(*) AS EmployeeCount\nFROM Employee;\n```
...

qwen2.5 推理出了正确的 SQL,可惜该SQL在一段文字中,所以在后面执行sql会有“SQL语法错误”。

  • deepseek-r1

返回一大段推理过程,但是未推理出SQL语句。

问题2:“Which country’s customers spent the most?”

  • llama3.1
response SQL is:
SELECT T2.Country FROM Invoice AS T1 INNER JOIN Customer AS T2 ON T1.CustomerId = T2.CustomerId GROUP BY T2.Country ORDER BY SUM(T1.Total) DESC LIMIT 1;
result is:
[('USA',)]

完美!

问题3:“Describe the PlaylistTrack table.”

response SQL is:
SELECT * FROM `PlaylistTrack`
result is:
[(1, 3402), (1, 3389), (1, 3390), ...

不理想。

为了进一步探索 create_sql_query_chain 都做了什么,我们可以在 此语句后面执行:

print(chain.get_prompts()[0].pretty_print())

打印出的提示词为:

You are a SQLite expert. Given an input question, first create a syntactically correct SQLite query to run, then look at the results of the query and return the answer to the input question.
Unless the user specifies in the question a specific number of examples to obtain, query for at most 5 results using the LIMIT clause as per SQLite. You can order the results to return the most informative data in the database.
Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in double quotes (") to denote them as delimited identifiers.
Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.
Pay attention to use date('now') function to get the current date, if the question involves "today".

Use the following format:

Question: Question here
SQLQuery: SQL Query to run
SQLResult: Result of the SQLQuery
Answer: Final answer here

Only use the following tables:
{table_info}

Question: {input}

可见,创建这个链实际上是执行了上述的提示词,可能我们修改一下提示词或者自定义一个链才能让 qwen2.5deepseek-r1 正常工作。

我们也可以用下面更优雅的代码实现将问题转换为SQL:

def execute_query_2(llm_model_name,question: str):
    """把问题转换为SQL语句并执行"""

    llm = ChatOllama(model=llm_model_name,temperature=0, verbose=True)
    execute_query = QuerySQLDataBaseTool(db=db)
    write_query = create_sql_query_chain(llm, db)
    chain = write_query | execute_query
    response = chain.invoke({"question": question})
    print(f'response SQL is:\n{response}')

回答问题

现在,我们已经有了自动生成和执行查询的方法,我们只需要将原始问题和 SQL 查询结果结合起来即可生成最终答案。我们可以通过再次将问题和结果传递给 LLM 来实现这一点:

def ask(llm_model_name,question: str):
    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: """
    )

    llm = ChatOllama(model=llm_model_name,temperature=0, verbose=True)
    execute_query = QuerySQLDataBaseTool(db=db)
    write_query = create_sql_query_chain(llm, db)
    chain = (
        RunnablePassthrough.assign(query=write_query).assign(
            result=itemgetter("query") | execute_query
        )
        | answer_prompt
        | llm
        | StrOutputParser()
    )

    response = chain.invoke({"question": question})
    print(f'Answer is:\n{response}')

我们看看上述 LCEL(LangChain Expression Language) 中发生的事情:

LangChain 表达式语言 (LCEL) 采用声明式方法从现有 Runnable 构建新的 Runnable,最常用的表达式是 | ,它可以把前后两个链或者其它 Runnable 组件串联起来:前面组件的输出可以提供给后面的组件作为输入。
更多内容参见:LangChain Expression Language (LCEL) 。

  • 在第一个 RunnablePassthrough.assign 之后,会生成一个包含两个元素的 runnable
    {"question": question, "query": write_query.invoke(question)}
    其中 write_query 将生成一个 SQL 查询来回答问题。
  • 在第二个 RunnablePassthrough.assign 之后,我们添加了第三个元素 result ,它的内容由 execute_query.invoke(query) 生成, query 是在上一步中计算的。
  • 这三个元素输入被格式化为提示并传递到 LLM
  • StrOutputParser() 提取输出消息的字符串内容。
    请注意:我们正在将 LLM、工具、提示和其他链组合在一起,但由于它们都实现了 Runnable 接口,因此它们的输入和输出可以绑定在一起:前面的输出可以作为后面的输入。

下面我们使用 llama3.1 ,用三个问题看看 ask 方法的输出内容。

  • 问题1:“How many Employees are there?”
There are 8 employees.
  • 问题2:“Which country’s customers spent the most?”
The country whose customers spent the most is the USA.

总结

从这次演练的效果看,在基于 langchain 框架,使用 LLM(大语言模型) 可以生成 SQL 语句,这使得我们可以说一句“人话”,计算机就可以自动查询 SQLite,并且像人一样告诉我们结果。
可惜 qwen2.5deepseek-r1 在该领域与 langchain 的集成不太理想。


代码

本文涉及的所有代码以及相关资源都已经共享,参见:

  • github
  • gitee

为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。

参考

  • Build a Question/Answering system over SQL data

🪐感谢您观看,祝好运🪐

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2306478.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

速通HTML

目录 HTML基础 1.快捷键 2.标签 HTML进阶 1.列表 a.无序列表 b.有序列表 c.定义列表 2.表格 a.内容 b.合并单元格 3.表单 a.input标签 b.单选框 c.上传文件 4.下拉菜单 5.文本域标签 6.label标签 7.按钮标签 8.无语义的布局标签div与span 9.字符实体 HTML…

博客系统完整开发流程

前言 通过前⾯课程的学习, 我们掌握了Spring框架和MyBatis的基本使用, 并完成了图书管理系统的常规功能开发, 接下来我们系统的从0到1完成⼀个项⽬的开发. 企业开发的流程 1. 需求评审(产品经理(PM)会和运营(想口号),UI,测试,开发等沟通) ,会涉及到背景/目标/怎么做,可能会有多…

【C语言】指针笔试题

前言:上期我们介绍了sizeof与strlen的辨析以及sizeof,strlen相关的一些笔试题,这期我们主要来讲指针运算相关的一些笔试题,以此来巩固我们之前所学的指针运算! 文章目录 一,指针笔试题1,题目一…

【Qt】桌面应用开发 ------ 绘图事件和绘图设备 文件操作

文章目录 9、绘图事件和绘图设备9.1 QPainter9.2 手动触发绘图事件9.3 绘图设备9.3.1 QPixmap9.3.2 QImage9.3.3 QImage与QPixmap的区别9.3.4 QPicture 10、文件操作10.1 文件读写10.2 二进制文件读写10.3 文本文件读写10.4 综合案例 9、绘图事件和绘图设备 什么时候画&#x…

python与C系列语言的差异总结(3)

与其他大部分编程语言不一样,Python使用空白符(whitespace)和缩进来标识代码块。也就是说,循环体、else条件从句之类的构成,都是由空白符加上冒号(:)来确定的。大部分编程语言都是使用某种大括号来标识代码块的。下面的…

OpenCV(9):视频处理

1 介绍 视频是由一系列连续的图像帧组成的,每一帧都是一幅静态图像。视频处理的核心就是对这些图像帧进行处理。常见的视频处理任务包括视频读取、视频播放、视频保存、视频帧处理等。 视频分析: 通过视频处理技术,可以分析视频中的运动、目标、事件等。…

【C++设计模式】观察者模式(1/2):从基础到优化实现

1. 引言 在 C++ 软件与设计系列课程中,观察者模式是一个重要的设计模式。本系列课程旨在深入探讨该模式的实现与优化。在之前的课程里,我们已对观察者模式有了初步认识,本次将在前两次课程的基础上,进一步深入研究,着重解决观察者生命周期问题,提升代码的安全性、灵活性…

在 CentOS 7.9上部署 Oracle 11.2.0.4.0 数据库

目录 在 CentOS 7.9上部署 Oracle 11.2.0.4.0 数据库引言安装常见问题vim粘贴问题 环境情况环境信息安装包下载 初始环境准备关闭 SELinux关闭 firewalld 安装前初始化工作配置主机名安装依赖优化内核参数限制 Oracle 用户的 Shell 权限配置 PAM 模块配置swap创建用户组与用户,…

计算机毕业设计SpringBoot+Vue.js足球青训俱乐部管理系统(源码+文档+PPT+讲解)

温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…

基于 DeepSeek LLM 本地知识库搭建开源方案(AnythingLLM、Cherry、Ragflow、Dify)认知

写在前面 博文内容涉及 基于 Deepseek LLM 的本地知识库搭建使用 ollama 部署 Deepseek-R1 LLM知识库能力通过 Ragflow、Dify 、AnythingLLM、Cherry 提供理解不足小伙伴帮忙指正 😃,生活加油 我站在人潮中央,思考这日日重复的生活。我突然想&#xff0c…

QSplashScreen --软件启动前的交互

目录 QSplashScreen 类介绍 使用方式 项目中使用 THPrinterSplashScreen头文件 THPrinterSplashScreen实现代码 使用代码 使用效果 QSplashScreen 类介绍 QSplashScreen 是 Qt 中的一个类,用于显示启动画面。它通常在应用程序启动时显示,以向用户显…

「软件设计模式」责任链模式(Chain of Responsibility)

深入解析责任链模式:用C打造灵活的请求处理链 引言:当审批流程遇上设计模式 在软件系统中,我们经常会遇到这样的场景:一个请求需要经过多个处理节点的判断,每个节点都有权决定是否处理或传递请求。就像企业的请假审批…

蓝桥杯嵌入式客观题以及解释

第十一届省赛(大学组) 1.稳压二极管时利用PN节的反向击穿特性制作而成 2.STM32嵌套向量终端控制器NVIC具有可编程的优先等级 16 个 3.一个功能简单但是需要频繁调用的函数,比较适用内联函数 4.模拟/数字转换器的分辨率可以通过输出二进制…

low rank decomposition如何用于矩阵的分解

1. 什么是矩阵分解和低秩分解 矩阵分解是将一个矩阵表示为若干结构更简单或具有特定性质的矩阵的组合或乘积的过程。低秩分解(Low Rank Decomposition)是其中一种方法,旨在将原矩阵近似为两个或多个秩较低的矩阵的乘积,从而降低复…

ubuntu离线安装Ollama并部署Llama3.1 70B INT4

文章目录 1.下载Ollama2. 下载安装Ollama的安装命令文件install.sh3.安装并验证Ollama4.下载所需要的大模型文件4.1 加载.GGUF文件(推荐、更容易)4.2 加载.Safetensors文件(不建议使用) 5.配置大模型文件 参考: 1、 如…

JNA基础使用,调用C++返回结构体

C端 test.h文件 #pragma oncestruct RespInfo {char* path;char* content;int statusCode; };extern "C" { DLL_EXPORT void readInfo(char* path, RespInfo* respInfo); }test.cpp文件 #include "test.h"void readInfo(char* path, RespInfo* respInfo…

解锁养生密码,拥抱健康生活

在快节奏的现代生活中,养生不再是一种选择,而是我们保持活力、提升生活质量的关键。它不是什么高深莫测的学问,而是一系列融入日常的简单习惯,每一个习惯都在为我们的健康加分。 早晨,当第一缕阳光洒进窗户&#xff0c…

OpenCV(6):图像边缘检测

图像边缘检测是计算机视觉和图像处理中的一项基本任务,它用于识别图像中亮度变化明显的区域,这些区域通常对应于物体的边界。是 OpenCV 中常用的边缘检测函数及其说明: 函数算法说明适用场景cv2.Canny()Canny 边缘检测多阶段算法,检测效果较…

spark的一些指令

一,复制和移动 1、复制文件 格式:cp 源文件 目标文件 示例:把file1.txt 复制一份得到file2.txt 。那么对应的命令就是:cp file1.txt file2.txt 2、复制目录 格式:cp -r 源文件 目标文件夹 示例:把目…

OpenHarmony全球化子系统

OpenHarmony全球化子系统 简介系统架构目录相关仓 简介 当OpenHarmony系统/应用在全球不同区域使用时,系统/应用需要满足不同市场用户关于语言、文化习俗的需求。全球化子系统提供支持多语言、多文化的能力,包括: 资源管理能力 根据设备类…