Spring Boot + 本地大模型(Ollama/DeepSeek) + MyBatis-Plus 企业级智能体数据分析系统从零到一源码全解析

📑 目录导航

  1. 项目全景架构设计
  2. 项目依赖配置与环境搭建 (pom.xml)
  3. [数据库 DDL 与专属只读账号隔离 (init.sql)](#数据库 DDL 与专属只读账号隔离 (init.sql))
  4. 多数据源物理隔离配置 (application.yml & DataSourceConfig.java)
  5. [主业务模块:MyBatis-Plus 极简 CRUD (SysUser.java / Mapper / Service / Controller)](#主业务模块:MyBatis-Plus 极简 CRUD (SysUser.java / Mapper / Service / Controller))
  6. 智能体大模型配置与通信层 (AgentProperties.java / ChatMessage.java / LlmClient.java)
  7. 数据库动态元数据感知器 (DatabaseSchemaService.java)
  8. 底层只读安全沙箱与执行器 (StatsDataService.java)
  9. [智能体工具箱与 Text-to-SQL 工具 (AgentTool.java / AgentToolRegistry.java / DatabaseQueryTool.java / SystemMetricsTool.java)](#智能体工具箱与 Text-to-SQL 工具 (AgentTool.java / AgentToolRegistry.java / DatabaseQueryTool.java / SystemMetricsTool.java))
  10. 智能体大脑调度中枢与流式引擎 (AgentChatService.java)
  11. [Web REST 控制器与 SSE 端点 (AgentController.java)](#Web REST 控制器与 SSE 端点 (AgentController.java))
  12. 前端流式交互看板核心实现 (index.html)
  13. 生产避坑与性能调优总结

一、项目全景架构设计

传统的管理后台每次遇到统计报表需求,都需要开发人员手动编写 SQL、DAO 和 Controller。

本项目构建了一套双引擎协同架构

  • 引擎 A(日常业务开发) :使用 MyBatis-Plus 连接拥有 root 权限的主数据源,进行增删改查;
  • 引擎 B(AI 智能体统计) :大模型(Ollama / DeepSeek)充当实时分析师,自动感知 MySQL 表结构,现场手写只读 SELECT 语句,通过 ai_readonly 专属只读连接池和 500 行防爆熔断器安全执行,并通过 SSE (Server-Sent Events) 向前端流式打字输出!

二、项目依赖配置与环境搭建 (pom.xml)

项目基于 Spring Boot 2.6.13Java 8 开发。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Enterprise Spring Boot AI Agent Demo</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
    </properties>

    <dependencies>
        <!-- 1. Spring Boot Web Starter:提供 MVC 控制器、REST 接口与 SSE 流式推送 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 2. OkHttp 3:用于与本地 Ollama 或云端 DeepSeek 高性能 HTTP/SSE 通信 -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.3</version>
        </dependency>

        <!-- 3. Google Gson:用于解析复杂的 Function Calling JSON 报文与 Schema 转换 -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.9</version>
        </dependency>

        <!-- 4. Spring JDBC Starter:提供 HikariCP 连接池与 JdbcTemplate 原生底层支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- 5. MySQL 8 官方驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <!-- 6. MyBatis-Plus 增强框架:用于日常主业务极简增删改查 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>

        <!-- 7. 单元测试组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
            </plugin>
        </plugins>
    </build>
</project>

三、数据库 DDL 与专属只读账号隔离 (init.sql)

1. 创建 AI 专属只读账号与安全授权(必须在 MySQL 中执行)

sql 复制代码
-- 1. 创建 AI 专用的只读用户(密码建议包含大小写字母和特殊字符)
CREATE USER 'ai_readonly'@'%' IDENTIFIED BY 'AiPassword@2026';

-- 2. 严格只授予 demo_db 数据库的 SELECT (查询) 和 SHOW VIEW (视图) 权限
-- ⚠️ 绝对不给 INSERT, UPDATE, DELETE, DROP, ALTER, GRANT 等任何破坏性写权限!
GRANT SELECT, SHOW VIEW ON `demo_db`.* TO 'ai_readonly'@'%';

-- 3. 刷新权限表生效
FLUSH PRIVILEGES;

-- 4. 验证权限
SHOW GRANTS FOR 'ai_readonly'@'%';

2. 业务数据表结构与测试数据 (init.sql)

sql 复制代码
CREATE DATABASE IF NOT EXISTS `demo_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `demo_db`;

-- 1. 设备资产表 (tb_device)
CREATE TABLE IF NOT EXISTS `tb_device` (
    `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '设备主键ID',
    `device_name` VARCHAR(100) NOT NULL COMMENT '设备名称',
    `device_type` VARCHAR(50) NOT NULL COMMENT '设备类型(CAMERA/SENSOR/GATEWAY/SERVER)',
    `area_name` VARCHAR(50) NOT NULL COMMENT '所在区域(A区/B区/数据中心/园区一号楼)',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '设备状态(1: 在线, 0: 离线/故障)',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备资产表';

-- 2. 设备告警记录表 (tb_alarm)
CREATE TABLE IF NOT EXISTS `tb_alarm` (
    `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '告警主键ID',
    `device_id` BIGINT NOT NULL COMMENT '关联设备ID',
    `alarm_level` VARCHAR(20) NOT NULL COMMENT '告警级别(LOW/MEDIUM/HIGH/CRITICAL)',
    `alarm_desc` VARCHAR(200) NOT NULL COMMENT '告警描述内容',
    `alarm_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '告警发生时间',
    INDEX `idx_device_id` (`device_id`),
    INDEX `idx_alarm_time` (`alarm_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备告警记录表';

-- 3. 系统用户表 (sys_user)
CREATE TABLE IF NOT EXISTS `sys_user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '用户主键ID',
    `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
    `nickname` VARCHAR(50) NOT NULL COMMENT '用户昵称',
    `age` INT NOT NULL DEFAULT 18 COMMENT '年龄',
    `gender` VARCHAR(10) NOT NULL DEFAULT '未知' COMMENT '性别(男/女/保密)',
    `is_vip` TINYINT NOT NULL DEFAULT 0 COMMENT '是否VIP会员(1:是, 0:否)',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '账号状态(1:正常, 0:冻结)',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

-- 4. 订单与交易表 (t_order)
CREATE TABLE IF NOT EXISTS `t_order` (
    `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '订单主键ID',
    `order_no` VARCHAR(64) NOT NULL UNIQUE COMMENT '订单编号',
    `user_id` BIGINT NOT NULL COMMENT '下单用户ID',
    `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额(元)',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态(0:待支付, 1:待发货, 2:已完成, 3:已退款)',
    `category` VARCHAR(50) NOT NULL COMMENT '商品品类(数码科技/家居生活/食品生鲜/企业服务)',
    `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单交易表';

-- 插入初始模拟真实数据
INSERT IGNORE INTO `tb_device` VALUES
(1, '1号楼高清监控球机', 'CAMERA', 'A区', 1, '2026-06-01 10:00:00'),
(2, '2号机房温湿度传感器', 'SENSOR', '数据中心', 1, '2026-06-05 11:30:00'),
(3, '地下车库烟雾探测器', 'SENSOR', 'B区', 0, '2026-06-10 14:20:00'),
(4, '园区主智能物联网网关', 'GATEWAY', 'A区', 1, '2026-07-01 09:00:00'),
(5, '3号楼全景AI摄像机', 'CAMERA', 'C区', 0, '2026-07-15 16:45:00'),
(6, '东区水浸报警传感器', 'SENSOR', 'B区', 1, '2026-08-01 08:10:00'),
(7, '核心负载均衡服务器A', 'SERVER', '数据中心', 1, '2026-08-10 12:00:00'),
(8, '边缘计算网关02', 'GATEWAY', 'C区', 1, '2026-08-12 15:30:00');

INSERT IGNORE INTO `tb_alarm` VALUES
(1, 1, 'LOW', '网络轻微抖动重连', '2026-07-02 10:15:00'),
(2, 3, 'HIGH', '烟雾浓度超标预警', '2026-07-10 15:00:00'),
(3, 3, 'CRITICAL', '设备离线失联', '2026-07-11 09:30:00'),
(4, 5, 'MEDIUM', '画面遮挡异常', '2026-07-20 18:00:00'),
(5, 5, 'HIGH', '设备高温预警 (75°C)', '2026-07-21 02:00:00'),
(6, 6, 'LOW', '电池电量低于20%', '2026-08-05 12:00:00'),
(7, 2, 'HIGH', '机房温度超过32度', '2026-08-18 09:15:00'),
(8, 7, 'MEDIUM', 'CPU使用率瞬时达到85%', '2026-08-18 10:40:00');

INSERT IGNORE INTO `sys_user` VALUES
(1, 'zhangsan', '张三', 26, '男', 1, 1, '2026-05-10 08:30:00'),
(2, 'lisi', '李四', 32, '男', 1, 1, '2026-06-12 11:20:00'),
(3, 'wangwu', '王五', 21, '男', 0, 1, '2026-07-01 14:15:00'),
(4, 'zhaoliu', '赵六', 29, '女', 1, 1, '2026-07-18 09:45:00'),
(5, 'sunqi', '孙七', 45, '男', 0, 1, '2026-08-01 16:30:00'),
(6, 'zhouba', '周八', 23, '女', 0, 1, '2026-08-15 10:00:00'),
(7, 'wujiu', '吴九', 35, '男', 1, 1, '2026-08-18 08:12:00'),
(8, 'zhengshi', '郑十', 28, '女', 0, 1, '2026-08-18 11:25:00');

INSERT IGNORE INTO `t_order` VALUES
(1, 'ORD20260812001', 1, 2999.00, 2, '数码科技', '2026-08-12 09:30:00', '2026-08-12 09:25:00'),
(2, 'ORD20260812002', 2, 450.00, 2, '家居生活', '2026-08-12 14:10:00', '2026-08-12 14:05:00'),
(3, 'ORD20260813001', 3, 128.50, 2, '食品生鲜', '2026-08-13 10:00:00', '2026-08-13 09:55:00'),
(4, 'ORD20260814001', 4, 6800.00, 2, '数码科技', '2026-08-14 16:20:00', '2026-08-14 16:15:00'),
(5, 'ORD20260815001', 1, 320.00, 3, '家居生活', '2026-08-15 11:00:00', '2026-08-15 10:50:00'),
(6, 'ORD20260816001', 5, 8800.00, 2, '企业服务', '2026-08-16 15:30:00', '2026-08-16 15:00:00'),
(7, 'ORD20260817001', 2, 1599.00, 2, '数码科技', '2026-08-17 19:40:00', '2026-08-17 19:35:00'),
(8, 'ORD20260818001', 7, 3499.00, 2, '数码科技', '2026-08-18 09:00:00', '2026-08-18 08:50:00'),
(9, 'ORD20260818002', 8, 268.00, 1, '食品生鲜', '2026-08-18 11:30:00', '2026-08-18 11:28:00'),
(10, 'ORD20260818003', 4, 188.00, 0, '家居生活', NULL, '2026-08-18 13:10:00');

四、多数据源物理隔离配置 (application.yml & DataSourceConfig.java)

1. application.yml

yaml 复制代码
server:
  port: 8080

# ============================================================================
# 【多数据源配置】:实现主业务与 AI 智能体的账号权限物理隔离
# ============================================================================
spring:
  datasource:
    # 1. 业务主数据源:供系统正常业务代码(用户注册、下单、修改等读写操作)使用
    business:
      url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
      username: root
      password: your_root_password  # 拥有完整的增删改查权限
      driver-class-name: com.mysql.cj.jdbc.Driver

    # 2. AI 智能体专属只读数据源:专供大模型智能体执行统计查询,严格只读隔离
    ai-readonly:
      url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
      username: ai_readonly
      password: AiPassword@2026     # 仅有 SELECT 只读权限
      driver-class-name: com.mysql.cj.jdbc.Driver

# ============================================================================
# 【智能体大模型配置】:支持本地 Ollama 与 DeepSeek/OpenAI 云端模型一键无缝切换
# ============================================================================
agent:
  llm:
    base-url: http://localhost:11434/v1
    api-key: ollama               # 本地运行无需真实 Key,填任意字符即可
    model-name: qwen2.5:7b        # 本地已下载的模型名 (推荐 qwen2.5:7b 或 qwen2.5:1.5b)
    temperature: 0.1              # 数据统计推荐 0.0 ~ 0.2,保证数据准确确定性
    timeout-seconds: 300          # 超时时间(秒),本地推理生成长文本建议设为 180~300 秒

2. 多数据源 Java 配置类:`DataSourceConfig.java`(file:///e:/code/demoAi/demo/src/main/java/com/example/demo/agent/config/DataSourceConfig.java)

核心原理讲解

  • 我们在 businessDataSource 上打上 @Primary,作为整个 Spring 容器的"默认数据源",这样 MyBatis-Plus 在启动时就会默认绑定到该数据源上,业务开发完全感知不到多数据源的存在!
  • aiDataSource 则是给 AI 智能体定制的只读数据源,配合 aiJdbcTemplate 加上了最大 500 行的查询限制与 5 秒超时保护。
java 复制代码
package com.example.demo.agent.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    // =========================================================================
    // 1. 业务主数据源配置 (Primary - 拥有 root 读写权限)
    // =========================================================================
    @Primary
    @Bean(name = "businessDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.business")
    public DataSourceProperties businessDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Primary
    @Bean(name = "businessDataSource")
    public DataSource businessDataSource() {
        return businessDataSourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    @Primary
    @Bean(name = "businessJdbcTemplate")
    public JdbcTemplate businessJdbcTemplate(@Qualifier("businessDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    // =========================================================================
    // 2. AI 智能体专属只读数据源配置 (aiDataSource - 严格只读权限 + 熔断防爆)
    // =========================================================================
    @Bean(name = "aiDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.ai-readonly")
    public DataSourceProperties aiDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "aiDataSource")
    public DataSource aiDataSource() {
        return aiDataSourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean(name = "aiJdbcTemplate")
    public JdbcTemplate aiJdbcTemplate(@Qualifier("aiDataSource") DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        // 配置安全防爆熔断限制:
        jdbcTemplate.setMaxRows(500);       // 1. 单次查询最多返回 500 行,防止大报表引发 JVM 内存溢出 (OOM)
        jdbcTemplate.setQueryTimeout(5);    // 2. 单次查询最长执行 5 秒,防止慢查询拖垮 MySQL
        return jdbcTemplate;
    }
}

五、主业务模块:MyBatis-Plus 极简 CRUD

在项目中,日常业务开发继续享受 MyBatis-Plus 带来的极简体验。

1. 实体类:`SysUser.java`

java 复制代码
@TableName("sys_user")
public class SysUser {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("username")
    private String username;

    @TableField("nickname")
    private String nickname;

    @TableField("age")
    private Integer age;

    @TableField("gender")
    private String gender;

    @TableField("is_vip")
    private Integer isVip;

    @TableField("status")
    private Integer status;

    @TableField("create_time")
    private Date createTime;

    public SysUser() {}

    public SysUser(String username, String nickname, Integer age, String gender, Integer isVip, Integer status) {
        this.username = username;
        this.nickname = nickname;
        this.age = age;
        this.gender = gender;
        this.isVip = isVip;
        this.status = status;
        this.createTime = new Date();
    }

    // Getter & Setter ...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getNickname() { return nickname; }
    public void setNickname(String nickname) { this.nickname = nickname; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getGender() { return gender; }
    public void setGender(String gender) { this.gender = gender; }
    public Integer getIsVip() { return isVip; }
    public void setIsVip(Integer isVip) { this.isVip = isVip; }
    public Integer getStatus() { return status; }
    public void setStatus(Integer status) { this.status = status; }
    public Date getCreateTime() { return createTime; }
    public void setCreateTime(Date createTime) { this.createTime = createTime; }
}

2. Mapper 接口:`SysUserMapper.java`

java 复制代码
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
    // 继承 BaseMapper,直接拥有 insert、selectById、selectList、updateById、deleteById 等!
}

3. 业务层:`UserService.java`

java 复制代码
@Service
public class UserService extends ServiceImpl<SysUserMapper, SysUser> {

    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    // 1. 【增】
    public Long createUser(SysUser user) {
        if (user.getCreateTime() == null) {
            user.setCreateTime(new java.util.Date());
        }
        boolean success = this.save(user);
        log.info("【MyBatis-Plus 新增用户】:结果={}, 生成ID: {}", success, user.getId());
        return user.getId();
    }

    // 2. 【查】
    public SysUser getUserById(Long id) {
        return this.getById(id);
    }

    // 3. 【查列表】
    public List<SysUser> getAllUsers() {
        return this.list(new LambdaQueryWrapper<SysUser>().orderByDesc(SysUser::getId));
    }

    // 4. 【Lambda 条件查询】
    public List<SysUser> findVipUsers(Integer isVip) {
        return this.list(new LambdaQueryWrapper<SysUser>()
                .eq(isVip != null, SysUser::getIsVip, isVip)
                .eq(SysUser::getStatus, 1)
                .orderByDesc(SysUser::getCreateTime));
    }

    // 5. 【改】
    public boolean updateUser(SysUser user) {
        boolean success = this.updateById(user);
        log.info("【MyBatis-Plus 修改用户】:ID={}, 结果={}", user.getId(), success);
        return success;
    }

    // 6. 【删】
    public boolean deleteUser(Long id) {
        boolean success = this.removeById(id);
        log.info("【MyBatis-Plus 删除用户】:ID={}, 结果={}", id, success);
        return success;
    }
}

4. 控制器:`UserController.java`

java 复制代码
@RestController
@RequestMapping("/api/user")
@CrossOrigin(origins = "*")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping
    public Map<String, Object> create(@RequestBody SysUser user) {
        Long id = userService.createUser(user);
        Map<String, Object> res = new HashMap<>();
        res.put("success", id != null);
        res.put("message", "用户创建成功 (MyBatis-Plus)");
        res.put("userId", id);
        return res;
    }

    @GetMapping("/{id}")
    public Map<String, Object> getById(@PathVariable Long id) {
        SysUser user = userService.getUserById(id);
        Map<String, Object> res = new HashMap<>();
        res.put("success", user != null);
        res.put("data", user);
        return res;
    }

    @GetMapping("/list")
    public Map<String, Object> list() {
        List<SysUser> users = userService.getAllUsers();
        Map<String, Object> res = new HashMap<>();
        res.put("success", true);
        res.put("total", users.size());
        res.put("data", users);
        return res;
    }

    @PutMapping("/{id}")
    public Map<String, Object> update(@PathVariable Long id, @RequestBody SysUser user) {
        user.setId(id);
        boolean success = userService.updateUser(user);
        Map<String, Object> res = new HashMap<>();
        res.put("success", success);
        res.put("message", success ? "更新成功 (MyBatis-Plus)" : "更新失败");
        return res;
    }

    @DeleteMapping("/{id}")
    public Map<String, Object> delete(@PathVariable Long id) {
        boolean success = userService.deleteUser(id);
        Map<String, Object> res = new HashMap<>();
        res.put("success", success);
        res.put("message", success ? "删除成功 (MyBatis-Plus)" : "用户不存在");
        return res;
    }
}

六、智能体大模型配置与通信层

1. 配置映射类:`AgentProperties.java`

java 复制代码
@Component
@ConfigurationProperties(prefix = "agent.llm")
public class AgentProperties {

    private String baseUrl = "http://localhost:11434/v1";
    private String apiKey = "ollama";
    private String modelName = "qwen2.5:7b";
    private Double temperature = 0.1;
    private Integer timeoutSeconds = 300;

    /**
     * 系统级人设提示词 (System Prompt) - 生产级严厉约束版
     */
    private String systemPrompt = "你是一个企业级专业的系统数据统计与分析智能体(Data Analytics Agent)。\n"
            + "你的底层直接连接着真实的 MySQL 生产数据库。\n\n"
            + "【⚠️ 生产级核心行为守则 ⚠️】:\n"
            + "1. 【禁止展示 SQL 或计划】:终端用户是企业客户,看不懂技术代码。绝对严禁向用户输出任何 SQL 语句、严禁回复'我将为您执行SQL...'、'正在编写查询...'等过程性废话!\n"
            + "2. 【必须真正调用工具】:当用户询问任何涉及设备、告警、用户、订单、销售等系统数据时,你必须【立即直接调用 executeSql 工具】去查库,严禁只说不查!\n"
            + "3. 【时间与日期查询规范】:\n"
            + "   - 统计【本月】:`DATE_FORMAT(时间字段, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')`(严禁写 `>= CURDATE()`,否则会把本月1号到昨天的历史数据遗漏!)\n"
            + "   - 统计【今日】:`DATE(时间字段) = CURDATE()`\n"
            + "   - 统计【近7天】:`时间字段 >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)`\n"
            + "4. 【最终输出格式】:拿到数据库真实数据后,用专业的中文业务口吻,使用 Markdown 表格、数字加粗和清晰的条目直接汇报统计结果,并给出 1~2 条业务建议。";

    // Getter & Setter ...
    public String getBaseUrl() { return baseUrl; }
    public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
    public String getApiKey() { return apiKey; }
    public void setApiKey(String apiKey) { this.apiKey = apiKey; }
    public String getModelName() { return modelName; }
    public void setModelName(String modelName) { this.modelName = modelName; }
    public Double getTemperature() { return temperature; }
    public void setTemperature(Double temperature) { this.temperature = temperature; }
    public Integer getTimeoutSeconds() { return timeoutSeconds; }
    public void setTimeoutSeconds(Integer timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; }
    public String getSystemPrompt() { return systemPrompt; }
    public void setSystemPrompt(String systemPrompt) { this.systemPrompt = systemPrompt; }
}

2. 消息实体类:`ChatMessage.java`

java 复制代码
public class ChatMessage {

    private String role;     // system | user | assistant | tool
    private String content;  // 文本内容

    @SerializedName("tool_calls")
    private List<ToolCall> toolCalls; // 模型发出的工具调用列表

    @SerializedName("tool_call_id")
    private String toolCallId; // 工具调用 ID

    private String name; // 工具名称

    public ChatMessage() {}
    public ChatMessage(String role, String content) {
        this.role = role;
        this.content = content;
    }

    public static ChatMessage system(String content) { return new ChatMessage("system", content); }
    public static ChatMessage user(String content) { return new ChatMessage("user", content); }
    public static ChatMessage assistant(String content) { return new ChatMessage("assistant", content); }
    public static ChatMessage tool(String toolCallId, String name, String content) {
        ChatMessage msg = new ChatMessage("tool", content);
        msg.setToolCallId(toolCallId);
        msg.setName(name);
        return msg;
    }

    // Getter & Setter ...
    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    public List<ToolCall> getToolCalls() { return toolCalls; }
    public void setToolCalls(List<ToolCall> toolCalls) { this.toolCalls = toolCalls; }
    public String getToolCallId() { return toolCallId; }
    public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public static class ToolCall {
        private String id;
        private String type = "function";
        private FunctionCall function;

        public String getId() { return id; }
        public void setId(String id) { this.id = id; }
        public String getType() { return type; }
        public void setType(String type) { this.type = type; }
        public FunctionCall getFunction() { return function; }
        public void setFunction(FunctionCall function) { this.function = function; }
    }

    public static class FunctionCall {
        private String name;
        private String arguments;

        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getArguments() { return arguments; }
        public void setArguments(String arguments) { this.arguments = arguments; }
    }
}

3. 通信客户端:`LlmClient.java`

java 复制代码
@Component
public class LlmClient {

    private static final Logger log = LoggerFactory.getLogger(LlmClient.class);
    private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");

    @Autowired
    private AgentProperties properties;

    private OkHttpClient httpClient;
    private final Gson gson = new GsonBuilder().setPrettyPrinting().create();

    @PostConstruct
    public void init() {
        int timeout = (properties.getTimeoutSeconds() != null && properties.getTimeoutSeconds() > 0) 
                ? properties.getTimeoutSeconds() : 300;
        
        // 配置 300 秒连接与读取超时,防止本地模型推理长报表时断连
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(timeout, TimeUnit.SECONDS)
                .readTimeout(timeout, TimeUnit.SECONDS)
                .writeTimeout(timeout, TimeUnit.SECONDS)
                .callTimeout(timeout + 30, TimeUnit.SECONDS)
                .build();
    }

    /**
     * 同步请求方法(带工具列表定义)
     */
    public ChatMessage chat(List<ChatMessage> messages, List<Map<String, Object>> tools) throws IOException {
        String baseUrl = properties.getBaseUrl();
        if (baseUrl.endsWith("/")) {
            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
        }
        String endpoint = baseUrl + "/chat/completions";

        Map<String, Object> requestBodyMap = new LinkedHashMap<>();
        requestBodyMap.put("model", properties.getModelName());
        requestBodyMap.put("messages", messages);
        requestBodyMap.put("temperature", properties.getTemperature());

        if (tools != null && !tools.isEmpty()) {
            requestBodyMap.put("tools", tools);
            requestBodyMap.put("tool_choice", "auto");
        }

        String jsonPayload = gson.toJson(requestBodyMap);
        log.info("向大模型发送请求 [{}], 请求体长度: {}", endpoint, jsonPayload.length());

        Request.Builder requestBuilder = new Request.Builder()
                .url(endpoint)
                .post(RequestBody.create(jsonPayload, JSON_MEDIA_TYPE));

        if (properties.getApiKey() != null && !properties.getApiKey().trim().isEmpty()) {
            requestBuilder.addHeader("Authorization", "Bearer " + properties.getApiKey().trim());
        }

        try (Response response = httpClient.newCall(requestBuilder.build()).execute()) {
            String responseStr = response.body() != null ? response.body().string() : "";
            
            if (!response.isSuccessful()) {
                throw new IOException("大模型服务返回错误 HTTP " + response.code() + ": " + responseStr);
            }

            JsonObject root = JsonParser.parseString(responseStr).getAsJsonObject();
            JsonArray choices = root.getAsJsonArray("choices");
            JsonObject firstChoice = choices.get(0).getAsJsonObject();
            JsonObject messageObj = firstChoice.getAsJsonObject("message");

            ChatMessage assistantMsg = new ChatMessage();
            assistantMsg.setRole(messageObj.has("role") ? messageObj.get("role").getAsString() : "assistant");

            if (messageObj.has("content") && !messageObj.get("content").isJsonNull()) {
                assistantMsg.setContent(messageObj.get("content").getAsString());
            }

            if (messageObj.has("tool_calls") && !messageObj.get("tool_calls").isJsonNull()) {
                JsonArray toolCallsArr = messageObj.getAsJsonArray("tool_calls");
                List<ChatMessage.ToolCall> toolCalls = new ArrayList<>();
                for (JsonElement element : toolCallsArr) {
                    JsonObject tcObj = element.getAsJsonObject();
                    ChatMessage.ToolCall toolCall = new ChatMessage.ToolCall();
                    toolCall.setId(tcObj.get("id").getAsString());
                    toolCall.setType(tcObj.has("type") ? tcObj.get("type").getAsString() : "function");

                    JsonObject funcObj = tcObj.getAsJsonObject("function");
                    ChatMessage.FunctionCall func = new ChatMessage.FunctionCall();
                    func.setName(funcObj.get("name").getAsString());
                    func.setArguments(funcObj.get("arguments").getAsString());
                    toolCall.setFunction(func);

                    toolCalls.add(toolCall);
                }
                assistantMsg.setToolCalls(toolCalls);
            }

            return assistantMsg;
        }
    }

    /**
     * SSE 逐字打字机流式请求方法
     */
    public String chatStream(List<ChatMessage> messages, Consumer<String> onDeltaCallback) throws IOException {
        String baseUrl = properties.getBaseUrl();
        if (baseUrl.endsWith("/")) {
            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
        }
        String endpoint = baseUrl + "/chat/completions";

        Map<String, Object> requestBodyMap = new LinkedHashMap<>();
        requestBodyMap.put("model", properties.getModelName());
        requestBodyMap.put("messages", messages);
        requestBodyMap.put("temperature", properties.getTemperature());
        requestBodyMap.put("stream", true); // 开启 SSE 流式输出

        String jsonPayload = gson.toJson(requestBodyMap);
        Request.Builder requestBuilder = new Request.Builder()
                .url(endpoint)
                .post(RequestBody.create(jsonPayload, JSON_MEDIA_TYPE));

        if (properties.getApiKey() != null && !properties.getApiKey().trim().isEmpty()) {
            requestBuilder.addHeader("Authorization", "Bearer " + properties.getApiKey().trim());
        }

        StringBuilder fullContent = new StringBuilder();

        try (Response response = httpClient.newCall(requestBuilder.build()).execute()) {
            ResponseBody body = response.body();
            if (body == null) throw new IOException("大模型流式响应体为空");

            BufferedSource source = body.source();
            while (!source.exhausted()) {
                String line = source.readUtf8Line();
                if (line == null || line.trim().isEmpty() || line.startsWith(":")) continue;

                if (line.startsWith("data:")) {
                    String data = line.substring(5).trim();
                    if ("[DONE]".equalsIgnoreCase(data)) break;

                    try {
                        JsonObject json = JsonParser.parseString(data).getAsJsonObject();
                        JsonArray choices = json.getAsJsonArray("choices");
                        if (choices != null && choices.size() > 0) {
                            JsonObject choice = choices.get(0).getAsJsonObject();
                            JsonObject delta = choice.getAsJsonObject("delta");
                            if (delta != null && delta.has("content") && !delta.get("content").isJsonNull()) {
                                String chunk = delta.get("content").getAsString();
                                fullContent.append(chunk);
                                if (onDeltaCallback != null) {
                                    onDeltaCallback.accept(chunk);
                                }
                            }
                        }
                    } catch (Exception ignored) {}
                }
            }
        }
        return fullContent.toString();
    }

    public boolean checkConnectivity() {
        try {
            String baseUrl = properties.getBaseUrl();
            String endpoint = (baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl) + "/models";
            Request request = new Request.Builder().url(endpoint).get().build();
            try (Response response = httpClient.newCall(request).execute()) {
                return response.isSuccessful();
            }
        } catch (Exception e) {
            return false;
        }
    }
}

七、数据库动态元数据感知器 (DatabaseSchemaService.java)

这是实现 100% 零硬编码 SQL 的核心组件。它通过 JDBC 原生 DatabaseMetaData 在运行时自动读取 MySQL 的表结构:

java 复制代码
@Service
public class DatabaseSchemaService {

    private static final Logger log = LoggerFactory.getLogger(DatabaseSchemaService.class);

    // 显式绑定 AI 专属只读数据源
    @Autowired(required = false)
    @org.springframework.beans.factory.annotation.Qualifier("aiDataSource")
    private DataSource dataSource;

    @Autowired(required = false)
    @org.springframework.beans.factory.annotation.Qualifier("aiJdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    /**
     * 自动扫描数据库,动态生成供大模型阅读的精准 Schema 结构描述
     */
    public String getDatabaseSchemaDescription() {
        if (dataSource == null) return "(当前未连接数据库)";

        StringBuilder schemaBuilder = new StringBuilder();
        try (Connection connection = dataSource.getConnection()) {
            DatabaseMetaData metaData = connection.getMetaData();
            String catalog = connection.getCatalog(); // 当前数据库名称,如 demo_db

            schemaBuilder.append("【数据库当前所有数据表及字段结构如下】:\n");

            // 1. 查询当前数据库下的所有数据表 (TABLE)
            ResultSet tablesRs = metaData.getTables(catalog, null, "%", new String[]{"TABLE"});
            while (tablesRs.next()) {
                String tableName = tablesRs.getString("TABLE_NAME");
                String tableRemarks = tablesRs.getString("REMARKS");

                schemaBuilder.append("\n📁 数据表: `").append(tableName).append("`");
                if (tableRemarks != null && !tableRemarks.trim().isEmpty()) {
                    schemaBuilder.append(" (说明: ").append(tableRemarks).append(")");
                }
                schemaBuilder.append("\n  字段列表:\n");

                // 2. 深入查询该表的所有列(字段)元数据
                ResultSet columnsRs = metaData.getColumns(catalog, null, tableName, "%");
                while (columnsRs.next()) {
                    String colName = columnsRs.getString("COLUMN_NAME");
                    String colType = columnsRs.getString("TYPE_NAME");
                    int colSize = columnsRs.getInt("COLUMN_SIZE");
                    String colComment = columnsRs.getString("REMARKS");

                    schemaBuilder.append("    - `").append(colName).append("` ").append(colType);
                    if (colSize > 0 && !"DATETIME".equalsIgnoreCase(colType) && !"TIMESTAMP".equalsIgnoreCase(colType) && !"INT".equalsIgnoreCase(colType)) {
                        schemaBuilder.append("(").append(colSize).append(")");
                    }
                    if (colComment != null && !colComment.trim().isEmpty()) {
                        schemaBuilder.append(" -- 注释: ").append(colComment);
                    }
                    schemaBuilder.append("\n");
                }
                columnsRs.close();
            }
            tablesRs.close();

        } catch (Exception e) {
            log.error("自动提取数据库元数据失败: {}", e.getMessage());
            return "提取数据库元数据失败: " + e.getMessage();
        }

        return schemaBuilder.toString();
    }

    /**
     * 获取数据库所有表的统计摘要(用于前端左侧边栏展示)
     */
    public List<Map<String, Object>> getTableSummaries() {
        List<Map<String, Object>> list = new ArrayList<>();
        if (jdbcTemplate == null) return list;

        try {
            List<Map<String, Object>> tables = jdbcTemplate.queryForList(
                    "SELECT TABLE_NAME, TABLE_COMMENT " +
                            "FROM information_schema.TABLES " +
                            "WHERE TABLE_SCHEMA = DATABASE()");
            for (Map<String, Object> t : tables) {
                Map<String, Object> map = new LinkedHashMap<>();
                String tableName = String.valueOf(t.get("TABLE_NAME"));
                map.put("tableName", tableName);
                map.put("comment", t.get("TABLE_COMMENT") != null ? t.get("TABLE_COMMENT") : "无备注");
                
                Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM `" + tableName + "`", Integer.class);
                map.put("rowCount", count != null ? count : 0);
                list.add(map);
            }
        } catch (Exception e) {
            log.warn("获取数据表列表异常: {}", e.getMessage());
        }
        return list;
    }
}

八、底层只读安全沙箱与执行器 (StatsDataService.java)

java 复制代码
@Service
public class StatsDataService {

    private static final Logger log = LoggerFactory.getLogger(StatsDataService.class);
    private final long startTimeMillis = System.currentTimeMillis();

    // 显式绑定 AI 专属只读连接池 (ai_readonly)
    @Autowired(required = false)
    @org.springframework.beans.factory.annotation.Qualifier("aiJdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    /**
     * 通用只读 SQL 动态执行器
     */
    public Map<String, Object> executeReadOnlySql(String sql) {
        Map<String, Object> result = new LinkedHashMap<>();
        
        if (jdbcTemplate == null) {
            result.put("error", "数据库连接未就绪,请检查 application.yml 数据源配置");
            return result;
        }

        if (sql == null || sql.trim().isEmpty()) {
            result.put("error", "执行的 SQL 不能为空");
            return result;
        }

        String trimmedSql = sql.trim();
        String upperSql = trimmedSql.toUpperCase();

        // 安全沙箱二次防御:严格只允许 SELECT / SHOW / DESC / EXPLAIN
        if (!upperSql.startsWith("SELECT") && !upperSql.startsWith("SHOW") 
                && !upperSql.startsWith("DESC") && !upperSql.startsWith("EXPLAIN")) {
            result.put("error", "【安全拦截】:系统仅允许执行只读查询(SELECT)操作,禁止任何修改或删除数据的破坏性指令!");
            return result;
        }

        try {
            log.info("Agent 动态执行大模型生成的 SQL: {}", trimmedSql);
            List<Map<String, Object>> rows = jdbcTemplate.queryForList(trimmedSql);
            result.put("rowCount", rows.size());
            result.put("data", rows);
            result.put("executedSql", trimmedSql);
        } catch (Exception e) {
            log.warn("SQL 执行异常: {}", e.getMessage());
            result.put("error", "SQL 执行失败: " + e.getMessage());
            result.put("executedSql", trimmedSql);
        }

        return result;
    }

    /**
     * 获取系统底层运行指标与 JVM 性能统计
     */
    public Map<String, Object> getSystemMetrics() {
        Map<String, Object> metrics = new LinkedHashMap<>();

        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        long heapUsed = memoryMXBean.getHeapMemoryUsage().getUsed() / (1024 * 1024);
        long heapMax = memoryMXBean.getHeapMemoryUsage().getMax() / (1024 * 1024);
        long heapCommitted = memoryMXBean.getHeapMemoryUsage().getCommitted() / (1024 * 1024);

        long uptimeSeconds = (System.currentTimeMillis() - startTimeMillis) / 1000;
        long hours = uptimeSeconds / 3600;
        long minutes = (uptimeSeconds % 3600) / 60;
        long seconds = uptimeSeconds % 60;

        metrics.put("systemStatus", "NORMAL (正常)");
        metrics.put("dbConnected", jdbcTemplate != null);
        metrics.put("jvmVersion", System.getProperty("java.version"));
        metrics.put("osName", System.getProperty("os.name") + " " + System.getProperty("os.arch"));
        metrics.put("cpuCores", Runtime.getRuntime().availableProcessors());
        metrics.put("currentThreads", ManagementFactory.getThreadMXBean().getThreadCount());
        metrics.put("heapMemoryUsedMb", heapUsed + " MB");
        metrics.put("heapMemoryCommittedMb", heapCommitted + " MB");
        metrics.put("heapMemoryMaxMb", (heapMax < 0 ? "不限" : heapMax + " MB"));
        metrics.put("uptime", String.format("%d小时 %d分 %d秒", hours, minutes, seconds));
        metrics.put("currentTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        return metrics;
    }
}

九、智能体工具箱与 Text-to-SQL 工具

1. 工具接口:`AgentTool.java`

java 复制代码
public interface AgentTool {
    String getName();
    String getDescription();
    Map<String, Object> getParametersSchema();
    Object execute(Map<String, Object> arguments);
}

2. 工具注册中心:`AgentToolRegistry.java`

java 复制代码
@Component
public class AgentToolRegistry {

    @Autowired(required = false)
    private List<AgentTool> tools = new ArrayList<>();

    private final Map<String, AgentTool> toolMap = new HashMap<>();

    @PostConstruct
    public void init() {
        if (tools != null) {
            for (AgentTool tool : tools) {
                toolMap.put(tool.getName(), tool);
            }
        }
    }

    public List<Map<String, Object>> getOpenAiToolsDefinition() {
        List<Map<String, Object>> definitions = new ArrayList<>();
        for (AgentTool tool : toolMap.values()) {
            Map<String, Object> toolDef = new LinkedHashMap<>();
            toolDef.put("type", "function");

            Map<String, Object> functionDef = new LinkedHashMap<>();
            functionDef.put("name", tool.getName());
            functionDef.put("description", tool.getDescription());
            functionDef.put("parameters", tool.getParametersSchema());

            toolDef.put("function", functionDef);
            definitions.add(toolDef);
        }
        return definitions;
    }

    public List<Map<String, Object>> getToolListForUI() {
        List<Map<String, Object>> list = new ArrayList<>();
        for (AgentTool tool : toolMap.values()) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("name", tool.getName());
            item.put("description", tool.getDescription());
            list.add(item);
        }
        return list;
    }

    public Object executeTool(String toolName, Map<String, Object> arguments) {
        AgentTool tool = toolMap.get(toolName);
        if (tool == null) {
            return Collections.singletonMap("error", "未找到指定工具: " + toolName);
        }
        try {
            return tool.execute(arguments != null ? arguments : Collections.emptyMap());
        } catch (Exception e) {
            return Collections.singletonMap("error", "执行工具异常: " + e.getMessage());
        }
    }
}

3. 核心 Text-to-SQL 工具:`DatabaseQueryTool.java`

java 复制代码
@Component
public class DatabaseQueryTool implements AgentTool {

    @Autowired
    private StatsDataService statsDataService;

    @Autowired
    private DatabaseSchemaService schemaService;

    @Override
    public String getName() {
        return "executeSql";
    }

    @Override
    public String getDescription() {
        String schema = schemaService.getDatabaseSchemaDescription();
        
        return "【必须调用的真实数据库查询工具】当用户询问任何关于数据、设备、告警、订单、用户、统计报表的问题时,你必须直接调用本工具并传入你编写的 SELECT 语句去获取真实数据!\n"
                + "严禁将 SQL 输出在回复文本中,必须通过工具调用传入!\n"
                + "数据库当前所有数据表及字段结构如下:\n"
                + schema + "\n\n"
                + "【MySQL 常用时间与日期统计标准规范 (必须严格遵守)】:\n"
                + "1. 【本月数据】:使用 `DATE_FORMAT(时间字段, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')` 或 `YEAR(时间字段)=YEAR(CURDATE()) AND MONTH(时间字段)=MONTH(CURDATE())`\n"
                + "   ⚠️ 注意:严禁写 `>= CURDATE()`!因为 CURDATE() 代表今天(如 18号),写 `>= CURDATE()` 会把本月 1 号至 17 号的数据错误过滤掉!\n"
                + "2. 【今日数据】:使用 `DATE(时间字段) = CURDATE()`\n"
                + "3. 【昨日数据】:使用 `DATE(时间字段) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)`\n"
                + "4. 【近7天数据】:使用 `时间字段 >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)`\n"
                + "5. 【本年数据】:使用 `YEAR(时间字段) = YEAR(CURDATE())`\n\n"
                + "【编写 SQL 规则】:\n"
                + "1. 编写标准的 MySQL 只读 SELECT 语句(支持 COUNT, SUM, AVG, GROUP BY, WHERE, ORDER BY, DATE_FORMAT, JOIN 等)。\n"
                + "2. 严禁编写任何修改删除语句。";
    }

    @Override
    public Map<String, Object> getParametersSchema() {
        Map<String, Object> schema = new HashMap<>();
        schema.put("type", "object");

        Map<String, Object> properties = new HashMap<>();
        Map<String, Object> sqlProp = new HashMap<>();
        sqlProp.put("type", "string");
        sqlProp.put("description", "大模型编写的标准 MySQL SELECT 语句,例如: SELECT COUNT(*) FROM tb_alarm WHERE DATE_FORMAT(alarm_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')");
        properties.put("sql", sqlProp);

        schema.put("properties", properties);
        schema.put("required", Collections.singletonList("sql"));
        return schema;
    }

    @Override
    public Object execute(Map<String, Object> arguments) {
        if (arguments == null || !arguments.containsKey("sql")) {
            return Collections.singletonMap("error", "缺失参数 'sql'");
        }
        String sql = String.valueOf(arguments.get("sql"));
        return statsDataService.executeReadOnlySql(sql);
    }
}

4. 系统健康工具:`SystemMetricsTool.java`

java 复制代码
@Component
public class SystemMetricsTool implements AgentTool {

    @Autowired
    private StatsDataService statsDataService;

    @Override
    public String getName() {
        return "getSystemMetrics";
    }

    @Override
    public String getDescription() {
        return "获取系统与服务器底层的实时运行指标和健康状态。包含:JVM 堆内存已用/最大/分配值、CPU 核心数、当前活动线程数、系统连续运行时长、操作系统与 Java 版本等。";
    }

    @Override
    public Map<String, Object> getParametersSchema() {
        Map<String, Object> schema = new HashMap<>();
        schema.put("type", "object");
        schema.put("properties", Collections.emptyMap());
        schema.put("required", Collections.emptyList());
        return schema;
    }

    @Override
    public Object execute(Map<String, Object> arguments) {
        return statsDataService.getSystemMetrics();
    }
}

十、智能体大脑调度中枢与流式引擎 (AgentChatService.java)

这是整个系统最精彩的核心大脑。它实现了:

  1. ReAct 智能调度循环
  2. 小模型 SQL 泄露自动拦截兜底 (Fallback Interceptor)
  3. SSE 实时流式打字机推送
java 复制代码
@Service
public class AgentChatService {

    private static final Logger log = LoggerFactory.getLogger(AgentChatService.class);
    private static final int MAX_TOOL_ITERATIONS = 5;

    private static final Pattern SQL_BLOCK_PATTERN = Pattern.compile("```(?:sql)?\\s*(SELECT[\\s\\S]+?)```", Pattern.CASE_INSENSITIVE);
    private static final Pattern RAW_SQL_PATTERN = Pattern.compile("(SELECT[\\s\\S]+?FROM[\\s\\S]+?)(?:;|$)", Pattern.CASE_INSENSITIVE);

    @Autowired
    private LlmClient llmClient;

    @Autowired
    private AgentToolRegistry toolRegistry;

    @Autowired
    private AgentProperties properties;

    private final Gson gson = new Gson();
    private final Map<String, List<ChatMessage>> sessionStore = new ConcurrentHashMap<>();

    /**
     * 【流式打字机执行引擎】:向前端实时推送思考进度、SQL 执行状态与逐字回答 (SSE)
     */
    public void processUserMessageStream(String sessionId, String userMessage, SseEmitter emitter) {
        if (sessionId == null || sessionId.trim().isEmpty()) {
            sessionId = "default-session";
        }

        List<ChatMessage> conversation = sessionStore.computeIfAbsent(sessionId, k -> {
            List<ChatMessage> list = new ArrayList<>();
            list.add(ChatMessage.system(properties.getSystemPrompt()));
            return Collections.synchronizedList(list);
        });

        conversation.add(ChatMessage.user(userMessage));
        List<String> toolsInvoked = new ArrayList<>();

        try {
            // 1. 发送初始思考状态
            sendSseEvent(emitter, "status", Collections.singletonMap("text", "🧠 智能体正在理解意图并匹配数据库表结构..."));

            int iteration = 0;
            while (iteration < MAX_TOOL_ITERATIONS) {
                iteration++;

                List<Map<String, Object>> toolsDef = toolRegistry.getOpenAiToolsDefinition();
                ChatMessage assistantResponse = llmClient.chat(new ArrayList<>(conversation), toolsDef);

                // 机制 1:标准 Tool Calling 工具调用分支
                if (assistantResponse.getToolCalls() != null && !assistantResponse.getToolCalls().isEmpty()) {
                    conversation.add(assistantResponse);

                    for (ChatMessage.ToolCall toolCall : assistantResponse.getToolCalls()) {
                        String toolName = toolCall.getFunction().getName();
                        String argsJson = toolCall.getFunction().getArguments();

                        Map<String, Object> args = parseArguments(argsJson);
                        String sql = args.containsKey("sql") ? String.valueOf(args.get("sql")) : "";

                        // 推送工具执行状态与正在执行的 SQL 语句
                        Map<String, Object> toolStatus = new HashMap<>();
                        toolStatus.put("tool", toolName);
                        toolStatus.put("sql", sql);
                        toolStatus.put("text", "⚡ 正在执行 SQL 查询真实数据...");
                        sendSseEvent(emitter, "tool", toolStatus);

                        // 执行 Java 查库
                        Object toolResult = toolRegistry.executeTool(toolName, args);
                        toolsInvoked.add(toolName);

                        String resultJson = gson.toJson(toolResult);
                        conversation.add(ChatMessage.tool(toolCall.getId(), toolName, resultJson));
                    }

                    sendSseEvent(emitter, "status", Collections.singletonMap("text", "📊 数据库数据已获取,正在极速排版生成统计报告..."));

                    // 第二阶段:进入 SSE 流式打字机输出
                    StringBuilder fullAnswer = new StringBuilder();
                    llmClient.chatStream(new ArrayList<>(conversation), delta -> {
                        fullAnswer.append(delta);
                        sendSseEvent(emitter, "delta", Collections.singletonMap("content", delta));
                    });

                    conversation.add(ChatMessage.assistant(fullAnswer.toString()));
                    sendSseEvent(emitter, "done", Collections.singletonMap("toolsInvoked", toolsInvoked));
                    emitter.complete();
                    return;

                } else {
                    // 机制 2:智能 SQL 自动拦截兜底 (Fallback Interceptor)
                    String content = assistantResponse.getContent();
                    String extractedSql = extractSelectSql(content);

                    if (extractedSql != null && !toolsInvoked.contains("executeSql") && iteration <= 2) {
                        Map<String, Object> toolStatus = new HashMap<>();
                        toolStatus.put("tool", "executeSql");
                        toolStatus.put("sql", extractedSql);
                        toolStatus.put("text", "⚡ 自动拦截提取 SQL 并执行...");
                        sendSseEvent(emitter, "tool", toolStatus);

                        Map<String, Object> args = new HashMap<>();
                        args.put("sql", extractedSql);
                        Object toolResult = toolRegistry.executeTool("executeSql", args);
                        toolsInvoked.add("executeSql");

                        String resultJson = gson.toJson(toolResult);
                        conversation.add(assistantResponse);
                        conversation.add(ChatMessage.user("【系统指令】:系统已为你自动执行了上述 SQL,数据库返回的真实数据为:" 
                                + resultJson + "。请立即向终端客户输出最终的业务数据分析报告(使用表格、加粗数字、业务结论),严格禁止在回复中输出任何 SQL 语句或过程!"));

                        sendSseEvent(emitter, "status", Collections.singletonMap("text", "📊 正在整理分析报表并流式呈现..."));

                        StringBuilder fullAnswer = new StringBuilder();
                        llmClient.chatStream(new ArrayList<>(conversation), delta -> {
                            fullAnswer.append(delta);
                            sendSseEvent(emitter, "delta", Collections.singletonMap("content", delta));
                        });

                        conversation.add(ChatMessage.assistant(fullAnswer.toString()));
                        sendSseEvent(emitter, "done", Collections.singletonMap("toolsInvoked", toolsInvoked));
                        emitter.complete();
                        return;
                    }

                    // 直接回答(流式推送纯文本)
                    conversation.add(assistantResponse);
                    sendSseEvent(emitter, "delta", Collections.singletonMap("content", content));
                    sendSseEvent(emitter, "done", Collections.singletonMap("toolsInvoked", toolsInvoked));
                    emitter.complete();
                    return;
                }
            }

            sendSseEvent(emitter, "error", Collections.singletonMap("message", "超过最大调用轮数限制"));
            emitter.complete();

        } catch (Exception e) {
            log.error("流式 Agent 执行异常: ", e);
            sendSseEvent(emitter, "error", Collections.singletonMap("message", e.getMessage()));
            emitter.complete();
        }
    }

    private void sendSseEvent(SseEmitter emitter, String eventName, Object data) {
        try {
            emitter.send(SseEmitter.event().name(eventName).data(gson.toJson(data)));
        } catch (Exception ignored) {}
    }

    private String extractSelectSql(String text) {
        if (text == null || text.trim().isEmpty()) return null;
        Matcher blockMatcher = SQL_BLOCK_PATTERN.matcher(text);
        if (blockMatcher.find()) return cleanSql(blockMatcher.group(1));
        Matcher rawMatcher = RAW_SQL_PATTERN.matcher(text);
        if (rawMatcher.find()) return cleanSql(rawMatcher.group(1));
        return null;
    }

    private String cleanSql(String sql) {
        if (sql == null) return null;
        String clean = sql.replaceAll("```", "").trim();
        if (clean.endsWith(";")) clean = clean.substring(0, clean.length() - 1).trim();
        if (clean.toUpperCase().startsWith("SELECT") && clean.toUpperCase().contains("FROM")) return clean;
        return null;
    }

    public void clearSession(String sessionId) {
        if (sessionId != null) sessionStore.remove(sessionId);
    }

    private Map<String, Object> parseArguments(String argsJson) {
        if (argsJson == null || argsJson.trim().isEmpty()) return Collections.emptyMap();
        try {
            Type type = new TypeToken<Map<String, Object>>() {}.getType();
            Map<String, Object> map = gson.fromJson(argsJson, type);
            return map != null ? map : Collections.emptyMap();
        } catch (Exception e) {
            return Collections.emptyMap();
        }
    }
}

十一、Web REST 控制器与 SSE 端点 (AgentController.java)

java 复制代码
@RestController
@RequestMapping("/api/agent")
@CrossOrigin(origins = "*")
public class AgentController {

    @Autowired
    private AgentChatService agentChatService;

    @Autowired
    private AgentToolRegistry toolRegistry;

    @Autowired
    private AgentProperties properties;

    @Autowired
    private LlmClient llmClient;

    @Autowired
    private DatabaseSchemaService schemaService;

    private final ExecutorService executor = Executors.newCachedThreadPool();

    /**
     * 【实时 SSE 流式打字机问答接口】
     */
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter streamChat(
            @RequestParam String message,
            @RequestParam(defaultValue = "default-session") String sessionId) {
        
        SseEmitter emitter = new SseEmitter(300_000L); // 5分钟超时
        executor.execute(() -> {
            try {
                agentChatService.processUserMessageStream(sessionId, message, emitter);
            } catch (Exception e) {
                try { emitter.completeWithError(e); } catch (Exception ignored) {}
            }
        });
        return emitter;
    }

    /**
     * 【获取工具列表接口】
     */
    @GetMapping("/tools")
    public Object getTools() {
        return toolRegistry.getToolListForUI();
    }

    /**
     * 【获取数据库表信息接口】
     */
    @GetMapping("/database/tables")
    public Object getDatabaseTables() {
        return schemaService.getTableSummaries();
    }

    /**
     * 【清空会话上下文接口】
     */
    @PostMapping("/clear")
    public Map<String, Object> clearSession(@RequestParam(defaultValue = "default-session") String sessionId) {
        agentChatService.clearSession(sessionId);
        Map<String, Object> res = new HashMap<>();
        res.put("success", true);
        res.put("message", "会话历史已清空");
        return res;
    }

    /**
     * 【系统与模型状态自检接口】
     */
    @GetMapping("/status")
    public Map<String, Object> getStatus() {
        Map<String, Object> status = new HashMap<>();
        status.put("baseUrl", properties.getBaseUrl());
        status.put("modelName", properties.getModelName());
        status.put("temperature", properties.getTemperature());
        status.put("connected", llmClient.checkConnectivity());
        status.put("registeredToolsCount", toolRegistry.getToolListForUI().size());
        return status;
    }
}

十二、前端流式交互看板核心实现 (index.html)

前端基于原生 JavaScript + marked.js 实现 Markdown 渲染,并使用浏览器原生 EventSource 监听 SSE 流:

javascript 复制代码
// 发送消息 (实时 SSE 打字机流式输出)
function sendMessage() {
    const input = document.getElementById('userInput');
    const message = input.value.trim();
    if (!message) return;

    input.value = '';
    document.getElementById('sendBtn').disabled = true;

    // 1. 渲染用户消息
    appendUserMessage(message);

    // 2. 创建智能体回复气泡框架
    const { row, bubble, statusBox, contentBox } = createAgentStreamingBubble();

    // 3. 建立 SSE 连接
    const streamUrl = `/api/agent/stream?message=${encodeURIComponent(message)}&sessionId=${encodeURIComponent(sessionId)}`;
    const eventSource = new EventSource(streamUrl);

    let accumulatedText = "";

    // 监听:状态更新事件 (status)
    eventSource.addEventListener('status', (e) => {
        const data = JSON.parse(e.data);
        statusBox.style.display = 'block';
        statusBox.innerHTML = `<span class="tool-badge"><i class="fa-solid fa-spinner fa-spin"></i> ${data.text}</span>`;
        scrollToBottom();
    });

    // 监听:工具执行事件 (tool)
    eventSource.addEventListener('tool', (e) => {
        const data = JSON.parse(e.data);
        statusBox.style.display = 'block';
        const sqlInfo = data.sql ? `<br/><code style="font-size:0.75rem; color:#94a3b8;">${escapeHtml(data.sql)}</code>` : '';
        statusBox.innerHTML = `<span class="tool-badge"><i class="fa-solid fa-bolt"></i> 调用工具: ${data.tool}</span>${sqlInfo}`;
        scrollToBottom();
    });

    // 监听:打字机逐字输出事件 (delta)
    eventSource.addEventListener('delta', (e) => {
        const data = JSON.parse(e.data);
        if (data.content) {
            accumulatedText += data.content;
            // 实时渲染 Markdown 表格与排版
            contentBox.innerHTML = marked.parse(accumulatedText);
            scrollToBottom();
        }
    });

    // 监听:完成事件 (done)
    eventSource.addEventListener('done', (e) => {
        eventSource.close();
        document.getElementById('sendBtn').disabled = false;
        document.getElementById('userInput').focus();
    });

    // 监听:异常错误
    eventSource.addEventListener('error', (e) => {
        eventSource.close();
        document.getElementById('sendBtn').disabled = false;
        if (!accumulatedText) {
            contentBox.innerHTML = `<p style="color: #ef4444;">❌ 流式连接异常,请检查后端服务是否启动。</p>`;
        }
    });
}

十三、生产避坑与性能调优总结

  1. 日期范围陷阱(极其重要)
    • ❌ 错误示范:WHERE alarm_time >= CURDATE()CURDATE() 代表今天 18 号,会导致 1 号到 17 号的告警被错误过滤!);
    • ✅ 正确规范:WHERE DATE_FORMAT(alarm_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
  2. 小模型 SQL 泄露自动拦截兜底
    • 7B/1.5B 等轻量模型在未触发 tool_calls 时,可能直接在文本中输出 SQL。我们在 `AgentChatService.java`(file:///e:/code/demoAi/demo/src/main/java/com/example/demo/agent/service/AgentChatService.java) 中通过正则表达式自动捕获、执行并重写,保证 100% 生产客户零感知
  3. 极速体验调优
    • 本地 CPU 用户建议使用 ollama run qwen2.5:1.5b,问答可在 2~3 秒内极速响应
    • 生产环境建议切换至 DeepSeek-V3 官方 API,兼具顶级推理性能与极速响应。

🏁 结语

至此,你已经拥有了一套完整的 Spring Boot + 本地大模型 + MyBatis-Plus + 多数据源隔离 + 实时 SSE 流式打字机 的生产级架构体系!

相关推荐
星火10242 小时前
【LangChain4j系列07】结构化输出与类型安全
人工智能·后端
用户298698530142 小时前
3 种方法,轻松将 PowerPoint 转换为 PDF 格式
人工智能·后端·c#
吃饱了得干活2 小时前
为什么你的Service越写越臃肿?三层架构的“业务逻辑层”是个黑盒
java·后端·架构
趴下吧2 小时前
spring boot启动流程总结
java·spring boot·spring
何时梦醒2 小时前
Docker 容器化入门:从「我电脑能跑」到「哪台机器都能跑」
后端·docker·面试
颜进强2 小时前
11 - 从需求拆解到 OpenSpec:为什么不要直接敲 /opsx:explore
前端·后端·ai编程
foggyprojects2 小时前
AI 说销售额下降了,哪些客户拖累了结果?
后端
用户852495071842 小时前
NestJS 架构实战:给后端代码请来一位“项目经理
后端
唐青枫2 小时前
一个点号省掉一堆类型:Zig .{} 语法、类型推导与实战
后端