拒绝手写 SQL:用 Python + DeepSeek 打造 Text-to-SQL 智能数据查询助手

49 阅读6分钟

摘要:SQL 很强,但也很烦。本文将带你实战构建一个基于 Python 和 DeepSeek 大模型的 Text-to-SQL 智能体。通过动态 Schema 注入和 Prompt 工程,让 LLM 充当“翻译官”,实现用自然语言直接查询本地数据库。

0. 引言:SQL 的痛与 AIGC 的药

在传统的后端开发中,CRUD(增删改查)占据了我们大量的时间。对于前端开发者、产品经理或数据分析师来说,SQL 更是那道难以逾越的“叹息之墙”——稍微忘记一个 JOIN 语法或者字段名拼错,查询就报错。

AIGC 时代的到来,正在重构数据库的交互方式。

如果我们能将 LLM(大语言模型)作为中间层,让它理解人类的自然语言(Natural Language),并根据数据库结构自动翻译成机器语言(SQL),那么数据库的操作门槛将无限趋近于零。这就是 Text-to-SQL

今天,我们拒绝手写 SQL,用 Python 配合国产推理编程能力极强的 DeepSeek,手撸一个智能数据查询助手。

1. 技术选型:SQLite + DeepSeek

为什么选择这套组合?

  • SQLite

    • 轻量级:Python 原生内置,无需安装 MySQL/PostgreSQL 服务,一个 .db 文件就是整个数据库。
    • 广泛应用:不要小看它,你的微信聊天记录、浏览器历史记录,底层都是 SQLite。它非常适合作为 AI Agent 的本地记忆体或原型验证。
  • DeepSeek (深度求索)

    • 代码能力强:DeepSeek-V3/R1 在代码生成和 SQL 逻辑推理上表现优异。
    • 兼容性好:完全兼容 OpenAI SDK 协议,这意味着我们可以直接复用成熟的工具链。

2. 核心原理:Prompt Engineering for SQL

很多新手直接把问题扔给 LLM:“帮我查查工资最高的员工”。
LLM 会一脸懵逼:“你有哪些表?字段叫 salary 还是 wage?”

要实现精准的 Text-to-SQL,核心在于 Schema Injection (结构注入)

2.1 动态获取 Schema

我们需要让 Python 自动读取数据库的“元数据”,而不是手动把表结构复制给 AI。在 SQLite 中,我们可以使用 PRAGMA table_info(table_name) 来获取列名和类型。

2.2 Prompt 设计三要素

一个优秀的 SQL 生成 Prompt 必须包含:

  1. Role (角色) :你是一个 SQL 专家。
  2. Context (上下文) :这是我的数据库表结构 CREATE TABLE ...。
  3. Constraints (约束) :只输出 SQL,不要 Markdown,不要废话。

3. 实战代码解析

下面是完整的、经过 Bug 修正的 Python 代码。它实现了从“建表造数据”到“AI 生成 SQL”再到“执行查询”的完整闭环。

3.1 环境准备

Bash

# 确保安装了 OpenAI SDK(用于调用 DeepSeek)
!pip install openai

3.2 完整实现

Python

import sqlite3
from openai import OpenAI

# 配置 DeepSeek 客户端
# 注意:实际开发中请将 API Key 放入环境变量
client = OpenAI(
    api_key='sk-xxxx',  # 替换为你的 DeepSeek API Key
    base_url='https://api.deepseek.com/v1',
)

def init_db(db_path="test.db"):
    """初始化数据库:建表并插入模拟数据"""
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    # 1. 每次运行前清理旧表,防止 'UNIQUE constraint failed' 报错
    cursor.execute("DROP TABLE IF EXISTS employees")
    
    # 2. 创建表结构
    cursor.execute("""
       CREATE TABLE employees(
           id INTEGER PRIMARY KEY,
           name TEXT,
           department TEXT,
           salary INTEGER
       )
    """)
    
    # 3. 插入模拟数据
    sample_data = [
        (6, "程浩", "开发部", 32000),
        (7, "张三", "销售部", 20000),
        (8, "草威威", "开发部", 33000),
        (9, "李四", "销售部", 15000)
    ]
    cursor.executemany('INSERT INTO employees VALUES(?,?,?,?)', sample_data)
    conn.commit()
    print("数据库初始化完成,数据已注入。")
    return conn

def get_table_schema(conn, table_name="employees"):
    """
    核心逻辑:动态获取数据库表结构
    PRAGMA table_info 返回格式: (cid, name, type, notnull, dflt_value, pk)
    """
    cursor = conn.cursor()
    # 注意:fetchall() 返回的是列表,原代码中的逗号会导致它变成元组
    columns = cursor.execute(f"PRAGMA table_info({table_name})").fetchall()
    
    # 将元组数据拼接成类似 CREATE TABLE 的定义字符串
    # col[1] 是字段名, col[2] 是字段类型
    schema_str = f"CREATE TABLE {table_name} (\n" 
    schema_str += ",\n".join([f"  {col[1]} {col[2]}" for col in columns])
    schema_str += "\n)"
    return schema_str

def ask_deepseek_sql(query, schema):
    """构建 Prompt 并调用 LLM"""
    prompt = f"""
    你是一个 SQLite 数据库专家。请根据以下表结构,生成对应的 SQL 查询语句。
    
    【表结构 Schema】:
    {schema}
    
    【用户问题】:
    {query}
    
    【严格约束】:
    1. 仅输出 SQL 语句本身,不要输出 Markdown 格式(如 ```sql ... ```)。
    2. 不要输出任何解释性文字。
    """
    
    try:
        response = client.chat.completions.create(
            model="deepseek-chat", # 推荐使用 chat 模型,速度快且稳定
            messages=[{"role": "user", "content": prompt}],
            temperature=0 # 设置为 0,让输出尽可能确定
        )
        # 修复原代码中的 response.choice 属性错误
        sql = response.choices[0].message.content.strip()
        # 双重保险:清洗可能存在的 markdown 符号
        return sql.replace('```sql', '').replace('```', '')
    except Exception as e:
        return f"API调用失败: {e}"

# --- 主程序运行 ---

# 1. 初始化
conn = init_db()

# 2. 获取 Schema 上下文
schema_context = get_table_schema(conn)
print(f"\n[Context] 当前表结构:\n{schema_context}")

# 3. 用户提问
user_question = "开发部部门员工的姓名和工资是多少?"
print(f"\n[User] 提问: {user_question}")

# 4. LLM 生成 SQL
generated_sql = ask_deepseek_sql(user_question, schema_context)
print(f"[AI] 生成 SQL: {generated_sql}")

# 5. 执行生成的 SQL
if generated_sql.upper().startswith("SELECT"):
    cursor = conn.cursor()
    try:
        results = cursor.execute(generated_sql).fetchall()
        print(f"\n[DB] 查询结果:")
        for row in results:
            print(row)
    except sqlite3.Error as e:
        print(f"SQL 执行错误: {e}")
else:
    print("生成的语句不是查询语句,跳过执行。")

conn.close()

3.3 代码运行结果

Text

数据库初始化完成,数据已注入。

[Context] 当前表结构:
CREATE TABLE employees (
  id INTEGER,
  name TEXT,
  department TEXT,
  salary INTEGER
)

[User] 提问: 开发部部门员工的姓名和工资是多少?
[AI] 生成 SQL: SELECT name, salary FROM employees WHERE department = '开发部';

[DB] 查询结果:
('程浩', 32000)
('草威威', 33000)

4. 关键点总结

  1. Cursor 的复用:在 Python 中,cursor 是数据库操作的句柄。无论是 PRAGMA 获取结构,还是 EXECUTE 执行查询,都离不开它。
  2. Schema 的重要性:代码中的 get_table_schema 函数是连接 LLM 与 数据库的桥梁。如果没有这一步,DeepSeek 就算再聪明,也猜不到你的字段叫 department 还是 dept。
  3. 清洗输出:LLM 即使被要求“只输出 SQL”,有时也会礼貌地加上 Markdown 格式。在代码中加入 .replace('```', '') 是工程化落地的必要手段。

5. 展望

这就结束了吗?这只是开始。
目前的 Text-to-SQL 只是实现了“查询翻译”。未来的 Data Agent 将会更加强大:

  • 分析能力:查询出数据后,直接调用 Python matplotlib 库画出图表。
  • 多表关联:通过更复杂的 Prompt,处理 JOIN 等高阶逻辑。
  • 安全校验:在执行 SQL 前增加一层校验,防止 DROP TABLE 等恶意操作。

DeepSeek + Python,正在让每一个普通开发者拥有操作数据的“上帝视角”。快去试试吧!