让非技术人员直接用自然语言查询数据库,是 Text2SQL 最直观的应用价值,但从生成一条可执行 SQL 到构建可靠的数据应用,中间仍有不少工程问题。下面将使用 DeepSeek API 和 SQLite 完成一个轻量原型,并重点分析 Schema 提供、写操作校验、多表查询与事务一致性等关键环节。
在“Vibe Coding”(氛围编程)时代,代码开发的平权正在发生,而数据平权(Data Democracy) 也紧随其后。Text2SQL 技术让非技术人员只需通过自然语言描述,就能直接与数据库对话并获取结果。
今天,我们将基于 SQLite 和 DeepSeek API,从零构建一个轻量级的 Text2SQL 系统。本文不仅包含核心代码实现,还会分享在实际开发中遇到的“数据库锁”、“多表 Schema 缺失”以及“事务处理”等实战踩坑经验。
正如我们的 readme106.md 笔记中提到的:
作为系统内置的文件型数据库,SQLite 是一个轻量级的数据库,无需安装,无需配置,无需管理。
这让它成为我们验证 Text2SQL 逻辑的最佳沙盒。
我们通过 OpenAI SDK 兼容的方式接入 DeepSeek:
python
from openai import OpenAI
client = OpenAI(
api_key="sk-8dbb...", # 你的 DeepSeek API Key
base_url="https://api.deepseek.com/v1"
)
我们创建一个 employees 表,并尝试批量插入数据。
python
import sqlite3
conn = sqlite3.connect("test.db")
# 创建一个游标对象,执行sql操作
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS employees (
id INTEGER PRIMARY KEY,
name TEXT,
department TEXT,
salary INTEGER
)
""")
sample_data = [
(6, "胡航", "销售", 50000),
(7, "牛奶", "工程", 75000),
(8, "倩倩", "销售", 60000),
(9, "月月", "工程", 80000),
(10, "黄仁勋", "市场", 55000),
]
# insertmany 批量插入,提升性能
cursor.executemany(
"INSERT INTO employees VALUES (?,?,?,?)",
sample_data
)
conn.commit()
⚠️ 踩坑提示: 在实际运行 Notebook 时,很容易遇到
OperationalError: database is locked。这是因为 SQLite 默认写锁机制较严格,特别是在 Jupyter 反复执行同一个 Cell 或存在未关闭的连接时。建议在重跑前conn.close(),或者确保每次只运行一次初始化逻辑。
大模型需要知道表结构才能写 SQL。注释中写得非常清楚:Schema 应详细描述每个表的字段和类型,这有助于模型理解表的结构和关联。
python
# 获取数据库Schema
# SQLite命令,查看employees表的列名、类型等结构信息。
schema = cursor.execute("PRAGMA table_info(employees)").fetchall()
print(schema)
# 列表推导式
# 使用 f-string(格式化字符串字面量),将列名和类型拼接成一个字符串。
schema_str = "CREATE TABLE EMPLOYEES (n" + "n".join([f"{col[1]} {col[2]}" for col in schema]) + "n)"
print("数据库Schema:")
print(schema_str)
python
# 销售部门平均工资多少?先分组再算平均
# text2sql 数据平权
# vibe coding 平权了代码开发
def ask_deepseek(query, schema):
# f 作为模版
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
接下来我们看看大模型对于自然语言的 SQL 生成能力。
question = "工程部门员工的姓名和工资是多少?"SELECT name, salary FROM EMPLOYEES WHERE department = '工程'; ✅question = "在销售部门增加一个新员工,姓名为张三,工资为45000"INSERT INTO EMPLOYEES (name, department, salary) VALUES ('张三', '销售', 45000); ✅question = "将倩倩的工资调整为55000"UPDATE EMPLOYEES SET salary = 55000 WHERE name = '倩倩'; ✅question = "删除市场部门的黄仁勋"DELETE FROM EMPLOYEES WHERE id = 12 AND name = '张三';张三 是上一个上下文插入的,模型将其与 黄仁勋 的删除混淆,并错误地估计了 id=12。这也告诉我们,生产环境中必须对 Text2SQL 生成的增删改语句进行严格校验或引入 Human-in-the-loop。结合 readme106.md 中的设计,我们要进一步引入 departments 表(包含 id, name, manager)。
在尝试插入部门数据时:
python
cursor.execute("""
CREATE TABLE IF NOT EXISTS departments(
id INTEGER PRIMARY KEY,
name TEXT,
manager TEXT
)
""")
sample_departments = [
(1, "销售", "王经理"),
(2, "工程", "李经理"),
(3, "市场", "张经理")
]
# 执行 executemany 时可能会遇到 IntegrityError
如果多次运行,会遇到 IntegrityError: UNIQUE constraint failed: departments.id。这是因为 CREATE TABLE IF NOT EXISTS 不会清空表,主键冲突会导致插入失败。建议使用 INSERT OR IGNORE 或事先 DROP TABLE。
当我们提出需求:question = "根据两个表之间的关系,列出每个部门的员工人数和平均工资"
模型给出:
sql
SELECT department, COUNT(*) AS employee_count, AVG(salary) AS average_salary FROM EMPLOYEES GROUP BY department;
? 发现问题了吗?
生成的 SQL 只用了 EMPLOYEES 表,根本没有连表(JOIN)departments。这是因为我们在构建 schema_str 时,只提取了 employees 表的 Schema,模型根本不知道 departments 表的存在!这提醒我们,在做多表 Text2SQL 时,提供全量 Schema 至关重要。
针对 readme106.md 中提到的业务场景:
orderproduct count-1pay在 Text2SQL 落地到实际业务时,我们必须引入事务(Transaction) 。单纯让 LLM 生成一句 SQL 是不够的,因为像“生成订单”、“扣减库存”、“付款”往往需要多条 SQL 保持原子性。一旦其中某一步骤失败,必须整体回滚(Rollback),确保数据一致性。
python
try:
# 开启事务
cursor.execute("BEGIN TRANSACTION")
# 大模型生成的三条相关SQL
# cursor.execute(sql_order)
# cursor.execute(sql_product)
# cursor.execute(sql_pay)
conn.commit()
except Exception as e:
conn.rollback()
通过这个实战项目,我们验证了 Text2SQL 数据平权的可行性。大模型在处理标准 CRUD 甚至部分聚合查询(如 GROUP BY)时已经游刃有余。
但在走向生产的过程中,我们依然需要警惕: