大模型直接写 SQL 在企业数仓里为什么不稳定?NL2LF2SQL 三层架构的工程解法

0 阅读16分钟

导语

企业级 NL2SQL 的天花板在哪?亿问 Data Agent 自研语义层 SemanticDB,用实体-事件建模业务世界,通过 NL2LF2SQL 三层架构把业务意图稳定翻译成可信查询,从机制上杜绝幻觉,让每个数字可追溯。

亿问 Data Agent 是一款面向企业经营分析场景的私有化数据分析 Agent,通过自研的NL2LF2SQL引擎杜绝幻觉,帮助业务分析师高效生产可信分析报告。

上一期,我们拆开了亿问 Data Agent 的产品全貌——一个敢在经营分析会上使用的 Agent,需要同时跨过业务语义、数据可信与闭环交付三道关。其中反复出现的一个关键词是"自研的NL2LF2SQL引擎"。

从这期开始,我们进入技术解析。第一个要讲的,是整个系统的地基——语义层 SemanticDB:它为什么存在,解决什么问题,以及为什么"让大模型直接写 SQL"这条看起来最短的路,在真实企业场景里走不通。


这篇文章讲什么

在亿问 Data Agent里,我们自研的SemanticDB 承担语义层的角色。

本篇文章介绍了 SemanticDB 为什么是亿问 Data Agent的核心基础设施:它如何用实体和事件表达业务世界,如何用 LogicForm 承接自然语言理解结果,如何把业务意图稳定地落到 SQL、API 或 URL 等不同执行方式,以及为什么这条 NL2LogicForm2SQL 路线比直接 NL2SQL 更适合真实企业场景。

图片

亿问 Data Agent 不直接把自然语言翻译成 SQL。我们的整体链路是:

自然语言 -> LogicForm -> SQL

其中,自然语言到 LogicForm 由亿问 Data Agent的上层理解模块(Alisa)完成。

语义数据库 SemanticDB 负责两件事:

  1. 用实体和事件表达业务世界。
  2. 把 LogicForm 稳定地翻译成不同数据库可以执行的查询。

图片

所以,SemanticDB 不是 SQL 生成器前面的一个"薄封装",而是亿问 Data Agent能不能稳定理解业务、复用口径、跨库执行的基础。

为什么不是直接 NL2SQL

直接 NL2SQL 看起来路径最短:用户问一句话,系统生成一段 SQL,然后去数据库执行。

但真实业务里,问题很少这么简单。

用户说"销售额""门店""本月""新客""同比",这些词并不等同于数据库里的某个字段。它们背后通常有固定的业务口径、跨表关系、时间规则、权限限制和数据库方言差异。

如果直接让大模型生成 SQL,大模型就必须同时完成几件事:

  • 听懂用户真正想问什么。
  • 找到相关的数据表和字段。
  • 判断指标口径。
  • 推理表之间的关系。
  • 处理时间范围和同比环比。
  • 生成特定数据库能执行的 SQL。

这条链路太长,任何一步错了,最后的答案都会错。更麻烦的是,很多错误不是 SQL 执行失败,而是 SQL 能执行,但业务含义错了。

语义层要解决的,就是把这些不应该临场猜测的东西提前沉淀下来,让大模型不再直接面对混乱的物理数据结构,而是面对一套更接近业务语言的结构化表达。

用实体和事件表达业务世界

我们的语义层采用实体和事件的方式建模。

图片

实体表示相对稳定的业务对象,例如:客户、产品、门店、品牌、区域、组织。

事件表示围绕这些实体发生的业务事实,例如:销售、下单、访问、支付、入库、出库。

这种建模方式接近业务人员理解世界的方式。业务不是围绕数据库表发生的,而是围绕"谁、在什么时间、对什么对象、发生了什么事"展开的。

例如,"客户购买产品"是一类事件;客户和产品是实体,购买是事件。再比如,"门店产生销售"也是事件;门店是实体,销售是事件。

图片

当系统用实体和事件表达业务世界后,很多自然语言问题就会变得更清晰:

  • "华东地区女装销售额"可以理解为:在销售事件中,找到门店所属区域和产品品类,再统计销售额。
  • "最近三个月新客数"可以理解为:围绕客户实体和首次发生的相关事件,按时间窗口统计。
  • "各城市门店销售排名"可以理解为:销售事件关联门店实体,再沿着门店的地理关系聚合。

这里的关键是:系统不再把问题看成"从哪几张表查什么字段",而是先把问题理解成"哪些业务对象之间发生了什么关系"。

语义层表达的不只是字段

传统数据字典通常告诉我们:某张表里有哪些字段,每个字段是什么类型。

语义层要表达的更多。

图片

它需要知道一个业务对象有哪些属性,也需要知道这些属性在业务上意味着什么。例如:

  • 一个字段是金额,还是普通数字。
  • 一个字段是比例,还是可累加指标。
  • 一个字段是地址,还是普通文本。
  • 一个字段是否有枚举值。
  • 一个字段是否表示层级,例如区域、省、市、区县。
  • 一个对象是否有生命周期,例如门店开店和关店。

这些信息对查询结果非常重要。

如果系统知道"销售额"是金额,它就可以按金额指标处理;如果系统知道"区域"有层级,它就可以支持按省、市、区县聚合;如果系统知道某些存储值只是内部编码,它就可以在结果里还原成业务人员能读懂的名称。

图片

所以,语义层不是简单给数据库字段起中文名,而是让字段进入业务语境。

LogicForm 是中间语义表达

在亿问 Data Agent的方案里,自然语言不会直接变成 SQL,而是先变成 LogicForm。

可以把 LogicForm 理解成一种中间语义表达:它既不是自然语言,也不是数据库 SQL,而是结构化地描述"用户想查什么"。

图片

它表达的重点包括:

  • 要查询哪个业务对象或事件。
  • 用什么条件筛选。
  • 需要哪些指标。
  • 按什么维度分组。
  • 如何排序、分页或限制结果。
  • 是否需要同比、环比、占比、排名等分析逻辑。

例如,用户问"今年华东地区女装销售额及同比最高的 10 个省市",SemanticDB 里的 LogicForm 可以写成这样:

{
  "schema": "sales",
  "query": {
    "产品": {
      "schema": "product",
      "query": {
        "品类": "女装"
      }
    },
    "店铺_地址": {
      "schema": "geo",
      "query": {
        "name": "华东地区"
      }
    },
    "日期": {
      "$offset": {
        "year": 0
      }
    }
  },
  "preds": [
    {
      "pred": "销售额",
      "operator": "$sum",
      "name": "总销售额"
    },
    {
      "pred": "销售额",
      "operator": "$yoy",
      "name": "销售额同比"
    }
  ],
  "groupby": {
    "_id": "店铺_地址",
    "level": "省市"
  },
  "sort": {
    "总销售额": -1
  },
  "limit": 10
}

这段 JSON 看起来比自然语言复杂,但它表达的能力也更强。它不是 SQL,也不是给业务人员手写的格式,而是自然语言和数据库查询之间的中间语义。它可以同时表达业务对象、跨对象筛选、时间条件、聚合指标、同比分析、分组层级、排序和结果数量。LogicForm 能表达得越丰富,亿问 Data Agent能够稳定覆盖的业务问题也就越多。

这种中间表达的好处是,上游模型只需要生成业务意图,不需要直接拼 SQL;下游执行器再根据语义模型、时间规则、数据库类型,把它翻译成真正可执行的查询。

这就是 NL2LogicForm2SQL 的核心:让模型负责理解,让语义层负责约束,让执行层负责落地。

从 LogicForm 到 SQL,语义层做了什么

LogicForm 不是简单套模板生成 SQL。中间会经过一系列语义处理。

图片

1. 把不完整表达补成可执行表达

用户问题和上游模型生成的结果,往往不会包含所有执行细节。

例如,"销售额"可能需要被理解为某个金额指标的求和;"今年"需要变成明确的起止时间;"按区域"可能需要找到业务对象之间的地理关系。

语义层会把这些简略表达补全,变成可以执行的结构。

这一步非常重要,因为它降低了对大模型输出格式的苛刻要求。上游只要表达对了业务意图,语义层就能继续把它整理成稳定的查询。

2. 统一时间语义

时间是业务分析里最容易出错的地方。

"今年""本月""截至目前""同比""环比"这些词看起来简单,但在企业数据里往往涉及数据更新延迟、统计周期、表粒度和业务口径。

例如,今天是 9 月 10 日,但销售数据只更新到 9 月 9 日。那么"今年销售额同比"应该比较到 9 月 10 日,还是 9 月 9 日?

如果没有统一规则,每次都让模型临时生成 SQL,很容易出现同一个问题不同次回答口径不一致。

语义层会把这些时间表达统一处理,让不同问法、不同指标、不同数据表尽量遵循一致的时间口径。

3. 处理跨业务对象的关系

很多问题天然是跨对象的。

用户问"上海各区县销售额",底层可能需要从销售事件关联到门店,再从门店关联到地理区域。用户不会说这些关联关系,也不应该要求用户说。

语义层提前定义了业务对象之间的关系,所以执行时可以沿着这些关系找到正确路径。

这让跨表查询从"模型临场猜 Join"变成"系统按业务关系推理"。

4. 拆解复杂指标

一些指标不是单条简单查询能完成的。

例如:

  • 同比需要当前周期和同期周期。
  • 环比需要当前周期和上一周期。
  • 占比需要分子和分母。
  • 达成率需要实际值和目标值。
  • 多指标分析可能涉及不同业务对象。

语义层会把复杂指标拆成多个可执行部分,再把结果合并回来。

这相当于在查询执行前做了一次语义层面的规划:先理解一个复杂问题由哪些子问题构成,再决定如何执行和合并。

5. 适配不同数据源

企业里的数据源往往不止一种。

同样的业务问题,底层可能落在 ClickHouse、MySQL、PostgreSQL、Oracle、SQL Server、StarRocks 或其他数据库上。不同数据库在时间函数、正则、数组、子查询、分页、聚合语法上都有差异。

语义层把上层业务表达和下层执行方式隔离开。

大多数场景下,SemanticDB 会把 LogicForm 翻译成 SQL;但这不是唯一形态。有些数据并不来自传统数据库,而是来自业务 API、外部服务或 URL 型数据源。对于这类场景,同一套 LogicForm 也可以被转换成 API 请求或 URL 查询。

上游只需要表达业务问题;至于最终应该生成 SQL、调用 API,还是拼出 URL 查询,由语义层根据数据源类型处理。

这样,亿问 Data Agent就不会被某一种数据库或某一种访问方式绑定住。

6. 把结果还原成业务可读的数据

数据库返回的是原始结果,但用户需要的是业务结果。

有些数据库里存的是编码,展示时应该还原成业务名称;有些分组字段背后是一个实体,展示时不应该只是一个 ID;有些时间、金额、比例、层级字段,也需要按业务语义进行包装。

所以语义层不仅负责生成 SQL,也负责让查询结果重新回到业务语义中。

最终返回给上层的,不只是数据库查出来的行列,而是带有业务解释的数据。

有语义层之后,NL2SQL 的问题变小了

严格来说,我们并不是在做传统意义上的 NL2SQL。

我们是在把问题拆成两段:

NL2LogicForm:理解业务意图
LogicForm2SQL:稳定执行业务意图

这样做之后,难度被重新分配了。

图片

大模型不再需要一次性完成所有事情。它主要负责理解用户语言,并选择合适的业务对象、指标、维度和时间条件。

语义层负责:

  • 约束可用的业务概念。
  • 复用统一的指标口径。
  • 推理对象之间的关系。
  • 处理时间规则。
  • 适配数据库差异。
  • 包装业务结果。

这使得系统更稳定,也更容易治理。

语义层带来的几个核心优势

优势一:减少模型自由发挥

直接 NL2SQL 时,模型面对的是整个数据库结构。表名、字段名、关系路径、指标口径都可能需要模型猜。

有了语义层,模型面对的是经过整理的业务世界。它需要选择业务概念,而不是临时发明 SQL。

这会显著降低幻觉空间。

优势二:业务口径可以复用

没有语义层时,很多业务规则只能写在提示词里。提示词可以提醒模型,但不能真正保证一致性。

有了语义层后,指标、时间、关系和结果解释都可以沉淀成系统能力。

这意味着同一个指标在不同问题、不同报表、不同用户那里可以复用同一套口径。

优势三:跨表问题更可靠

NL2SQL 最容易错的地方之一,就是跨表关联。

语义层提前描述业务对象之间的关系,系统知道销售和门店、产品、客户之间如何关联,也知道门店和区域之间如何关联。

因此,用户问跨对象问题时,系统可以走业务关系,而不是让模型临时猜连接路径。

优势四:时间分析更一致

经营分析中,时间口径一旦错,结论就会错。

语义层把"今年""本月""同比""环比""截至目前"等表达统一处理,避免每次都让模型单独生成时间条件。

这对 BI 类问题尤其关键。

优势五:数据库变化对上层影响更小

上层表达的是业务意图,不是某一种数据库的 SQL。

当底层数据源变化,或者同一套业务模型运行在不同数据库上时,语义层可以承担方言适配工作,上层自然语言理解部分不需要大幅调整。

这让系统更适合多项目、多数据源、多客户环境。

优势六:更容易测试和排错

有了 LogicForm 作为中间表达,系统就不再是一个黑盒。

当结果不符合预期时,可以逐层判断:

  • 自然语言是否理解错了?
  • 业务对象或指标是否选错了?
  • 时间条件是否归一化正确?
  • 跨对象关系是否走对了?
  • SQL 生成是否符合数据库方言?
  • 结果包装是否符合业务预期?

这比直接检查一段模型生成的 SQL 更可控。

从 Agent 视角看,语义层真正提供了什么

对亿问 Data Agent的上层智能体来说,SemanticDB 提供的不是一个普通查询接口,而是一个可执行的业务语义运行时。

图片

它提供的是:

  • 一个稳定的业务世界模型。
  • 一套结构化的中间语义表达。
  • 一条从业务意图到数据库查询的执行链路。
  • 一层对数据库差异的屏蔽。
  • 一套可持续沉淀的业务口径资产。

所以,上层模型真正要做的事情,不再是"直接写对 SQL",而是:

  1. 识别用户问题中的业务意图。
  2. 选择正确的业务对象、维度、指标和时间条件。
  3. 生成合格的 LogicForm。

剩下的跨表关系、时间对齐、数据库方言、复杂指标拆解、结果包装,都由语义层处理。

这就是 NL2LogicForm2SQL 相比直接 NL2SQL 更工程化的地方。它不是让模型一次性做完所有事,而是让模型做它擅长的语义理解,把结构化执行交给系统。

总结

语义层不是为了让 SQL 生成多一层流程,而是为了把数据问答从"一次性生成"升级成"可建模、可复用、可测试、可治理"的系统。

它把底层物理数据抽象成业务世界,把自然语言问题收敛成结构化意图,再把结构化意图稳定翻译成数据库查询。

对一个真正要在复杂业务场景里工作的亿问 Data Agent来说,SemanticDB 这样的语义层不是可选项,而是系统成立的前提。

下一篇:Alisa

和传统指标中台里的 MQL 相比,LogicForm 的表达能力更高一个级别。

它不只是表达"取哪个指标、按哪个维度聚合",还可以表达更复杂的分析结构。例如:

  • from 嵌套查询能力,用来表达先查询、再基于中间结果继续分析的场景。
  • piecewise 动态分组能力,用来表达按业务规则临时切分区间或分组的场景。
  • currency 汇率计算能力,用来表达多币种、多汇率口径下的金额分析。
  • 同比、环比、占比、达成率、跨对象筛选等更复杂的业务分析逻辑。

这也带来了下一个关键问题:既然 LogicForm 的表达能力更强,它的语法和语义也更复杂,那么在 NL2LogicForm 阶段,大模型如何稳定生成正确的 LogicForm?

进一步说,当一个企业有数百张表、成千上万个字段、复杂的业务对象关系和大量指标口径时,大模型又如何突破上下文长度限制,准确找到和当前问题相关的 schema 知识?

这就是亿问 Data Agent的另一个核心系统要解决的问题。

下一篇文章,我们会介绍 Alisa:它如何在复杂 schema 空间中选择正确语义上下文,以及如何让 NL2LogicForm 从"看模型发挥"变成一个稳定、可控、可持续优化的工程系统。


下期预告

语义层定义了业务世界的结构,LogicForm 定义了查询的语义。但 LogicForm 不会凭空出现——用户说的是"华东女装今年怎么样",不是一段 JSON。

下一期,我们回答一个更前面的问题:NL2LogicForm 这一层到底为什么必须存在? 如果没有它,系统会怎样失稳?有了它之后,企业问答从"能演示"到"真能用",中间到底跨过了什么?敬请期待。