AI Agent查数据库:NL2SQL工程落地与安全护栏实践
把数据库查询交给 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: fBearer {LLM_KEY}}, jsonpayload, timeout30 ) 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你的只读数据库地址, port3306, user只读账号, password密码, database业务库, read_timeout10, write_timeout10, charsetutf8mb4 ) 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。误解枚举值业务里status1代表已支付模型以为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 常见问题排查链路真实排障顺序我建议按下面这条链路走先看用户问题本身是不是包含多个意图是不是口语化太严重。再看模型返回有没有解析出 SQLSQL 是否完整字段是否来自我们的表结构说明。接着看代码层校验是否被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 接手效果会好很多。我自己的测试顺序一直是先本地库再只读副本再慢慢开放给小组使用。每次有人问我要不要直接把全库表结构发给模型我的建议都是先别急按业务域拆分想清楚哪些数据可以暴露再谈自然语言查询。真正让人放心的不是提示词写得多完美而是从连接数据库那一刻开始所有环节都有边界。