LLM,听我说 - 焦虑型开发者变成安全型 SQL 验证篇
第一部 · 自由交给 LLM,准确交给系统
李
李承宰 · NHN AD
平台服务实验室 · 2026 年 10 月 · 约 6 分钟
写在前面
大家好,我是想要掌控 LLM 的焦虑型开发者李承宰。这篇文章记录了我如何以确定性的方式控制 LLM 编写的 SQL。我打算分两篇来写:作为第 1 篇的本文讲 LLM 写完 SQL 之后到执行为止的过程,第 2 篇讲如何让 LLM 把 SQL 写对。
我在开发一款供代理商营销人员使用的广告管理服务,其中我负责的是通过聊天查询投放效果的功能。营销人员过去要下载效果数据,再用 Excel 公式汇总,我希望这件事只需一个问题就能完成。
这怎么就不行呢?
一开始 SQL 是由代码生成的。也就是我在上一篇文章中介绍的 SQL 构建器:AI 从句子中提取维度、指标、期间和筛选条件,构建器再用这些值按子句拼装 SQL。同样的输入总会得到同样的 SQL,因此是确定性的,也很容易测试。
但它太费工夫了。每出现一个新需求,就要大改参数和拼装规则。更重要的是,用户的问题无穷无尽,而构建器只能回答事先写好的分支。一旦遇到 “在比上周转化减少的关键词中,把预算提高 15% 后,按广告费从高到低显示 10 个” 这样的问题,每次都得新写一个分支。
于是我改成了让 LLM 直接写 SQL。
从代码按固定分支拼装 SQL,变成 LLM 编写、代码检查
自由度要高,但要确定性、准确率 99%,还要顺便给我结果文件
然而需求没有变。这些是广告主看的数字,所以 SQL 必须是确定性的,准确率也必须在 99% 以上。可是 LLM 对同一个问题每次都会写出不同的 SQL。它并不是同样输入就给出同样输出的函数,这理所当然,而且也无法保证它总能写出正确的 SQL。
最难的是语法正确但 SQL 与我们的意图不同的时候,以及 SQL 正确但 LLM 的回答不准确的时候。比如不先分别汇总效果和转化,而是逐行直接连接,广告费行就会按转化类型的数量被复制,但查询照常运行,表格也填得像模像样。
语法正确、查询也能跑,但广告费按转化类型的数量膨胀
多个广告主共用一张表,只要账户条件错一处就会混入别人的数据,还出现过把 “最近 7 天” 算成 6 天的日期错误。
账户条件错一处、日期差一天,也不会报错
回答也出过错。即使 SQL 运行正确,模型在转述结果时也会出错:把只有 30 行的合计说成账户总计,或把 35、114、81 这样参差不齐的值笼统说成 “每月 100 次左右”。
让同一个模型再审查一遍,也只是重复同样的错误。于是我决定,解读问题和编写 SQL 尽量交给 LLM,而从这条 SQL 写好到执行为止,用代码严格管控。
解读和编写交给 LLM,挑选、填充、检查和计数交给代码
管控 SQL
本节只讲 LLM 写完 SQL 之后到执行为止的过程。写 SQL 之前的事情放到第 2 篇再讲。
LLM 不会把账户 ID 和期间直接写进 SQL,只用 :id、:start_date、:end_date 留出位置。这些标记不是 SQL 语法,而是我们系统内部的约定,表示“这里由服务器填入”。ID 用用户在界面上选择、由后端通过会话传来的账户填入;期间则由 LLM 传入 period_days=7 之类的参数,服务器计算日期后填入。
LLM 只标记位置,值由服务器填入
写好的 SQL 会用 sqlglot 解析,并在语法树上检查。写入查询、缺少分区条件的 SQL、读取与会话不同 ID 的 SQL 都会被拒绝。我最在意的是 JOIN。不先分别汇总效果和转化就逐行连接,会被视为 grain 不匹配的 JOIN 而拒绝。LLM 也经常使用 CTE,所以这项检查会对每个 CTE 单独进行。
在语法树上判定 CTE 和 JOIN;在同样的数据上运行时,错误的那条会让广告费膨胀
按类型查看广告系列 A 的转化时也一样。给每个类型行都附上广告费,同一笔广告费就会重复出现,一旦在结果文件或 Excel 里求和就会膨胀。所以检查器会拒绝这类 SQL,并返回“把类型展开成列”的理由。修正后的 SQL 让每个广告系列保持一行,把购买、购物车、注册分别放到各自的列里。
按类型查看时,把类型展开成列而不是行
通过规则检查的 SQL 会再用 Athena 的 EXPLAIN 确认一次。它不读取数据、只生成执行计划,因此没有费用或时间上的执行开销,还能提前发现不存在的列、类型不匹配的比较等引擎错误。
SQL 写好之后的检查。被拒绝时会带着理由回到 LLM
啊,想了想,回答里能不能再加点洞察?
还有人提出希望回答里带上一些简单的洞察。为了让 LLM 能给出丰富的回答,服务器会生成包住那条 SQL 的汇总查询,一并计算行数、合计、分布以及前后几行再交给它。LLM 以这些值为依据,指出哪里大、哪里空。
汇总查询按照用 sqlglot 读出的输出列类型,依规则生成。可加总指标给出合计和分布,比率给出分子之和 ÷ 分母之和,名称和 ID 给出去重计数。行与行之间的比较判断只使用同事开发的计算工具给出的值,回答中的数字是否存在于结果中,则用正则表达式比对并记录。
按规则判定列类型生成汇总查询,再把结果交给 LLM
只执行人工批准的 SQL,结果以 CSV 和回答的形式发出
写在最后
最近我去了优雅兄弟主办的技术大会 WOOWACON。会上有很多基于 LLM 的服务,其中我和一位讲翻译功能的讲者聊天时,听到了 “LLM 是翻译系统的翻译器” 这句话,深有同感。
我也是这么想的。LLM 把用户的请求翻译成我们系统的语言。只要告诉它系统的规划意图和目的、谁在用、能做什么,LLM 就会据此解读请求并作答。从某种意义上说,也可以把它称作编译服务的编译器。
第 2 篇会讲如何让这个翻译器把 SQL 写对。
感谢阅读。