尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java构建数据库语义网关:让自然语言安全翻译成SQL

发布时间:2026/9/19 6:39:44

资讯中心
01
ARTICLE

Java构建数据库语义网关:让自然语言安全翻译成SQL

Java构建数据库语义网关:让自然语言安全翻译成SQL
带过 BI 项目的人多少都有体会报表指标几十个口径全靠 Excel 传来传去业务方开口要数开发的回复永远是“下周”等报表终于上线需求可能已经变了。传统 BI 用了一轮又一轮最后还是卡在“人找数”这件事上。后来 Agent 概念火起来自然语言直接查库、执行分析、自动生成图表成了新的答案但真要把自然语言安全稳定地翻译成 SQL 并落库执行中间缺的不是算法而是一层靠谱的“语义”翻译器。于是我用 Java 写了一个轻量级数据库语义网关起名 DatI。它没有做大而全的可视化报表只是把“用户意图 - 指标/维度/过滤条件 - 可执行的 SQL - 受控的结果集”这条链路以网关形式统一收口既能服务 BI 报表也能暴露给 Agent 做工具调用。这篇文章完整记录了我的设计思路、核心实现、实操过程以及从 BI 迁移到 Agent 架构过程中积累的避坑经验适合对 BI、Text-to-SQL、Java 后端、Agent 工具层兴趣的开发者参考。1. 为什么我从 BI 转向 Agent然后做了这个叫 DatI 的网关1.1 BI 做久了最难受的不是报表是“取数链条”传统 BI 的打法是“建数仓 - 建模 - 做报表 - 定权限”听起来很流程化但真正跑起来最先崩的不是可视化而是语义层。同一张订单表财务看的是“含税销售额”运营看的是“GMV”市场又盯着“曝光带来的成交额”。你不可能给每个部门建一套物理表最终就会反复出现两类问题要么指标口径在 Excel 里拉齐要么在代码里硬写。我当时维护的报表平台已经积累了几百张宽表和上千个指标看板多少其实不重要了真正的瓶颈是开发链条太长。业务想临时看一个新维度比如“华东区上周新客的客单价”传统 BI 要经历“提需求 - 排期 - 改模型 - 发布”这条链路一折腾就是好几天。新客怎么定义、客单价取哪个分子分母、华东区要不要含福建每一条都在消耗人力而这些都是典型的语义问题。这条路走到后期团队时间大量消耗在“翻译”而不是“分析”。我们不是不会写 SQL是业务语言和数据库 schema 之间的映射关系太脆弱。我那时候就开始想如果能把语义层的管理做成一件系统化的事让查数和分析这两个环节解耦很多瓶颈会自然消失。1.2 数据库语义网关到底解决什么问题Agent 是最近两年的热点但落到“让 Agent 帮我查个数据”这个场景问题并没有想象中简单。LLM 确实能根据表结构生成 SQL可一旦涉及业务口径、行级权限、数据脱敏、方言适配通用大模型很容易翻车。最典型的是提示词里给了表结构它把“销售额”理解成了订单表里的原始金额而不是你指标体系中约定的“剔除退款后的净销售额”。数据库语义网关的核心作用就是把“业务语义”和“物理表结构”之间这道翻译过程从人工转为程序化。它在 LLM 和数据库之间插一层所有请求不再直接写 SQL而是走“自然语言 - 语义解析 - SQL 生成 - 安全校验 - 执行”。好处很明显口径只有一个地方维护权限在网关层统一拦截数据库方言由适配器处理Agent 只需调用网关暴露的工具接口不用关心底层库长什么样。我把 DatI 定位成“翻译中枢”而不是 BI。它不会画图不会做复杂的透视表只专注一件事把请求变成受控的、可解释的 SQL 去执行。这样无论上游是 BI 报表、IM 里的聊天机器人还是 Agent 编排框架都能复用同一套语义逻辑。1.3 DatI 的定位连接自然语言和数据库的“翻译中枢”DatI 这个名字来自 Data Intent也就是“数据意图”。设计时候我给自己定了三条边界轻量、可解释、安全可控。轻量意味着不绑定任何重型计算引擎走标准 JDBC 直接连库可解释意味着每次查询都能看到语义到 SQL 的完整推导过程安全可控意味着所有 SQL 都要经过校验、脱敏和权限过滤才能放行。更适合的类比是一个懂业务、懂权限、懂 SQL 方言的“老 DBA”被包装成了网关服务。开发可以查元数据Agent 可以查字典业务可以直接问。三个角色拿到的能力是一样的内核只需要维护一份语义层配置。这种设计让 DatI 既能当 BI 的数据服务端也能接 Agent 工具避免了重复建设。2. 整体设计一个轻量级语义网关在 Java 生态下怎么搭2.1 一条查询请求在 DatI 内部的完整流转路径我习惯把完整链路画成一条流水线DatI 核心就六个环节顺序固定请求接入、意图解析、语义映射、SQL 生成、安全校验、执行返回。每个环节只干一件事前一个环节的输出是后一个环节的输入这样出了问题很容易定位。请求接入负责接收各种形态的输入可能是报表前端传的查询参数也可能是 Agent 发来的自然语言文本。意图解析阶段会判断这个问题到底是想查数、看趋势、算比例还是想了解表结构不同意图走不同分支。语义映射是 DatI 最核心的部分把自然语言里提到的词比如“华东”“月度”“销售额”映射到语义层里定义的维度、指标和过滤条件。SQL 生成阶段根据语义映射的结果组装成符合目标数据库方言的 SQL 语句。安全校验会检查表权限、行级权限、字段脱敏、危险操作拦截。都通过后才交给 JDBC 执行最后把结果按统一结构返回。整条链路是同步阻塞还是异步响应取决于场景但 DatI 底层已经把每个环节拆成可替换的组件不同的环境可以灵活插拔。2.2 为什么选 Java技术栈与关键依赖选 Java 首先是被存量系统绑定的。我们的数据平台、BI 后端、权限中心都是 Java 技术栈用 Java 写网关可以直接复用现有统一登录、审计、配置中心这些中间件不需要额外引入 Go 或者 Python 服务。如果做一个内部工具还要重新接一遍工单系统、告警系统那就不叫轻量了。具体依赖我用得不多但每一样都是刚需。Spring Boot 负责 Web 框架和 Bean 管理HikariCP 做数据库连接池Flyway 管理元数据表的版本变更Caffeine 做本地缓存Jackson 处理 JSON 序列化。LLM 调用部分通过 HTTP 调用模型服务没有引入太重的东西。Java 21 是我现在用的主版本虚拟线程在处理大量 Agent 并发请求时非常香几百个自然语言查询同时进来不会被传统线程池卡得很难看。依赖少的好处是部署简单一个 fat jar 丢到服务器就能跑。因为要走 JDBC 访问不同的数据源我把数据库驱动和方言适配器做成了 SPI 插件默认自带 MySQL、PostgreSQL、ClickHouse 三套GaussDB、Doris、Greenplum 之类的都可以照着扩展。2.3 语义层建模用 YAML 管理指标、维度和同义词语义层是 DatI 的灵魂我用 YAML 文件来管理不建重型模型表。一开始考虑过把语义模型放在数据库里动态维护方便页面配置但后来觉得 YAML 更符合“配置即代码”的习惯也方便做 Git 版本管理和 code review。一个典型的语义配置包含数据源、表、指标、维度、同义词五类信息。指标定义了业务上怎么算比如“净销售额 SUM(amount) - SUM(refund_amount)”并绑定物理表的字段和聚合方式。维度定义了下钻和筛选的字段比如“日期-月份-周-日”层级“区域-省-市”层级。同义词是连接用户语言和 schema 的桥梁用户说“老板”“GMV”“营收”可能都指向同一个指标。YAML 非常适合这种场景层级清晰注释方便不支持复杂逻辑反而能逼着设计者保持简单。实际跑起来之后会发现绝大多数自然语言查询的歧义都能在语义层通过同义词和别名解决而不是靠每次都在提示词里堆解释。3. 核心实现细节从自然语言到可执行 SQL 的关键环节3.1 模型调用与提示词模板的设计细节DatI 没有自研大模型而是把 LLM 当作“语义解析器”来用输入是用户的自然语言输出是一份结构化的 JSON描述意图、目标指标、维度和过滤条件。为了不让模型乱发挥我把输出格式约束得很死不只靠“请你输出 JSON”而是直接给一个严格的 JSON Schema并要求模型只输出 JSON不做任何解释。提示词模板里我设计了这么几个区块角色设定、语义层字典、用户问题、输出格式、少量示例。角色设定固定为“你是企业数据分析助手必须基于给定的语义层定义理解问题禁止臆造字段”。语义层字典不是把所有表字段都塞进去而是只放指标、维度和同义词避免模型被物理表结构带偏。输出格式示例{ intent: trend_query, metrics: [net_sales], dimensions: [month], filters: [ {field: region, op: in, value: [华东, 华南]} ], time_range: {start: 2025-01-01, end: 2025-12-31} }少量示例我精心挑了三到五条每一条都覆盖一种容易写错的场景比如“本月”“上月”这类相对时间“环比增长”这类计算型提问。实测下来模型只要不缺失语义层字典里的核心信息输出稳定性可以到 90% 以上剩下不稳定的靠后端校验和自动纠正兜底。3.2 SQL 生成后的四重校验LLM 生成 SQL 最大的问题不是“不会生成”而是“偶尔会生成错的”。为了把这种偶发问题挡在数据库之前我在 DatI 里实现了四个校验环节首尾相连缺一不可。第一重是结构校验用 SQL 解析器把语句解析成抽象语法树检查是不是只包含 SELECT 查询是不是有 DELETE、UPDATE、DDL、CTE 递归等危险动作。第二重是权限校验把查询涉及的表、字段、行级条件与当前身份的权限点做比对没有权限的字段直接拒绝行级数据范围则强行改写 SQL注入预先配置的条件。第三重是质量校验检查生成的 SQL 有没有遗漏过滤条件、有没有把指标和维度搞混、有没有潜在的全表扫描风险。第四重是资源校验限制查询超时时间、最大返回行数、扫描行数上限防止一条自然语言把线上库拖垮。这里有个很关键的实现思路校验不通过不是直接拒绝就完事而是把错误信息回传给上游让模型根据错误做一次自纠正。比如模型生成“没有 group by 维度”的 SQL错误提示会明确告诉它“查询了维度 month 但缺少 GROUP BY month”模型重新生成一次多数情况下能自我修正。3.3 数据源适配与方言处理不同数据库的 SQL 方言差异比想象中大得多。分页语法MySQL 是 LIMITPostgreSQL 也兼容 LIMIT/OFFSET但 SQL Server 用 OFFSET FETCHClickHouse 又有自己的 SETTINGS 子句。聚合函数、日期函数、类型转换函数的差异更细比如日期截断在 MySQL 用 DATE_FORMAT在 PostgreSQL 用 date_trunc在 ClickHouse 用 toStartOfMonth。DatI 的 SQL 生成器不会直接拼方言而是先构建一份与方言无关的查询描述包括查询的表、指标、维度、过滤条件、排序、分页。然后由方言适配器把这份描述转换成具体的 SQL。新增数据源只需要写一个适配器实现方言差异的那几个方法就行比如时间粒度转换、分页语法、标识符加不加引号。方言适配器还要负责元数据同步比如读取表注释、字段注释、字段类型、主键和索引信息。这些元数据会缓存并交给语义层使用用来校验 LLM 生成的内容是否越界。连接池参数也不一样ClickHouse 适合大数据量的流式读取MySQL 则要保持合理的最大连接数避免 Agent 并发时把连接池打满。3.4 结果格式化、缓存与数据脱敏执行 SQL 只是第一步返回给 Agent 或 BI 的结果必须是一份结构良好的数据最好还附带上“这段数据是怎么查出来的”说明。DatI 对所有结果集做统一包装字段列表、行数据、行数、耗时、SQL 原文、语义映射过程这样前端或者 Agent 拿到结果后既能展示也能判断 SQL 是否合理。缓存我采取的是“两层策略”元数据缓存和结果缓存。元数据缓存用 Caffeine配置 5 分钟过期避免 LLM 每次请求都重复查字典。结果缓存只对“条件是明确且查询时间范围较短”的请求生效用查询 hash 作为 key默认 60 秒过期避免不同用户共同查询同一份热点数据时反复打库。数据脱敏被放在结果格式化之前字段级规则在语义层里声明。手机号、身份证号、客户名称这类敏感字段不是简单打星号而是根据权限配置做部分隐藏或完全隐藏。比如客户经理能看到后四位管理员能看到全部普通用户只能看到加脱敏后的值。这些规则在 SQL 层面执行反而最稳妥你会发现如果只靠应用层脱敏用户总能在 SQL 里绕过去。4. 实操演示用“月度销售额趋势”跑通一次完整链路4.1 准备数据环境和数据源接入我拿一张实际的订单表来演示表结构相对简单但很典型order_id、customer_id、region、province、order_date、amount、refund_amount、channel。这张表存了约 50 万条订单数据覆盖 2024 到 2025 年。我把它放在 MySQL 里并在 DatI 的管理端配置了数据源连接。配置数据源的时候有一点特别重要给 DatI 创建的数据库账号一定不要给 DDL 权限只给 SELECT 权限。如果 Agent 工具被人恶意利用最坏情况下也只是读库不会把表结构改了。连接池的最大连接数我设置为 20超时时间设置为 15 秒任何一条查询单次最大扫描行数设置为 200 万行。接入数据源后DatI 会自动读取表结构生成一份初步的元数据。但自动读出来的信息只是物理层业务层还是需要人工补充哪个字段是订单日期哪个字段是退款金额哪个字段代表区域这些关系靠 AI 猜是不靠谱的。我把基础表信息存到了元数据管理区为后面注册语义层做准备。4.2 注册语义层并验证元数据接下来我在语义层里定义了两类指标和两组维度。销售额指标定义为net_sales SUM(amount - refund_amount)并且设置同义词“营收”“净销售额”“GMV”退款率定义为refund_rate SUM(refund_amount) / SUM(amount) * 100.0同义词为“退货率”。维度包括月份 dimension来源是DATE_FORMAT(order_date, %Y-%m)区域 dimension字段是region层级为“华东-江苏-南京”这种默认注册的配置。YAML 的核心配置段大概是metrics: - name: net_sales desc: 净销售额 订单金额 - 退款金额 synonyms: [营收, 净销售额, GMV, 销售额] physical: table: orders expression: SUM(amount - refund_amount) - name: refund_rate desc: 退款率 退款金额 / 订单金额 * 100 synonyms: [退货率, 退款比例] physical: table: orders expression: SUM(refund_amount) / NULLIF(SUM(amount), 0) * 100.0 dimensions: - name: month desc: 按自然月聚合 synonyms: [月份, 月度, 按月] physical: table: orders expression: DATE_FORMAT(order_date, %Y-%m)注册完一定要做一次“元数据验证”我会在 DatI 管理端跑一次语义层自检比如验证net_sales引用的字段在 orders 表里是否存在、类型是否是数值型、同义词之间有没有冲突。这一步很值得很多低级错误能在发布前被提前拦下来。4.3 发起查询、观察指标到 SQL 的完整转换语义层就绪后我直接在 DatI 的调试窗口输入“2025 年每个月的净销售额趋势按区域拆开看华东和华南”。DatI 会把这句话交给意图解析和语义映射模块最终生成一份中间表示。我看过日志清晰记录了模型输出的 JSON 结构包含 intent 为 trend_query指标 net_sales维度 month、region过滤条件是 region in 华东/华南 且时间范围是 2025 年全年。然后方言适配器把它翻译成 MySQL 的 SQL 并执行SELECT DATE_FORMAT(order_date, %Y-%m) AS month, region, SUM(amount - refund_amount) AS net_sales FROM orders WHERE region IN (华东, 华南) AND order_date 2025-01-01 AND order_date 2026-01-01 GROUP BY month, region ORDER BY month ASC实际执行耗时约 300 毫秒返回了 24 条记录。DatI 的响应 JSON 里不仅有数据还把“同步的 SQL”“整条语义链路”“权限校验结果”“脱敏规则”一并返回这样如果以后数据对不上我可以回溯是哪一步出了问题而不是对着一个莫名奇妙的数字干瞪眼。4.4 对比 BI 人工建模和 DatI 自动生成的差异同样一个需求在传统 BI 里要走一遍“需求评审 - 逻辑模型设计 - 物理建模 - ETL - 报表配置”顺利的话是半天不顺利可能要两天。其中一个典型问题就是业务方说“净销售额”开发先去翻指标字典再看订单表里有没有现成字段没有就重新开发一套计算逻辑费时费力。DatI 场景下类似的查询直接在微服务里被表达成一次工具调用Agent 甚至可以在几秒内把 2024 和 2025 两年的月度趋势拼接在一起做同比对比。BI 不是被替代了而是把“取数”这部分工作交给了语义网关把人力省下来去做报表的排版、数据故事的包装和业务建议的输出。我并不是说 DatI 能直接替代专业的 BI 建模工具但把“口径统一”和“取数自动化”这两个问题解决掉BI 项目的交付速度和稳定性能上一个台阶这一点是实打实的。5. 踩坑记录与排查技巧实录5.1 提示词纯度不够SQL 花了甚至列名乱猜第一次跑通链路的时候我把整个表结构都放进了提示词包括字段名、字段注释、索引信息。结果模型在生成 SQL 时偶尔会直接使用物理表字段比如“channel”来回答渠道问题完全不记得我定义的同义词甚至编出不存在的字段名。后来我把“表结构”从提示词中去掉只留语义层字典和少量示例模型的表现反而稳定了很多。这背后的逻辑很简单语义层字典是抽象的、边界清晰的模型只需要做映射物理表结构信息量太大干扰了模型的选择空间。如果确实需要让模型查询物理字段应该也是通过语义层补齐的方式而不是把整张表的 DDL 扔进去。另外一个教训是LLM 调用本身有随机性同一个问题连续问三次可能生成三个不同的 SQL。DatI 采用了 temperature 设为 0、给定固定 seed 的方式大幅提升复现率。调参后相同请求生成的 SQL 基本收敛到同一份调试成本降低明显。5.2 同义词和列名歧义引发的“表里不一”业务里最典型的情况是“用户”这个词可能指客户表里的 customer也可能指操作员表里的 operator。自然语言如果没有上下文光靠同义词匹配很难判断。我第一次测试时用户问“用户数”DatI 直接匹配到了客户表但实际上他想要的是“活跃操作员数”。解决思路是在语义层引入“上下文优先级”当同一个词映射到多个指标或维度时按权重和最近使用频率排序并在中间表示里保留所有候选结果让 Agent 或用户做确认。DatI 里我实现了一个简单的消歧列表命中多个候选时会返回 candidates 字段而不是武断地选择其中一个。这也符合“语义翻译中枢”的定位不懂就带着候选向上游求证而不是假装全懂。遇到这种情况最怕的是静默选错。哪怕最终结果不对只要中间过程被记录、上报后面还可以通过日志分析去优化语义层的同义词配置形成正循环。5.3 分页、时区和数值类型映射问题数据库自带的类型映射在 Java 里不算难但细节很容易忽略。MySQL 的 decimal 字段在 JDBC 里默认会映射到 BigDecimal如果直接 JSON 序列化数字可能变成字符串导致前端图表算不了数值。我的处理方式是统一把数值型结果转成 double 或 long并在格式化层明确指定精度。时区问题同样隐蔽。业务系统存的时间一般是东八区但服务部署的机器可能是 UTC直连数据库时如果不指定连接参数查询时间范围可能整体偏移 8 小时。DatI 在数据源配置里强制指定连接时区并在时间维度处理时统一按目标数据库的时区语义生成 SQL不能在应用层乱转否则会引入双倍偏移。分页问题相对基础但我还是建议在语义层做统一约束比如默认最大分页数是 1000Agent 请求不允许直接传入一个超大 limit。曾经有人直接问“把 2024 年所有订单明细给我”生成的 SQL 没有 limit差点把拉宽表数据的内存撑爆。现在所有请求默认带上LIMIT 500并提示用户可以自行追加分页。5.4 慢查询与限流不让一条自然语言拖垮数据库LLM 生成 SQL 的随机性意味着即使语义层配置正确偶尔也存在一次低效查询比如对 2000 万行做了无过滤条件的聚合。DatI 的方案是多管齐下一条是 SQL 质量校验阶段预估扫描行数一条是执行前注入强制超时一条是慢查询自动熔断。实际项目中我把单个数据源的最大并发查询数配置为 5如果超过就排队等待而不是直接涌入数据库单条 SQL 超过 10 秒就强制终止并返回超时错误。同时把每条执行完的 SQL 记录下来维护一个慢查询列表定期根据日志优化提示词或语义层配置。这个设计在 Agent 场景尤为重要因为 Agent 常常会并行调用多个工具如果每个工具都放一条大查询进来数据库连接池瞬间被占满最后谁都跑不完。限流和排队看似牺牲了一点响应速度其实是保住了整体的可用性。6. 把网关接到 Agent 框架里以及我接下来的计划6.1 用标准化工具协议接入业务 AgentDatI 不绑定某个 Agent 框架我倾向于通过标准化工具协议方式接入。常见做法是基于 MCP 思路把 DatI 封装成一个“查询数据”的工具Agent 想查数时只需要调用这个工具传入自然语言描述和权限身份。DatI 返回结果后Agent 再去做分析、绘图或编排下一个动作。这样一来Agent 不需要关心指标字典是什么、权限怎么算、SQL 怎么写这些底层能力都被网关接走了。目前我已经在内部项目里尝试把 DatI 接入基于 Java 的 AI Agent 框架只需约 50 行代码就能注册成一个工具调用稳定性和可维护性都明显优于让 Agent 直接访问数据库。工具接入时要注意入参和出参的约束入参只需要 question 和 user_token出参则包含 data、sql、latency、trace_id。这样 Agent 能拿到“人话”和“SQL 真相”两份信息即使模型在分析时出了偏差最终结论也有据可查。6.2 评测集给 Text-to-SQL 做回归测试做语义网关最怕的不是功能不够炫而是“改了 A 指标B 查询悄悄坏了”。所以我从第一个内部版本开始就维护了一套评测集合把历史查询中出现过的自然语言问题整理成回归用例每次修改语义层或提示词模板后都必须跑一遍。评测集大概包含 60 条左右的典型问题覆盖趋势、占比、排行、同环比、筛选、明细查看。自动化流水线会逐个调用 DatI 的调试接口把生成的 SQL 和预期 SQL 做结构化对比比如对比查询表、指标、维度、过滤条件、时间范围统计准确率。准确率低于 95% 时发布流程会被自动卡住。这套回归测试帮了我很大的忙有几次我在优化同义词规则的时候把“环比增长率”的指标映射改偏了传统调试方法可能要看半天日志才发现评测集直接一秒抓了出来。想长期维护好 Text-to-SQL 系统评测集绝对不是可有可无的而是基础设施。6.3 下一步从被动取数走向主动数据服务跑通“自然语言查数”之后我在想的一个方向是把 DatI 从“等待问题”的被动取数工具升级成“主动发现问题”的数据服务。比如监控指标连续三天下跌DatI 可以主动推送一份 AI 分析报告说哪些区域、哪些渠道的下降贡献最大并附带可验证的明细 SQL。这个方向对语义网关的要求更高不只是执行查询还要理解指标变化背后的维度拆解逻辑。但核心仍然是同一份语义层指标、维度、权限、脱敏规则全都复用只多了一层自动化巡检的调度器。从 BI 到 Agent 的转变实际上就是“度量的定义权”从人手里逐渐转移到中央化、可解释性的服务层让数据能被更多角色、更多场景直接消费。我个人的体会是做这类系统不要一开始就追求“全自动”先把口径、权限、审计、评测这些地基打扎实再考虑自动化。地基稳了上面搭什么角色都接得住地基不稳AI 再聪明也只是把错误放大得更快而已。如果你也在做 Agent 或者 BI 相关项目建议可以先从语义层和权限层入手把“是不是真的理解了业务需求”这件事用能验证的方式确认下来。这一层做扎实后再去对接模型和 Agent你会发现所有上层的事情都顺了很多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。