Text-to-SQL Agent:除了生成 SQL,还得管这三件事
做 Text-to-SQL 做了两年多,从最早纯靠提示词调大模型,到后面自己搭完整的 Agent 链路,最大的感受是:SQL 生成只是入场券。今天大部分团队卡住的不是“模型能不能写出一条正确的 SELECT”,而是 SQL 生成之后那一堆破事——权限、数据血缘、执行安全、结果解释、失败恢复。标题里说的“三件事”,其实就是我在生产环境里被反复教育出来的三个核心环节:执行前拦截、执行中管控、执行后反馈。这篇就把完整的拆解思路和落地细节写出来,希望能让正在做 Agent 开发的同行少踩几个坑。
先说清楚本文适合谁:正在设计或维护 Text-to-SQL Agent 的工程师、负责数据平台接入的交付同学,以及想搞清楚“为什么别人的 Agent 演示那么顺、一上生产就拉胯”的产品负责人。我会按实际落地顺序来讲,不绕弯子。
1. 整体设计思路:把“生成 SQL”降级为中间步骤
很多团队拿到需求第一反应是“我要让大模型生成 SQL”,然后疯狂调 prompt、换模型、做 few-shot。我早期也是这样,直到某天线上出了一个低级事故:模型生成的 SQL 把一张全量事实表的过滤条件写漏了,直接导致下游报表跑了三小时重算。从那之后我彻底改了一个认知:Text-to-SQL Agent 的核心不是语言模型,而是围绕 SQL 的管控闭环。
1.1 核心需求解析
站在用户视角,自然语言问“上个月华东区各品类销售额”,需求链条其实有三层:
- 第一层:把自然语言翻译成可执行的 SQL(这是模型干的活);
- 第二层:确认这条 SQL 是否安全、是否可解释、是否真能跑通(这是系统该干的活);
- 第三层:把执行结果用用户能看懂的方式回传,并处理失败与异常(这是工程该干的活)。
大多数开源 demo 只做了第一层,所以我强调“生成只是入场券”。而本文说的“三件事”,本质是把第二、三层显式拆出来,作为 Agent 的三个核心模块:权限与语义校验(Pre-execution)、执行沙箱与熔断(Execution)、结果解释与反馈(Post-execution)。这三个模块加在一起,才是一个 Agent 开发闭环。
1.2 为什么必须管这三件事
直接原因就三条:
- 数据库是生产资产:SQL 一旦执行,可能产生全表扫描、锁竞争、甚至误删数据。大模型现在写复杂 SQL 的正确率远没到能裸奔的程度,必须有防护层。
- 用户要的不是 SQL 而是答案:如果返回的是“执行成功、影响 100 行”,用户依旧不知道这 100 行意味着什么;Agent 得负责把结果转成自然语言或图表结论。
- 故障是常态:表结构变更、字段改名、权限漂移、模型幻觉,任何一个环节出错都会让链路中断。没有反馈机制,Agent 就成了哑巴工具。
用生活类比来说:Text-to-SQL Agent 像一个新来的数据分析师。分析师的 SQL 写得再快,如果他不知道哪些表能碰、跑挂了不会恢复、说不出结果背后的含义,你也不敢把生产库交给他。所以“三件事”本质上是给这个虚拟分析师配了三个 supervisor:一个管事前审批,一个管事中监督,一个管事后复盘。
2. 执行前拦截:权限、表结构校验与语义安全
这一节是“三件事”里的第一件。我把它放在最前面,因为所有事故里,事前拦截能拦住的占六七成。模型生成 SQL 后,不能直接丢给数据库,要先过三道检查。
2.1 权限校验:最小权限原则落到 SQL 粒度
很多系统权限控制是“表级”的——你能读 sales 表,不能读 salary 表。但实际生产里粒度必须更细:同一个表里可能有敏感列(用户手机号、身份证)、可能有按分区限定的数据范围(只能看本部门)。SQL 粒度权限的意思就是:解析出 SQL 引用的库、表、列、分区,再和权限图谱比对。
具体做法不复杂:
- 用 SQL 解析器(Python 生态推荐 sqlglot,Java 生态可以选 JSqlParser)把生成 SQL 解析成 AST;
- 从 AST 提取
SELECT、FROM、JOIN、WHERE、GROUP BY涉及的库表列; - 将这些引用与“用户-角色-表-列-行级策略”的权限矩阵做交集判断;
- 任意一项越权,直接返回“SQL 生成失败,原因:无权访问 xxxx”,不给执行机会。
这里有三个细节值得注意:一是通配符*必须展开后再校验,否则SELECT *会绕过列级权限;二是子查询和 CTE 内部的引用也要递归解析,只查顶层 FROM 是漏的;三是函数调用,比如COUNT(phone),要能从函数参数里识别列引用。我见过好几起事故都是因为只看顶层导致子查询裸奔。
提示:权限校验组件最好独立部署成服务,通过 gRPC 或 HTTP 提供给 Agent 调用,不要和大模型推理进程混在一起。权限策略变更时才能独立热更新,也避免一方故障拖垮另一方。
2.2 表结构校验:让 SQL 落在真实 schema 上
模型幻觉不仅体现在 SQL 语法,更多体现在“表名编造”和“列名编造”。比如模型觉得用户问“销售额”就该有total_sales列,但真实表里叫sales_amount。处理方式就是拿 schema 信息强制约束生成阶段和校验阶段。
生成阶段的做法:把相关表的 schema 摘要(表名、列名、列类型、注释、主外键、分区键)作为上下文注入 prompt。但注意,schema 不能全量塞,一张宽表几百列,塞进去既浪费 token 又分散注意力。我常用的策略是:
- 先做一个轻量“列选择器”:让模型先产出候选表和候选列,再基于候选列表检 schema 元数据;
- 或者用 embedding 召回:把列名、注释做向量化,根据用户问题召回 topN 列。
校验阶段则简单粗暴:执行前把 SQL AST 里的每个表名列名,逐个到 schema 注册表里去查,查不到的直接打回。这一步我建议做成硬校验,不要给模型“编一个相似列”的机会。宁可告诉用户“查无此列”,也不要让 Agent 自由发挥,否则错误成本太高。
2.3 语义安全:识别危险模式与成本预估
权限和表结构校验通过,不代表 SQL 就安全。还得看语义层面:
- 是否可能全表扫描:
SELECT *加无过滤条件的大表查询,预估扫描行数是否超过阈值; - 是否会造成锁竞争:事务里对大表执行
UPDATE/DELETE,或未带WHERE的更新; - 是否属于危险操作:
DROP、TRUNCATE、ALTER,一律拦截(除非有专人审批流程); - 是否符合成本预算:通过表的统计信息(行数、平均行宽)估算扫描量,超出配额直接拒绝或询问用户。
这部分的实现很多团队会忽略,觉得“模型不会生成删库语句吧”。实测下来,在 agent 多步推理的场景里,模型为了完成“把这批数据清理掉”这样的用户指令,真的有可能生成 DROP 或不带 WHERE 的 DELETE。再加上 Text-to-SQL 链路里如果有“自动执行上一步 SQL”的设定,风险成倍放大。所以语义安全校验不能省。
3. 执行中管控:沙箱、超时、熔断与观察性
第二件事是执行过程管控。事前校验得再严,总有漏网之鱼;网络抖动、数据库负载飙升、慢 SQL 卡死连接,这些都要在执行层处理。我把这节拆成四个维度。
3.1 执行沙箱:能读不能写、能预览不落库
如果条件允许,Text-to-SQL Agent 最好接入独立的只读副本或分析型数仓,而不是直连业务主库。原因很现实:生成 SQL 的执行计划不可控,你无法保证每次都是完美索引命中的轻量查询。沙箱的核心是最小副作用:
- 连接只读账号,
SELECT以外的语句直接被数据库权限拒绝; - 如果必须读写,走事务并强制
ROLLBACK,或者把写操作重定向到临时表; - 设置
statement_timeout(PostgreSQL 可设置单条语句超时)、max_execution_time(MySQL 的 max_execution_time 只对 SELECT 生效)等数据库端限制; - 返回行数限制:在 SQL 外包一层
LIMIT,或使用游标分批取数,避免一次性拉回千万行。
注意:只读账号一定要验证过“真的只读”。有的 DBA 给账号授权时把
SELECT权限给全了,但忘了回收CREATE TEMPORARY TABLE或存储过程权限,这类漏洞在 agent 场景同样会成为攻击面。上线前建议专门做一轮权限核验脚本。
3.2 超时与熔断:别让一条 SQL 拖死整个会话
大模型生成慢 SQL 太常见了。模型不知道表的数据分布,不知道索引情况,生成的 SQL 可能因为 join 顺序不佳跑几分钟。执行层必须设置两级保护:
- 单条语句超时:数据库端设置
statement_timeout,应用端再设置一个更短的客户端超时(比如数据库端 60s,应用端 45s)。应用端超时先触发时,主动 cancel 数据库会话,避免连接池被慢查询占满。 - Agent 级熔断:统计单个会话内 SQL 执行失败的次数或累计耗时,超过阈值就终止整个 Agent 链路,要求用户重新描述问题。防止“模型反复生成同类错误 SQL”形成死循环。
熔断参数要根据业务调:BI 查询类系统可放宽到 120s,交互式问答建议 15~30s 内返回,否则用户体验崩盘。我在生产里见过一个调优技巧:先让模型生成时会习惯性带上预估扫描行数,如果模型判断扫描行数超过阈值,就改写为聚合查询或提示用户加过滤条件,从源头降低超时概率。
3.3 观察性:每一步都要留痕
Agent 链路里最痛苦的事是什么?是模型生成的 SQL 错了,但用户只看到“查询失败”,你也看不到是哪一步出了问题。所以在设计时就要内置全链路日志:
- 记录用户原始问题、模型生成的 SQL、校验结果、执行状态、耗时、返回行数;
- 把 SQL 和 schema 版本绑定记录,方便回溯“是不是上游表结构变更导致失败”;
- 对成功样本和失败样本做标注,沉淀为后续微调或 few-shot 的语料。
这一步看起来不“性感”,但长期价值极大。我见过很多团队绩效汇报时张口要“效果数据”,结果发现连日志都没接,只能靠肉眼翻。观察性不是一个功能,而是 Text-to-SQL Agent 后续迭代的地基。
4. 执行后反馈:结果解释、智能采样与错误恢复
第三件事是 SQL 执行完之后的动作。很多人觉得“执行成功就结束了”,但用户真正需要的是对结果的理解。这节我拆成三块:结果转自然语言、结果采样与可视化、错误恢复与多轮修正。
4.1 结果解释:从行数据到结论
SQL 返回的是一张二维表,用户是带着自然语言问题来的,所以 Agent 必须把表翻译回语言。基础做法是:把结果集摘要 + 原始问题一起交给大模型,让它生成一段结论性回答。但这里有个大坑:如果结果集很大,不能全量塞给大模型。我常用的策略是:
- 先对结果做聚合统计,比如总行数、前 N 行样例、数值列的 min/max/avg;
- 模型基于这些统计量生成回答,而不是逐行翻译;
- 如果用户需要明细,再提示“已返回前 100 行样例,可导出完整结果”。
这个方案的好处是 token 消耗可控、回答聚焦。比如用户问“哪个月销售额最高”,你返回给模型的不应该是几百行月度明细,而是“1月 1200万、2月 980万……”这些统计摘要,模型自然能给出简洁结论。
4.2 结果采样与可视化:图表语言比数字更直观
Text-to-SQL 的价值不仅是“查出数”,还要“看懂数”。执行后可以自动判断结果是否适合可视化:如果查询结果只有两三列且是“维度-指标”结构,直接建议画柱状图或折线图;如果是多维表,则建议用透视表。实现上可以把结果按图表 JSON schema 输出,让前端直接渲染。
我个人经验:别让模型自由选择图表类型,因为它经常会选个花哨但不适合的图。更好的做法是预设规则,比如:
- 一个维度 + 一个指标 → 柱状图或条形图;
- 时间维度 + 指标 → 折线图;
- 两个维度 + 一个指标 → 堆叠柱状图或热力图;
- 占比场景 → 饼图(但只有不超过 5 个分类时建议用)。
这些规则可以由数据平台团队预置,模型只负责判断图的标题和结论,不要让它决定图类型。
4.3 错误恢复与多轮修正:Agent 的“自我修复”能力
SQL 执行失败时,Agent 不能直接摆烂说“语句执行错误”。更合理的做法是:把异常信息作为反馈,让模型自动改写 SQL,重试一两次。比如:
- 报错是“列不存在”,把真实 schema 中的相似列名列表反馈给模型;
- 报错是“函数不存在”,把该方言支持的函数名列表反馈给模型;
- 报错是“超时”,提示模型增加过滤条件或改写为更轻量的聚合。
但重试必须有次数上限(我一般设 2 次),并且要记录每一次的失败原因。否则遇到模型死循环,API 费用和数据库压力都扛不住。还有一个我踩过的坑:不能把原始报错原文直接抛给模型,数据库的报错有些会泄露表结构细节,也会让模型学到不该学的“绕过技巧”,所以要做一次脱敏,只提取错误类型和可操作字段。
5. 完整落地方案:从提示词到模块编排
讲完三大模块,最后给出一套可参考的落地骨架。这里不贴完整代码,但会把模块边界、接口定义、和关键配置讲清楚,方便你直接抄作业。
5.1 模块编排:Agent 主控逻辑
我理解中的 Agent 主控逻辑大概是这样一个状态机(不用纠结术语,重点是流程):
- 接收用户问题,先做问题理解与改写(判断是否涉及时间、地域、指标口径);
- 调用 schema 检索模块,召回候选表与列,拼装上下文;
- 大模型生成候选 SQL(可生成多条,用自洽性选择得分高的);
- 进入执行前校验流水线:权限校验 → 表结构硬校验 → 语义安全与成本预估;
- 若校验失败,将失败原因反馈给模型,要求改写(最多 N 次);
- 校验通过后进入执行沙箱,设置超时与行数限制;
- 执行成功 → 结果解释与可视化建议 → 返回用户;
- 执行失败 → 提取脱敏错误 → 反馈模型改写 → 重试;
- 全部重试失败 → 返回“无法完成,这是失败原因”并给出人工反馈渠道。
这个状态机看起来简单,但落地时最难的是第 2 步的 schema 检索和第 4 步的校验流水线。这两个模块做扎实了,后面就顺了。
5.2 关键技术选型参考
给一个我实际用下来相对顺手的组合,不一定是最优,但能少走弯路:
| 模块 | 推荐方案 | 说明 |
|---|---|---|
| SQL 解析 | sqlglot | 跨方言、AST 解析稳、支持转译,Python 生态首选 |
| 方言支持 | sqlglot 转译 | 让模型统一生成一种方言,再转译到目标库,减少模型方言错误 |
| 权限策略存储 | 自研 + 中间表 | 用策略表存“用户-角色-资源”关系,配合数据权限服务 |
| 执行沙箱 | read-only 副本 / 数仓 | 优先独立账号;写操作走临时库 |
| 大模型 | GPT-4o / Claude / 开源 Qwen 系列 | 生成和改写用同一个模型即可,重试提示词里加错误上下文 |
| 可观测性 | 结构化日志 + 追踪 ID | 每条请求生成 trace_id,贯穿全部日志 |
注意 sqlglot 的坑:它对不同方言的 AST 解析支持程度不一样,生成 SQL 时尽量让模型输出标准 SQL 或你选定的一种方言,再由 sqlglot 转译。如果让模型直接输出“方言 A 原生语法”,sqlglot 解析时偶尔会翻车,尤其是 MySQL 的LIMIT写法、SQL Server 的TOP、分页 offset 等地方。
5.3 生产环境配置清单
最后附一份我在生产环境用的配置清单,你可以直接对照检查:
- 数据库账号:只读、无 DDL 权限、无临时表权限、单用户连接数限制;
- 语句超时:PostgreSQL
statement_timeout=30s,应用端客户端超时 25s; - 返回行数:默认
LIMIT 200,明细导出走异步任务; - 重试次数:执行错误 2 次、校验错误 2 次,总计不超过 4 次;
- 日志字段:trace_id、用户 ID、原始问题、生成 SQL、校验结果、执行耗时、错误信息、返回行数;
- 风险操作名单:DROP、TRUNCATE、ALTER、DELETE、UPDATE 全部写入黑名单,特殊情况走审批流;
- 模型参数:temperature 0~0.2,生成多条候选时 0.2;改写重试时 0,避免越改越飘。
这里有个值得强调的点:DevOps 常把“模型生成的 SQL”直接当作可信输入,这是错的。所有在大模型输出和真实执行之间的环节,都必须用确定性代码去兜底,而不是再用大模型去判断“这个 SQL 安不安全”。大模型可以做语义层面的分析和建议,但最终放行与否,必须由规则引擎做二元决策。
6. 常见问题与排查技巧实录
最后分享几个在实战里高频踩坑的问题,每条都附了排查思路,可以直接当成速查表用。
6.1 表名对了列名却全是幻觉,怎么办?
现象:模型生成的 SQL 表名完全正确,但 WHERE 和 SELECT 里的列名频繁出现不存在的列。排查顺序:
- 先看 schema 检索模块是不是只注入了表名、没注入列名;
- 再看注入的列名是否有注释,模型在缺少注释时更容易瞎编语义字段;
- 最后看是否做了硬校验,如果只是“建议”而非“强制”,模型很可能无视。
解决方向:列名与注释在 prompt 里加粗排列,TOP 列数 30 左右;保留硬校验逻辑,模型生成的列名即使错了也要能准确报错并反馈真实列名列表。
6.2 执行超时,但数据库端慢查询日志里没有对应记录
这个遇到过好多次。原因一般是应用端先超时取消,但数据库连接的 cancel 没生效(尤其 MySQL 的KILL QUERY和驱动版本有关),导致慢查询其实还在跑。排查时:
- 检查应用端超时后是否主动调用了 cancel 接口;
- 检查数据库连接池是否复用了一个已取消但实际还在跑的连接;
- 统一在应用层做“超时→kill→记录日志”的原子操作,不要只依赖数据库端超时。
6.3 用户说“给我看数据”,Agent 返回了“执行成功”却没下文
这是典型的缺“执行后反馈”模块。结果解释不是可选项,而是必选项。做法:执行成功后强制走“结果摘要 → 模型生成结论”流程,哪怕结论是“查询成功,无数据”,也要给用户一个明确反馈,而不是抛出空结果。
6.4 同一问题多轮对话下,模型容易把上下文搞混
比如用户先问“华东区销量”,再问“那华南呢”,Agent 如果每次都用独立 SQL 生成,不携带上文,很容易丢条件。最佳实践是维护一个“对话级查询上下文”:把上一轮的解析结果、过滤条件、指标口径摘要传给下一轮的 schema 检索和 prompt 构建。但要注意上下文的 token 预算,只传结构化摘要,不传历史 verbose 日志。
6.5 Agent 开发时,如何判断是模型问题还是链路问题?
我的经验法则是:把同一个问题用固定 prompt 直接问大模型,如果模型能输出正确 SQL,说明问题出在链路上下文构造或模块编排;如果模型本身输出错误,再判断是 prompt 引导不足还是模型能力不足。这样划分能省掉大量 debug 时间。链路问题优先查 schema 检索和上下文拼装,模型问题优先查示例要不要加到 few-shot 里。
结尾:一点个人体会
做了这么久 Text-to-SQL 和 Agent 开发,我最大的体会是:这个方向的难点从来不在“让大模型学会写 SQL”,而在于你愿不愿意把系统当作一个生产级工程来做。权限、熔断、观察性、错误恢复,都是听起来不酷但真正决定生死的东西。如果你正在做类似的 Agent 项目,我建议先不要急着上复杂方案,把本文说的“执行前拦截、执行中管控、执行后反馈”三个闭环先补上,哪怕每个模块用最朴素的方式实现,整体稳定性都会有质的提升。最后再分享一个小技巧:上线前准备一套“故意捣乱”的测试集,包括越权查询、危险 SQL、超时语句、列名幻觉、空结果,用这套用例专门打自己的系统,比任何演示数据都管用。