为什么要写这个系列

做Java后端十年,我接触过不少企业的核心系统。

金融、电商、政务——行业不同,但底层的现状惊人地相似:生产系统还在Java 8,框架停在Spring Boot 2.x甚至更早,代码跑了很多年,没人敢轻易动。

去年开始,几乎每个项目都在谈接AI。

但真正动手的时候,团队就卡住了。

不是因为不懂大模型,而是老系统本身接不住。JDK版本不够,Spring AI引不进来;依赖树牵一发动全身,升级一个包怕带崩一片;生产流量压着,不敢拿主流程赌一个AI试点。

更危险的是硬塞。我见过团队在老系统的Service层直接new一个HttpClient调模型API,Prompt拼在业务代码里,超时没配、降级没有。模型响应慢的时候,老系统的订单查询线程池被占满,主流程跟着卡住。还有团队把用户手机号、身份证原样送进Prompt,过了两个月才被安全审计发现。

这种事见多了,我就开始想一个问题:老系统不具备直接接入AI的条件,这是不是大多数企业的常态?

答案是肯定的。而且这不应该成为接不了AI的理由。

核心思路其实就三条:

老系统少改 —— 不升级 JDK,不引入 Spring AI 依赖
AI 能力旁路 —— 独立部署,老系统通过 HTTP 或 MQ 调用
企业边界先行 —— 脱敏、审计、降级、幂等比模型调用更重要

这个系列就是把这三条线展开成10讲。

从AI Gateway到MCP工具中心,从SQL Agent到RAG知识库,从工单Agent到多Agent研发团队——每一讲都围绕同一个前提:

你的老系统还在跑Java 8,你不能为了AI去赌它的稳定性。

每讲配套一个可运行的Maven Lab,不讲空架构,不写Hello World。你跑得通Demo,看得到边界,拿得到代码。

这就是我做这个系列的原因。

这个系列帮你建立企业 AI 接入的架构意识。每个 Demo 都可独立运行,每讲都配有边界设计和企业避坑。

但意识不等于系统。当你真正要在自己的项目里落地时,会发现:

  • 10 个独立 Lab 的组件需要整合成一个完整的 AI 能力中心
  • Stub 模型需要替换成真实模型调用
  • 权限、审计、脱敏需要从 Demo 级别升级到生产级别

先把架构意识打好,落地时才能走得更稳。


Java 8老系统SQL Agent实战:AI生成候选SQL,安全引擎拦截后再执行

企业里最容易被老板、业务方理解的AI场景之一,是自然语言查数据。

比如:

统计本月销售额最高的 10 个商品
查询本月退款订单数量

听起来很简单:

用户说一句话
AI 生成 SQL
数据库执行
返回结果

但真正放到企业系统里,这条链路不能这么走。

第1讲我们说,Java 8老系统接AI要走旁路,AI不直接改业务状态。

第2讲我们说,老系统能力要先包装成可授权、可审计、可脱敏的工具。

这一讲继续往前走:如果这个工具不是普通API,而是"查数据库",边界要更严格。

因为数据库查询一旦失控,影响的不只是一个接口,而是整片业务数据。

AI 生成 DELETE 怎么办?
AI 查敏感字段怎么办?
AI 写了慢 SQL 怎么办?
AI 越权查了别的租户怎么办?
AI 把查询结果原样返回给模型怎么办?

所以这一讲的重点不是"让AI会写SQL"。

重点是:

模型只能生成候选SQL,候选SQL必须先经过安全引擎,再决定能不能执行。

自然语言查库最容易被误解

很多人一听自然语言查库,第一反应就是Text2SQL。

这当然有价值,但企业里真正卡住的地方通常不是模型能不能写出一条SQL,而是这条SQL能不能被信任。

在企业系统里,模型输出不能被当成执行指令。它最多只是一个候选产物:

用户问题
        ↓
模型生成候选 SQL
        ↓
安全引擎审核
        ↓
只读执行器执行
        ↓
结果摘要返回

这和第2讲的工具授权思路一致。第2讲管的是API Tool,第3讲管的是数据库查询Tool:

AI 能不能查这张表?
能查哪些字段?
查询范围和条数怎么限制?
结果能不能原样返回?

边界没有变,只是风险更集中。

只读为什么仍然危险

很多团队会觉得,只要不让AI执行UPDATEDELETE,只允许SELECT,风险就小了。

这只说对了一半。只读不等于安全。

一个企业可接受的查询工具,至少要回答6个问题:

权限:这个操作者是否有权查这类数据?
租户:查询是否被限制在当前 tenantId?
参数:时间范围、条数、聚合维度是否可控?
字段:是否包含手机号、证件号、内部备注等敏感字段?
审计:谁在什么时候问了什么,生成了什么 SQL,是否执行?
返回值:查询结果能否原样给模型或前端?

本讲Demo里,SqlQueryRequest已经保留了tenantIdoperatorId。它们不是装饰字段。

真实项目里,SQL Agent必须知道:谁在查、代表哪个租户查、为什么查、查到了什么、有没有被安全策略拒绝。

本讲Demo最终效果

代码目录:

code/spring-ai-enterprise-lab/labs/chapter03-sql-agent

运行:

.\compile-and-run.ps1

跑完后能看到四类结果:

  • 合法销售额查询:放行。
  • 合法退款数量查询:自动补LIMIT 50
  • 删除数据意图:被拦截。
  • 查询手机号:被拦截。

这四类结果想证明的是:

候选 SQL 不能直接执行
确定性安全代码必须挡在数据库前面

端到端走一遍

整体链路:

SqlQueryRequest
        ↓
SqlAgentService
        ↓
StubSqlGenerateService
        ↓
CandidateSql
        ↓
SqlSafetyEngine
        ↓
SqlSafetyDecision
        ↓
ReadOnlySqlExecutor
        ↓
SqlResultSummarizer
        ↓
SqlQueryResult

第一步,用户输入自然语言问题,组装成SqlQueryRequest

new SqlQueryRequest("demo", "u1001", question)

第二步SqlAgentService调用StubSqlGenerateService生成候选SQL。

注意这里叫CandidateSql——它不是最终SQL,也不是可以立刻执行的SQL。

第三步,候选SQL进入安全引擎:

SqlSafetyDecision decision = sqlSafetyEngine.inspect(candidateSql, SchemaSnapshot.orderReport());

如果安全引擎拒绝,链路直接结束,不会进入执行器。

第四步,如果安全引擎放行,进入只读执行器。这里执行的也不是原始候选SQL,而是安全引擎处理后的safeSql

比如缺少LIMIT时,安全引擎会自动补上LIMIT 50

第五步SqlResultSummarizer把查询结果变成摘要,最终返回。

这条链路的原则:

模型负责生成候选 SQL
安全引擎负责决定能不能执行
只读执行器只执行安全 SQL
摘要器只返回业务需要的结果

代码结构

src/main/java/com/ynzz/lab/chapter03
├── Chapter03Demo.java
├── agent
│   ├── StubSqlGenerateService
│   ├── SqlAgentService
│   ├── ReadOnlySqlExecutor
│   └── SqlResultSummarizer
├── common
│   ├── CandidateSql
│   ├── SqlQueryRequest
│   └── SqlQueryResult
└── safety
    ├── SchemaSnapshot
    ├── SqlSafetyDecision
    └── SqlSafetyEngine

agent负责生成、执行和总结。safety负责判断候选SQL是否安全。

现在还没有接真实模型。StubSqlGenerateService先模拟模型生成候选SQL,把注意力集中在安全引擎上。等安全链路跑通后,再把Stub替换成真实模型调用。

这个顺序很重要。如果一上来就接模型,注意力会被prompt、模型效果、API Key牵走。但企业真正关心的是:候选SQL到底能不能执行?

Schema白名单

本讲模拟了一个报表表:

CREATE TABLE order_report (
  id BIGINT PRIMARY KEY,
  order_id VARCHAR(64) NOT NULL,
  product_name VARCHAR(128) NOT NULL,
  customer_level VARCHAR(32) NOT NULL,
  order_month VARCHAR(16) NOT NULL,
  amount DECIMAL(12, 2) NOT NULL,
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL
);

这张表故意不包含手机号、身份证、邮箱等敏感字段。

重要设计原则:

SQL Agent 优先连接报表库、分析库、只读副本
不要让模型直接靠近生产业务库

SchemaSnapshot维护了三类信息:allowedTablesallowedColumnssensitiveColumns

关键原则:

不要把完整数据库结构直接交给模型。模型只应该看到它能查询的部分。

SQL安全引擎

SqlSafetyEngine做了5件事:

1. 只允许 SELECT
2. 拦截 INSERT / UPDATE / DELETE / DROP / ALTER
3. 校验表白名单
4. 拦截敏感字段
5. 强制 LIMIT

用户问"删除所有测试订单数据",Stub生成:

DELETE FROM order_report WHERE status = 'TEST'

安全引擎返回:

{
  "blocked": true,
  "blockReason": "WRITE_OPERATION_NOT_ALLOWED",
  "summary": "SQL 已被安全引擎拦截,未执行。"
}

运行效果

合法查询:放行,返回摘要。

没带LIMIT的退款查询:安全引擎自动补上LIMIT 50

敏感字段查询(“查询所有客户手机号”):拦截,blockReason=SENSITIVE_FIELD_NOT_ALLOWED

真实项目里还应该把这些信息写入审计记录:原始问题、候选SQL、安全决策、执行状态、结果摘要、tenantId、operatorId。

企业避坑

  1. 不要让AI直连生产库 — 至少用只读数据源,最好从报表库、分析库开始。
  2. 不要把完整Schema给模型 — 模型只需要知道允许查询的表和字段。
  3. 不要相信模型会生成安全SQL — 安全检查必须由确定性代码完成。
  4. 不要忽略LIMIT — 没有限制的SELECT也可能拖垮系统。
  5. 不要忘记租户和操作者上下文 — 不同租户、角色允许看到的数据范围不同。
  6. 不要把查询结果原样返回 — 要做字段裁剪、敏感信息处理和摘要控制。
  7. 不要缺审计 — 自然语言查库必须记录完整调用链路。

从 Demo 到落地,还差什么

本讲帮你验证了"候选 SQL + 安全引擎"的核心思路,但企业查库场景落地还差几步:

SQL Parser 替换字符串判断:当前 Demo 用简单字符串匹配做安全检查。真实项目需要 SQL Parser(如 JSqlParser)支持复杂 SQL 语法——子查询、JOIN、嵌套 SELECT 等场景。

多租户行级权限:本讲在请求里带了 tenantId,但安全引擎没有按租户过滤数据。真实项目里同一张表,不同租户只能看到自己的数据,行级权限必不可少。

完整审计服务:当前 Demo 只在控制台打印信息。真实项目需要审计服务:异步写入、查询接口、告警规则、合规报表。

查询资源配额:不同租户的查询频率和返回行数应该有配额限制,防止一个租户的查询拖垮整条链路。

如果你正在推进企业 AI 查库能力落地,后续会有完整版把这些组件整合成企业级 SQL Agent。

小结

模型只生成候选 SQL
确定性代码决定能不能执行
查询结果经过控制后再返回

这就是企业Java系统接AI时必须补上的一层。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐