Text2SQL + SQLite:用自然语言查询本地数据库的实战指南
1. 这个项目到底在做什么我先说结论Text2SQL 不是什么新概念但能在 2025 年还值得拿出来认真聊是因为它真的从“实验室玩具”变成了“能落地的生产力工具”。尤其是搭配 SQLite 这种单文件数据库门槛低、见效快特别适合个人开发者、数据分析师、还有那些被领导和业务同事追着要数据的苦命人。一句话解释这个项目你写一句大白话比如“帮我查一下上个月销售额最高的前十个商品”系统自动把它翻译成 SQL 语句然后去 SQLite 数据库里执行最后把结果返回给你。整个过程里你不需要知道表结构长什么样不需要会写 JOIN不需要理解 LEFT JOIN 和 INNER JOIN 的区别到底是什么。这里有个容易误会的点需要先说清楚Text2SQL 并不是“AI 自动建库建表”它解决的是“查询”这一环。你仍然需要有一个结构良好的数据库只是把“写 SQL 的人”这个角色从“懂技术的你”替换成了“AI”。所以这个项目适合谁来用适合所有手里有 SQLite 数据文件、但是不想每次查数据都打开 DB Browser 手动拼 SQL 的人也适合想在本地快速搭一个“自然语言数据查询助手”的开发者。我自己的使用场景是这样的手头有几个 SQLite 数据库有的是爬虫抓下来的商品数据有的是内部系统的操作日志有的是一堆 CSV 导入的报表数据。每次业务方说“帮我看看这个数据怎么回事”我都得先回忆表结构再翻以前的 SQL 笔记最后写一条贼长的查询语句。用了 Text2SQL 之后我只需要把数据库 schema 丢给模型然后用中文描述需求剩下的活它干我只需要检查一下生成的 SQL 靠不靠谱。这个项目选 SQLite 作为落地目标我个人觉得非常聪明。SQLite 是单文件数据库一个.db文件就是全部数据没有独立的服务进程不需要账号密码不需要网络配置非常适合做 Text2SQL 的“第一个吃螃蟹”的场景。而且 SQLite 支持标准 SQL 的大部分语法表结构可以通过sqlite_master系统表拿到schema 信息非常干净这对大模型理解数据结构极其友好。2. 核心原理拆解Text2SQL 是怎么把中文变成 SQL 的2.1 底层逻辑这不是搜索引擎是“翻译 生成”很多人对 Text2SQL 有误解以为它是从一个预先写好的 SQL 模板库里做匹配就像搜索引擎查关键词一样。实际上现在的实现思路完全不是这样。它的核心是大语言模型LLM的生成能力本质上是“翻译”——把自然语言翻译成 SQL。这个翻译过程需要什么至少需要四样东西第一数据库的表结构信息也就是 schema。包括有哪些表、每张表有哪些字段、字段的类型是什么、有没有主键和外键。这一步至关重要模型不知道你的表长什么样就不可能生成正确的 SQL。第二一些示例数据或者字段注释。比如一个字段叫created_at光看名字模型可能猜是创建时间但具体是下单时间还是注册时间最好在 schema 里给它加一句注释。或者干脆抽几条真实数据让模型看看长什么样。第三用户的自然语言查询。这个就是我们输入的“帮我查一下……”之类的话。第四一个能理解以上所有信息的大模型。模型把前三样东西放在一起推理出用户的意图然后生成一条符合 SQLite 语法的 SQL 语句。这里有个特别关键的细节SQL 生成不是“写出来”就完事了它还要被“执行”。所以整个链路里最容易被忽略的是——模型生成的 SQL 可能语法是对的但逻辑是错的。比如用户问“最近一周的订单金额”模型有可能理解成“最近七天的数据”也有可能理解成“本周一到今天的数据”。这个歧义不是模型能解决的它需要你提前在 schema 或者提示词里定义清楚“最近一周”的语义。2.2 为什么选 SQLite 而不是 MySQL、PostgreSQLSQLite 在 Text2SQL 场景里有几个其他数据库比不了的优势第一个优势是 schema 获取极其简单。连接 MySQL 你得处理权限、驱动、字符集这些破事连接 PostgreSQL 得搞懂它的 pg_catalog 那一套系统表。而 SQLite直接查sqlite_master就能拿到所有的建表语句一个PRAGMA table_info(表名)就能拿到字段列表。这种清爽感对程序开发来说是巨大的效率提升。第二个优势是零配置、真单文件。数据库就是一个文件你把文件路径告诉程序它就能连上。不需要像 MySQL 那样先安装、再初始化、再开端口、再设密码。对于本地工具类项目来说这几乎是零成本起步。第三个优势是执行环境好模拟。SQLite 支持内存数据库比如:memory:这意味着你可以在不污染真实数据的前提下先让模型生成 SQL然后在内存数据库里跑一遍做语法验证。这个技巧我后面会专门讲非常实用。第四个优势跟文本处理有关。SQLite 的 SQL 语法对字符串处理、JSON 操作的支持虽然不如 PostgreSQL 那么庞大但应付常见查询绰绰有余。而且它用到的类型系统比较简单模型生成 SQL 时不容易在类型转换上翻车。2.3 Schema 的两种玩法全量注入 vs 按需裁剪刚才提到 schema 信息是 Text2SQL 的地基那具体怎么把 schema 塞给大模型我试过两种路线各有利弊。第一种是“全量注入”。就是把所有表的建表语句、字段注释、示例值全部拼成一个超长的文本作为系统提示词丢给模型。好处是模型能看到全貌对于涉及多表 JOIN 的复杂查询理解得更准。坏处是当数据库表很多、字段很多的时候提示词会变得非常长。大模型对超长上下文的处理能力有限而且 token 消耗也是真金白银。第二种是“按需裁剪”。先让模型基于用户的自然语言查询挑选可能相关的表只把这几张表的 schema 注入提示词。这个方案 token 开销小响应速度快但是引入了一个新问题模型挑错了表怎么办所以通常需要一个 fallback 机制第一次查询没返回结果或者报错了就把更多表补充进去再试一次。我的建议是如果数据表少于十张无脑用全量注入省心最重要。如果是几十张表的大库用按需裁剪 自动补表机制。我在本地测试时甚至试过先用一个轻量模型做“选表”再用主力模型做“生成 SQL”效果也还行但成本和复杂度都上去了不太推荐普通项目这么做。3. 实操篇搭一个本地 Text2SQL 助手我踩过的坑全在这里3.1 技术选型别一上来就整 LangChain先试试裸 API老实说现在很多人一提到做大模型应用第一反应就是上 LangChain。但 Text2SQL 这个场景我强烈建议你从裸 API 开始。为什么不一开始就用框架因为 LangChain 有自己的一套抽象比如SQLDatabaseChain、create_sql_query_chain听起来很美好但在实际使用中你会碰到一堆问题——它默认的行为可能跟你的需求不一致改起来又很别扭。而且框架为了兼容各种数据库和模型内部做了很多事情你反而搞不清楚到底哪一步出了问题。裸 API 的思路其实简单到让人意外把 schema、用户问题、示例拼成一个字符串调用大模型的 chat completion 接口拿到返回的 SQL然后执行、返回结果。就这么简单。我用的技术栈是这样的Python 3.10sqlite3Python 内置不需要额外装驱动OpenAI 兼容接口的大模型我本地用的是 Ollama 部署的 Qwen 系列模型也兼容 OpenAI 的 API 格式FastAPI 做 HTTP 接口方便后续接 Web 前端或者微信机器人之类的东西这里有个非常重要的经验选模型时优先看“SQL 生成能力”而不是“对话能力”。有些模型聊天很流畅但生成 SQL 时容易出现低级错误比如字段名编造、语法拼错。我实测下来Qwen 系列和 DeepSeek 系列在 SQL 生成上明显比其他一些模型稳尤其是面对较复杂的多表查询时。3.2 核心代码一个 100 行的 Text2SQL 最小实现我先给一个最小可用的流程后续你可以根据自己的情况扩展。第一步读取数据库 schemaimport sqlite3 def get_schema(db_path: str) - str: conn sqlite3.connect(db_path) cursor conn.cursor() # 获取所有表的建表语句 cursor.execute(SELECT sql FROM sqlite_master WHERE typetable AND sql IS NOT NULL) tables_sql [row[0] for row in cursor.fetchall()] # 额外附带每张表的字段信息和示例值 schema_parts [] for sql in tables_sql: schema_parts.append(sql ;) conn.close() return \n\n.join(schema_parts)这里有个小细节值得说sqlite_master里的sql字段存的就是完整的建表语句包括字段名、类型、约束条件。这些信息对大模型生成准确的 SQL 帮助极大比额外去查PRAGMA table_info要直观得多。第二步构造提示词。我试过很多种写法最终固定下来一个比较稳的模板def build_prompt(schema: str, question: str) - str: return f 你是一个 SQLite 数据库专家。请根据数据库 schema 和用户需求生成一条 SQLite SQL 查询语句。 数据库 schema {schema} 用户需求{question} 要求 1. 只输出 SQL 语句不要输出任何解释性文字。 2. 使用 SQLite 兼容语法。 3. 如果查询涉及多个表注意使用正确的 JOIN 方式。 4. 字段名必须从 schema 中获取严禁编造不存在的字段。 5. 如果用户需求不明确生成你认为最合理的查询。 这个模板看起来简单但里面每一句话都有它的作用。要求 1 “只输出 SQL” 非常关键。之前我没加这句话的时候模型经常在 SQL 前后加一堆“好的根据您的要求……”之类的废话程序没法直接执行还得写额外的解析逻辑去抽取 SQL 片段。现在直接让模型只输出 SQL省事太多了。要求 4 “严禁编造字段” 是我吃过大亏之后总结出来的。有次模型生成了一条 SQL里面有customers.id但实际表里根本没有id这个字段是模型自己臆想出来的。后来我不仅加了这个要求还在程序层面做了字段名校验——从 schema 里提取出所有合法字段名然后检查模型生成的 SQL 里出现的字段名是否都合法。第三步调用大模型并执行 SQLimport sqlite3 import json import requests def text2sql(db_path: str, question: str, api_url: str, api_key: str, model: str): schema get_schema(db_path) prompt build_prompt(schema, question) payload { model: model, messages: [ {role: system, content: 你是一个严谨的数据库工程师。}, {role: user, content: prompt} ], temperature: 0 } headers {Authorization: fBearer {api_key}} response requests.post(api_url, jsonpayload, headersheaders) data response.json() sql data[choices][0][message][content].strip() # 去掉可能的 markdown 代码块围栏 if sql.startswith(): sql sql.split(\n, 1)[1].rsplit(, 1)[0].strip() conn sqlite3.connect(db_path) cursor conn.cursor() try: cursor.execute(sql) columns [desc[0] for desc in cursor.description] rows cursor.fetchmany(20) # 限制返回行数 return {sql: sql, columns: columns, rows: rows} except Exception as e: return {sql: sql, error: str(e)} finally: conn.close()注意我把temperature设成了 0。做过大模型开发的人都懂SQL 是需要精确性的东西不能让模型自由发挥。温度越高越容易出现“看起来合理但实际错误”的 SQL。必须让模型每次都选最确定的那个输出。fetchmany(20)也是个防御性设计。如果用户问的是“查一下所有数据”模型可能会生成一条不带LIMIT的查询如果表有几百万行程序会卡死。先限制 20 行既保证响应速度也避免内存被打爆。3.3 运行效果实测从“能用”到“好用”的差距在哪我先用一个真实的 SQLite 数据库来测试。假设有一个电商数据库里面有customers、orders、order_items、products四张表。我分别问了几个不同难度的问题记录一下效果。第一题“查一下北京市的客户数量。”模型生成的 SQLSELECT COUNT(*) AS customer_count FROM customers WHERE city 北京市;正确没有毛病。第二题“每个城市的客户数量按数量从高到低排。”模型生成的 SQLSELECT city, COUNT(*) AS customer_count FROM customers GROUP BY city ORDER BY customer_count DESC;也正确。这个题目虽然简单但能考察模型对GROUP BY和ORDER BY的理解。第三题“上个月销售额最高的前五个商品是什么”这个问题就开始有挑战性了。模型需要理解销售额 单价 × 数量涉及订单表和商品表还要做时间过滤和聚合排序。模型生成的 SQL 长这样SELECT p.name AS product_name, SUM(oi.quantity * oi.price) AS total_sales FROM order_items oi JOIN products p ON oi.product_id p.id JOIN orders o ON oi.order_id o.id WHERE o.created_at date(now, start of month, -1 month) AND o.created_at date(now, start of month) GROUP BY p.name ORDER BY total_sales DESC LIMIT 5;基本正确但有个问题对“上个月”的理解是标准的当月语义而很多业务场景里“上个月”指的是自然月。实现上date(now, start of month, -1 month)处理得也对逻辑没毛病。这里我反而觉得模型的理解比很多业务人员的中文理解要严谨。但也不是每次都这么顺利。有一次我问“查看一下两个表数据的差异。”这种问题就太模糊了模型懵了生成了一条连表都不对的 SQL。所以这里有一个很重要的结论用户问得越模糊模型生成正确的概率越低。你要在应用层面给出提示比如“您可以在问题中指明具体要对比的表、要对比的字段、以及对比的维度”。3.4 单文件数据库的妙用用:memory:做 SQL 预验证这是我个人最喜欢的一个技巧强烈推荐给所有做 Text2SQL 的人。刚才说了模型可能生成语法错误、或者字段名错误的 SQL。如果直接把这条 SQL 放到正式数据库上执行虽然 SQLite 的连接通常很快但你不想每次都拿错误 SQL 去碰真实数据尤其是在生产环境里。解决方案是利用 SQLite 的内存数据库做一次“预演”。做法是这样的用sqlite3.connect(:memory:)打开一个空的内存数据库但内存数据库默认是没有表结构的。所以你要把 schema 重新建一遍——不用真正插入数据只要表结构对就行。def validate_sql(schema_sql: str, sql: str) - bool: conn sqlite3.connect(:memory:) try: cursor conn.cursor() # 先执行所有建表语句重建表结构 cursor.executescript(schema_sql) # 再验证 SQL 的语法和字段 cursor.execute(fEXPLAIN QUERY PLAN {sql}) return True except Exception as e: return False finally: conn.close()为什么要用EXPLAIN QUERY PLAN因为这条语句不会真正执行查询只是让 SQLite 解析并生成执行计划。如果 SQL 语法错误、字段不存在、表不存在这一步就会直接报错。它能验证 SQL 的合法性而不会产生真实的数据读取。这个预验证的价值非常大。第一可以在喂给模型的结果上加一层保护防止错误 SQL 执行造成语义混乱虽然 SELECT 不会改数据但有些模型还会不小心生成 INSERT 或者 DELETE。第二可以做自动修复循环——如果验证失败把错误信息反馈给模型让它修正 SQL。比如你可以这样写MAX_ATTEMPTS 3 for attempt in range(MAX_ATTEMPTS): sql generate_sql(schema, question) if validate_sql(schema_sql, sql): execute_sql(sql) break else: question f你的 SQL 有错误{note}请修正。用户需求{original_question}这就是一个“自我纠错”机制。实测下来模型在第一次生成出错后看到错误信息再生成的正确率大幅提升。4. 进阶玩法让 Text2SQL 更聪明的几个技巧4.1 few-shot给模型一点“例题”前面说的是“零样本”生成模型靠自己的 SQL 能力和 schema 信息来推断。但有些时候你的数据库表结构不够直观字段名含义模糊光靠 schema 模型很难猜对。这时候最好的办法是给它几个“例题”——也就是 few-shot。比如你可以在系统提示词里加上类似这样的例子以下是一个示例 用户问题查一下 2024 年所有订单的总金额 正确的 SQLSELECT SUM(total_amount) AS total FROM orders WHERE strftime(%Y, order_date) 2024; 用户问题查一下每位客户最近一次下单时间 正确的 SQLSELECT customer_id, MAX(created_at) AS last_order_time FROM orders GROUP BY customer_id;不要小看这几个例子。大模型在参考了正确示例之后生成的 SQL 风格会向着示例靠拢包括 SQL 写法习惯、聚合方式的偏好、日期处理的方式等。这在团队开发里特别有用——你可以把团队既有的 SQL 规范沉淀成几个 few-shot 示例让 AI 生成的 SQL 风格跟团队保持一致。顺便提一句few-shot 示例的质量比数量重要。三五个好用且典型的例子远胜过十个乱写一气的例子。最好每个例子针对一种常见查询类型聚合、多表 JOIN、子查询、时间窗口等。4.2 让模型“先解释再写代码”CoT 的妙用Chain-of-Thought思维链在大模型领域是个成熟技巧同样适用于 Text2SQL。做法是不要直接让模型输出最终 SQL而是让它先一步一步描述自己的分析过程再给出 SQL。比如请一步步分析用户的查询需求 1. 首先确定需要用到哪些表。 2. 然后确定表之间的关联条件。 3. 接着确定需要的字段和聚合方式。 4. 最后根据分析写出 SQLite SQL 语句。实测发现加了这一步之后生成的 SQL 在复杂逻辑上的正确率明显提高尤其是多表 JOIN 和子查询场景。但代价是响应变慢token 消耗也增加。这里有个折中的做法可以在普通查询时不用思维链只有在 SQL 执行报错、或者模型自己“不确定”时才启用。怎么判断模型“不确定”可以在提示词里要求模型输出一个置信度评分比如“如果你对生成的 SQL 没有完全把握请在最后一行标注 LOW_CONFIDENCE”。程序检测到这个标记后自动走思维链重试逻辑。4.3 让模型“看到”数据不是只有 schema还有样例对于某些字段值比较特殊的列比如状态字段status的值可能是0、1、2模型光看 schema 很难知道1到底表示“已支付”还是“已退款”。解决方法是把字段的示例值也注入提示词。做法很简单查询每个字段的前几个不同的值拼进 schema 文本里def get_sample_values(conn, table, column, limit5): cursor conn.cursor() cursor.execute(fSELECT DISTINCT \{column}\ FROM \{table}\ LIMIT {limit}) return [str(row[0]) for row in cursor.fetchall()]然后把示例值加在字段说明旁边比如orders.status TEXT -- 示例值pending, paid, shipped, cancelled这样一来模型不仅知道字段的类型还知道字段的实际值范围。用户如果问“看看有多少订单发货了”模型看到示例值里有shipped就自然能联想到WHERE status shipped。这个技巧对数据质量参差不齐的数据库提升特别大。但注意一个坑不要在全部字段上都跑示例值否则 schema 会变得很大、token 消耗猛涨。只需要对文本类型、且名字含义不那么清晰的字段做这件事。字段名本身就说明问题的比如created_at、age、amount没必要加示例值。4.4 多轮对话记住上下文不用每次从头解释很多 Text2SQL 的上手实现是“一次性问答”——用户提问生成 SQL执行结束。但实际使用中用户经常会追问“那北方地区呢”、“去年的数据是多少”这种场景下如果每次都要把完整的问题重新描述体验会非常割裂。所以一个完整可用的 Text2SQL 工具多轮对话能力几乎是必须的。实现多轮对话核心做法是把对话历史也拼进提示词里。说白了就是——之前的每一轮问答都作为上下文喂回给模型。conversation_history [] while True: user_input input(请输入你的查询) conversation_history.append({role: user, content: user_input}) messages [ {role: system, content: SYSTEM_PROMPT}, *conversation_history ] # 调用模型生成 SQL 并执行 # ... result 查询结果... conversation_history.append({role: assistant, content: result})但这里有个隐患如果对话历史太长提示词会超出上下文窗口而且后面轮次的提问会被前面大量的历史信息稀释。我自己的做法是只保留最近三轮对话更早的历史直接丢弃。这个长度足够处理“继续追问”的场景又不会让模型犯迷糊。5. 常见问题与排查技巧实录这部分没有废话全是实操中踩过的坑。5.1 模型生成的 SQL 是 MySQL 语法不是 SQLite 语法大模型的训练数据里MySQL 和 PostgreSQL 的资料远比 SQLite 多所以模型有时候会不自觉地把习惯带偏。最常见的错误包括用了反引号包裹字段名MySQL 风格SQLite 虽然也接受但更标准的做法是双引号。用了LIMIT ? OFFSET ?但参数类型不对。用了IFNULL不生效SQLite 里应该用IFNULL也是可以的但有些场景用COALESCE更稳妥。字符串拼接用了CONCAT()但 SQLite 的拼接符号是||。解决办法提示词里明确声明“只允许使用 SQLite 标准语法”并且在预验证阶段通过EXPLAIN QUERY PLAN把语法错误直接拦截下来。另外可以在提示词里专门加一句话“SQLite 不支持CONCAT()函数字符串拼接请用||SQLite 不支持NOW()请使用datetime(now)或date(now)。”这句话非常管用。5.2 SQLite 数据库本身就是乱码这锅不能甩给 Text2SQL热搜词里有个“delphi sqlite 亂碼”说明很多人在 SQLite 里遇到过中文乱码问题。这一点在做 Text2SQL 的时候极容易踩坑——如果数据库里存的文本本来就是乱码AI 生成的 SQL 再正确也没用。SQLite 的文本存储默认是 UTF-8如果乱码说明数据在写入的时候就编码不对。常见的场景是从老旧的 CSV 文件导入CSV 文件本身是 GBK 编码用 Python 的sqlite3直接插入时没有指定编码转换中文就变成了一堆乱码。排查步骤很简单用 DB Browser for SQLite 打开数据库如果界面里直接显示乱码说明数据层就有问题跟 Text2SQL 无关。修复方案是在导入阶段用encodinggbk读取 CSV再以 UTF-8 写入 SQLite。5.3 错误 SQL 导致程序卡死前面提到过我自己在线上一开始就吃过大亏。用户问了个“查所有数据”模型生成了不带LIMIT的SELECT *一张几十万行的表数据全拉下来了程序直接卡死。从那以后我在执行 SQL 前强制加了一层拦截用正则检查生成的 SQL 里是否有LIMIT没有的话自动在最后面补一个LIMIT 100。虽然这个方案不能覆盖所有情况比如子查询内的数据量爆炸但至少挡住了最危险的那一类。另外第二步是给 SQL 执行包装一个超时机制。Python 的sqlite3默认没有超时控制你可以用signal模块给整个执行流程加一个时间限制超过 10 秒就强制中断返回错误信息。5.4 用户的问题太笼统导致 SQL 生成结果不稳定比如用户问“看下订单情况”这就太笼统了。模型不知道你想看订单总量、金额、还是某个时间段的数据生成的 SQL 可能每次都不一样结果自然不稳定。解决办法有两个方向一个是引导用户把问题说清楚在界面上做好提示另一个是给模型一个“默认行为”定义——比如“如果用户需求过于笼统默认查询最近 30 天的数据按日期汇总订单数量和总金额”。有了这个兜底逻辑模型的输出就稳定多了。5.5 常见问题速查表问题可能原因解决方向生成的 SQL 语法错误模型不熟悉 SQLite 方言提示词强化预验证拦截生成的 SQL 查不到数据字段名或表名错误schema 注入不完整用sqlite_master获取完整 schema字段含义理解错误字段名晦涩或类型模糊在 schema 中补充字段注释和示例值多表查询结果异常表关联逻辑理解错误使用思维链提示先分析再生成响应速度太慢模型上下文过长或使用了大模型裁剪 schema减少历史对话轮次程序内存溢出查询结果集过大强制加LIMIT限制返回行数6. 扩展思路这个项目还能往哪些方向走Text2SQL SQLite 这个组合往上走能接到复杂数据分析往下走能做成工具插件。我这里说几个我自己觉得特别有前途的扩展方向供你参考。6.1 做成一个本地桌面工具DB Browser for SQLite 的“智能替身”现在市面上有 DB Browser for SQLite、SQLiteStudio、DBeaver 这些工具它们都是强大的数据库浏览和编辑工具但都不具备“自然语言查询”能力。如果你做一个桌面 App左边是数据库浏览右边是聊天框用户直接打字问数据然后把生成的 SQL 和执行结果展示出来这个体验是质的提升。技术上也不复杂用 Electron 或者 Tauri 做壳底层调 Python 的 Text2SQL 服务或者直接用 Node.js 调用大模型 APINode 也能操作 SQLite比如better-sqlite3完全可以做成单文件安装包。6.2 自动报表生成器从自然语言查询到 Excel 报告仅输出查询结果还不够爽很多用户要的是“把这个结果整理成报表”。所以扩展方向是把 Text2SQL 的输出结果接上pandas或者openpyxl让用户用一句话生成 Excel 表、柱状图、趋势图。比如用户说“帮我生成上个月各品类的销售趋势图”Text2SQL 负责查出各品类各天销售额Python 负责画图最后输出一张图片或者一个.xlsx文件。这个方向非常适合企业内部的数据需求场景价值空间比单纯查询大得多。6.3 接入聊天机器人微信、钉钉、飞书里的数据助手这个方向很多公司在做但个人开发者也可以用 SQLite Text2SQL 轻松实现。把 FastAPI 服务包装一层接入企业微信机器人的回调接口用户在群里发一句“查一下这几天的退货订单”机器人自动回复查询结果。但这里有个安全性的问题必须提醒如果你把 Text2SQL 服务暴露到外部平台一定要做好权限控制和 SQL 类型限制。比如只允许SELECT禁止INSERT、UPDATE、DELETE、DROP等操作。在代码层可以直接用一个正则或者 AST 解析来判断 SQL 的第一个关键字。另外还要做数据库文件的备份机制防止有人构造恶意提示让模型生成危险 SQL。6.4 用本地模型跑 AI 私有的 Text2SQL很多项目的痛点是不能把数据库 schema 和问题内容发送到云端 API因为数据保密要求高。但是本地部署大模型这条路已经成熟了——Ollama 就能跑各种开源模型Qwen 系列的 SQL 能力在本地模型里属于第一梯队。实测下来一个 7B 参数的量化模型跑在 MacBook 上生成一条简单 SQL 大概需要五到十秒复杂 SQL 可能二十秒以上。对比云端 API 的“秒回”确实慢了不少但带来了数据隐私的绝对安全。如果你的数据不敏感、追求响应速度用云端 API 更省事如果数据是老板的心头肉乖乖上本地模型。7. 写在最后的一些经验和心得做这个项目几个月下来最大的感受是Text2SQL 的本质不是“把 SQL 这个技能废掉”而是“让数据的使用门槛降下来”。过去一个不懂 SQL 的同事想查数据要么求人要么等排期。现在他可以自己直接问。而一个懂 SQL 的开发者也不必每次都从零写查询而是把注意力放在复杂逻辑和异常排查上。效率的提升是实实在在的。另外我自己踩过一个巨大的坑值得最后再强调一次不要盲目相信模型生成的 SQL。它再聪明也可能在字段名、表关联、时间条件这些细节上犯低级错误。所以任何 Text2SQL 系统都必须有一个“人机协作”的兜底逻辑——要么是预验证拦截要么是结果审查要么是执行前人工确认。生产线上的退化思维永远是对的AI 负责效率人负责正确。最后再分享一个小技巧吧在你把所有 schema 都丢给模型之前先花半小时手工清理一下表结构信息。把没用的字段比如内部标记位、废弃的表、含义不明的命名全部标注清楚。这一步花的时间会在后续每一次查询里加倍赚回来。好的 schema 描述就是 Text2SQL 系统最好的训练数据。