当模型输出动态 SQL 时,如何采用参数化查询重写?
解读
在大模型落地业务系统时,模型可能直接生成“可执行 SQL 字符串”,例如“SELECT * FROM user WHERE id = ” + userInput。
国内监管对数据安全、等保 2.0、个人信息保护要求极严,任何字符串拼接 SQL 都会被安全团队一票否决。
因此,面试官真正想考察的是:
- 你能否在 LLM 输出侧拦截危险 SQL;
- 你能否把不可信部分转化为参数占位符;
- 你能否在 LLMOps 流程里把“参数化重写”做成自动化卡点,而不是靠人眼 review。
知识点
- 参数化查询(PreparedStatement / ORM 参数绑定):SQL 骨架与数据分离,数据库驱动自动转义。
- AST 解析与重写:用 JSqlParser、Druid SQL Parser 把 LLM 输出的字符串解析成语法树,将字面量节点替换成 ? 或 :name 占位符,同时收集参数列表。
- 白名单校验:只允许 SELECT,禁止 DROP/ALTER;表名、列名必须在元数据白名单内。
- LLM 后处理链路:在模型输出→服务化封装之间插入“SQL 重写微服务”,返回
{sql: "SELECT … WHERE id = ?", params: [123]}。 - 国产数据库兼容:OceanBase、PolarDB、TiDB 均支持 标准 JDBC 参数化协议,占位符统一用 ? 可避免方言差异。
- 性能与并发:重写逻辑必须 <10 ms,用单例 Parser + 对象池;高并发场景可下沉到 Go 或 Rust sidecar。
- 审计与监控:把重写前后 SQL 都打印到统一日志格式(traceId+userId+sqlHash),方便等保审计和事后溯源。
答案
以国内最常见的 Spring Boot + MyBatis Plus + Druid 为例,给出可直接落地的三步法:
- 拦截:在 Controller AOP 里识别 LLM 返回字段
generatedSql。 - 解析与重写:
String sql = llmOutput.getGeneratedSql(); Statement stmt = CCJSqlParserUtil.parse(sql); if (!new SqlWhitelistVisitor().isSafe(stmt)) { throw new SecurityException("SQL 含高危操作"); } ParameterizedRewriter rewriter = new ParameterizedRewriter(); String safeSql = rewriter.rewrite(stmt); // 返回 SELECT … WHERE id = ? List<Object> params = rewriter.getParams(); // 返回 [123] - 执行:
其中 Mapper XML 使用Map<String,Object> map = new HashMap<>(); map.put("safeSql", safeSql); map.put("params", params); return sqlSession.selectList("com.xxx.mapper.DynamicSqlMapper.execSafe", map);${safeSql}作为静态骨架,#{params}作为参数绑定,MyBatis 会自动完成 JDBC parameterMapping。
核心要点:
- 绝不把 LLM 原文直接送进 JDBC;
- 所有字面量必须替换成占位符;
- 重写服务要灰度发布+实时熔断,一旦解析失败立即降级到“人工审核”流程,保证线上 0 阻断。
拓展思考
- 多轮对话场景:用户可能分三次补充“时间范围、状态、分页”,模型最终才拼出完整 SQL。
解决思路:在对话状态机里维护一个“参数累积器”,每轮只让模型输出“增量条件”,后端用 AST merge 而不是字符串拼接,避免最后一轮爆炸性注入。 - 国产信创环境:如果数据库换成达梦、人大金仓,驱动仍支持 JDBC 4.2 参数化,但占位符名大小写敏感,需在重写阶段统一转大写并做双引号转义。
- LLMOps 持续监控:把“SQL 重写耗时、解析失败率、参数化成功率”埋点到 Prometheus,配置告警规则 >1% 失败就@值班,实现安全左移。
- 未来趋势:用模型即服务(MaaS)反向训练一个小型 SQL 参数化模型(百兆级),输入用户问题,直接输出参数化骨架+参数列表,跳过传统解析器,延迟可再降 50%,但需投入人工标注 5 万条安全 SQL 对,这是团队下一步 OKR。