📑 目录导航
- 项目全景架构设计
- 项目依赖配置与环境搭建 (pom.xml)
- [数据库 DDL 与专属只读账号隔离 (init.sql)](#数据库 DDL 与专属只读账号隔离 (init.sql))
- 多数据源物理隔离配置 (application.yml & DataSourceConfig.java)
- [主业务模块:MyBatis-Plus 极简 CRUD (SysUser.java / Mapper / Service / Controller)](#主业务模块:MyBatis-Plus 极简 CRUD (SysUser.java / Mapper / Service / Controller))
- 智能体大模型配置与通信层 (AgentProperties.java / ChatMessage.java / LlmClient.java)
- 数据库动态元数据感知器 (DatabaseSchemaService.java)
- 底层只读安全沙箱与执行器 (StatsDataService.java)
- [智能体工具箱与 Text-to-SQL 工具 (AgentTool.java / AgentToolRegistry.java / DatabaseQueryTool.java / SystemMetricsTool.java)](#智能体工具箱与 Text-to-SQL 工具 (AgentTool.java / AgentToolRegistry.java / DatabaseQueryTool.java / SystemMetricsTool.java))
- 智能体大脑调度中枢与流式引擎 (AgentChatService.java)
- [Web REST 控制器与 SSE 端点 (AgentController.java)](#Web REST 控制器与 SSE 端点 (AgentController.java))
- 前端流式交互看板核心实现 (index.html)
- 生产避坑与性能调优总结
一、项目全景架构设计
传统的管理后台每次遇到统计报表需求,都需要开发人员手动编写 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.13 和 Java 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)
这是整个系统最精彩的核心大脑。它实现了:
- ReAct 智能调度循环;
- 小模型 SQL 泄露自动拦截兜底 (Fallback Interceptor);
- 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>`;
}
});
}
十三、生产避坑与性能调优总结
- 日期范围陷阱(极其重要) :
- ❌ 错误示范:
WHERE alarm_time >= CURDATE()(CURDATE()代表今天 18 号,会导致 1 号到 17 号的告警被错误过滤!); - ✅ 正确规范:
WHERE DATE_FORMAT(alarm_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')。
- ❌ 错误示范:
- 小模型 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% 生产客户零感知。
- 7B/1.5B 等轻量模型在未触发
- 极速体验调优 :
- 本地 CPU 用户建议使用
ollama run qwen2.5:1.5b,问答可在 2~3 秒内极速响应; - 生产环境建议切换至
DeepSeek-V3 官方 API,兼具顶级推理性能与极速响应。
- 本地 CPU 用户建议使用
🏁 结语
至此,你已经拥有了一套完整的 Spring Boot + 本地大模型 + MyBatis-Plus + 多数据源隔离 + 实时 SSE 流式打字机 的生产级架构体系!