一、什么是 Text2SQL
Text2SQL 是典型的大模型落地场景:用自然语言提问,AI 自动生成标准 SQL 语句,直接在数据库中执行并返回结果。业务人员不需要学习 SQL 语法,用大白话就能查数据;开发者也可以用它快速搭建数据查询原型。
本文用最轻量化的方案实现完整 Text2SQL 流程:
- 数据库:Python 内置 SQLite,零安装、单文件存储
- 大模型:DeepSeek,兼容 OpenAI 接口,调用简单
- 全程无重型框架,100 行左右代码跑通全流程
二、前置准备
-
Python 3.8+ 环境
-
DeepSeek API Key(官网申请即可)
-
安装官方 OpenAI SDK(DeepSeek 接口完全兼容)
pip install openai
三、分步实现全过程
3.1 初始化 SQLite 数据库
先创建员工表 employees 和部门表 departments,并插入测试数据。
🚩 经典踩坑:主键重复报错 因为 SQLite 数据库文件会持久化保存,重复执行插入代码会导致主键冲突,抛出
UNIQUE constraint failed: employees.id。 解决方案:插入前先清空旧数据,或使用INSERT OR IGNORE跳过重复记录。
python
import sqlite3
# 连接数据库,文件不存在自动创建
conn = sqlite3.connect("test.db")
cursor = conn.cursor()
# 1. 创建员工表
cursor.execute("""
CREATE TABLE IF NOT EXISTS employees(
id INTEGER PRIMARY KEY,
name TEXT,
department TEXT,
salary INTEGER
)
""")
# 清空旧数据,避免重复执行主键冲突
cursor.execute("DELETE FROM employees")
conn.commit()
# 插入测试数据
employee_data = [
(6, "黄佳", "销售", 50000),
(7, "张三", "工程", 75000),
(8, "黄仁勋", "市场", 90000)
]
cursor.executemany("INSERT INTO employees VALUES (?,?,?,?)", employee_data)
# 2. 创建部门表
cursor.execute("""
CREATE TABLE IF NOT EXISTS departments(
id INTEGER PRIMARY KEY,
name TEXT,
manager TEXT
)
""")
cursor.execute("DELETE FROM departments")
conn.commit()
department_data = [
(1, "销售", "王经理"),
(2, "工程", "力经理"),
(3, "市场", "张经理")
]
cursor.executemany("INSERT INTO departments VALUES (?,?,?)", department_data)
conn.commit()
print("数据库初始化完成")
3.2 自动获取数据库 Schema
大模型生成 SQL 的前提是知道表结构 。我们通过 SQLite 内置的 PRAGMA table_info() 自动读取字段信息,拼接成标准建表语句格式,作为 Prompt 上下文传给大模型。
python
def get_db_schema(table_list):
"""自动获取指定表的结构,拼接成 Schema 字符串"""
schema_str = ""
for table in table_list:
# 查询表的字段信息:cid, name, type, notnull, dflt_value, pk
columns = cursor.execute(f"PRAGMA table_info({table})").fetchall()
# 拼接字段行
col_lines = [f"{col[1]} {col[2]}" for col in columns]
# 拼成建表语句格式
schema_str += f"CREATE TABLE {table} (\n" + ",\n".join(col_lines) + "\n);\n\n"
return schema_str
# 获取两张表的完整 Schema
schema_str = get_db_schema(["employees", "departments"])
print("完整数据库 Schema:")
print(schema_str)
输出效果:
sql
CREATE TABLE employees (
id INTEGER,
name TEXT,
department TEXT,
salary INTEGER
);
CREATE TABLE departments (
id INTEGER,
name TEXT,
manager TEXT
);
3.3 封装 DeepSeek 调用:Prompt 是核心
这一步是 Text2SQL 效果好坏的关键。很多人跑不起来,本质都是 Prompt 没写对。
🚩 高频踩坑点
- 字段幻觉:模型编造不存在的字段名 → Schema 必须准确
- 值翻译错误 :数据库存中文 "工程",模型生成
'Engineering'→ Prompt 强制说明值为中文- 输出带格式:返回带 ```sql 代码块 → 明确要求只输出纯 SQL
- 多余解释:模型附带文字说明 → 严格限制输出范围
ini
from openai import OpenAI
# 初始化 DeepSeek 客户端
client = OpenAI(
api_key="你的DeepSeek_API_Key",
base_url="https://api.deepseek.com/v1"
)
def ask_deepseek(query, schema):
prompt = f"""
这是数据库的表结构:
{schema}
重要规则:
1. department 字段存储的是中文部门名称,例如:销售、工程、市场,不要翻译成英文
2. 只输出纯 SQL 语句本身,不要任何 Markdown 格式、反引号、代码块标记
3. 不要任何额外解释、说明文字,只返回 SQL
根据上面的表结构,生成 SQL 回答以下问题:
{query}
"""
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
# 去掉首尾空格换行,避免多余字符影响执行
return response.choices[0].message.content.strip()
3.4 执行 SQL 并输出结果
把模型生成的 SQL 直接交给 SQLite 执行,加上异常捕获,方便定位问题。
python
def run_sql(sql):
try:
result = cursor.execute(sql).fetchall()
# 如果是写操作,提交事务
if sql.strip().upper().startswith(("INSERT", "UPDATE", "DELETE")):
conn.commit()
return result
except Exception as e:
return f"SQL 执行错误:{e}"
3.5 效果测试
测试 1:查询类问题
scss
question = "工程部门员工的姓名和工资是多少?"
sql = ask_deepseek(question, schema_str)
print("生成 SQL:", sql)
print("查询结果:")
for row in run_sql(sql):
print(row)
预期生成 SQL:
ini
SELECT name, salary FROM employees WHERE department = '工程';
输出结果:
arduino
('张三', 75000)
测试 2:写入类问题
scss
question = "在销售部门增加一个新员工,姓名为李四,工资为45000"
sql = ask_deepseek(question, schema_str)
print("生成 SQL:", sql)
print("执行结果:", run_sql(sql))
测试 3:多表查询
scss
question = "查询所有部门的信息"
sql = ask_deepseek(question, schema_str)
print("生成 SQL:", sql)
print("查询结果:")
for row in run_sql(sql):
print(row)
四、完整可运行代码
python
import sqlite3
from openai import OpenAI
# ========== 1. 数据库初始化 ==========
conn = sqlite3.connect("test.db")
cursor = conn.cursor()
# 员工表
cursor.execute("""
CREATE TABLE IF NOT EXISTS employees(
id INTEGER PRIMARY KEY,
name TEXT,
department TEXT,
salary INTEGER
)
""")
cursor.execute("DELETE FROM employees")
employee_data = [
(6, "黄佳", "销售", 50000),
(7, "张三", "工程", 75000),
(8, "黄仁勋", "市场", 90000)
]
cursor.executemany("INSERT INTO employees VALUES (?,?,?,?)", employee_data)
# 部门表
cursor.execute("""
CREATE TABLE IF NOT EXISTS departments(
id INTEGER PRIMARY KEY,
name TEXT,
manager TEXT
)
""")
cursor.execute("DELETE FROM departments")
department_data = [
(1, "销售", "王经理"),
(2, "工程", "力经理"),
(3, "市场", "张经理")
]
cursor.executemany("INSERT INTO departments VALUES (?,?,?)", department_data)
conn.commit()
# ========== 2. Schema 获取 ==========
def get_db_schema(table_list):
schema_str = ""
for table in table_list:
columns = cursor.execute(f"PRAGMA table_info({table})").fetchall()
col_lines = [f"{col[1]} {col[2]}" for col in columns]
schema_str += f"CREATE TABLE {table} (\n" + ",\n".join(col_lines) + "\n);\n\n"
return schema_str
schema_str = get_db_schema(["employees", "departments"])
# ========== 3. DeepSeek 封装 ==========
client = OpenAI(
api_key="你的DeepSeek_API_Key",
base_url="https://api.deepseek.com/v1"
)
def ask_deepseek(query, schema):
prompt = f"""
这是数据库的表结构:
{schema}
重要规则:
1. department 字段存储中文部门名称,不要翻译成英文
2. 只输出纯 SQL,不要 Markdown、反引号、解释文字
3. 表名和字段名严格使用上面给出的名称
问题:{query}
"""
response = client.chat.completions.create(
model="deepseek-chat",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content.strip()
# ========== 4. SQL 执行 ==========
def run_sql(sql):
try:
result = cursor.execute(sql).fetchall()
if sql.strip().upper().startswith(("INSERT", "UPDATE", "DELETE")):
conn.commit()
return result
except Exception as e:
return f"执行错误:{e}"
# ========== 5. 测试 ==========
if __name__ == "__main__":
q1 = "工程部门员工的姓名和工资是多少?"
sql1 = ask_deepseek(q1, schema_str)
print("Q1 SQL:", sql1)
print("Q1 结果:", run_sql(sql1), "\n")
q2 = "查询所有部门的经理是谁"
sql2 = ask_deepseek(q2, schema_str)
print("Q2 SQL:", sql2)
print("Q2 结果:", run_sql(sql2))
conn.close()
五、避坑指南
- 主键冲突:重复执行插入代码必报错,开发调试时先清空表再插入
- 字段名不一致 :表字段是
dept就不要让模型猜department,Schema 必须和真实表完全一致 - 中文值翻译:数据库存中文就必须在 Prompt 里强调,模型默认会翻译成英文
- 输出格式污染:必须明确要求 "只输出纯 SQL",否则带代码块会执行失败
- 安全风险 :生产环境必须拦截
DROP、ALTER、DELETE FROM 表等危险 SQL,不能直接执行用户生成的语句
六、扩展优化方向
- 结果自然语言化:把查询结果再传给大模型,转成通顺的中文回答
- 少样本提示:在 Prompt 里加入 2-3 组「问题 - SQL」示例,准确率大幅提升
- 多表联查:补充表与表之间的关联关系说明,支持 JOIN 查询
- 错误自修复:SQL 执行报错后,把错误信息回传给大模型,让它自动修正重写
这个轻量化方案非常适合快速原型验证、内部小工具开发,后续可以基于它扩展出更复杂的数据分析助手。