
Text2SQL让 Agent 自己写 SQL 自己查你正在做了一个内部数据分析平台产品经理说我想直接问问题就能看到数据不想写 SQL了。你说好然后打开数据库看了看那二十多张表、几十个字段心想这事要是 AI 能做不就爽歪歪用户说上个月注册用户有多少Agent 理解语义自动生成一条SELECT COUNT(*) FROM users WHERE ...执行把结果返回给用户。全程不需要人自己写 SQL。这个流程有个专门的名字叫Text2SQL自然语言转 SQL。拆开来看核心就是翻译 执行两件事。本文将实现一个最小可用的 Text2SQL Agent。目录整体流程第一步告诉模型表结构第二步让模型生成 SQL第三步执行 SQL 并返回结果安全问题小结整体流程用户用自然语言说出问题Agent 翻译成 SQL语句然后执行再把结果用自然语言总结回来。四个环节中有两次 LLM 调用一次数据库查询。其实就是给模型加一个翻译工具和一个执行工具和我们之前讲的 Tool Use 机制完全一样见笔者主页。整个流程可以归纳为三步给模型看表结构让AI知道数据库长什么样子模型生成 SQL根据用户的自然语言问题生成对应的查询语句执行 SQL 并返回执行查询操作用自然语言回复用户第一步告诉模型表结构模型不知道你的数据库具体有哪些表、每个表有哪些字段。所以让他生成SQL之前得先把表结构告诉它。在 Spring AI 中我们可以用Tool注解把获取表结构注册成一个工具让模型在需要的时候主动调用。用DatabaseMetaData从数据库连接中动态读取表结构比手动维护一份文本靠谱得多Tool(description获取数据库表结构信息。在执行SQL查询前应先调用此方法了解表结构以便生成正确的SQL语句。返回所有表的名称、列名、数据类型和注释。可传入表名查看指定表的结构。)publicStringgetDatabaseSchema(ToolParam(description可选指定要查看的表名。不传则返回所有表的结构,requiredfalse)StringtableName){try(ConnectionconndataSource.getConnection()){DatabaseMetaDatametaDataconn.getMetaData();StringBuildersbnewStringBuilder( 数据库表结构 \n\n);try(ResultSettablesmetaData.getTables(conn.getCatalog(),conn.getSchema(),tableName!null?tableName:%,newString[]{TABLE})){booleanfoundfalse;while(tables.next()){foundtrue;StringtblNametables.getString(TABLE_NAME);Stringremarkstables.getString(REMARKS);sb.append(【表: ).append(tblName).append(】);if(remarks!null!remarks.isBlank()){sb.append(remarks);}sb.append(\n);try(ResultSetcolumnsmetaData.getColumns(conn.getCatalog(),conn.getSchema(),tblName,%)){while(columns.next()){StringcolNamecolumns.getString(COLUMN_NAME);StringtypeNamecolumns.getString(TYPE_NAME);intcolSizecolumns.getInt(COLUMN_SIZE);StringcolRemarkscolumns.getString(REMARKS);Stringnullablecolumns.getString(IS_NULLABLE);sb.append( - ).append(colName).append(: ).append(typeName).append(().append(colSize).append());if(NO.equals(nullable)){sb.append( [NOT NULL]);}if(colRemarks!null!colRemarks.isBlank()){sb.append( ().append(colRemarks).append());}sb.append(\n);}}sb.append(\n);}if(!found){return未找到(tableName!null?名为 tableName 的:)表;}}returnsb.toString();}catch(Exceptione){return获取表结构失败: e.getMessage();}}模型拿到这个工具后遇到用户提问会先调用getDatabaseSchema了解数据库长什么样然后再生成 SQL。这就是 Tool Use 的机制模型输出结构化的工具调用请求代码端负责执行。第二步让模型生成 SQL有了表结构信息模型就能根据用户的自然语言生成 SQL 了。这里的关键是不要让模型直接回复用户而是让它先输出 SQL交给工具执行。在 Spring AI 中我们用Tool注解注册一个executeQuery方法并在描述里写清楚这是执行 SQL 的工具。模型看到工具列表后遇到数据查询类的问题就会自动调用它Tool(description执行SQL查询语句。仅支持SELECT只读查询不支持INSERT/UPDATE/DELETE等写操作。执行前请先调用getDatabaseSchema了解表结构。)publicStringexecuteQuery(ToolParam(description要执行的SQL SELECT语句如 SELECT COUNT(*) FROM users)Stringsql,ToolParam(description最大返回行数默认100最大1000,requiredfalse)IntegermaxRows){// ... 实现见下文}注意描述里的那句仅支持 SELECT 只读查询。这是安全策略只让Agent执行查询等无风险操作。第三步执行 SQL 并返回结果模型生成了 SQL接下来就是真正连数据库执行查询。来看executeQuery的核心实现Tool(description执行SQL查询语句。仅支持SELECT只读查询不支持INSERT/UPDATE/DELETE等写操作。)publicStringexecuteQuery(ToolParam(description要执行的SQL SELECT语句)Stringsql,ToolParam(description最大返回行数默认100最大1000,requiredfalse)IntegermaxRows){// 第一步安全校验StringvalidationErrorvalidateSql(sql);if(validationError!null){return 查询失败 \nSQL: sql\n错误: validationError;}// 第二步确定返回行数上限intlimit(maxRows!nullmaxRows0)?Math.min(maxRows,MAX_ALLOWED_ROWS):defaultMaxRows;try(ConnectionconndataSource.getConnection()){conn.setReadOnly(true);// 设置连接为只读try(Statementstmtconn.createStatement()){stmt.setQueryTimeout(queryTimeoutSeconds);// 设置查询超时booleanhasResultSetstmt.execute(sql);if(!hasResultSet){return 查询完成 \nSQL: sql\n该语句没有返回结果集;}try(ResultSetrsstmt.getResultSet()){ResultSetMetaDatametars.getMetaData();intcolumnCountmeta.getColumnCount();// 读取数据不超过 limit 行ListString[]rowsnewArrayList();introwCount0;while(rs.next()rowCountlimit){String[]rownewString[columnCount];for(inti1;icolumnCount;i){Objectvalrs.getObject(i);row[i-1]val!null?val.toString():NULL;}rows.add(row);rowCount;}booleantruncatedrs.next();// 判断是否还有更多数据returnformatAsTable(sql,meta,columnCount,rows,truncated);}}}catch(SQLExceptione){return 查询失败 \nSQL: sql\n错误: e.getMessage();}}查询结果返回给模型后模型会用自然语言总结结果回复用户。比如用户问上个月注册了多少新用户模型生成 SQL 查到数字是 1523然后回复“上个月共有 1523 名新用户注册。”在 Spring Boot 中注册这个工具只需要把它交给 Spring AI 的ToolCallbackProviderBeanpublicSqlToolsqlTool(DataSourcedataSource){returnnewSqlTool(dataSource,100,30);}Spring AI 会自动扫描Tool注解把getDatabaseSchema和executeQuery注册为模型可用的工具。模型看到工具列表后遇到数据查询类问题就会按顺序调用先调getDatabaseSchema了解表结构再调executeQuery执行 SQL。跑一遍看看效果。假设 users 表有实际数据流程大概是这样的两轮 LLM 调用一次数据库查询搞定。安全问题讲到Text2SQL有一个绕不开的话题安全。让模型生成 SQL 然后直接执行万一模型生成了一条DROP TABLE users怎么办或者生成了DELETE FROM orders呢模型虽然被 prompt 约束了只生成 SELECT但 prompt 不是铁板一块。精心构造的 prompt injection 可能绕过这个限制。所以必须在代码层面做防护不能只靠 prompt。来看validateSql方法的四道防线privateStringvalidateSql(Stringsql){if(sqlnull||sql.isBlank()){returnSQL 语句不能为空;}Stringnormalizedsql.strip().replaceAll(;\\s*$,).toUpperCase();// 第一道防线只允许 SELECT 和 WITHCTE 查询if(!normalized.startsWith(SELECT)!normalized.startsWith(WITH)){StringfirstTokennormalized.contains( )?normalized.substring(0,normalized.indexOf( )):normalized;return安全拦截 - 仅允许 SELECT 查询检测到禁止的操作: firstToken;}// 第二道防线禁止危险关键词用正则精确匹配单词边界String[]forbiddenKeywords{INSERT,UPDATE,DELETE,DROP,ALTER,CREATE,TRUNCATE,REPLACE,MERGE,GRANT,REVOKE,EXEC,EXECUTE,CALL,INTO OUTFILE,INTO DUMPFILE,LOAD_FILE,COPY,PG_READ_FILE,PG_WRITE_FILE};for(Stringkeyword:forbiddenKeywords){PatternpatternPattern.compile(\\bkeyword\\b);if(pattern.matcher(normalized).find()){return安全拦截 - 检测到禁止的关键字: keyword;}}// 第三道防线禁止注释防止绕过检查if(normalized.contains(--)||normalized.contains(/*)){return安全拦截 - SQL 中不允许包含注释;}returnnull;// 校验通过}再加上executeQuery里的两道执行层防线防线在哪防什么只允许 SELECT / WITHvalidateSql阻止写操作正则匹配危险关键词validateSql防止嵌套攻击如SELECT * FROM users; DROP TABLE users禁止注释符号validateSql防止绕过检查如SELECT * -- ; DROP TABLE usersconn.setReadOnly(true)executeQuery数据库层面拒绝写操作stmt.setQueryTimeout()executeQuery防止慢查询拖垮数据库行数上限MAX_ALLOWED_ROWSexecuteQuery防止全表扫描返回百万行数据模型的输出永远不能被完全信任所以必须在代码层面兜底。prompt 约束是第一层validateSql是第二层数据库连接的readOnly是第三层。三层配合才靠谱。小结Text2SQL 的核心思想把理解问题交给模型把执行查询交给代码两者通过 SQL 这个桥梁连接。实现一个 Text2SQL Agent本质上就是在 AgentLoop 的基础上注册两个工具getDatabaseSchema和executeQuery。模型负责理解用户意图、生成 SQL代码负责连接数据库、执行查询、安全检查。整个过程和 Agent 调用 bash 工具没有区别模型输出工具调用请求代码端查注册表、执行、返回结果。唯一需要额外注意的是安全。模型的输出不能被完全信任必须在代码层面做防护validateSql做关键词过滤conn.setReadOnly(true)在数据库层面拒绝写操作setQueryTimeout防止慢查询。prompt 约束是第一层代码检查是第二层数据库连接限制是第三层。三层都到位了Text2SQL Agent 才能在生产环境安心使用。