部门沉淀了大量业务底表数据,要开放给本部门和其他部门查数,既包含生产服务调用,也包含 ad hoc 提数。为此我设计并实现了一套数据服务化平台,前端管 UI,后端跑 AI 服务。

它一开始只做一件事:把写好的 SQL 变成可调用的 HTTP/RPC 接口。写好 SQL,配一下,就是一个接口。这个定位它干得很好。

需求一变,问题跟着来了:

  • 查询参数越来越多,可选组合各异,调用方(包括 AI)搞不清每个查询要哪些参数、能怎么排序分组
  • 成百上千个查询散落各处,没有统一的发现入口
  • 安全审计只能逐条人工盯

做到这一步我才发现,这个平台的价值不在"把 SQL 变成 API",而在让查询自描述、可发现、可被机器安全消费。

下面是这套设计的复盘。SQL 模板是纯文本协议,条件块解析是算法,MCP 元工具是协议层设计,跟具体语言和框架无关,换任何技术栈都成立。

一、为什么不直接让 AI 连数据库

业界 Text-to-SQL 走的就是这条路:给 AI 一个数据库连接,让它自己写 SQL。听起来最灵活,企业里几乎没人敢这么上生产,因为 AI 直连数据库保证不了五件事:

  • 口径:同一个"库存周转率",AI 这次算出来的逻辑和下次可能不同。企业里指标口径必须稳定,否则下游报表对不上。
  • 安全:把库交给 AI,等于把表结构、敏感字段、跨表关联能力全暴露出去。审计一条动态 SQL,比审计几千条预定义 SQL 难得多。
  • 性能:自由 SQL 是慢查询的源头。预定义查询可以提前调优、限制扫描分区;AI 现场生成的 SQL 没有这层保障,一句漏了分区过滤的全表扫描就能把 OLAP 引擎打挂。
  • 稳定性:AI 直连库没有限流和行数兜底,一次失控的并发请求或超大结果集就能把连接池打满,拖垮整个库,波及所有依赖它的业务。预定义查询服务在入口限流、在执行层限制行数,把库和调用方隔开。
  • 权限:企业想按指标粒度授权(“你能查库存率,不能查毛利”),数据库的权限模型到表和列为止,管不到指标粒度。

市面上的 SQL→API 方案分两类:

  • 轻量映射型(如 PostgREST):把表直接暴露成 REST 接口,不解决口径问题,业务指标本来也不是单表查询。
  • 低代码平台型:带 SQL 编辑器的数据服务平台,接近需求,但有四个共同缺口

重新设计的出发点,就是把这四个缺口当一等公民:

  • 接口合约靠外部配置,会和 SQL 漂移 → SQL 即合约:参数 schema 从 SQL 本体解析,不维护第二份
  • 参数可选性靠人工标注,会和结构矛盾 → 结构即语义:必填选填从 SQL 结构自动推断,不靠人工声明
  • 没有面向 AI agent 的原生发现机制 → 原生 AI 发现:用 MCP 暴露固定元工具,让 AI 自己发现
  • SQL 执行是封装黑盒,不可审计 → 可审计的执行层:SQL 渲染全链路白盒,每个拼接点的输入来源可追溯

二、AI 扮演什么角色

AI 在这个项目里分两个阶段扮演不同角色。

  • 开发态——AI 是 SQL 作者:定义新指标时,AI 根据需求和我设计的模板语法帮写 SQL,我只需要澄清业务需求并审阅产出。我只关注业务口径对不对,不需要关心 SQL 语法和模板语法(/*IF*/、@param 这些)。AI 把"学一套模板语法"的成本吃掉了。
  • 运行态——AI 是查询调用方:查数时,AI 只能调用已发布指标、组装参数,不能现场生成 SQL 去执行。边界很硬:只能调已发布查询、不能跨查询 JOIN、不能写入、不能创造新查询或改口径。

口径变更也在这条边界里:人改 SQL,人审阅,重新发布,AI 在运行态碰不到口径。这是保护,不是限制。

这个区分是整套设计的支点:AI 写 SQL 的能力放在开发态,那里有人审阅;运行态完全关掉。 前者解决"写查询慢",后者解决"敢不敢上线"。

开发态写 SQL,运行态只调
开发态写 SQL,运行态只调

三、如何设计模板语法

同一个 SQL 文件既是给人读的查询,也是机器可解析的接口合约。这样就不用维护"查询定义"和"接口文档"两份会漂移的东西。载体是一套写在 SQL 开头的 header 注释协议:

-- @param regionId string required 区域ID
-- @param vendorCodes string-list optional 供应商编码列表
-- @param config_tenantId string required 租户ID(配置注入)
-- @param derived_periodDate date optional 派生统计日期
-- @out itemId string 商品ID
-- @out regionName string 区域名称
-- @out orderQty long 订单量
-- @sortable itemId,orderQty
-- @groupable regionName
-- @example regionId "R001"
SELECT item_id AS itemId, region_name AS regionName, SUM(order_qty) AS orderQty
FROM sales_table
WHERE 1=1
  /*IF:regionId*/ AND region_id IN (:regionId) /*END*/
  /*IF:vendorCodes*/ AND vendor_code IN (:vendorCodes) /*END*/
  AND tenant_id = :config_tenantId
  AND dt = :derived_periodDate
/*GROUP_BY*/
/*ORDER_BY*/
/*LIMIT*/

语法解决三件事。

动态渲染

参数值走命名参数 :name,执行时转占位符绑定,值不进 SQL 文本,这是地基。选填过滤走条件块 /*IF:param*/.../*END*/,传了保留、没传整块移除。分组排序走占位符 /*GROUP_BY*/ /*ORDER_BY*/,调用方传字段列表,渲染时替换。

防注入的边界

WHERE 参数值走参数化绑定,但列名必须拼进 SQL——数据库不支持 GROUP BY ?,参数化覆盖不到,只能靠白名单:正则兜底 + @sortable/@groupable 声明的字段白名单,调用方只能传声明过的字段。完整的风险分级见第五节。

自动推断与显式声明

能从 SQL 结构可靠推断的就自动推断,结构里读不出来的才要求显式声明。

  • 自动推断:required 从结构读——参数在条件块外必填、块内选填,不需要人声明,人工声明反而会和结构漂移。但人声明的 required 和结构实际位置可能矛盾(声明必填却只在条件块内,永远走不到),需要保存时做冲突检测挡住。
  • 必须显式声明:参数类型(:regionId 看不出是字符串还是数字)、给 AI 看的描述和示例、哪些列可排序可分组(度量列不适合分组,结构读不出)、哪些参数该由系统控制而非用户传(租户 ID 必须配置注入,让用户传就能越权)。header 只补这些信息盲区,不是配置中心。

发布时自动生成调用说明

发布时系统从 header 和 SQL 结构生成完整调用说明:HTTP 路径、RPC 接口签名、参数 schema(名称、类型、必填、描述)、可排序与可分组字段白名单、输出列及其必填性、请求和响应示例。调用方拿到这份说明就能直接对接,不用另外维护接口文档。

header 声明一次,运行时校验、AI 发现、调用文档三处共用同一份数据源。这是"SQL 即合约"的落点:合约不只是给机器解析的,也直接给调用方用。

SQL 文件是唯一事实来源
SQL 文件是唯一事实来源

四、如何设计 MCP

MCP 这层服务两方:管理方(定义和发布查询的人)和消费方(调查询的 AI agent)。管理方这边也是 AI。开发态的 AI 要按一套固定顺序走完质量保证链路,我把这套顺序编进了工具调用里。

管理方:把质量保证链路编码成 MCP 工具 SOP

管理方开发一个查询,必须走一条有强制顺序的 SOP,不能写完 SQL 直接存:

摸清表结构 → 编写带 header 的 SQL 模板 → 静态检查参数和质量提示并迭代修正 → 真实执行验证 → 与用户对齐口径并等待确认 → 保存 → 发布

管理方 SOP 的三道闸
管理方 SOP 的三道闸

这套 SOP 的价值在于质量门槛被嵌进了工具调用顺序,不靠人自觉:

  • 第一道闸——静态检查:必填参数和条件块位置冲突、白名单未命中、参数缺描述或示例,都会报 WARN/ERROR,挡住低质量模板。
  • 第二道闸——真实执行:验证参数绑定和返回结构。
  • 第三道闸——口径对齐:AI 必须向用户复述过滤范围、聚合粒度、输出字段含义,未获确认不得保存。

三道闸都过,才进保存和发布。

这套做法是把开发规范固化成 AI 能执行的指令。传统做法是写一份开发文档,靠开发者自觉遵守;这里每个质量检查点都是一个 MCP 工具,不按 SOP 走,工具直接报错。文档不会漂移成"写了没人看"。

消费方

消费方两类,需求完全不同:

  • 生产服务调用:业务产品按契约调用已发布指标接口,走 HTTP/gRPC,参数和返回结构在发布时就固定了。这类调用方要的是稳定和性能,调用说明在发布时自动生成,直接对接。
  • ad hoc 提数:AI agent 调用,走 MCP 的 3 个固定元工具(list_services → get_service_info → execute_query),先发现再理解再执行。tool 数量不随业务增减,避免几百个指标撑爆 AI 上下文。鉴权拦在 tools/call 而不是 tools/list——MCP 协议下 list 是全局返回的,能发现不代表能调用,真正的数据访问在 call 时才校验 token 和限流。

两类消费方共享同一执行内核和安全逻辑。

ad hoc 提数的三步发现
ad hoc 提数的三步发现

五、如何进行安全审计

SQL 渲染全链路白盒可审计,每个拼接点的输入来源都可追溯。注入面与防护:

注入面防护方式
WHERE 参数值:name → ? + 参数化绑定,值不进 SQL 文本
列名(GROUP BY/ORDER BY)正则白名单 ^[a-zA-Z_][a-zA-Z0-9_]*$ + @sortable/@groupable 字段白名单
ORDER BY 方向枚举校验 ASC/DESC
LIMIT/OFFSET整数计算值

按输入来源给拼接点分级

WHERE 参数值走命名参数绑定,值不进 SQL 文本;GROUP BY/ORDER BY 的列名必须拼进 SQL,参数化覆盖不到,只能靠白名单。这两条之外,执行层还有些拼接躲不掉,比如设置查询超时 SET query_timeout = 30——部分 OLAP 引擎的 SET 语句不支持占位符。审计不能停在"有字符串拼接就危险",得按输入来源分级:

拼接内容输入来源风险评级依据
SET query_timeout = + 值配置中心,经类型解析为 int无类型安全,非用户输入
EXPLAIN + sql管理员预审的模板低已过校验,非运行时输入
硬编码常量代码无不可变
WHERE 参数值用户/AI 输入高必须参数化

审计要做的是确认每个拼接点的输入来源可信。要求消灭所有拼接,会逼着工程师引入更复杂的抽象,出错面反而变大。

六、这套设计没解决什么

运行态那几条硬边界是刻意的,它们把 AI 的能力关在可审计的范围里。剩下几件事是还没解决的:

跨指标不能 JOIN。 一个 SQL 内部写 JOIN、CTE、窗口函数都可以,跨表取数不受限制。不能的是跨查询 JOIN:不能拿两个已发布指标对接,也不能把一个指标的查询当另一个的子查询。跨指标的分析只能拆成多次调用,自己在外面对齐维度再拼结果。

模板语法还是得学一遍。 AI 能写 SQL 和模板语法,但审阅的人得看得懂渲染后的 SQL 长什么样。语法越复杂,审阅成本越高。这套语法只留了命名参数、条件块、分组排序占位符三个概念,就是为了压住这个成本。

tool 数量固定,返回条数没固定。 list_services 始终返回全部服务。指标从几百涨到几千时,这个列表本身就开始占 AI 上下文。

结语

回过头看,这套设计做了两件事:

  • AI 写 SQL 的能力放在开发态(有人审阅),运行态完全关掉。
  • 运行态的 AI 只当查询调用方,不当 SQL 作者。

企业敢不敢把数据开放给 AI,最后取决于查询本身够不够结构化。查询结构化了,AI 在运行态就没有生成 SQL 的机会,护栏从"运行时拦截"变成"压根不给这条路"。

Logo

更多推荐