SaaS · 查询引擎 · LLM 应用复盘
从点选式的分步配置,到接入大模型的一句话查询
发布时间:2026-09-03 17:02
本文完整复盘一个 SaaS 低代码平台上「多表复合查询」功能的两次演进——第一代用单表查询 + ID 集合回灌(拆掉 JOIN)在多租户下实现可控的跨表查询,却在半年后被验证「能力到位、门槛过高」;第二代引入大模型做自然语言入口,复用原有引擎,并通过两段式路由、JSON 修复重试、KV 双向转换等一组工程护栏,让 80% 以上的多表查询一句话跑通。
文中所有表名、字段均已脱敏为通用中性名词(订单、客户、商品、仓库…),聚焦方法论、设计权衡与工程经验,不依赖任何具体行业,可自由迁移适用。
关键词低代码 / 无代码LLM 应用自然语言查询多表查询提示词工程SaaS 多租户从 SQL 到 AI
READING GUIDE
本文适合以下读者,并期望你至少带走其中一点:
关注「在一个成熟 SQL 底座上,如何用单表查询组合出跨表能力」,以及两段式路由如何化解超长提示词;可重点看第 2、5 章。
关心「如何给不可靠的 LLM 输出套护栏」,以及与大模型反复打磨的实战细节;可重点看第 4、6 章。
想理解「为什么点选式低代码门槛高、以及 AI 交互为何能拉平普惠」;可重点看第 3、7、8 章。
如果你时间有限,只看「TL;DR」+ 第 6 章(十个坑)即可掌握这篇文章最硬核、最可复用的一部分。
PROLOGUE
我们在超兔一体云平台上,开发低代码交互的查询工具。支撑业务方跨表分析的核心组件,就是我们围绕多表复合查询引擎做的两代开发:从点选式的分步配置,到接入大模型的自然语言查询。作为研发该引擎的技术人员,我们在前一代沉淀了一套成熟的查询底座,又在后一代把这层交互整个交给了大模型。业务方在平台上搭表、搭表单、搭流程,最终沉淀出一套几十张相互关联的业务表。随着业务跑起来,一个高频诉求浮出水面:
「把(条件A)的(订单),关联上(条件B)的(客户),看看谁…」
这类查询有几个共同特征:
在关系型数据库里,这种需求最「标准」的答案当然是 JOIN。但在「一套库养几百上千个租户」的 SaaS 架构里,JOIN 并没有想象中美好。这正是我们第一代方案动刀的起点。
而站在更大的时间轴上看,这两代方案恰好把过去几年两条最受关注的技术路径——低代码/无代码的「点选式」与大模型的「一句话式」——在同一个功能上完整地各走了一遍。行业里围绕二者的争论有个尖锐的落点:低代码/无代码本就不是给终端用户用的,它帮开发者把「写代码」降级成「点界面」,却把「点界面」这件靠人手工、因硬编码在前端而难以被工具自动复制的事留了下来,因而在大模型时代几乎找不到立足之地。我们这个发生在普通用户身上的小切面,恰恰能在最贴近业务的一层印证这个判断——这是第八章要展开的核心观点,这里先按下不表。
GENERATION ONE
对单租户自建系统,JOIN 是家常便饭;但对 SaaS 多租户,我们反复权衡后决定避免把多表 JOIN 直接下推到数据库,理由有三个:
| 顾虑 | 说明 |
|---|---|
| 资源占用 | 条件不固定、跨多表 + 大字段(如明细、富文本)JOIN,很容易把库压垮;一个租户的「极端查询」可能拖垮同实例上的其他租户。 |
| 慢查询/死锁 | 高并发写多的系统里,复杂 JOIN 和主从复制之间存在锁竞争风险。 |
| 隔离与公平 | 多租户场景更在意「把负载控制在查询维度、按租户隔离」,而不是让数据库引擎去赌索引和连接策略。 |
一句话概括我们的取舍哲学:用计算时间换数据库负载,把复杂的关联从 SQL 层抬到业务逻辑层。这在多租户下是更「温和」、更可控的做法。
不去 JOIN,却要获得等价的跨表结果,秘诀是用多张表的单表查询 + 内存中的 ID 集合拼接。核心套路三步走:
关联ID IN (集合) 的形式回灌成主表的主键过滤条件,逐级缩小主表结果。这样每一层查询都是标准单表查询,数据库可以正常走索引、分页、缓存;复杂的关联分析全部发生在应用层,且天然按租户隔离。
我们把它形象地叫做 「IN 管道」:上一级的 ID 集合变成下一级的 IN 条件,一级一级传下去,直到回到主表。
要做到「程序化地点选、无关代码地组合」,光有查询引擎还不够,还需要一份描述「这张表能提供什么」的元数据,我们叫它表档案(Table Profile)。它是一份结构化 JSON,记录了:
表档案是整个体系的「唯一事实来源」(Single Source of Truth)。查询引擎本身是通用的、不感知业务;一切「这张表有哪些字段、字段是枚举还是人名」都从档案里读。这意味着加一张新表,只需要新增一份档案,不碰引擎代码。
用户在界面上的每一次点选,背后都走这样一条链路:
做到的:一个通用引擎,能覆盖非常多条件不固定的多表组合查询,且每层都是单表查询,负载可控、租户隔离清晰、可加表不改引擎。我们当时对它很满意。
留下的:它把「能力」做到了,却把「门槛」也留下了——这个门槛最终决定了一代产品的命运,见下一章。
THE COLD WATER
功能在 3、4 月上线。到 7、8 月回看数据,我们得到一个冷静的事实:
这个功能只有一些「深度用户」和「粉丝级用户」在用。
用的人:理解表之间关系的老用户,愿意啃界面、一层层配关系。这批人是真正的受益者。
不用的人:占绝大多数的普通用户。不是功能跑不起来,而是他们一眼看不懂、也不想学。
我们复盘出,门槛由三层「认知税」构成:
更扎心的对比是一句话:普通用户要表达的诉求,一两句话就能说清楚;但用点选界面把它配出来,可能要花 2 分钟左右,还要夹带上面那些认知。价值(能查复杂数据)是真的,但获取价值的成本把绝大多数人挡在了门外。
回看大模型之所以能快速普及,本质不是它逻辑多强,而在于两点交互特质:
即便我们清楚大模型是靠「概率生成」而非「严格推理」,但用户认可的是交互本身——低门槛、说人话、即时反馈。对我们的启示非常直接:
既然查询引擎(第一代)已经成熟可靠,那 AI 方案不需要再造一个引擎,只需要在前面加一个「听得懂人话的大脑」,把自然语言翻译成引擎能吃的结构化查询要求。复用已踩过坑的成熟引擎,用 LLM 替换「点选」这层输入。
GENERATION TWO
第二代在整个系统里只做了两个大的环节:
分析用户自然语言描述的多表查询逻辑,结构化为查询要求。
第一代已经打磨成熟的复合查询引擎,原封不动接上。
这避免了「为 AI 重造轮子」,是第二代能快速落地、且一上来就有 80% 命中率的关键。
这是整个 AI 方案的「翻译官」,也是和大模型博弈最激烈的地方。它要产出类似这样的结构化查询要求(已脱敏):
json{
"mainTable": "商品",
"conditions": [
{ "field": "库存", "op": "gt", "value": 0 }
],
"steps": [
{
"relationTable": "订单",
"via": "商品ID",
"condition": {
"field": "下单日期",
"op": "within",
"value": "近90天"
}
}
]
}
翻译官不是一次就能做好的。它和大模型的每一次握手都可能失败,于是我们在其后挂了 修复 + 校验 + 重试 三层保险(详见第六章)。关键词:不要把大模型的「一次输出」当成可靠输入,而要把它当成一个需要持续对齐、校验、纠偏的子系统。
有了结构化查询要求,剩下就是第一代引擎的主场:
这一点特别重要:LLM 只是把话说清楚,它一次 SELECT 都不会执行;真正干活、兜底正确性的,仍是那套验证过的查询引擎。
TWO-STAGE ROUTING
这是在我们认为第二代「已经完成 80%」时,被迫做、也做得最值的一次改造。
第二代初期,做法相对朴素:把所有表的档案一股脑塞进提示词,让大模型自己去挑。平心而论,这套做法在体验上是合格的——我们选用的是非思考的快速模型,单次生成在 5~8 秒之间,响应速度没有问题。真正的导火索出现在表数量到 25 张左右时:提示词体积逼近 3 万 token,在智能体平台侧触及了上下文限额。超限带来两个问题:
我们意识到:「把所有表都喂给模型」这条路,是条死路。
改造后的核心动作,是把一次问答拆成两段:
只给大模型一张「表名 + 一句话说明」的轻量索引 + 表关系图,让它召回相关表。这里我们刻意采用「宁多勿少(recall more than necessary)」策略——多召回不致命,漏召才是致命的。
只把第一段选中的那几张表的完整档案注入提示词,生成精确的查询计划。
这段逻辑把「表的数据」和「大模型的分析结构」彻底解耦了。改造后,智能体人设稳定在 8000 字出头且不再随表数量增长;无论未来是 40 张、50 张还是 100 张表,第一段都只看一层轻量索引,提示词长度基本恒定;第二段则永远只关心「和本次问题相关的那几张」。扩表从此不再受提示词长度约束,同时也因为单次注入的档案变短,降低了长提示词带来的输出漂移。
两段式有个隐患:LLM 按直觉选表,但表之间可能不是直接关联,需要中间表搭桥。例如用户问「近 90 天下单的客户」,模型第一段可能只召回「客户」和「订单」,但两者实际要通过「订单_明细」甚至「订单_商品」间接关联。
于是我们在表路由后加了一个 闭合(closure) 步骤:拿到召回表集合后,基于预生成的「表关系图」,用 BFS / 最短路径 自动把缺失的中间表补进集合,保证路由结果在关系上「自洽」。
完整的「用户提问 → 结构化 → 执行」时序如下(标注了每一步的兜底/重试):
TEN PITFALLS
如果说第一代的技术主旋律是「架构权衡」,那第二代的主题就是「和概率生成做纠错」。这一章集中复盘我们踩过的坑和对应的工程化解法。
先给出全貌:LLM 输出从「不可靠」到「可用」,中间隔着一条完整的护栏流水线——
后面的十个坑,本质上都是这条流水线上某一段的具体实现细节。
坑:大模型输出的 JSON 时常携带全角标点(: , {)、或结构末尾多一个逗号,导致 JSON.parse 直接崩。
解法:在解析前增加一个 _repairJson:把全角标点转半角、去掉字符串外的尾逗号。这能静默修复掉很多小毛病,把「偶发失败」挡在解析的最前面。
js// 示意:先用宽松手段修复,不行再进入重试路径
function repairJson(raw) {
return raw
.replace(/[\uFF0C\uFF1B\uFF1A]/g, (c) => ({
'\uFF0C': ',', '\uFF1B': ';', '\uFF1A': ':',
}[c]))
.replace(/,\s*([}\]])/g, '$1'); // 去尾逗号
}
坑:修复后仍然解析失败,或者字段非法。
解法:不再盲目整体重试,而是把错误位置 + 前后 40 字上下文回传给大模型,让它「看着现场改」。带现场的重试比无脑重说一遍的命中率高得多。
坑:用户说「近 90 天」,模型可能把它解析成错误的起始日期,或干脆编造一个不存在的时间 token(如 last_30_days),导致查错范围。
解法(两招并用):
[当前系统日期:YYYY-MM-DD],让模型能正确地算相对日期;坑:枚举/人员/选择类字段库里存的是数字 K(如 上海≈25),但用户说的是中文 V(上海),且查询(WHERE)和结果展示(结果侧)两个方向都要转,方向正好相反。
解法:做成双向转换,并且规则完全对称:
| 方向 | 目的 | 手段 |
|---|---|---|
| WHERE 侧 V→K | 把用户/计划里的中文筛选项转成库里的存储值再打库 | 走字典/名称接口取到 {key, value},把 V 映射成 K |
| 结果侧 K→V | 把库里返回的数字/ID 翻译成用户看得懂的展示值 | 主表取数后用字典翻译枚举/人员/选择ID,多选按逗号拆分、用「、」重新拼接 |
这个转换是查询正确性和展示可读性最后的护城河,做了以后,细节瑕疵(如个别值没映射)就地保留原值并打日志,不阻塞整条查询。
坑:如果直接取翻译后的展示值回来做过滤,会把 K→V 和 V→K 搞乱。
解法:主表取数要求带原始存储值(接口 flag=1),返回后才能统一在结果侧做 K→V 翻译。取数用原值,展示才做二次翻译,方向清晰不打架。
坑:为了让模型能做「计数」类统计,把所有表都注入了 id 字段,但模型可能误把它当普通筛选条件用,造成错误查询。
解法:在档案里对 id 明确标注 「仅用于聚合 count,不可作为筛选条件」,并把这条规则也写进人设,从源头约束。
坑:提示词模板里曾出现两个替换占位符指向同一份表档案,结果档案被双份注入,token 直接翻倍(曾观察到 2.7 万+ token 的超大提示词,定位修复后回落到 1.5 万左右),既浪费又加重漂移。
解法:收敛模板,全篇只保留文末一个档案占位符,从根上杜绝双份注入。token 用量显著回落。
坑:如果档案内联在人设里,每次加表都要重排人设,且不同入口容易不一致。
解法:人设常量、档案动态注入。智能体人设只写固定的行为规则与输出约束,表档案作为每次请求的变量注入;加表只改档案、不改人设,天然保持一致。这也让「人设」从一串会随业务膨胀的长文本,固化成一个稳定、可维护的常量。
坑:推理型(思考型)模型单次出计划可能要几十秒甚至更久,普通用户等得不耐烦;且超时/网络错误如果不处理,就是一段看不懂的报错。
解法:给请求设置较长且可调的超时时间(推理模型需要在 60~120 秒量级),并把超时、网络错误统一包装成中文、清晰、常驻(不随操作消失)的提示,告诉用户「正在等模型,或已超时」,而不是甩一段 stack trace。一个附带经验:如果对延迟敏感,尽量选非思考的快速模型——我们后来就为此从思考模型切换到了去思考的快速模型。
坑:初版把所有失败都叫「解析失败」,用户和模型都分不清问题到底出在「JSON 格式」还是「表/字段本身不存在」,纠偏没有方向。
解法:把错误明确分成两类——「JSON 语法错误」 与 「表档案校验失败」,让系统向模型分别回传不同的反馈提示,纠错更有方向。
IMPACT
把两代做下来,简单算一笔账:
| 第一代(点选式) | 第二代(一句话) | |
|---|---|---|
| 输入方式 | 逐层选表、配关系、加条件 | 一句自然语言 |
| 表达成本 | 约 2 分钟的连续点选 + 认知税 | 10 秒内说清诉求 |
| 能力上限 | 支持任意复杂条件组合 | 80%+ 的常用复合查询直达 |
| 用户构成 | 深度用户 / 粉丝级用户 | 全量普通用户 |
| 扩展性 | 加表不改引擎 | 加表不改引擎、不改人设 |
第一代把「复杂多表查询」从「要写 SQL」降级成「点选配置」,能力到了,但对多数人门槛仍在;第二代把「点选配置」再降级成「说一句话」。实测下来,至少 80% 的多表复合查询,通过自然语言描述就能直接跑通。
80%+
多表复合查询,一句话直接跑通
5~10 倍
门槛拉平后的潜在普惠人群
而这 80% 还只是「存量用户」口径下的统计。如果把 LLM 交互带来的用户增量算进去,这个功能的潜在普惠人群可能提升 5~10 倍——因为门槛被拉平,普通用户第一次敢用了;而且随着口碑扩散,使用数据会呈现扩散式增长。
从项目价值的角度,我们认为第二代是一次成功且很有意义的升级探索:它没有推翻第一代的引擎,而是在一个已验证的底座上,用 LLM 替换了「门槛最高、价值最薄」的交互层,把一项「高手专属」能力变成了「人人可用」能力——这正是「以降门槛换取普惠」的典型样本。
THE ERA VERDICT
第一章末尾我们埋了个伏笔——这两代方案不是一次普通的升级,而是把两条时代路径在同一个小切面上各走了一遍。现在回到这个视角展开。
如果只把这两代方案放在「一个查询功能」的尺度里看,它只是一次成功的升级。但把「点选配置」和「一句话查询」这两类交互方式放到更大的时间轴上,它恰好构成了过去几年低代码/无代码浪潮与今天AI 浪潮的一个缩影。
先把术语对齐:
前者强调「配置」,后者强调「表达」。表面看只是操作方式不同,本质上是把人的心智负担,从「理解机器逻辑」转移回了「用人的语言说话」。
最近很多开发者唱衰低代码/无代码,理由并不是「能力不够」,而恰恰是它的交互方式注定的。归纳下来有三点:
| 症结 | 说明 |
|---|---|
| 受众错位 | 低代码/无代码严格说不是给终端用户用的,它是帮开发者/搭建者快速实现能力的工具。一旦交给「不懂表结构的普通用户」,仍然要求对方理解领域模型,门槛并没有消失。 |
| 不可自动复制 | 点选操作本质是「人手工点击界面」,而很多界面级逻辑都硬编码在前端,很难像普通 API 那样被 Cursor 这类 AI 编程工具或 MCP 这类标准协议抽象、接管、批量复制。 |
| 重度依赖人工 | 哪怕你脑子里想得再快再清晰,也必须一步不落地点一遍。这种「人肉点按」无法随需求扩张而自动化,也扛不住规模化复制。 |
一句话:低代码/无代码把「写代码」降级成了「点界面」,但「点界面」这件事本身,恰恰是 AI 最难以高效复制的那种劳动。
当 AI 编程(Vibe Coding)可以用自然语言直接生成、修改、调用代码时,低代码/无代码「降低编码门槛」的价值几乎被覆盖。绝大多数过去非用低代码/无代码不可的场景,现在都能被 AI 编程彻底颠覆。
它真正残留的空间,大概率只有很小范围、做配置升级/配置优化的局部——而且即便如此,也更可能以「被 AI 驱动」的方式存在,而不是以「人手工点选」的方式存在。
有意思的地方在于:行业里这场争论大多围绕开发者展开——低代码/无代码对开发者失去了吸引力。而我们的案例发生在终端用户层:
即便不是开发者、只是普通业务用户,「点选配置」和「一句话查询」之间的差距依然大到天壤之别。
第一代把 SQL 降级成点选,对「非开发者」门槛依然高;第二代用一句话平掉门槛,才真正普惠了终端用户。这补上了两件关键证据:
所以,如果给这两代方案找一个时代注脚,它可以浓缩成一句话:
低代码/无代码改变了「开发者怎么搭」,而大模型改变了「每个人怎么用」——前者是工具升级,后者是交互范式的换代,也就是技术的时代进步。
EPILOGUE
把第 8 章放回工程语境,两代演进可以浓缩成三步棋:「拆掉 JOIN、换掉入口、再加上大脑」。
展望上,我们下一步会往三个方向走:支持更多表的更广业务域、沉淀高频查询为「常用查询」注入、探索让 LLM 不只是翻译查询、还能辅助解释结果。而这些,都已站在两代方案共同打下的地基上。
如果你也在做低代码平台的查询、或正想把 LLM 接入某个成熟功能,希望本文的「两个阶段、两次架构决策、十个工程坑」能给你一个可复制的心智模型:
先问自己:要新增的是「引擎」还是「入口」?如果想降的是门槛,那大概率是后者——复用你的成熟底座,只把交互层交给大模型。
BOUNDARY
把经验讲清之外,也把边界交代清楚,避免误用:
这篇复盘来自超兔一体云查询引擎的研发一线——两个阶段、两次架构决策、十个工程坑,都来自真实项目的反复打磨。如果你觉得有收获,欢迎把它分享给正在做低代码平台或 LLM 应用的朋友。
文中涉及的内部表名、字段名、部署细节均已脱敏处理;所有业务均为通用中性命名。