Text2SQL 实战:用 DeepSeek 大模型实现自然语言查询 SQLite 数据库

16 阅读9分钟

Text2SQL 实战:用 DeepSeek 大模型实现自然语言查询 SQLite 数据库

让 AI 帮你写 SQL,告别繁琐的表结构记忆和语法调试

在日常开发中,我们经常需要与数据库打交道。写 SQL 查询本身并不复杂,但当表变多、字段变复杂时,记字段名、写 JOIN、调条件总是让人分心。Text2SQL 技术应运而生——你只需要用自然语言描述“我要什么”,AI 直接帮你生成 SQL 语句。本文将带你从零开始,用 Python + SQLite + DeepSeek 大模型搭建一个完整的 Text2SQL 系统,并深度剖析每个底层细节。


一、整体架构预览

我们先看一眼最终实现的效果:

graph LR
    A[自然语言问题] --> B[DeepSeek 大模型]
    B --> C[生成 SQL 语句]
    C --> D[SQLite 执行]
    D --> E[返回查询结果]

用户问:“工程部门员工的姓名和工资是多少?”
系统输出:SELECT name, salary FROM EMPLOYEES WHERE department = '工程';
然后自动执行并打印结果。整个过程无需手写一行 SQL。


二、环境准备:OpenAI 兼容的 DeepSeek 客户端

首先,我们需要调用 DeepSeek 的 API。DeepSeek 提供了与 OpenAI 完全兼容的接口,所以直接用 openai 库即可。

from openai import OpenAI
client = OpenAI(
    api_key="sk-dbd5d0d816254a66825aa2c0841a35b2",
    base_url="https://api.deepseek.com/v1"
)

🔍 知识点拆解:

  • OpenAI 兼容性:DeepSeek 的接口与 OpenAI 保持一致,这意味着你可以用任何支持 OpenAI SDK 的工具链来调用 DeepSeek 模型,迁移成本极低。
  • base_url:指向 DeepSeek 的服务端点,而不是 OpenAI 的 https://api.openai.com/v1。这相当于告诉客户端“我换了个后端”。
  • api_key:这里直接写在了代码里,实际项目中务必用环境变量或密钥管理服务,防止泄露。

三、SQLite:文件型关系数据库

我们选用 SQLite 作为演示数据库,因为它无需安装、无需配置、单文件存储,非常适合本地开发和教学。

3.1 连接与游标

import sqlite3
conn = sqlite3.connect("test.db")   # 创建/连接数据库文件
cursor = conn.cursor()              # 创建游标对象

🔍 游标(Cursor)是什么?
游标是数据库操作中的“指针”,它代表一个执行 SQL 语句的上下文环境。你可以通过游标来执行 SQL、获取结果。一个连接可以创建多个游标,每个游标独立管理自己的执行状态。底层实现中,游标会维护一个结果集和当前读取位置。

3.2 建表与批量插入

cursor.execute("""
CREATE TABLE IF NOT EXISTS employees (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    department TEXT,
    salary INTEGER
)
""")

sample_data = [
    (6, "张三", "销售", 5000),
    (7, "李四", "市场", 6000),
    (8, "王五", "研发", 7000),
    (9, "赵六", "工程", 8000),
    (10, "王二", "市场", 9000),
]
cursor.executemany(
    "INSERT INTO employees VALUES (?,?,?,?)", 
    sample_data
)
conn.commit()

🔍 重点知识点:

  • IF NOT EXISTS:避免重复执行脚本时表已存在报错,保证幂等性。
  • AUTOINCREMENT:在 SQLite 中,INTEGER PRIMARY KEY 默认就会自增,但加上 AUTOINCREMENT 可以保证自增的值永不重用(更安全,但稍微慢一点)。
  • executemany:批量插入的利器。它接受一个 SQL 模板(用 ? 作为占位符)和一个元组列表,内部会一次性编译 SQL 并逐行绑定参数。相比循环执行 execute,这能大幅减少 SQL 解析次数,提升性能。
  • 占位符 ?:SQLite 使用 ? 作为参数占位符(位置绑定),而不是 %s:name。这可以有效防止 SQL 注入,因为参数值是独立于 SQL 语句传输的,不会被当作代码执行。
  • 事务与 commit()executemany 默认不会自动提交,必须显式调用 conn.commit() 将更改持久化到磁盘。这是事务控制的基础——要么全部成功,要么全部回滚。你可以用 conn.rollback() 撤销未提交的修改。

四、获取数据库 Schema(表结构信息)

要让 AI 生成正确的 SQL,必须告诉它数据库中有哪些表、每个表的字段名和类型。这就是 Schema(模式)的作用。

schema = cursor.execute("PRAGMA table_info(employees)").fetchall()
print(schema)
# 输出: [(0, 'id', 'INTEGER', 0, None, 1), (1, 'name', 'TEXT', 0, None, 0), ...]

🔍 PRAGMA table_info 是什么?
PRAGMA 是 SQLite 特有的“编译指令”或“元数据查询命令”。table_info(table_name) 返回一个结果集,每一行代表表中的一个列,包含:

  • 列序号(cid)
  • 列名(name)
  • 列类型(type)
  • 是否非空(notnull)
  • 默认值(dflt_value)
  • 是否为主键(pk)

我们用 fetchall() 取出所有列信息,然后拼接成建表语句的字符串形式:

schema_str = "CREATE TABLE EMPLOYEES (\n" + "\n".join([f"{col[1]} {col[2]}" for col in schema]) + "\n)"

🔍 列表推导式与 f-string
[f"{col[1]} {col[2]}" for col in schema] 遍历每个列,取出 nametype,拼接成类似 "id INTEGER" 的字符串,再用换行符连接。这样我们就得到了一个可读的 Schema 描述,供 AI 理解。


五、核心函数:ask_deepseek —— 自然语言转 SQL

def ask_deepseek(query, schema):
    prompt = f"""
    这是一个数据库的Schema:
    {schema}
    根据这个Schema, 请输出一个SQL查询来回答以下问题。
    只输出SQL 查询语句本身,不要使用任何Markdown格式,
    不要包含反引号、代码块标记或额外说明。
    问题:{query}
    """
    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        max_tokens=2048,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

🔍 提示词工程(Prompt Engineering)的关键点:

  • 明确约束:“只输出 SQL 查询语句本身”——防止 AI 输出解释、Markdown 代码块或多余文字,否则我们后续无法直接 execute
  • 注入 Schema:将表结构作为上下文提供给模型,这是 Text2SQL 的核心。没有 Schema,AI 只能猜测字段名,错误率极高。
  • 使用最新模型:这里用了 deepseek-v4-flash(实际可用 deepseek-chat 等),DeepSeek 在代码生成和 SQL 任务上表现优异。
  • max_tokens:限制输出长度,防止过长回复。

🔍 底层调用过程
client.chat.completions.create 发送 HTTP 请求到 DeepSeek 服务端,服务端运行大模型推理,返回一个 JSON 响应。我们从中提取 choices[0].message.content 即生成的 SQL。


六、实战检验:查询、插入、更新、删除

6.1 查询(SELECT)

question = "工程部门员工的姓名和工资是多少?"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)   # SELECT name, salary FROM EMPLOYEES WHERE department = '工程';

results = cursor.execute(sql_query).fetchall()
for row in results:
    print(row)     # ('赵六', 8000)

AI 自动识别了“工程部门”对应字段 department,并只选择了 namesalary 两列,完美!

6.2 插入(INSERT)

question = "在销售部门增加一个新员工,姓名为张三,工资为45000"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)   # INSERT INTO EMPLOYEES (name, department, salary) VALUES ('张三', '销售', 45000);
cursor.execute(sql_query)
conn.commit()

注意:这里 AI 没有指定 id,因为 id 是自增主键,数据库会自动分配。它甚至用了完整的列名列表,这是最佳实践。

6.3 更新(UPDATE)

question = "将王二的工资调整为55000"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)   # UPDATE EMPLOYEES SET salary = 55000 WHERE name = '王二';
cursor.execute(sql_query)
conn.commit()

AI 直接根据“王二”这个姓名作为条件,非常合理(假设姓名唯一)。

6.4 删除(DELETE)

question = "删除市场部门的王二"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)   # DELETE FROM EMPLOYEES WHERE department='市场' AND name='王二';
cursor.execute(sql_query)
conn.commit()

这里 AI 加上了 department 条件以避免误删同名员工,安全考虑很到位。


七、多表关联:理解表间关系

很多业务场景不止一张表。我们新增一个 departments 表,存储部门经理信息。

cursor.execute("""
CREATE TABLE IF NOT EXISTS departments(
    id INTEGER PRIMARY KEY,
    name TEXT,
    manager TEXT
)
""")
sample_departments = [
    (1, "销售", "王经理"),
    (2, "工程", "李经理"),
    (3, "市场", "张经理")
]
cursor.executemany("INSERT INTO departments VALUES (?,?,?)", sample_departments)
conn.commit()

此时数据库有两张表:employeesdepartments。我们需要把完整的 Schema 重新拼接给 AI。

tables = ["employees", "departments"]
schema_str = ""
for table in tables:
    schema = cursor.execute(f"PRAGMA table_info({table})").fetchall()
    schema_str += f"CREATE TABLE {table} (\n" + "\n".join([f"{col[1]} {col[2]}" for col in schema]) + "\n);\n\n"

现在,我们问一个涉及两张表的问题:

question = "根据两个表之间的关系,列出每个部门的员工人数和平均工资"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)
-- 输出:
-- SELECT d.name, COUNT(e.id) AS employee_count, AVG(e.salary) AS avg_salary 
-- FROM departments d LEFT JOIN employees e ON d.name = e.department 
-- GROUP BY d.id, d.name;

🔍 AI 如何发现“关系”?
它通过字段名相似性(departments.nameemployees.department)推断出两张表可以通过部门名称关联。虽然我们没有显式定义外键,但 AI 利用语义理解完成了 JOIN 条件的推断。这就是大模型的强大之处。

🔍 LEFT JOIN 与 GROUP BY

  • 使用 LEFT JOIN 保证没有员工的部门也会出现(计数为 0)。
  • GROUP BY d.id, d.name 按部门分组,然后计算每组的员工数(COUNT(e.id))和平均工资(AVG(e.salary))。

八、总结与思考

通过本文,我们完整实现了一个 Text2SQL 系统,核心收益如下:

传统方式Text2SQL 方式
需要记住所有表名、字段名只需用业务语言描述需求
需要掌握 SQL 语法细节AI 自动生成符合语法的 SQL
多表关联容易出错AI 推断关联关系
调试 SQL 耗时生成即正确(大部分情况下)

🔑 关键知识点回顾:

  1. SQLite 游标(Cursor):执行 SQL 的上下文,管理结果集。
  2. executemany:批量绑定性插入,提升性能并防止注入。
  3. 事务提交 commit():确保数据持久化,可回滚。
  4. PRAGMA table_info:获取表结构的元数据命令。
  5. 提示词工程:清晰限定输出格式(纯 SQL),并注入完整 Schema。
  6. 多表 JOIN:大模型通过字段语义自动发现关联,无需外键定义。

🚀 延伸思考:

  • 如果表结构复杂(几十张表),如何选择相关的表送入 Prompt?可以先用向量检索或规则筛选。
  • 如果生成 SQL 有误,可以加入“校验-重试”机制(例如执行失败则让 AI 修正)。
  • 在实际生产环境,建议加上 SQL 执行权限限制,防止误删数据。

Text2SQL 正在改变我们与数据交互的方式——它让数据查询不再是程序员的专利,产品、运营同学也能用自然语言自由探索数据。而这一切,得益于大模型对代码和结构化数据的深刻理解。

希望本文能帮你快速上手 Text2SQL,并在自己的项目中发挥创造力。动手试试吧,你的下一个应用可能就是“对话式 BI”工具!