1. 先别急着写代码Text2SQL 到底在解决什么这几年大模型火起来之后“用自然语言查数据库”这个老话题又被翻了出来。说白了就是 Text2SQL你输入一句人话比如“上个月各渠道的订单量和退款率”系统自动把它翻译成 SQL去数据库里查完再告诉你结果。这事听起来很爽但落地的时候坑特别多。先不说复杂的多表关联、嵌套子查询光是“销售额”到底是指订单金额还是实付金额不同业务口径都能吵半天。更别提大模型偶尔一本正经地编出一个不存在的字段名你还没法反驳它。我之前也试过那种重方案拉一个大模型服务、搞向量库、配一堆编排框架结果光环境折腾就花了两天。后来反思了一下对于个人项目或者中小团队来说其实根本用不着那么重的架构。一个 DeepSeek 的 API加上 SQLite 这种轻量数据库完全可以在半小时内搭出一个够用的 Text2SQL 查询助手。我个人的体会是这类项目成功的关键其实不在于模型多聪明而在于你怎么设计“约束”。数据库结构约束得越死模型自由发挥的空间越小出来的 SQL 就越靠谱。这篇就老老实实记录一下我自己的搭建过程包括踩过的坑和最终沉淀下来的那套配置。这个方案适合谁如果你手头有 SQLite 数据文件想用自然语言快速做查询分析又不想引入一大堆基础设施同时你有多多少少的 Python 基础那这篇应该能帮你少走不少弯路。2. 整体设计思路让模型在“封闭世界”里做选择题2.1 为什么选 DeepSeek 而不是更大牌的模型选型这事我纠结过一阵。市面上的模型很多有些效果确实强但价格也感人。Text2SQL 这种场景有个特点请求量大、单次任务不算特别难、对延迟有一定要求但对“角色扮演”“情感理解”这种能力基本用不上。DeepSeek 的 API 价格只有头部模型的一个零头而且代码生成能力实测下来相当能打。我试过让它写一些正则表达式、解析脚本反应都很快。后来我又在网上搜了一圈发现不少人在本地跑 Text2SQL 实验时也拿它做主力社区里的口碑还不错。当然也不是说非它不可。只要你代码里用的是 OpenAI 兼容的接口格式换模型基本就是改个 base_url 和 key 的事。但我的建议是从 DeepSeek 起步完全够了先把整个链路跑通后面有钱了再往上换也不迟。2.2 SQLite 在这种场景下的独特优势很多人一听到数据库就想到 MySQL、PostgreSQL。但对于个人助手、内部工具这种场景SQLite 可能是最舒服的选择。首先它不需要装服务端一个 .db 文件就是整个数据库放到哪都能用。其次它是 Python 内置支持的不需要额外驱动。最关键的是它的 schema 信息可以通过一条命令完整读取这对 Text2SQL 来说太重要了——模型需要的表结构、字段名、类型全都现成。我做这个项目的目标很明确不追求上线百万用户不追求高并发只求我在命令行里问一句“最近七天哪些品类的库存周转最快”它能在几秒内给我一个靠谱的 SQL并且真的执行出结果。SQLite 完全够用还省了漫长的环境配置时间。2.3 核心架构一个循环搞定“问-转-查-答”整个系统本质上就是一个循环用户输入自然语言问题把数据库 schema、示例数据和用户问题拼进 Prompt调用 DeepSeek让它只输出 SQL在 SQLite 中执行 SQL拿到结果把结果交给模型让它用自然语言总结给用户看起来很简单对吧但每个环节都有细节。尤其是第二步的 Prompt 设计直接决定 SQL 的质量。我在实际开发中把整个 Prompt 拆成了三块系统指令、数据库上下文、用户问题。系统指令这部分大概如下你是一名资深 SQL 专家。根据给定的数据库结构将用户的中文问题转换为 SQLite SQL 语句。 只输出 SQL 代码不要输出任何解释、前后缀或多余文字。 要求 1. 字段名必须严格使用 schema 中存在的字段禁止臆造。 2. 如果用户问题存在歧义选择最合理的默认解释不要反问。 3. 涉及日期过滤时默认使用 yyyy-MM-dd 格式。 4. 使用 LIMIT 限制返回行数最多 20 行。 5. 如果无法生成输出 -- ERROR: 原因这段指令看起来普通但每一句都是踩坑踩出来的。比如“只输出 SQL 代码”这句话救了我无数次——没有它模型会经常在 SQL 前后加“好的以下是您需要的查询”这种废话解析起来特别烦。数据库上下文部分就是我说的 schema 信息后面会详细讲怎么自动生成。第三部分就是拼上用户当前的问题。核心就一句话别让模型自由发挥给它一个封闭的选择环境。3. 环境准备与工具链尽量不装多余的东西3.1 Python 依赖清单依赖少到这个程度是我后来才意识到的。真正核心的就三个包openai因为 DeepSeek 接口兼容 OpenAI 格式所以直接用它当客户端sqlalchemy用它的 inspect 功能读取数据库结构很方便pandas最后展示结果表格用的如果只是命令行打印也可以不装安装命令一句话pip install openai sqlalchemy pandas如果你平时用 uv 这种新工具也可以uv add同样的包名。这仨包都不重安装过程基本不会出幺蛾子。3.2 获取 DeepSeek API Key 的正确姿势去 DeepSeek 开放平台注册账号然后创建一个 API Key。创建的时候注意Key 只会完整显示一次一定要先复制保存好页面一关你就再也看不到了。有一点需要提醒API 调用是花小钱的但新用户一般会有一些免费额度个人调试足够了。我自己的使用强度下跑了很久账上也没扣几块钱相比折腾本地模型的电费这个成本基本可以忽略。环境变量我建议这样配方便以后切换模型export DEEPSEEK_API_KEY你的key在代码里读取环境变量这样就不会把 Key 写死在代码仓库里。3.3 准备一张带主键和索引的测试表工欲善其事必先利其器。我准备了一张模拟的销售订单表字段尽量贴近真实业务方便后面验证效果id主键自增order_id业务订单号product_category商品品类product_name商品名称sale_amount销售金额quantity销售数量region销售区域order_date订单日期建表语句如下CREATE TABLE sales_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id TEXT NOT NULL, product_category TEXT NOT NULL, product_name TEXT NOT NULL, sale_amount REAL NOT NULL, quantity INTEGER NOT NULL, region TEXT NOT NULL, order_date TEXT NOT NULL ); CREATE INDEX idx_order_date ON sales_orders(order_date); CREATE INDEX idx_category ON sales_orders(product_category);这里埋了几个小心机。日期字段我用 TEXT 存储而不是 SQLite 的日期类型因为 Text2SQL 时模型能更好地理解字符串格式。索引也刻意建了一方面提升大数据量下聚合查询的速度另一方面告诉模型这两个字段经常被用于过滤和分组。数据我提前插了大概几百条覆盖了不同品类、不同区域、不同日期分布这样模型在生成 SQL 时能有足够的示例数据做参考。4. 实操过程与核心代码一步步搭出整个链路4.1 第一步自动抽取数据库 SchemaText2SQL 的 Prompt 质量很大程度上取决于你喂给模型的 schema 信息。用 SQLAlchemy 的 inspect 模块可以非常干净地拿到所有表结构和字段信息。import sqlalchemy as sa engine sa.create_engine(sqlite:///sales.db) inspector sa.inspect(engine) schema_lines [] for table_name in inspector.get_table_names(): columns inspector.get_columns(table_name) col_str , .join([f{col[name]} {col[type]} for col in columns]) schema_lines.append(fCREATE TABLE {table_name} ({col_str});) schema_text \n.join(schema_lines) print(schema_text)这样得到的 schema 是一段精简版建表语句比原始建表语句更干净模型也更好理解。我还额外做了一件事把每一列的注释信息加进去比如“sale_amount 是实际成交金额单位元”。SQLite 里可以存在扩展表里也可以用 Python 字典硬映射我图省事直接放了三个字段的额外说明但效果特别好。4.2 第二步设计 Prompt 模板与示例注入如果只是把问题拼到 schema 后面就让模型去写小规模测试还行稍微复杂一点的问题就容易跑偏。我的做法是加了两条 few-shot 示例让模型明白“用户说什么样的话就该输出什么样的 SQL”。prompt_template 【系统指令】 你是一名资深 SQL 专家。根据给定的数据库结构将用户的中文问题转换为 SQLite SQL 语句。 只输出 SQL 代码不要输出任何解释、前后缀或多余文字。 要求 1. 字段名必须严格使用 schema 中存在的字段禁止臆造。 2. 如果用户问题存在歧义选择最合理的默认解释不要反问。 3. 涉及日期过滤时默认使用 yyyy-MM-dd 格式。 4. 使用 LIMIT 限制返回行数最多 20 行。 5. 如果无法生成输出 -- ERROR: 原因 【数据库结构】 {schema_text} 【示例】 用户问题按品类统计总销售额结果从高到低排序 SQLSELECT product_category, SUM(sale_amount) AS total_sales FROM sales_orders GROUP BY product_category ORDER BY total_sales DESC LIMIT 20; 用户问题东北区域最近一个月卖了多少件商品 SQLSELECT SUM(quantity) AS total_quantity FROM sales_orders WHERE region 东北 AND order_date date(now, -1 month) LIMIT 20; 【用户问题】 {question} 【SQL】 这里有个细节想提一下。第一个示例正好讲清楚“怎么统计销售额”第二个示例展示“中文区域名直接映射到字段值”。模型看到这两个例子后遇到类似问法就能直接对齐不用自己猜。4.3 第三步编写调用函数与安全执行有了 Prompt 模板接下来的代码就是把整个流程封起来。from openai import OpenAI import os client OpenAI( api_keyos.environ.get(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com ) def generate_sql(question: str, schema_text: str) - str: prompt prompt_template.format(schema_textschema_text, questionquestion) resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.1, max_tokens500, ) sql resp.choices[0].message.content.strip() if sql.startswith(): sql sql.strip() if sql.startswith(sql): sql sql[2:] return sql.strip()temperature0.1是我调出来的经验值。Text2SQL 属于近乎确定性的任务温度太高模型容易发散编造奇怪的 SQL温度太低又可能导致它过于保守连一点高级函数都不敢用。0.1 算是一个平衡点。执行代码长这样def execute_sql(sql: str): import pandas as pd if sql.startswith(-- ERROR): return sql try: df pd.read_sql_query(sql, engine) return df except Exception as e: return fSQL 执行出错: {e}这里直接用了 pandas 的 read_sql_query省去手动管理游标的麻烦。如果不想引入 pandas也可以用 sqlite3 模块自己 fetch 结果但 DataFrame 打印出来排版美观后面让模型总结时也好处理。4.4 第四步结果自然语言化拿到 DataF rame 之后直接把表格内容塞给模型让它用中文总结。这一步很多教程会忽略但实际使用体验差别巨大。你要是只把表格往终端里一扔用户还得自己看数字但如果模型说一句“华东区上个月销售额领先其中数码类目贡献最大”理解成本就低多了。def summarize(question, df): table_md df.to_markdown() resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: user, content: f用户问题是{question}\n查询结果如下\n{table_md}\n请用不超过100字的中文总结回答突出关键数据。} ], temperature0.3, max_tokens300, ) return resp.choices[0].message.content到这里一个完整的 Text2SQL 查询助手就跑通了。4.5 主程序把整个流程串起来最后再加一个简单的命令行循环def main(): schema_text build_schema_text() while True: question input(请输入你的问题输入 q 退出) if question.lower() q: break sql generate_sql(question, schema_text) print(生成的 SQL, sql) df execute_sql(sql) if isinstance(df, str): print(df) continue print(df) summary summarize(question, df) print(回答, summary) if __name__ __main__: main()注意这个build_schema_text()就是我前面抽 schema 那一步抽出来的函数。整个文件加起来不超过一百五十行不需要任何复杂架构就是一个脚本完事。提示这个脚本目前是全程同步阻塞的如果你后续想接到 Web 服务里建议把generate_sql和summarize都改成异步接口或者直接丢到消息队列里跑不然一个请求会把整个服务卡住。5. 踩坑实录这些细节决定 SQL 准不准5.1 字段名臆造问题与解决方案刚开始测试时我问“哪个品类的退货率最高”表里明明没有退货相关字段模型照样一本正经地编了个return_rate列出来。执行时直接报错“no such column”。后来我在系统指令里加了“禁止臆造字段名”同时把 schema 里每一列的作用写清楚问题大大缓解。但更彻底的解法是在 execute 之前做一次字段名校验。用正则把所有 SELECT 的字段名抽出来去 schema 的字段集合里比对一遍不存在的直接拒绝执行。import re def validate_columns(sql, schema_columns): selected re.findall(rSELECT\s(.*?)\sFROM, sql, re.IGNORECASE | re.DOTALL) if not selected: return False names re.findall(r([A-Za-z_][\w]*), selected[0]) for n in names: if n.upper() in (SUM, COUNT, AVG, MIN, MAX, AS, DISTINCT, GROUP, ORDER, BY, DESC, ASC, LIMIT): continue if n not in schema_columns: raise ValueError(f字段 {n} 不存在)这个方法虽然没法覆盖所有子查询场景但对付 80% 的简单查询完全够用。5.2 日期字段比较格式的坑SQLite 的日期处理跟 MySQL 不太一样。模型默认用CURDATE()或NOW()这些 MySQL 函数在 SQLite 里直接报错因为不存在这些函数。系统指令里我专门写了“使用 yyyy-MM-dd 格式”但模型还是会偶尔犯糊涂。后来我在系统指令里补了一条 SQLite 专用提示涉及日期函数时必须使用 date(now, -1 day) 这样的 SQLite 语法禁止使用 MySQL 的 CURDATE、NOW 等函数。加完这条之后日期类的查询准确率提升非常明显。5.3 DeepSeek 响应偶尔带 Markdown 代码块标记这是个很细但很烦的问题。几次调用后返回内容里出现了sql这种标记直接拼到 SQL 执行器里就会报语法错误。我在 generate_sql 里做了后处理把首尾的 Markdown 代码块标记去掉。虽然不是最优雅的方案但非常实用sql sql.strip() if sql.startswith(): lines sql.split(\n) lines lines[1:] if lines[0].strip() or lines[0].strip() sql else lines lines [l for l in lines if l.strip() ! ] sql \n.join(lines).strip()5.4 复杂查询的内存与性能问题如果表特别大模型又生成了一个不带 WHERE 的全表聚合SQLite 其实还好几十万行以内秒级返回。但如果你让模型分页输出所有明细那返回的结果可能非常大不仅占用内存还会把 summary 的 Prompt 撑爆。解决办法是在系统指令里硬性限制返回行数LIMIT 20。这个已经被我写死到 Prompt 里实测下来用户很少需要一次性看几千行明细真有需要再让他明确说“导出全部”我再单独写一个导出逻辑。6. 效果验证与边界场景分析6.1 基础查询效果输入“统计华东区近一周的销售总金额”模型给出的 SQL 是SELECT SUM(sale_amount) AS total_amount FROM sales_orders WHERE region 华东 AND order_date date(now, -7 days);执行后结果只用一个数字总结也很干净“近一周华东区销售总金额为 X 万元。”这一步跑通基本上核心链路就没问题了。6.2 稍微复杂的聚合查询我试了一个带分组、排序和过滤的组合问题“按品类统计上个月销售量前十的品类”。模型给出的 SQL 用到了 GROUP BY、ORDER BY 和 LIMIT完全符合 SQLite 语法字段名也没出错。说明 few-shot 示例里那个按品类统计的样例起到了很明显的导向作用。6.3 会出问题的边界情况当然这个简单方案也有它力不能及的时候。遇到这些情况模型的回复就会变得不太靠谱问题涉及多个表 JOIN而 schema 里没有表关系描述用户用词和字段命名差距过大比如字段叫price用户却说“成本”涉及非常细腻的时间逻辑比如“上月最后一周的工作日”这些情况我目前的策略是要么在 schema 里写清楚表关系和外键要么干脆让模型返回“-- ERROR无法确定查询逻辑”引导用户换种说法。与其让模型硬解出错误 SQL不如诚实地承认不会。7. 让这个助手更好用的几个后续扩展方向7.1 从 schema 注入升级为元数据管理用CREATE TABLE字符串当 schema 是最快的起步方式但它有两个问题一是字段含义只能靠名称猜二是表之间的关系完全没表达出来。后续最值得做的是引入一层简单的元数据表比如{ table: sales_orders, columns: { region: 销售区域值为华东/华南/华北等, sale_amount: 实际成交金额单位人民币元 } }然后把它转成一行一行的人类能读懂的描述喂给模型。你会发现同样一个问题模型理解得准确很多。7.2 加一层 SQL 安全检查OpenAI 兼容接口返回的 SQL 理论上可能在执行时搞破坏所以执行前最好加一层简单的安全拦截。比如禁止DROP、DELETE、UPDATE、INSERT开头对PRAGMA、ATTACH等特殊语句做白名单过滤对返回行数做硬性限制这些在生成 SQL 后、执行前加一个函数就能解决不算复杂。7.3 支持多轮对话与查询条件修正很多人问完第一句之后会紧接着说“把其中数码类的去掉”。现在的单次问答结构没法处理这种“上下文修正”。如果想做多轮可以在每次生成 SQL 前不传“问题的字符串”而是传“全部历史对话记录”让模型根据上下文理解最新意图。注意这种模式下 token 消耗会增加建议设置一个记忆窗口只保留最近三轮对话。8. 最后一点升级建议到目前为止我最大的一个体会是这种轻量级方案的失败绝大多数不是模型能力不足而是交互设计没给足上下文。你把 schema 写全、示例配多、系统指令写细几乎能覆盖百分之八十以上的日常查询。再就是迭代思路。不要一开始就追求完美先把能跑通的闭环做出来然后用真实问题去测试每发现一个典型错误就修正一处 Prompt 或校验逻辑慢慢你就会发现模型越来越“懂你”。如果你手头正好有 SQLite 数据我建议你今晚就照这个流程跑一遍。毕竟这套东西不挑环境、不挑硬件一条网线和一台普通的笔记本就够用了。