1. 为什么“让大模型直接写 SQL”这条路走不通
做过数据平台或者 BI 工具的人,大概率都动过这个念头:既然大模型这么能写代码,那让它直接根据用户的一句话生成 SQL 不就完了?我最早也是这么想的,2023 年那会儿拿几个主流模型试了一圈,demo 阶段确实惊艳,用户说“查一下上个月华东区销售额前十的门店”,模型唰唰给你吐出一段看着挺像样的 SQL。但只要往生产环境一放,问题就全冒出来了。
最要命的是不确定性。同一个问题问两遍,模型可能给你两种写法:一次用JOIN,一次用子查询;一次把过滤条件写在WHERE里,一次塞进HAVING。结果对不对先不说,光是这种漂移就让下游的缓存、审计、权限校验全部失效。更别提它偶尔会“幻觉”出一个根本不存在的字段名,或者把LEFT JOIN写成INNER JOIN,数据静默地少了一半,你还得等业务方来投诉才发现。
所以后来我转变了思路:大模型不该直接产出 SQL,它应该产出“意图”,而 SQL 由一套确定性的编译器来生成。这就是标题里说的“LLM 规划,编译器生成”。LLM 负责理解人话、拆解任务、规划步骤,把模糊的自然语言翻译成结构化的中间表示;真正落到 SQL 的那一步,交给一个规则明确、可测试、可复现的编译器。中间那层结构化表示,我把它叫做工作流契约——它既是 LLM 的输出规范,也是编译器的输入规范,两边都认这个契约,整个链路才稳。
这套东西解决的核心问题是:在保留自然语言交互体验的同时,把 SQL 生成的确定性拉回到工程可控的范围内。它适合谁?适合正在做 Text-to-SQL、数据问答、BI 自助分析、以及任何需要把 LLM 接入生产数据链路的团队。如果你只是写个玩具 demo,那直接调 API 就够了;但只要你要对结果的正确性、可审计性、可复现性负责,这套“规划 + 编译”的分层架构就值得认真考虑。
下面我按自己实际落地的经验,把这套东西从设计思路到实操细节完整拆一遍。涉及具体参数和步骤的地方,我会说明这是基于常见工程实践的合理方案,你可以根据自己的技术栈调整。
2. 整体架构设计:把“理解”和“生成”彻底分开
2.1 核心思路:LLM 只做它擅长的事
这套架构最根本的一条原则,就是职责分离。LLM 擅长的是语义理解、意图识别、上下文消歧,它不擅长的是精确的语法构造和严格的逻辑一致性。SQL 恰恰是后者——它有明确的语法、明确的语义、明确的执行计划。让 LLM 去干它不擅长的活,就是在给自己埋雷。
所以我把整条链路切成三段:
- 第一段:LLM 规划层。输入是用户的自然语言问题加上数据库的 schema 描述(表名、字段名、字段注释、枚举值等),输出是一份结构化的工作流 DSL。这份 DSL 描述的是“要做什么”,而不是“怎么写 SQL”。比如用户问“上个月华东区销售额前十的门店”,LLM 输出的不是 SQL,而是类似这样的东西:先按时间过滤(上个月),再按地区过滤(华东),然后按门店分组求和销售额,最后降序取前十。
- 第二段:契约校验层。LLM 输出的 DSL 不能直接信,得先过一遍校验:字段名是否真实存在?操作符是否合法?聚合函数的参数类型对不对?这一步是把 LLM 的“幻觉”挡在编译之前。
- 第三段:编译器层。把校验通过的 DSL 编译成目标方言的 SQL。这一步是纯确定性的,同样的 DSL 输入,永远产出同样的 SQL 输出。编译器内部维护着方言差异(MySQL、PostgreSQL、SQL Server 的语法细节)、字段映射、权限注入等逻辑。
这样切分之后,每一层都可以独立测试。LLM 层用准确率评估,编译器层用单元测试覆盖,契约层用 schema 校验。出了问题能快速定位是哪一层的锅,而不是面对一个黑盒抓瞎。
2.2 为什么用“工作流”而不是“AST”
你可能会问,为什么不直接让 LLM 输出抽象语法树(AST),非要搞个“工作流”的概念?我试过 AST 方案,问题是它对 LLM 太不友好了。AST 的节点类型繁多、嵌套深、对字段顺序敏感,LLM 生成的时候很容易在括号层级上出错,而且一旦出错整个树就废了,没法局部修复。
工作流 DSL 不一样,它更接近人类描述任务的思维方式:一串有序的步骤,每步做一个明确的动作。比如“过滤 → 过滤 → 分组聚合 → 排序 → 截断”。这种线性结构 LLM 生成起来稳得多,而且天然支持局部修改——如果只是排序方向错了,改一个字段就行,不用重构整棵树。
更重要的是,工作流天然对应执行计划。数据库执行 SQL 的时候本来就会生成执行计划,我们的 DSL 在概念上跟执行计划是同构的,只是层次更高。这让编译器的实现变得直观:每个工作流步骤映射到 SQL 的一个子句,步骤之间的依赖关系决定子句的嵌套方式。
2.3 契约的具体形态:一份 DSL 长什么样
我用的 DSL 是基于 JSON 的,因为 JSON 对 LLM 友好、易校验、易序列化。一个典型的工作流契约大概是这样:
{ "version": "1.0", "source": "orders", "steps": [ { "op": "filter", "field": "order_date", "operator": "between", "value": ["2024-04-01", "2024-04-30"] }, { "op": "filter", "field": "region", "operator": "eq", "value": "华东" }, { "op": "group", "keys": ["store_id", "store_name"], "aggregations": [ {"func": "sum", "field": "amount", "alias": "total_sales"} ] }, { "op": "sort", "field": "total_sales", "direction": "desc" }, { "op": "limit", "count": 10 } ] }这份契约里没有任何 SQL 语法,全是业务语义。source指定主表,steps是有序的操作列表。每个操作有明确的op类型,参数都是结构化的。编译器拿到这份契约,就能确定性地生成 SQL。
提示:DSL 的
version字段别省。我踩过坑,后来加了新操作类型,老版本的契约解析不了,没有版本号就没法做兼容。加个版本号,解析器按版本走不同分支,平滑升级。
2.4 方案选型的几个关键取舍
在落地过程中,有几个设计决策我反复权衡过,这里说一下我的选择和理由。
第一个取舍:DSL 的粒度。太粗,比如只给一个“查询”操作,那 LLM 等于没帮上忙,编译器还得自己猜;太细,比如把每个WHERE条件都拆成独立操作,LLM 生成负担重,容易出错。我最后选的是中等粒度:过滤、分组、排序、连接、限制、窗口这几类,每类一个操作。这个粒度下,LLM 生成的步骤数通常在 3 到 8 步之间,既不会太简单也不会太复杂。
第二个取舍:是否允许 LLM 直接写表达式。比如amount * 0.9这种计算。我一开始是禁止的,所有计算都得用预定义的聚合函数。后来发现业务需求太灵活,就开放了受限的表达式支持,但表达式必须用 DSL 的表达式语法,不能是裸 SQL 片段。这样编译器还能解析和校验,不至于失控。
第三个取舍:多表连接的表达。这是最麻烦的部分。我最终用的是显式的join操作,要求 LLM 明确指定连接类型、连接键、以及连接后的字段来源。虽然 LLM 生成起来比单表复杂,但胜在可控。隐式的连接推断我试过,准确率掉得厉害,放弃了。
3. 核心细节解析:LLM 规划层怎么调教
3.1 Schema 注入:给模型一份“地图”
LLM 要生成正确的工作流,前提是它得知道数据库里有什么。Schema 注入这一步看着简单,其实门道不少。我见过很多团队直接把SHOW TABLES的结果丢给模型,效果很差,因为模型不知道每个字段是干嘛的。
我的做法是构造一份增强 schema 描述,包含四类信息:
- 表名和字段名:这是基础,但要注意命名规范。如果字段名是
amt、qty这种缩写,一定要补上中文或英文注释,否则模型猜不准。 - 字段类型和语义:
amount是DECIMAL(18,2),表示金额;status是TINYINT,取值 1/2/3 分别代表什么。这些枚举映射必须写清楚,不然模型会把status = '已完成'直接写进去,而数据库里存的是数字。 - 表之间的关系:
orders.store_id关联stores.id,这种外键关系要显式告诉模型,否则它做多表连接时会瞎猜。 - 业务口径:比如“销售额”到底指
amount还是amount - discount,这种业务定义必须提前对齐,写进 schema 描述里。
这份描述我通常控制在 2000 token 以内,太长了模型注意力会分散。如果表特别多,就做按需检索:先用一个轻量模型或关键词匹配,从全量 schema 里筛出跟问题相关的几张表,再注入给规划模型。
3.2 Prompt 设计:把契约规范讲透
规划层的 prompt 是整个系统里最需要打磨的部分。我的 prompt 结构分四块:
第一块是角色和任务定义。明确告诉模型:你是一个查询规划器,你的输出必须是符合给定 schema 的 JSON,不要输出任何解释性文字。这一句能挡掉大量“好的,我来帮你分析一下……”这种废话。
第二块是 DSL 的完整规范。包括所有支持的op类型、每个操作的必填字段、字段的取值范围。这部分我建议用少样本示例(few-shot)来教,比纯文字描述有效得多。我会给 3 到 5 个覆盖不同场景的示例,从简单单表过滤到复杂多表聚合都有。
第三块是当前数据库的 schema 描述。就是上一节说的增强 schema。
第四块是输出格式约束。明确要求输出纯 JSON,并且给出 JSON Schema 的定义,让模型知道每个字段的类型要求。
这里有个实操心得:把 DSL 规范做成 JSON Schema 塞进 prompt,比用自然语言描述准确率高一大截。因为 JSON Schema 本身就是结构化的,模型对它的理解更精确。我实测下来,同样的任务,用 JSON Schema 描述比用文字描述,格式错误率能降一半以上。
3.3 输出解析与容错:别指望模型一次就对
即使 prompt 调得再好,模型输出也不可能 100% 合规。所以解析层必须做多层容错。
第一层是格式清洗。模型有时候会在 JSON 外面包一层 ```json 代码块,或者前面加一句“这是结果:”。解析前先用正则把非 JSON 内容剥掉。
第二层是结构校验。用 JSON Schema 校验器(比如 Python 的jsonschema库)检查输出是否符合契约规范。不符合的,把校验错误信息回传给模型,让它重试。我一般给两次重试机会,超过就降级到兜底策略。
第三层是语义校验。结构对了不代表语义对。比如模型可能引用了一个 schema 里不存在的字段,或者对字符串字段用了sum聚合。这些要在编译前查出来。我的做法是维护一份字段元数据表,记录每个字段的类型、是否可聚合、是否可过滤,校验时逐条比对。
第四层是兜底策略。如果重试后还是不行,就返回一个友好的错误提示,引导用户换个说法,或者给出几个候选的澄清问题。千万别硬编一个 SQL 出去,那比报错还糟糕。
注意:重试的时候,把上一次的错误信息一起塞回 prompt,模型修正的成功率会高很多。我试过只让它重试不给错误信息,基本等于重新抽卡,浪费 token 还没效果。
3.4 字段映射与枚举对齐:最容易翻车的地方
这一块我要单独拎出来讲,因为它是实际落地中翻车最多的地方。
用户说“华东区”,数据库里region字段存的是east_china还是华东还是HD?用户说“已完成”,status字段存的是3还是completed还是已完成?这些映射关系如果不在规划层处理掉,编译器生成的 SQL 查出来就是空结果,而且不报错,静默失败。
我的解决方案是维护一份值域映射表,作为 schema 描述的一部分注入给模型。映射表长这样:
| 业务术语 | 字段 | 数据库值 |
|---|---|---|
| 华东 | region | east_china |
| 已完成 | status | 3 |
| 上个月 | - | 动态计算 |
对于“上个月”这种相对时间,不能硬编码,得让模型输出一个时间表达式,由编译器在运行时计算。我的 DSL 里支持{"op": "filter", "field": "order_date", "operator": "between", "value": {"relative": "last_month"}}这种写法,编译器负责把last_month翻译成具体的日期区间。
这份映射表怎么维护?我的经验是从历史查询日志里挖。把用户常问的术语和实际执行的 SQL 对照起来,人工确认后沉淀进映射表。这是个持续积累的过程,一开始可能只有几十条,跑几个月就能覆盖大部分高频场景。
4. 编译器实现:把契约变成确定性 SQL
4.1 编译器的整体结构
编译器这块是整个系统里最“传统”的部分,也是最能体现工程功底的地方。我把它设计成三段式:解析、优化、代码生成。
解析阶段把 JSON 契约转成内部的中间表示(IR)。IR 是一组对象,每个对象对应一个操作节点,节点之间有明确的依赖关系。这一步会做完整的类型检查和语义校验,任何不合法的地方都在这里报错。
优化阶段对 IR 做等价变换,目的是生成更高效的 SQL。比如把多个连续的filter合并成一个WHERE子句,把limit下推到子查询里减少数据量,识别可以合并的聚合操作等。这一步是可选的,但做了之后生成的 SQL 质量明显更好。
代码生成阶段遍历 IR,按目标方言生成 SQL 字符串。这一步是纯字符串拼接,但因为前面已经校验过了,所以不会出错。方言差异通过模板 + 适配器的方式处理,每种数据库一套模板,公共逻辑抽到基类里。
4.2 从工作流到 SQL 的映射规则
每个工作流操作对应 SQL 的哪个部分,这个映射关系是编译器的核心。我整理了一张对照表:
| 工作流操作 | SQL 对应 | 说明 |
|---|---|---|
| filter | WHERE | 多个 filter 用 AND 连接 |
| group | GROUP BY + 聚合函数 | keys 进 GROUP BY,aggregations 进 SELECT |
| sort | ORDER BY | 支持多字段排序 |
| limit | LIMIT / TOP | 方言差异,MySQL 用 LIMIT,SQL Server 用 TOP |
| join | JOIN | 显式指定连接类型和条件 |
| window | 窗口函数 | OVER 子句 |
映射的时候有几个细节要注意。第一,filter 和 group 的顺序。如果 filter 在 group 之前,过滤条件进WHERE;如果在之后,进HAVING。这个顺序由 DSL 里步骤的先后决定,编译器要严格按顺序处理。第二,聚合字段的别名。DSL 里指定的alias要原样出现在 SQL 的AS后面,因为后续的 sort 可能引用这个别名。第三,limit 的位置。如果前面有 group,limit 要放在最后;如果没 group,可以下推到子查询优化性能。
4.3 方言适配:一套契约,多种数据库
实际项目里很少只对接一种数据库。我做过的一个项目要同时支持 MySQL、PostgreSQL 和 SQL Server,方言差异处理不好就是灾难。
我的做法是把方言差异抽象成配置,而不是写三套编译器。具体来说,差异点主要有这么几类:
- 分页语法:MySQL 用
LIMIT n,SQL Server 用TOP n或OFFSET FETCH,PostgreSQL 用LIMIT n。 - 字符串拼接:MySQL 用
CONCAT(),PostgreSQL 用||,SQL Server 用+。 - 日期函数:
DATE_FORMAT(MySQL)、TO_CHAR(PostgreSQL)、FORMAT(SQL Server)各不相同。 - 标识符引用:MySQL 用反引号,PostgreSQL 和 SQL Server 用双引号或方括号。
- 布尔值:MySQL 用 0/1,PostgreSQL 用
TRUE/FALSE。
我把这些差异做成一个方言配置对象,编译器在生成 SQL 的时候查配置。新增一种数据库,只需要加一份配置,不用改编译器核心逻辑。这个设计让我的方言扩展成本从“几天”降到了“几小时”。
4.4 权限注入:安全不能靠模型自觉
生产环境里,不同用户能查的数据范围是不一样的。销售只能看自己区域的,财务能看全量。这个权限控制绝对不能交给 LLM 去判断,必须由编译器强制注入。
我的做法是在编译阶段,根据当前用户的权限上下文,自动往工作流里插入filter操作。比如销售用户查询订单,编译器自动加上region = 'east_china'这个条件。这个注入对 LLM 是透明的,LLM 根本不知道有这回事,也就没法绕过。
权限规则我维护成一份策略表,记录每个角色对应的数据范围。编译时根据用户角色查策略,生成对应的过滤条件。这样即使 LLM 被诱导生成了越权的查询意图,编译器也会把它限制在权限范围内。
提示:权限注入的过滤条件要放在最前面,确保它在所有其他操作之前生效。我见过把权限条件放在
HAVING里的实现,结果聚合后的数据还是泄露了,这是个典型的安全漏洞。
5. 实操过程:从零搭一套可运行的链路
5.1 环境准备与技术栈选型
先说技术栈。规划层我用的是 Python,因为生态成熟,跟各种 LLM API 的对接库最全。编译器层我一开始也用 Python,后来为了性能换成了 Go,但如果你对性能要求不高,Python 完全够用,开发效率还更高。
具体依赖:
- LLM 调用:任意主流模型 API 都行,我用的是支持结构化输出的模型,能直接返回 JSON,省掉解析的麻烦。
- JSON Schema 校验:Python 用
jsonschema,Go 用gojsonschema。 - SQL 解析(用于测试):
sqlparse(Python)或vitess的 sqlparser(Go),用来验证生成的 SQL 语法正确。 - 测试框架:
pytest或 Go 自带的testing。
数据库这边,我建议先用 SQLite 做开发,因为它零配置、启动快,适合快速迭代。等逻辑稳定了再切到目标数据库做集成测试。
5.2 第一步:定义 DSL 和 JSON Schema
这是所有工作的起点。先把 DSL 的规范定下来,写成 JSON Schema。我建议从最小可用集开始,只支持filter、group、sort、limit四个操作,跑通了再逐步加join、window这些复杂操作。
JSON Schema 的定义要严格,每个字段的类型、必填性、取值范围都写清楚。这份 Schema 后面有三个用途:校验 LLM 输出、生成 prompt 里的规范说明、作为编译器的输入契约。一份定义三处复用,改一处全同步,这是保持一致性最省心的办法。
5.3 第二步:搭建规划层的调用链路
规划层的代码结构我建议分成四个模块:
- Schema 管理器:负责从数据库元数据里提取 schema,构造增强描述,按需检索相关表。
- Prompt 构造器:把角色定义、DSL 规范、schema 描述、输出约束拼成完整的 prompt。
- LLM 调用器:封装 API 调用,处理重试、超时、限流。
- 输出解析器:清洗、校验、容错、重试。
这四个模块之间用清晰的接口隔开,方便单独测试。比如 Prompt 构造器可以脱离 LLM 单独测,输出解析器可以喂各种畸形 JSON 测容错能力。
5.4 第三步:实现编译器的核心逻辑
编译器我建议用访问者模式实现。IR 的每个节点类型对应一个访问方法,遍历的时候根据节点类型分派。这样新增操作类型只需要加一个访问方法,符合开闭原则。
代码生成的时候,我习惯先构造一个SQL 片段树,每个片段是一个对象,记录自己的 SQL 文本和依赖关系。最后再把这棵树拍平成字符串。这样做的好处是可以在拍平之前做优化,比如合并相邻的WHERE条件、消除冗余的子查询。
5.5 第四步:端到端测试与效果评估
测试这块我分三层:
单元测试覆盖编译器的每个操作类型,输入固定的 DSL,断言输出的 SQL 字符串符合预期。这部分测试要跑得快,每次改代码都跑。
集成测试覆盖完整的链路,从自然语言问题到最终 SQL。这部分用真实的问题集,人工标注期望的 SQL,对比生成的 SQL 是否等价。等价判断不能只比字符串,得比执行结果——同样的数据下,两条 SQL 查出来的结果一样就算等价。
回归测试把历史出过问题的 case 沉淀下来,每次发版前跑一遍,确保老问题不复发。
评估指标我主要看三个:执行准确率(生成的 SQL 能跑出正确结果的比例)、契约合规率(LLM 输出一次通过校验的比例)、平均延迟(从提问到返回 SQL 的时间)。前两个衡量质量,第三个衡量体验。
6. 常见问题与排查技巧实录
6.1 问题速查表
| 现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 生成的 SQL 查不到数据 | 枚举值映射错误 | 检查值域映射表 | 补充映射关系 |
| 字段名不存在 | schema 描述过时 | 对比实际表结构 | 刷新 schema 缓存 |
| 聚合结果偏小 | JOIN 类型错误 | 检查 join 操作 | 确认 LEFT/INNER |
| 排序结果不对 | 别名引用问题 | 检查 sort 字段 | 统一别名规范 |
| 契约校验失败 | prompt 不够明确 | 看失败样本 | 补充 few-shot |
| 延迟过高 | schema 太大 | 统计 token 数 | 启用按需检索 |
| 权限越界 | 注入位置错误 | 检查注入顺序 | 前置到最前 |
6.2 几个我踩过的坑
坑一:把注释当字段名。有一次 schema 描述里字段注释写得太详细,模型把注释里的词当成了字段名,生成了一个不存在的字段。后来我在 schema 描述里明确区分了“字段名”和“字段说明”两个部分,用不同的标记隔开,问题就没了。
坑二:时间范围理解偏差。用户说“最近一周”,模型理解成“过去 7 天”,但业务方期望的是“本周一到今天”。这种歧义必须在 prompt 里明确口径,或者让模型输出相对时间表达式,由编译器按业务规则计算。我后来统一规定:所有相对时间都用 DSL 的relative表达式,编译器按配置的日历规则计算,不再让模型自己算日期。
坑三:多表连接的字段歧义。两张表都有name字段,模型生成的 DSL 里只写了name,编译器不知道该用哪张表的。解决方案是要求 DSL 里的字段必须带表前缀,或者编译器在解析时做歧义检测,发现歧义就报错让模型重试。
坑四:limit 和聚合的顺序。用户要“销售额前十的门店”,模型生成的步骤是先 limit 再 group,结果变成了“取前十条订单再按门店分组”,完全错了。正确的顺序是先 group 再 sort 再 limit。这个顺序问题我在 prompt 里加了明确的规则说明,并且用 few-shot 示例强化,才把准确率提上来。
6.3 独家避坑技巧
技巧一:给 DSL 加“意图注释”字段。我让模型在输出契约的时候,额外加一个intent字段,用一句话描述它理解的用户意图。这个字段不参与编译,但可以用来做人工审核和问题排查。当生成的 SQL 不对时,看一眼intent就知道是模型理解错了还是编译器翻译错了,定位效率翻倍。
技巧二:建立“问题-SQL”对照库。把线上跑过的每个问题、生成的契约、最终 SQL、执行结果都存下来。这个库既是回归测试的素材,也是优化 prompt 的依据。我定期从这个库里抽样分析,找出高频错误模式,针对性地补 few-shot 示例。
技巧三:编译器的错误信息要“可行动”。编译器报错的时候,别只说“字段不存在”,要说“字段amt在表orders中不存在,该表可用字段为:amount, quantity, ...”。这样模型重试的时候能直接修正,不用再猜。我实测下来,可行动的错误信息能让重试成功率提升 40% 以上。
技巧四:给 LLM 的输出加“置信度”。让模型在输出契约的同时,给每个步骤打一个 0 到 1 的置信度。编译器遇到低置信度的步骤,可以走更严格的校验,或者直接触发人工确认。这个机制在关键业务场景下特别有用,能有效拦截模型“瞎猜”的情况。
7. 这套架构的边界与扩展方向
7.1 它不擅长什么
说了这么多优点,也得说说这套架构的局限。第一,它不适合探索式分析。用户如果自己都不知道要查什么,边看边想,那工作流契约的固定结构反而成了束缚。这种场景更适合让模型直接生成 SQL,配合人工审核。第二,它对 schema 质量依赖极高。如果数据库字段命名混乱、注释缺失、枚举值没有文档,那规划层的准确率会大打折扣。上这套系统之前,先把数据字典整理好,这是前提。第三,复杂 SQL 的表达能力有限。像递归查询、复杂的窗口函数嵌套、存储过程调用这些,工作流 DSL 很难优雅地表达。遇到这类需求,我一般建议走“模型生成 + 人工审核”的旁路,不强行走编译器。
7.2 可以往哪些方向扩展
方向一:多轮对话支持。现在的契约是单轮的,用户问一句生成一个工作流。可以扩展成多轮,让用户在前一个工作流的基础上做增量修改,比如“再加一个按品类的分组”。这需要在契约里加一个base字段,引用上一个工作流,然后只描述差异部分。
方向二:查询结果的自然语言解释。SQL 生成只是第一步,用户拿到数据后还想知道“为什么”。可以在执行完 SQL 后,把结果数据喂给 LLM,让它生成一段自然语言总结。这部分跟生成 SQL 是解耦的,可以独立迭代。
方向三:跨数据源的联邦查询。现在的工作流只针对单一数据源。如果能把契约扩展成支持多个数据源,编译器负责生成跨源的查询计划,那就能覆盖更复杂的场景。这个方向技术难度大,但价值也高。
方向四:契约的版本化与灰度。当 DSL 规范演进时,不同版本的契约要能共存。我现在的做法是解析器按version字段走不同分支,未来可以做成插件式的解析器注册机制,新版本以插件形式接入,老版本继续服务,实现平滑升级。
我个人在实际操作中的体会是,这套“LLM 规划 + 编译器生成”的架构,本质上是在用工程手段给 LLM 的不确定性套上缰绳。它不是要限制 LLM 的能力,而是把 LLM 的能力引导到它真正擅长的方向上,同时用确定性的编译器兜住底线。这个思路不只适用于 SQL 生成,任何需要 LLM 输出结构化、可执行结果的场景,都可以借鉴。最后再分享一个小技巧:如果你刚开始做,别一上来就追求覆盖所有 SQL 特性,先把最简单的单表过滤查询跑通,把链路打通,再逐步加复杂度。我见过太多项目死在“想一步到位”上,反而是那些从最小可用集开始、小步快跑的团队,最后跑得最远。