智能问数要落地,绕不开一个环节:让 AI 直接查生产数据库。运维同学的第一反应通常是拒绝------让一个会幻觉的模型对着生产库写 SQL 并执行,万一注入怎么办,误删了表怎么办,查了没权限的数据怎么办。这些担心都对,方案也很明确:模型生成的 SQL 永远不直接执行,执行权从模型手里拿走,交给一道独立的安全引擎。
我们在 JBoltAI框架 的问数链路里,把这条底线落成了三层闸加一层方言适配的执行结构:模型只负责理解问题和生成候选 SQL,一条语句能不能跑,由安全引擎说了算。下面逐层拆开。
一、第一道闸:危险关键字与表名格式校验
模型输出的 SQL 先过文本层校验。DROP、DELETE、TRUNCATE、UPDATE 这类写操作关键字直接拦下------问数场景天然只读,没必要给模型开写权限,这一条堵住的是"误操作"整类风险,也顺手封掉了通过话术诱导模型删数据的路。表名同时做格式校验,不在已知清单里的表名一律拒绝,防止模型编造表名去试探。表名清单从哪来?来自数据注册环节------接入哪些库、哪些表对问数开放,注册时就登记在案,模型没见过的表名天然过不去。这一层成本极低,先把明显危险的构造挡在门外,后面的闸处理更隐蔽的问题。
二、第二道闸:动态值全部占位符化
文本校验挡不住伪装成正常输入的注入。用户问题里带一个引号、半段条件,拼进 SQL 就是一条新语句。所以第二道闸从机制上下手:SQL 里的动态值一律不拼接,全部转成占位符,由 JDBC 的 PreparedStatement 做参数绑定,参数只当数据、永远不当代码执行。在 JBoltAI框架 的引擎里这不是可选项,参数化是执行链路的默认行为,不依赖调用方自觉。注入这条路不是被防住的,是机制上不存在的。
直观对比一下:
-- 模型原始输出,不执行
SELECT order_no FROM sales_order
WHERE customer = '张三' OR '1'='1';
-- 过闸后实际执行的语句
SELECT order_no FROM sales_order
WHERE customer_id = ? -- 参数绑定传入
参数化还顺带解决另一个问题:模型爱自由发挥。列名写错、值类型对不上,在预编译阶段就会暴露,坏语句到不了真实数据库,预编译失败会连同原因一起记进问答日志,供建模侧排查。生成环节的毛病在闸门口就现形,而不是等运行时报一个看不懂的错。
三、第三道闸:语法树级的授权校验
前两道闸之后,还得回答一个权限问题:这条 SQL 碰的表和列,问的人有没有资格看?引擎用 JSqlParser 把 SQL 解析成语法树,逐个核对表名和列名------只能引用当前用户被显式授权的表和列,授权清单之外的一律拒绝,最小权限从账号层贯穿到语句层。语法树校验还有个好处:别名、子查询、注释这类绕过手段藏不住,不管 SQL 写得多绕,解析出的引用关系就摆在那里,清单之外的引用一律现形。
这道闸也是问数权限分级的工程底座。业务侧"老板能查、员工被拦"的规则,配置在 JBoltAI本体语义平台 的语义对象上,落到执行层就是这份授权清单,由语法树逐条兑现。权限配置和执行校验分成两层,改权限不用改代码,过闸标准始终跟着账号走。
四、方言层:国产库也要能跑
Text2SQL 要在企业里真正用起来,绕不开信创环境。不同数据库的方言差异不小,引擎做了方言层,覆盖六种数据库,达梦、金仓这类国产库都在其中。达梦这边保留字冲突是常见的坑,一条再普通的查询撞上保留字就报语法错,引擎里由专用拦截器自动改写;22 个自然语言问数用例、连续三轮真实生成验证全部通过,这条链路才算生产可用。国产化替换怕的就是这种细节,差一个保留字,整条问数链路就停在半路。
图数据库那边,JBoltAI框架 里也是同一套思路:Cypher 语句只读校验,graphId 强制隔离,不同企业的图数据互不可见。SQL 和 Cypher 两条链路,执行前的校验哲学一致------先证明这条语句安全,再谈执行。安全这件事没有例外通道,每条链路单独过闸,不存在"这条链路简单就免检"的说法。
五、生成与执行分离之后
三道闸加方言层,归结起来一句话:模型负责理解问题和生成候选 SQL,系统独立判断这条语句能不能跑。生成和执行分成两个角色,AI 的不确定性就被工程手段框住了,运维担心的注入、误删、越权,每一类都有对应的闸,每一步都有日志可回溯。
在向量空间JBoltAI 的体系里,这套安全引擎支撑着双平台的查询链路------JBoltAI本体语义平台 的智能问数、JBoltAI数字员工平台 的审单取数,背后查的每一张表都要过这三道闸。哪些表授权给谁,决定权始终在人手里;机器守住的是执行那一刻的底线。AI 查数要让人放心,靠的从来不是模型自觉,是机制。