把数据库查询交给 AI,听起来很省事,但真正动手做的人都知道,难点不在于让模型学会写 SQL,而在于你敢不敢让它连上生产库。这个方向通常叫 NL2SQL 或 Text-to-SQL,核心做法是让用户用自然语言提问,AI 负责生成查询语句,系统再去数据库执行并返回结果。适合做的场景很多,比如运营人员自助查数、管理后台的智能问答、报表工具的简单取数。但要真的做到让人放心,单靠提示词远远不够,还要在连接权限、SQL 校验、结果审计、失败重试这些工程层做一圈护栏。下面按实际落地顺序拆一遍,给打算让 AI Agent 接入数据库的工程师一份能照着做的参考。
1. 先想清楚:AI 查数据库到底承担哪一部分
很多人第一次看到“让 AI 查数据库”,第一反应是把它当成一个万能数据库客户端,直接用自然语言替换所有 SQL。这个预期很容易翻车。AI 在这里承担的只是“自然语言到 SQL 的转换”和“结果解读”这两层,真正执行查询、控制权限、管理连接,仍然需要一套工程代码来完成。
1.1 从自然语言到 SQL 的完整链路
一次正常的 AI 查询数据库,完整链路是这个样子的:
- 用户输入一句话,比如“上个季度华东区销量前十的商品有哪些”。
- 系统把这句话和数据库的结构信息一起发给大模型。
- 模型生成一条候选 SQL,同时给出对应的参数说明和需要执行的表名。
- 后端服务接收这条 SQL,先做合法性校验,再交给真正的数据库连接执行。
- 数据库返回结果集,后端把字段名、行数、耗时整理好。
- 系统把结果再送回模型,让模型用自然语言解释,或者直接以表格形式展示给用户。
这里最关键的一步是第 4 步。如果第 4 步没有控制好,前面的提示词写得再漂亮,后面都有可能把整个库暴露出去。所以你在设计的时候,不要把“AI”当成数据库的替代品,而要把它当成一个会生成 SQL 的工具人。真正决定查询能不能执行的,还是后端代码。
1.2 适合交给 AI 的查询类型
不是所有查询都适合交给 AI。先列一下适合的类型:
- 单表或少量表关联的查询。
- 按时间和维度过滤的聚合统计。
- 固定口径下的指标查询,比如销售额、订单数、用户数。
- 报表系统的“取数”动作,尤其适合让业务人员自助提问。
- 对结果要求不苛刻的探索性分析,用户愿意接受“可能不准确”的中间结果。
这些场景的共同特点是:查询逻辑相对简单,字段口径相对清楚,而且不会因为一条错误 SQL 造成数据丢失或修改。
1.3 哪些场景不要交给 AI
复杂场景必须谨慎:
- 多表复杂关联、多层嵌套子查询。
- 需要跨库事务的操作。
- 任何写操作,包括 INSERT、UPDATE、DELETE、DDL。
- 涉及敏感字段但不想让模型看见的场景。
- 查询结果直接影响交易、风控、生产执行的核心链路。
在这些场景里,AI 可以辅助生成 SQL 初稿,但最终执行必须由人到系统里确认。不要因为标题写着“放心交给 AI”,就真把生产库写权限开放出去。这个标题更准确的理解是:通过工程手段把风险控制住,你才敢把查询这件事委托给 AI。
2. 环境准备:模型、数据源和连接怎么搭
进入实操之前,先把环境准备好。很多问题不是模型能力不够,而是环境配置一开始就歪了。
2.1 模型选型:本地模型、API 和专用 NL2SQL 方案
先决定用哪一类模型。一般来说有三种选择:
| 方案 | 适合场景 | 主要成本 | 需要关注的指标 |
|---|---|---|---|
| 云端大模型 API | 快速验证、业务规模化 | API 调用费用、网络延迟 | 上下文长度、生成速度、稳定性 |
| 本地开源模型 | 数据不出内网、敏感业务 | GPU、内存、部署运维成本 | 模型体积、量化精度、推理速度 |
| 专用 NL2SQL 模型或工具 | 查询场景固定、追求稳定结果 | 工具维护成本 | 对数据库方言的支持、字段名称识别能力 |
如果是个人学习或团队原型验证,云端 API 最省事。只要你的数据允许走外网接口,并且公司安全规范允许,就可以先跑通。如果是企业内部生产环境,尤其是有客户隐私、财务数据、用户明细的场景,通常建议走本地模型或专用私有化方案。
不需要一上来就追求大参数模型。数据库查询这件事,很多情况下参数更小的模型也能完成,关键是你能不能把表结构、字段含义、查询限制传达清楚。还有一个常见的做法是,先不直接让模型生成 SQL,而是让模型先做“意图分类”,判断用户想查哪个模块,再切换到对应模块的专用查询模板。这样能明显降低模型理解负担。
2.2 数据库连接的权限设计与最小可用配置
这一步最重要,也是最容易偷懒的地方。
千万不要直接使用生产环境的最高权限账号。正确的做法是,单独创建一个专用账号,只授权给需要的表和视图,并且只开放 SELECT 权限。如果条件允许,再挂一个只读副本,让 AI 查询流量全部走副本,避免把生产库打挂。
我在第一次跑通方案时,通常会这样做:
- 复制一份业务数据到本地开发库,或者使用测试库。
- 创建一个只有 SELECT 权限的只读账号。
- 在应用层设置查询超时时间,比如 5 秒到 10 秒。
- 设置每次查询返回的行数上限,比如 100 行或 500 行。
- 记录每次查询的账号、IP、SQL 语句、耗时、返回行数。
这样做的原因是,AI 生成的 SQL 并不稳定。它可能在某个瞬间生成一个笛卡尔积,也可能因为用户问题含糊而查询全表。没有权限控制和超时保护,轻则拖慢数据库,重则泄露数据。
2.3 用最小样例验证链路
环境配好之后,不要直接拿业务需求测试。先准备一个最小样例,我一般用一张只有几十行的表,字段不超过十个,然后测试几个最基础的问题,比如:
- 总共有多少条记录?
- 某个字段的最大值和最小值是多少?
- 按某个分类分组后,每个组有多少条记录?
这些问题能覆盖 SELECT、聚合、分组、简单排序,基本可以验证“模型生成 SQL”和“后端执行 SQL”这条链路是否通。只要最小样例跑通,再逐步增加表数量和查询复杂度。
注意:第一次跑通时,不要一上来就开 Web 服务、加并发、接接口。先做成一个命令行脚本,能看到输入、输出、日志,这样排查起来最方便。
3. 把自然语言转成 SQL:可运行的最小流程
最小流程不需要很复杂的架构,一个 Python 脚本加一个模型服务就能完成。关键是把输入输出想清楚。
3.1 先给模型一份数据库字典
模型没见过你的数据库,它不知道“订单表”里哪个字段代表金额,也不知道“客户表”里“c_type”到底是什么意思。所以你要把数据库结构整理成模型能读懂的字典。
字典里至少包含:
- 表名
- 表的作用说明
- 每个字段的名称和类型
- 每个业务字段的中文含义
- 常见的枚举值含义
- 表与表之间的关系
比如这样:
表: orders 说明: 订单主表 字段: - id: 订单编号, bigint, 主键 - user_id: 用户编号, bigint, 关联 customers.id - product_id: 商品编号, bigint, 关联 products.id - amount: 订单金额, decimal(10,2), 单位元 - status: 订单状态, varchar, 枚举 pending/completed/cancelled - created_at: 创建时间, datetime如果表很多,可以按业务域拆成多份。不要一次性把全库几百张表都塞给模型,模型会眼花,生成 SQL 的准确率反而下降。我见过不少失败案例,都是因为元数据太庞大,模型在提示词里找不到重点。
3.2 用结构化输出约束模型
给模型发请求时,不要让它自由发挥,而是要求它输出固定格式的 JSON。下面是一个常见的调用示例:
import json import requests # 这里的接口地址和密钥,按你实际部署的模型服务填写 LLM_URL = "http://你的模型服务地址/v1/chat/completions" LLM_KEY = "你的密钥" system_prompt = """ 你是一个数据库查询助手。 你的任务是把用户的中文问题转换成 SQL 查询语句。 数据库类型: MySQL 约束: 1. 只输出 JSON,不要输出多余内容。 2. JSON 格式: {"sql": "生成的SQL", "tables": ["涉及的表名"]} 3. 如果问题不涉及数据查询,输出: {"sql": "", "tables": []} 4. 如果问题包含写操作意图,直接拒绝生成。 5. 不允许生成多语句 SQL。 表结构和字段含义: {表结构说明} """ user_question = "上个季度华东区销量前十的商品有哪些?" payload = { "model": "你的模型名称", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_question} ], "temperature": 0, "response_format": {"type": "json_object"} } resp = requests.post( LLM_URL, headers={"Authorization": f"Bearer {LLM_KEY}"}, json=payload, timeout=30 ) data = resp.json() content = data["choices"][0]["message"]["content"] print(content)为什么要设置temperature: 0?因为 SQL 生成是确定性任务,不需要模型发挥创造力。温度越低,输出越稳定。另外使用response_format要求 JSON 输出,能减少解析失败的概率。
提示词里,“只允许生成 SELECT 查询”这句话要反复强调。虽然模型不一定完全遵守,但它至少能拦住大部分错误。
3.3 SQL 解析和执行前校验
拿到模型返回的 JSON 后,解析出 SQL,不要直接扔给数据库执行。先做一次代码层校验。
常见的校验逻辑包括:
def is_safe_select(sql: str) -> bool: # 去掉首尾空白和最后的分号 sql = sql.strip().rstrip(";") # 必须首字母是 select if not sql.lower().startswith("select"): return False # 不允许包含;分隔的多语句 if ";" in sql: return False # 不允许出现明显危险关键字 dangerous = ["insert", "update", "delete", "drop", "alter", "create", "truncate"] first_word = sql.split()[0].lower() for word in dangerous: if word in sql.lower(): # 这里简单粗暴,实际要结合上下文判断 return False return True这个函数能挡住一部分问题,但不能当成绝对安全屏障。真正的底线仍然是数据库账号的只读权限和独立的限制账号。正则校验只是减少错误请求,防止因误操作带来的数据库压力。
如果 SQL 无法通过校验,就返回给用户一个友好提示,比如“这个问题暂时不能自动查询,请换个说法或联系数据分析师”。最怕的是模型生成 SQL 失败后,系统直接报一堆数据库异常堆栈给用户。
4. 从单条查询到稳定服务:工具调用和查询执行的控制
脚本跑通之后,下一步就是把它变成一个可以对外服务的接口,或者接进已有的 AI Agent。
4.1 用工具调用注册查询能力
现在的 AI Agent 普遍支持工具调用,也叫 Function Calling。你可以把“查询数据库”声明成一个工具,让模型在需要的时候主动调用。这个模式比“让模型直接生成 SQL 再执行”更安全一点,因为你可以把工具入参定义好,限制模型只能传参数,不能自由发挥。
举个例子,工具可以定义为:
- 工具名称:query_database
- 功能:执行只读 SQL 查询,返回结果集
- 入参:sql(只读查询语句)
- 返回值:字段列表、数据行、错误信息
模型决定调用时,后端会收到一个结构化的参数对象。你可以在这个环节加入校验、日志、限流、审计。很多团队愿意用 Agent 的方式来做,是因为它能更方便地串联多个数据源,也让模型在回答复杂问题时可以“先查 A 表,再查 B 表,最后汇总”。
4.2 执行层做查询包装
执行层不要直接拿原始 SQL 去连数据库。建议写一个查询包装器,统一处理超时、行数限制、错误转换。
一个简单的执行函数可能是这样的:
import pymysql from contextlib import closing def run_read_sql(sql: str, limit: int = 100): if not is_safe_select(sql): return {"ok": False, "error": "SQL 校验未通过"} # 如果 SQL 本身没有 limit,自动追加,防止全表兜底 if "limit" not in sql.lower(): sql = sql.rstrip(";") + f" LIMIT {limit}" conn = pymysql.connect( host="你的只读数据库地址", port=3306, user="只读账号", password="密码", database="业务库", read_timeout=10, write_timeout=10, charset="utf8mb4" ) try: with closing(conn) as conn: with conn.cursor() as cur: cur.execute(sql) rows = cur.fetchmany(limit) cols = [desc[0] for desc in cur.description] return {"ok": True, "cols": cols, "rows": rows} except Exception as e: return {"ok": False, "error": str(e)}这里的自动追加 LIMIT 是一个非常重要的兜底动作。原因是模型生成的 SQL 可能只写了SELECT * FROM orders,如果订单表有一千万行,直接执行会把数据库内存打爆。追加 LIMIT 至少能保证失控范围有限。
4.3 结果返回的字段映射和大小控制
数据库返回的结果通常是原始字段名,比如user_id、created_at。用户不一定理解。最好再加一步:把结果送回模型,让模型用更易读的方式解释,或者在前端把字段名映射成中文。
返回结果的大小要控制,不然模型处理长文本会变慢,接口响应也会超时。一般我对明细查询限制行数,对聚合查询限制列数,对长文本字段直接截断。
4.4 失败重试和日志记录
不要因为一次查询失败就让整个服务崩溃。常见的做法是:
- 第一次失败,如果是模型解析错误,可以让模型重新生成一次。
- 第二次失败,如果是数据库超时,就直接返回提示,不要无限重试。
- 每次查询都记录日志,包括用户问题、模型生成 SQL、校验结果、执行耗时、返回行数。
日志是排查问题的核心。没有日志,模型生成了一条错误 SQL,你只能看到接口报错,却不知道错在哪一步。有了日志,你可以很快判断是提示词问题、字段理解问题、SQL 语法问题还是数据库连接问题。
5. 防止 AI 幻觉和越界查询:安全与质量护栏
“AI 查数据库”最容易被诟病的一个点就是幻觉:模型一本正经地编造出看似合理的 SQL 或数据结果。这个问题不能完全消除,但可以用工程手段压到可接受范围。
5.1 数据库场景下 AI 幻觉的几种表现
在 NL2SQL 场景里,幻觉通常不是“编造几行数据”,而是下面这几种:
- 胡编字段名:表里根本没有
money,模型却写了money。 - 误解枚举值:业务里
status=1代表已支付,模型以为1代表退款。 - 自己补全条件:用户没提时间范围,模型默认加了一个“最近 30 天”。
- 直接编结果:用户要求统计某个指标的环比,模型实际上没有执行任何 SQL,而是直接根据训练知识编了一个“大约增长 23%”的答案。
这些情况一旦出现,就会让用户对系统失去信任。所以你要做的不是期待模型永远正确,而是在流程里加入“必须执行真实 SQL”的强约束,并且对结果做二次检查。
5.2 提示词层、执行层、结果层的三道护栏
我一般把护栏分成三层:
提示词层要做的事包括:明确表字段、明确业务口径、禁止猜测字段名、不确定时先描述表结构再询问用户。执行层要做的事包括:只允许 SELECT、只读账号、超时限制、行数限制、危险词拦截。结果层要做的事包括:返回结果必须来自数据库,不能让模型直接生成数据;如果查询结果为空,模型不能脑补解释。
加了护栏之后,幻觉仍然可能残留,但它至少不会出现在“伪造数据”这个最严重的层级上。模型最多是在 SQL 生成阶段犯错误,而被执行层拦截,或者返回空结果,用户能明显感觉到“这条问题没查出来”,而不是被骗。
5.3 敏感数据脱敏和审计
如果是企业内部系统,涉及用户手机号、邮箱、身份证、订单金额等敏感字段,一定要做脱敏和审计。
脱敏可以在两个位置做:一是在数据库层,给查询账号建视图,把敏感字段抹掉或打码;二是在接口层,对返回结果里的敏感字段做正则替换。推荐优先在数据库层处理,因为这样从源头就拦住了。即便模型生成了一个查询敏感字段的 SQL,数据库视图也会把数据挡住。
审计则要记录:谁在什么时间问了什么,模型生成过什么 SQL,是否执行成功,返回了多少条数据。这些日志在合规审查时很重要。
注意:不要以为加了提示词“不要查询用户手机号”就安全。模型不一定遵守。必须以数据库权限和视图为准。
6. 实测验证:如何判断方案可以放心用
很多人在本地测试时觉得效果不错,一放到真实场景就崩。原因大多是测试样例太少,或者没有定义“什么叫成功”。下面这套验证方法不需要复杂平台,一个脚本就能跑。
6.1 建立评测样例集
先准备 20 到 50 条查询问题,覆盖这些类型:
- 简单查询:单个条件过滤。
- 聚合统计:COUNT、SUM、AVG。
- 分组排序:GROUP BY + ORDER BY。
- 时间范围筛选。
- 多表关联。
- 模糊查询。
- 易错问题:字段名容易混淆、枚举值容易理解错误的查询。
- 拒答问题:带写操作、带删库、带敏感信息推测等。
每一条样例都要人工写好“标准答案”或“关键校验点”。比如某个查询的正确 SQL 应该包含哪几个表的哪些字段,期望返回的聚合值大概是什么范围。
6.2 用三个指标评估:准确率、成功率、稳定性
| 指标 | 定义 | 合格标准参考 |
|---|---|---|
| 准确率 | 生成 SQL 的业务结果和人工预期一致的比例 | 第一批至少 70% 以上 |
| 成功率 | 系统成功返回结果,没有报错的比例 | 90% 以上才算稳定 |
| 稳定性 | 同一条问题连续跑多次,结果结构保持一致 | 关键查询 100% 可重复 |
如果准确率太低,先不要加更多功能,优先完善表结构说明和业务口径。如果准确率还可以但成功率低,大概率是解析问题或 SQL 语法兼容问题。如果稳定性差,多半是模型温度过高,或者提示词里出现过长的上下文干扰。
6.3 常见问题排查链路
真实排障顺序,我建议按下面这条链路走:
- 先看用户问题本身:是不是包含多个意图,是不是口语化太严重。
- 再看模型返回:有没有解析出 SQL,SQL 是否完整,字段是否来自我们的表结构说明。
- 接着看代码层校验:是否被
is_safe_select拦住了。 - 然后看数据库执行:SQL 语法是否兼容当前数据库版本,超时没有,锁表没有。
- 最后看展示层:字段映射对不对,行数是不是被截断。
大多数问题出在第 2 步和第 4 步。第 2 步失败可以修提示词,第 4 步失败通常要改 SQL 方言适配或调整表结构说明。
常见报错和对应处理方式:
| 报错现象 | 常见原因 | 排查方向 |
|---|---|---|
| 模型返回空内容 | 上下文太长被截断,或模型服务超时 | 精简表结构说明,调整超时时间 |
| SQL 解析失败 | 模型没有遵守 JSON 格式 | 加 response_format 约束,降低 temperature |
| 字段不存在 | 表结构说明不准确或缺少该字段 | 核对数据库字典,补充字段定义 |
| 查询结果为空 | 用户问题缺乏时间或筛选条件 | 提示用户补充条件,或返回空结果说明 |
| 执行超时 | SQL 扫描数据量太大 | 加 LIMIT,设置语句级超时,走只读副本 |
7. 从 Demo 到生产:值得保留的边界和工程习惯
最后聊几个边界问题和长期使用建议。这些不是功能列表,而是踩过坑之后才明白的习惯。
7.1 低配置环境怎么跑
如果你的机器没有独立 GPU,或者只有 16G 内存,也能做一些尝试。方案是把模型服务换成 API 调用,本地只跑业务代码。如果必须本地跑模型,就选较小的量化模型,并把数据库只保留最重要的几张表,别做全库元数据注入。
低配置环境下的经验是:不要追求模型生成完美 SQL,宁可让它返回“无法理解”,也不要让它卡在推理里。可以把多个小模型并联,让一个模型做意图分类,另一个专做 SQL 生成,小模型在简单任务上有时比大模型更稳定。
7.2 生产化之前要做的事
如果要从学习 Demo 变成生产服务,下面这几件事必须提前做:
- 数据库账号改成独立的只读账号,权限最小化。
- SQL 执行全部走只读副本。
- 增加 API 限流,防止用户刷接口。
- 增加结果缓存,相同问题不重复查库。
- 完善日志和审计,可以追踪到人。
- 设定业务口径表,记录每个指标的标准定义。
- 对模型输出做 PII 检测,敏感数据直接拦截。
如果你是 Java 后端团队,可以直接用 Spring AI 这类框架去集成模型服务,它会帮你处理一部分工具调用和 Prompt 模板的问题。但无论用什么框架,上面的安全边界都得自己守住。
7.3 什么时候不要交给 AI
最后这句话很直接:当查询结果是用来做生产决策或直接面向用户展示重要数据时,至少初期应该让人工审核兜底。
AI 查询数据库,适合解决“取数效率”的问题,不适合解决“数据口径不清”的问题。如果业务口径本身就没有定义清楚,AI 只是把这种混乱包装得更流畅。先把指标字典和表结构说明做清楚,再让 AI 接手,效果会好很多。
我自己的测试顺序一直是:先本地库,再只读副本,再慢慢开放给小组使用。每次有人问我要不要直接把全库表结构发给模型,我的建议都是先别急,按业务域拆分,想清楚哪些数据可以暴露,再谈自然语言查询。真正让人放心的,不是提示词写得多完美,而是从连接数据库那一刻开始,所有环节都有边界。