我在 Hacker News 上发了一个项目,标题是 "Privacy-First Natural Language to SQL, No LLMs, No Uploads"。熟悉 HN 生态的朋友都知道,Show HN 是一个相当硬核的曝光渠道——没有营销包装,全球的工程师会直接对你的作品开火。我发这个项目的初衷,其实是从内部工具的实际痛点出发的。
团队里不少业务同学写 SQL 很吃力,日常又频繁需要从数据库里捞数。市面上已有的 NL2SQL 方案几乎清一色绑定大型语言模型,要么把数据送到云端推理接口,要么至少也要过一遍外部服务。这带来两个非常现实的问题:第一,敏感业务数据外泄的风险,很多公司根本不敢把库里的数据喂给第三方;第二,LLM 的调用成本和响应速度,在频繁交互的场景下并不理想。
所以我把整个方案推倒重来:不用任何 LLM,不发起任何网络请求,纯本地规则引擎实现自然语言到 SQL 的转换。数据不出机器,逻辑完全可解释,响应时间控制在毫秒级。这篇文章就把我整个开发过程中的设计取舍、实现思路和踩过的坑,完整梳理一遍。
1. 这个项目到底在解决什么问题
1.1 现有 NL2SQL 方案的三大痛点
先把行业现状摊开说。今天市面上绝大多数的自然语言转 SQL 工具,底层都是 LLM。你可以用 GPT、Claude、开源大模型,也可以自己微调一个专用模型,但无论什么路线,本质上都逃不开三个问题:成本、延迟、隐私。
成本不用多讲,按 token 计费的模式在复杂查询场景下很贵。一条上下文较长的查询请求,加上系统提示词和数据库 schema 描述,随随便便消耗几千 token。如果是团队内部高频使用,一个月下来账单相当可观。延迟更直接,网络请求加模型推理,常规情况下少则一两秒,多则十几秒,对习惯了在终端里秒出结果的人来说非常难受。
隐私是最微妙也最致命的一点。即便你用的是私有化部署的开源模型,数据至少还在自己的可控环境里。但很多中小团队为了省事,直接调用云端 API,这意味着表结构、字段名甚至查询结果都要经过第三方服务器。对金融、医疗、企业内部管理系统来说,这种模式基本过不了安全评审。
1.2 为什么“隐私优先”能成为切入点
我一开始的思路就很明确:能不能做一个完全不依赖外部服务的 NL2SQL 工具?数据不离开用户机器,模型不调用远程推理,甚至不需要安装 Python 环境之外的东西。
这里有一个重要的认知转变。做这个项目之前,我也默认“自然语言理解”必须靠神经网络。但深入想了一下,数据库查询这个场景其实极其受限。一个业务数据库不过几十张表、几百个字段,用户能发出的查询意图其实非常有限——无非是查什么、筛什么条件、怎么聚合、怎么排序。这套东西的复杂度天花板很低,完全没有到必须动用大模型的程度。
于是项目定位就出来了:面向结构化程度高、访问频繁、对隐私敏感的内部数据查询场景,做一个轻量、快速、可解释的本地 NL2SQL 引擎。和 LLM 方案拼的不是语义理解的宽度,而是在固定 schema 上的准确率、速度和零数据外泄的确定性。
2. 没有 LLM,怎么理解自然语言
2.1 核心假设:数据库查询是一个受限问题
很多人听到“不用 LLM 做 NL2SQL”的第一反应是不现实,但实际上这个问题的难度被 LLM 时代夸大了。语言模型解决的是开放世界的语义理解问题,而数据库查询只需要在封闭的 schema 空间里做映射和组合。
我可以把一个查询拆解成几个基本维度:
- 对象:用户要查哪些表、哪些字段
- 条件:对数据有怎样的过滤要求,等于、大于、区间、包含等
- 聚合:是否需要分组、计数、求和、平均
- 排序与限量:按什么排序,取前多少条
这些维度的组合方式虽然多,但每一种都可以用规则覆盖。比如“查一下 3 月份销量超过 100 的产品名称”这句话,拆开就是:对象(产品名称)、条件(销量 > 100、日期在 3 月)、结果输出(查询某个字段)。这完全不需要理解“产品”是什么哲学概念,只需要把它映射到表里的某个字段。
所以我的架构从一开始就往这个方向设计:用轻量级自然语言解析器做意图识别,不做开放式的语义建模。
2.2 规则引擎与轻量意图识别的组合拳
实现的时候,我放弃了传统的正则表达式暴力匹配——那东西写出来看着热闹,维护起来是灾难。我用的是一个更清晰的分层结构:
- 词典层:维护表名、字段名的同义词映射,比如“销售额”映射到
total_amount,“用户”映射到user表、“名字”映射到name字段。 - 意图层:识别查询中的动作类型,比如“统计/汇总/每个”指向 group by 和聚合函数,“比/大于/超过/在...之后”指向比较条件。
- 结构层:把识别出的意图片段按 SQL 语法树组织起来,生成最终的 SQL。
其中意图分类部分我用了一个非常轻量级的策略:关键词与上下文规则的混合。自然语言表达丰富的场景,用关键词表覆盖常见的表达方式;同义多变的场景,靠上下文规则去推导。比如“超过”“大于”“不低于”都指向>比较,但“不低于”本身带了一个否定前缀,需要在规则里单独处理。
这套逻辑的好处是完全可控。每一条规则都有据可查,生成的每条 SQL 都能解释来源。做不到的事情直接告诉用户“这个查询我无法理解”,而不是像 LLM 那样一本正经地生成一条错误的结果。
3. 解析管线的完整设计
3.1 输入预处理:分词、规范化与停用词
自然语言进系统的第一道关卡是预处理。英文环境我直接用空格分词加词形还原;中文环境需要额外处理分词问题。我用的是双向匹配的最大分词算法,配合自定义词典,把“销售额”“订单量”这类业务词优先识别,避免被拆碎。
规范化阶段做几件事:统一大小写、去除无效停用词、将中文数字和英文数字表达式统一成阿拉伯数字。这里有一个容易被忽略的细节——日期表达的处理。用户的输入千奇百怪,可能是“2024年3月”“去年”“上个月”“最近30天”,这些需要分别转换成 SQL 里的日期范围条件。
以“最近30天”为例,规则引擎会基于当前日期往前推 30 天,生成created_at >= '2024-01-01' AND created_at <= '2024-01-31'这样的区间条件。这里的关键点在于需要知道数据库里那张表的日期字段到底叫什么,这就需要 schema 信息参与映射。
3.2 Schema 感知的实体映射策略
我前面反复强调 schema 是核心资产。实现上,项目启动时会读取数据库的系统表,获取完整的表名、字段名、字段类型和外键关系,构建一棵内存中的 schema 树。
实体映射的本质是把自然语言里出现的名词短语,映射到 schema 树上的节点。这个映射分三层:
- 精确匹配:用户输入的表名或字段名与 schema 完全一致,这个最简单。
- 同义词匹配:通过词典把业务术语映射到底层字段,比如“手机号”映射到字段
mobile。 - 模糊匹配:基于编辑距离或子串匹配,处理用户记不清确切字段名的情况,比如输入“createtime”时能够命中
created_at。
第三层最容易出错。编辑距离阈值调太紧,匹配不到;调太松,产生大量误匹配。我最终的策略是:优先精确匹配,其次同义词,最后才用基于 schema 上下文(比如字段类型、所属表)的模糊匹配兜底,并且模糊匹配命中时必须向用户二次确认。
这一层的设计投入决定了整个系统体验的上限。很多方案在这块做得很粗糙,导致用户必须按数据库字段名“对着字典说话”,那自然语言就失去了意义。
3.3 条件解析与 SQL 组装的安全护栏
条件解析是最容易踩坑的地方。实体映射得到的是“哪些表和字段”,条件解析要回答的是“如何过滤”。比较符号、逻辑连接词、括号组合、类型转换,每一步都需要验证。
我举个例子:用户输入“价格不超过 100 且库存大于 0 的商品”。解析后得到条件price <= 100 AND stock > 0。这里面有个隐藏问题——用户说“价格不超过”,到底是<=还是<?我做了语义收紧处理,即“不超过”映射为<=,因为用户的隐含语义是包含 100 这个边界值的。这种细节没有标准答案,但在真实业务场景中直接决定了结果对不对。
SQL 组装阶段,我没有选择拼接字符串的方式,而是用一个结构化的查询构建器,逐步把 SELECT、FROM、WHERE、GROUP BY、ORDER BY、LIMIT 这些子句组装起来,每一层都做合法性校验。字段名来自 schema 树而不是用户输入的原样复制,从机制上避免 SQL 注入。用户输入永远只是“值”的载体,不参与结构生成。
4. 隐私架构与本地部署实现
4.1 数据流向:一次请求都不往外发
隐私优先说起来容易,做起来难,难在如何让用户真正信服。我在这块做了两个层面的保障。
第一是架构层面的强制约束。整个引擎作为一个本地库嵌入到应用中,所有的解析、映射、查询构建都在进程内完成,代码里没有任何 HTTP 客户端,也没有任何外部服务地址。从工程上就杜绝了数据外发的可能性。
第二是运行时的透明可审计性。系统会记录每次查询的完整解析链路:用户输入了什么,词典命中了哪些词条,映射到了哪个字段,生成了哪条 SQL。这个日志存在本地,用户随时可以回看。这种透明度是 LLM 黑盒方案不具备的。
另外我单独处理了数据库连接信息的敏感性。配置项支持环境变量注入和本机加密存储,不会以明文形式写入配置文件。
4.2 规则与词典的分发方式
不用 LLM 之后,规则引擎的迭代靠的是词典和规则的持续更新。词典文件以 YAML 格式维护,发布时打包进应用。每个部署环境可以自定义自己的业务词典。
这里有一个重要的设计决策:基础规则和业务词典要分层。基础规则负责通用的自然语言理解逻辑,这部分由引擎内置;业务词典则根据实际数据库 schema 和用户习惯动态配置,由部署方自行维护。
加载逻辑也很简单——启动时读取 YAML 词典,构建 Trie 树提升匹配效率。整个词典的加载和构建时间在几十毫秒以内,对启动流程几乎无感知。后续更新词典只需要重新加载,不用重启进程,这个细节在高频迭代场景下特别实用。
5. 和 LLM 方案的横向对比
5.1 能力边界与服务差异
客观讲,非 LLM 方案和 LLM 方案各有清晰的适用边界,谁也没办法完全替代谁。我用一张表把实际测试的对比结果放出来,供大家选型时参考:
| 对比维度 | 本方案(规则引擎) | LLM 方案 |
|---|---|---|
| 查询响应时间 | 毫秒级 | 1-10秒 |
| 单次查询成本 | 几乎为零 | 按 token 计费 |
| 数据隐私 | 全程本地,零外传 | 依赖部署方式,云端有风险 |
| 查询可解释性 | 完整解析链路可追溯 | 黑盒推理 |
| 复杂语义理解 | 覆盖常见场景,复杂问法需规则补齐 | 泛化能力强,能理解开放式表达 |
| 不确定性 | 结果确定,同输入必同输出 | 同样的输入可能产生不同结果 |
| 部署依赖 | 纯本地库,无需 GPU/网络 | 通常需要模型环境和算力 |
测试数据来自我内部用一套包含 12 张表、80 多个字段的中型业务 schema 做的实验。规则引擎单次解析平均耗时 18 毫秒,LLM 方案平均耗时 2.4 秒,差异非常直观。
5.2 选型建议:什么时候该用哪种方案
如果让我给结论,我会说:固定 schema 的重复性查询场景,规则引擎完胜;开放式数据分析场景,LLM 依然是更合适的选择。
细分来看,当团队有一个结构相对稳定的业务数据库,用户反复查询的是销量、用户量、订单状态、时间区间这些固定维度,规则引擎的方案不仅跑得快,关键是可维护。业务方提一个需求,你加一条词典规则就能搞定,不需要重新训练模型,也不会出现“同一句话这次查出来和上次不一样”的玄学问题。
反过来,如果用户面对的是非结构化数据、SSMS 里临时探索性查询、或者需要理解“帮我看看这两个月异常波动的可能原因”这种模糊问题,那规则引擎确实无能为力,老老实实上 LLM。
我的立场不是“规则替代 LLM”,而是“给用户多一个选择”。很多内部工具场景,用户要的只是跑得稳、查得快、数据安全,不关心背后是不是 AI。
6. 实操中踩过的坑与排错经验
6.1 同义词映射的陷阱
第一个大坑是同义词的冲突。同一个业务词在不同语境下可能指向不同的字段。比如“状态”这个词,在订单表里可能对应order_status,在用户表里对应user_status。如果词典只做全局映射,不做上下文约束,很容易在 多表 join 的时候映射错。
我的解决办法是同义词定义支持作用域限制。词典条目可以指定适用的表范围,比如:
synonyms: - word: 状态 field: status table: [orders, users] # 限定表范围 - word: 状态 field: user_type # 用户表里有时也叫状态 table: users匹配的时候会先锁定候选表,再在候选表范围内做同义词映射。这样虽然词典条数增加了,但误映射明显减少。
6.2 复杂嵌套查询的处理
第二个坑是常规规则引擎在嵌套查询面前力不从心。用户问“每个品类下销量最高的前三个产品”,这句话里的“每个品类”和“前三个”需要生成一个窗口函数或者在子查询里做排序分组。
一开始我的解析器是一个线性流程:识别对象、条件、聚合、排序。面对这种嵌套逻辑就直接傻了。后来的改进是把解析流程层级化:先识别主查询框架,再在主查询的某个组件里递归解析子条件。这种“嵌套解析”模式让系统能够正确处理一层到两层的子查询,超过三层的复杂查询依然会提示能力不足。
这里我的态度很明确:做不到就明说。系统会有意识地识别出自己无法解析的查询模式,返回一个友好的提示,告诉用户哪些部分没理解,而不是生成一条语义错误但语法合法的 SQL。这种“主动认怂”让用户对系统的信任感反而更高。
6.3 安全护栏的细节实现
安全永远是 NL2SQL 工具的生命线。LLM 方案最大的风险是模型在生成 SQL 时可能产生语义上偏差但语法合法的注入行为。规则引擎因为字段名不来自用户输入,结构也不由用户拼装,注入风险天然很低,但仍然有几个细节要注意。
第一个细节是值域的类型校验。用户输入的值会被解析成字符串、数字或布尔值,但最终拼进 SQL 时,必须按照目标字段的数据类型做一次强制转换。如果用户说“查询价格大于 abc 的产品”,系统应该直接报错,而不是把abc原样塞进 SQL。
第二个细节是 LIKE 条件的转义处理。用户输入里包含%或_时,如果不做转义,会让查询结果意外扩大。我专门写了一个转义函数,确保用户搜什么就匹配什么,不会因为通配符产生额外结果。
第三个细节是查询行数的兜底限制。自动生成的 SQL 默认加一个 LIMIT 1000 的保护,用户确认之后可以调整,但默认值永远保守。这避免因为一个宽松的过滤条件,把整个线上库拖垮。
6.4 调试技巧:规则日志就是最好的调试器
最后分享一个实操里最有用的经验:规则引擎的调试完全依赖日志,所以要在一开始就把日志设计好。我在实现里对每个解析阶段都输出中间结构——原始输入、分词结果、命中的词典词条、候选表、优先级排序、最终生成的 SQL。任何一个环节出问题,看日志就知道卡在哪里。
LLM 方案你只能看到输入和输出,中间过程是黑盒,没法调。规则引擎的优势就是中间每个环节都在掌控中,也正因为如此,日志的质量直接决定排查效率。我强烈建议做类似项目的朋友,把解析日志做成可读性高的多行文本,不要挤成一行 JSON,自己看着也省力。
结尾
做这个项目最大的收获,不是“不需要 LLM 也能做 NL2SQL”这个结论本身,而是让我重新审视了技术选型里那条默认的捷径。遇到“语言理解”相关的问题,我们太习惯直接上大模型了,很少停下来问一句:这个场景真的需要那么强的理解力吗?如果只是一个固定 schema 的数据库,几百个字段的组合空间其实有限得很,规则引擎完全可以覆盖大部分需求,而且跑得更快、更便宜、更让用户安心。
当然,规则引擎不是银弹,它有自己的能力边界。但我始终认为,工具的价值不取决于背后算法听起来是否时髦,而取决于是否解决了真实场景里的真实问题。从我在 HN 上收到的反馈和实际部署的效果来看,这个方向确实戳中了不少人的需求。如果你也有类似内部数据查询的场景,不妨试试非 LLM 的路线。哪怕只是把一个高频查询的手工流程自动化了,就已经值回开发成本了。