从自然语言到SQL,再加一道人工防线:Spring AI Alibaba 实战

L02-cover.webp

1. 引言

在 AI 应用开发中,如何让大语言模型(LLM)安全地执行数据库操作是一个核心挑战。直接让 AI 执行 SQL 存在风险——一旦生成错误的 SQL 语句,可能导致数据丢失或泄露。本文将介绍如何基于 Spring AI Alibaba 实现一个带有人工审批机制的智能 SQL 查询助手,让 AI 在执行危险操作前必须经过人类确认。

本文涉及的项目完整代码可以在 /ali/L02-human-in-loop 目录下找到。
在这里插入图片描述

2. 项目概述

本项目实现了一个智能 SQL 查询助手,核心特性包括:

  • 自然语言转 SQL:用户可以用自然语言描述查询需求,AI 自动生成并执行 SQL
  • 人工审批机制:所有 SQL 执行前需要用户确认(Human-in-the-loop)
  • 安全防护:自动拦截危险 SQL 操作(如 DROP、TRUNCATE 等)
  • 完整 CRUD 支持:支持 SELECT、INSERT、UPDATE、DELETE 四种操作
  • 对话式交互:基于 Web 界面提供流畅的用户体验

2.1 技术栈

组件 技术选型
AI 框架 Spring AI Alibaba
大模型 通义千问 (Qwen/Qwen3-8B)
Agent 架构 Spring AI Graph (ReAct Agent)
数据库 H2 内存数据库
Web 框架 Spring Boot + Thymeleaf

3. 核心概念解析

3.1 什么是 Human-in-the-loop?

Human-in-the-loop(人在循环中,简称 HITL)是一种人机协作模式,核心思想是在 AI 自动化流程中引入人类决策节点。在本项目中体现为:

用户请求 → AI 分析 → 生成 SQL → ⚠️ 暂停等待人类审批 → 执行/拒绝 → 返回结果

这种设计适用于以下场景:

  • 金融交易需要人工审核
  • 数据删除操作需要二次确认
  • 敏感数据查询需要授权
  • AI 置信度不高时需要人工判断

Human-in-the-loop 工作流程

3.2 Spring AI Graph 架构

Spring AI Alibaba 提供了基于 Graph(图结构)的 Agent 架构,核心组件包括:

  • Node(节点):Agent 中的处理单元
  • Edge(边):节点之间的连接逻辑
  • State(状态):整个对话的上下文信息
  • Checkpointer(检查点):保存状态快照,支持中断恢复
  • Hook(钩子):在特定时机插入自定义逻辑(如人工审批)

Spring AI Graph 架构组件

4. 核心实现

4.0. 架构流程图

我们先从整体的交互来看看这个自然语言转sql、并支持sql执行审批的流程是怎么运转的

在这里插入图片描述

4.1 项目依赖

首先需要在 pom.xml 中引入相关依赖:

<dependencies>
    <!-- Spring AI OpenAI 兼容支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
    
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- H2 数据库 -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- Thymeleaf 模板引擎 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

4.2 数据库配置

application.yml 中配置 H2 数据库和 AI 模型:

spring:
  ai:
    openai:
      api-key: ${silicon-api-key}  # 替换为你的 API Key
      base-url: https://api.siliconflow.cn
      chat:
        options:
          model: Qwen/Qwen3-8B
  
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:
  
  h2:
    console:
      enabled: true
      path: /h2-console
  
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop

4.3 订单实体类

定义订单表对应的 JPA 实体:

@Entity
@Table(name = "t_order")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "order_no", length = 64)
    private String orderNo;
    
    @Column(name = "customer_name", length = 128)
    private String customerName;
    
    @Column(name = "product_name", length = 256)
    private String productName;
    
    @Column(name = "quantity")
    private Integer quantity;
    
    @Column(name = "unit_price", precision = 10, scale = 2)
    private BigDecimal unitPrice;
    
    @Column(name = "total_amount", precision = 10, scale = 2)
    private BigDecimal totalAmount;
    
    @Column(name = "status", length = 32)
    private String status;
    
    // ... getters and setters
}

4.4 CURD 工具实现

这是核心组件之一,负责实际执行 SQL 操作:

@Component
public class CurdTool {
    
    /**
     * 执行 SQL 操作
     */
    public Map<String, Object> execute(JdbcTemplate jdbcTemplate, 
                                        String sql, List<Object> params) {
        // 1. 识别 SQL 类型
        SqlType sqlType = identifySqlType(sql);
        
        // 2. 根据类型执行
        switch (sqlType) {
            case SELECT:
                return executeSelect(sql, params);
            case INSERT:
                return executeInsert(sql, params);
            case UPDATE:
                return executeUpdate(sql, params);
            case DELETE:
                return executeDelete(sql, params);
            default:
                return Map.of("success", false, "message", "不支持的 SQL 操作");
        }
    }
    
    /**
     * SQL 安全性检查
     */
    public boolean isSqlSafe(String sql) {
        String[] dangerousKeywords = {
            "DROP ", "TRUNCATE ", "ALTER ", "GRANT ", "REVOKE ",
            "CREATE ", "EXEC ", "EXECUTE ", "CALL ", "LOAD_FILE"
        };
        
        String upperSql = sql.toUpperCase();
        for (String keyword : dangerousKeywords) {
            if (upperSql.contains(keyword)) {
                return false;
            }
        }
        return true;
    }
}

4.5 人工审批 Hook

这是实现 Human-in-the-loop 的关键组件(借助HumanInTheLoopHook,要求在执行 execute_sql 工具之前,必须完成人工的确认机制,只有再次接收到用户的反馈之后,才会继续往后面执行动作)

// 创建人工介入 Hook,对 SQL 执行工具添加审批机制
HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
        .approvalOn("execute_sql", ToolConfig.builder()
                .description("⚠️ SQL 执行操作需要审批!请确认 SQL 语句的安全性和正确性:")
                .build())
        .build();

4.6 Agent 初始化与配置

将所有组件组装在一起,创建完整的 Agent:

private ReactAgent initAgent() {
    // 1. 创建 SQL 执行工具
    ToolCallback curdToolCallback = FunctionToolCallback.builder(
        "execute_sql",
        (Map<String, Object> args) -> {
            String sql = (String) args.get("sql");
            
            // 安全检查
            if (!curdTool.isSqlSafe(sql)) {
                return "❌ 错误:SQL 包含危险操作,已被拦截!";
            }
            
            // 执行 SQL
            List<Object> params = (List<Object>) args.getOrDefault("params", null);
            Map<String, Object> result = curdTool.execute(jdbcTemplate, sql, params);
            
            return curdTool.formatResult(result);
        }
    )
    .description("执行 SQL 操作(支持 SELECT/INSERT/UPDATE/DELETE)")
    .inputType(Map.class)
    .build();
    
    // 2. 创建人工审批 Hook
    HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
            .approvalOn("execute_sql", ToolConfig.builder()
                    .description("⚠️ SQL 执行操作需要审批!")
                    .build())
            .build();
    
    // 3. 创建检查点保存器(用于中断恢复)
    MemorySaver memorySaver = MemorySaver.builder().build();
    
    // 4. 构建 Agent
    ReactAgent agent = ReactAgent.builder()
            .name("sql_query_agent")
            .model(chatModel)
            .instruction("""
                你是一个智能 SQL 助手,可以访问订单数据库(表名:t_order)。
                
                【表结构】
                - id: 订单 ID
                - order_no: 订单号
                - customer_name: 客户姓名
                - product_name: 产品名称
                - quantity: 数量
                - unit_price: 单价
                - total_amount: 总金额
                - status: 订单状态(已完成/待发货/待支付)
                
                【重要】
                1. 所有 SQL 操作都需要人工审批确认
                2. 禁止使用 DROP、TRUNCATE、ALTER 等危险操作
                3. 建议使用参数化查询
                """)
            .tools(curdToolCallback)
            .hooks(List.of(humanInTheLoopHook))
            .saver(memorySaver)
            .build();
    
    return agent;
}

4.7 审批流程处理

处理用户的首次请求和审批决策:

注意:首次请求,触发审批之后,我们新增了一个接口用于接收用户的审批结果;很容易想到,审批结果的实现中,需要续上第一次的请求,怎么维护这个完整的请求状态呢?

这就是 MemorySaver 的重要作用了,其次就是下面的实现是由后端来维护审批状态;这里也可以将相关状态给前端,让前端在调用 /sq/approval 接口时,一并返回给后端,这样后端就可以基于请求参数来恢复上次的请求状态了😊

@GetMapping("/sql")
public Map<String, Object> chat(String msg, String sessionId) {
    RunnableConfig config = RunnableConfig.builder()
            .threadId(sessionId != null ? sessionId : "default-session")
            .build();
    
    // 第一次调用 - 可能触发中断
    Optional<NodeOutput> result = reactAgent.invokeAndGetOutput(msg, config);
    
    // 检查是否需要人工审批
    if (result.isPresent() && result.get() instanceof InterruptionMetadata) {
        InterruptionMetadata interruption = (InterruptionMetadata) result.get();
        
        // 保存待审批状态
        pendingApprovals.put(sessionId, 
            new PendingApproval(interruption, config, msg, toolResultKey));
        
        // 返回审批信息
        return Map.of(
            "status", "pending_approval",
            "toolFeedbacks", convertToolFeedbacks(interruption.toolFeedbacks()),
            "message", "需要您的审批确认"
        );
    }
    
    return Map.of("status", "success", "messages", extractLastMessageText(result.orElse(null)));
}

@PostMapping("/sql/approval")
public Map<String, Object> handleApproval(@RequestBody Map<String, Object> request) {
    String sessionId = (String) request.get("sessionId");
    Boolean approved = (Boolean) request.get("approved");
    
    PendingApproval pending = pendingApprovals.get(sessionId);
    
    // 构建审批反馈
    InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder()
            .nodeId(pending.interruptionMetadata.node())
            .state(pending.interruptionMetadata.state());
    
    pending.interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> {
        InterruptionMetadata.ToolFeedback approvedFeedback =
            InterruptionMetadata.ToolFeedback.builder(toolFeedback)
                .result(approved 
                    ? InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED
                    : InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED)
                .build();
        feedbackBuilder.addToolFeedback(approvedFeedback);
    });
    
    // 继续执行
    RunnableConfig resumeConfig = RunnableConfig.builder()
            .threadId(sessionId)
            .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, 
                feedbackBuilder.build())
            .build();
    
    Optional<NodeOutput> finalResult = reactAgent.invokeAndGetOutput("", resumeConfig);
    
    return Map.of(
        "status", approved ? "approved" : "rejected",
        "message", approved ? "SQL 已执行成功" : "操作已取消"
    );
}

5. 前端交互实现

5.1 审批界面

当 Agent 检测到需要执行 SQL 时,会暂停并返回审批信息给前端:

async function sendMessage() {
    const response = await fetch(`/sql?msg=${encodeURIComponent(message)}&sessionId=${sessionId}`);
    const data = await response.json();
    
    if (data.status === "pending_approval") {
        // 显示审批卡片
        showApprovalCard(
            data.toolFeedbacks[0].arguments,  // SQL 语句
            data.toolFeedbacks[0].description  // 审批说明
        );
    } else {
        // 直接显示结果
        appendMessage('assistant', data.messages);
    }
}

async function handleApproval(approved) {
    const response = await fetch(`/sql/approval`, {
        method: 'POST',
        body: JSON.stringify({
            sessionId: sessionId,
            approved: approved
        })
    });
    
    // 更新审批状态
    updateApprovalStatus(approved, response.resultMessage);
}

5.2 界面效果

审批界面包含以下元素:

  • SQL 语句预览(代码高亮)
  • 操作说明描述
  • 批准/拒绝按钮
  • 审批状态标签(等待审批/已批准/已拒绝)
  • 执行结果展示

6. 使用示例

6.1 查询数据

查询所有已完成状态的订单

AI 会生成类似这样的 SQL:

{"sql": "SELECT * FROM t_order WHERE status = ?", "params": ["已完成"]}

然后弹出审批卡片,用户确认后执行。
在这里插入图片描述

6.2 插入数据

张三新买了一台macmin 花了3999

生成的 SQL:

{
    "sql": "INSERT INTO t_order (order_no, customer_name, product_name, quantity, unit_price, total_amount, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
    "params": [
        "ORD20260310002",
        "张三",
        "Mac mini",
        1,
        3999,
        3999,
        "待支付",
        "2026-03-10 10:00:00",
        "2026-03-10 10:00:00"
    ]
}

在这里插入图片描述

6.3 更新数据

张三的mac mini已经完成了支付

生成的 SQL:

{"sql": "UPDATE t_order SET status = ? WHERE order_no = ?", "params": ["已完成", "ORD20260310002"]}

在这里插入图片描述

6.4 删除数据

用户:删除订单号为 ORD20260309008 的订单

生成的 SQL:

{"sql": "DELETE FROM t_order WHERE order_no = ?", "params": ["ORD20260310002"]}

L02-8.webp

7. 安全机制

7.1 SQL 危险操作拦截

系统自动拦截以下危险操作:

操作类型 示例 处理方式
DROP DROP TABLE 直接拒绝
TRUNCATE TRUNCATE TABLE 直接拒绝
ALTER ALTER TABLE 直接拒绝
GRANT GRANT ALL 直接拒绝
REVOKE REVOKE ALL 直接拒绝
读写文件 INTO OUTFILE 直接拒绝

7.2 参数化查询

推荐使用参数化查询防止 SQL 注入:

-- ✅ 推荐
SELECT * FROM t_order WHERE customer_name = ?

-- ❌ 不推荐(容易导致 SQL 注入)
SELECT * FROM t_order WHERE customer_name = '" + input + "'

8. 扩展思考

8.1 进阶功能

基于当前架构,可以进一步扩展:

  1. 多工具审批:对不同工具设置不同的审批规则
  2. 批量审批:支持一次性审批多个操作
  3. 审批历史:记录所有审批操作用于审计
  4. 自动审批:对安全查询(如只读 SELECT)设置白名单
  5. LLM 预审:用另一个 AI 模型先做初步风险评估

8.2 生产环境注意事项

  • 使用生产级数据库(MySQL/PostgreSQL)替换 H2
  • 添加完整的日志和审计功能
  • 实现超时机制防止审批无限等待
  • 添加权限控制,不同用户有不同审批权限
  • 考虑使用消息队列实现异步审批

9. 总结

本文详细介绍了如何基于 Spring AI Alibaba 实现一个带有 Human-in-the-loop 特性的智能 SQL 查询助手。核心要点包括:

  1. 人工审批机制:通过 HumanInTheLoopHook 在危险操作执行前插入人工确认节点
  2. 检查点保存:使用 MemorySaver 保存状态,支持中断恢复
  3. 安全防护:多层防护确保只有安全的 SQL 能被执行
  4. 用户体验:前后端配合实现流畅的审批交互流程

这种设计模式不仅适用于 SQL 执行,在任何 AI 执行敏感操作的场景中都可以参考借鉴。

参考资料

零基础入门:

实战


Author: 一灰
Date: 2026-03-09
Tags: [Spring AI, Human-in-the-loop, AI Agent, SQL, 通义千问]


💬 互动环节:自然语言转SQL,若有数千张表,大模型如何知道查询哪张表呢?如果涉及到多表联合统计,又应该怎么实现呢?欢迎在评论区留言讨论!

如果觉得这篇文章有帮助,请点赞 👍 收藏 ⭐ 转发 🔄 支持一下!

Logo

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

更多推荐