【Java AI Agent智能存库调拨+BI问答】第五章 智能 BI 报表问答系统
章节五:智能 BI 报表问答系统
一、系统概述与场景介绍
1.1 项目背景
在企业日常运营中,数据分析和报表生成是不可或缺的核心环节。然而,传统的 BI(Business Intelligence)系统存在以下痛点:
- 技术门槛高:业务人员需要掌握 SQL 或专业的 BI 工具才能进行数据查询
- 响应周期长:每次数据需求都需要提工单给技术团队,等待排期和开发
- 灵活性差:固定报表难以满足多变的业务分析需求
- 数据利用率低:企业数据"存而易,用而难"的现象普遍存在
1.2 系统目标
本章节将开发一个自然语言到数据查询/分析的交互平台,让业务人员能够用"说人话"的方式获取数据洞察。系统的核心目标包括:
- 降低数据获取门槛:业务人员通过自然语言描述需求即可获取数据
- 自动化报表生成:系统自动生成 Excel 报表并通过邮件发送
- 智能 SQL 生成:利用大模型的推理能力,将非结构化需求转换为结构化查询
- 数据洞察自动化:对查询结果进行归纳、总结和洞察分析
1.3 系统架构设计
┌─────────────────────────────────────────────────────────────────┐
│ 用户交互层 │
│ (自然语言输入:"帮我统计本月各门店销售额") │
└──────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────────┐
│ Graph 智能编排层 │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ SQL生成节点 │───▶│ SQL执行节点 │───▶│ 报表/邮件节点 │ │
│ │ (翻译官) │ │ (执行者) │ │ (分析师) │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
│ ▲ │ │ │
│ │ ┌─────▼─────┐ │ │
│ └──────────────│ MySQL │◀─────────────┘ │
│ │ 数据库 │ │
│ └───────────┘ │
│ ▲ │
│ ┌──────────┴──────────┐ │
│ │ 向量数据库(Redis) │ │
│ │ (表结构知识库) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
系统角色分工:
| 角色 | 组件 | 职责 |
|---|---|---|
| 翻译官 | Graph + LLM | 将用户的自然语言问题,翻译成结构化的数据查询逻辑 |
| 执行者 | Java 后端 | 接收 Graph 生成的查询逻辑,安全地执行查询并返回结果 |
| 分析师 | Graph + LLM | 对查询结果进行归纳、总结和洞察分析 |
1.4 核心亮点
- 主导开发基于 Spring AI 的 ERP 自然语言交互查询系统,显著降低了业务人员的数据获取门槛
- 利用 Graph 的推理能力,将非结构化需求转换为结构化查询,实现数据洞察的自动化生成
- 深入解决企业数据"存而易,用而难"的普遍问题,让数据真正服务于业务决策
二、开发前置准备工作 — 表结构设计与数据初始化
2.1 数据库选型
本系统采用 MySQL 8.0 作为业务数据库,结合经典的星型模型进行数据仓库设计。星型模型由一个中心事实表和多个维度表组成,是数据仓库中最常用的建模方式。
2.2 维度表设计
维度表(Dimension Table)用于描述业务实体的属性信息,是数据分析的"角度"和"视角"。
2.2.1 商品维度表(dim_product)
-- =====================================================
-- 维度表:商品维度(dim_product)
-- =====================================================
DROP TABLE IF EXISTS dim_product;
CREATE TABLE dim_product (
product_id BIGINT PRIMARY KEY COMMENT '商品唯一 ID',
product_name VARCHAR(255) NOT NULL COMMENT '商品名称',
category_id BIGINT NULL COMMENT '商品分类 ID',
category_name VARCHAR(255) NULL COMMENT '商品分类名称',
brand VARCHAR(255) NULL COMMENT '品牌',
cost_price DECIMAL(10,2) NULL COMMENT '成本价',
retail_price DECIMAL(10,2) NULL COMMENT '建议零售价'
) COMMENT='商品维度表';
设计说明:
| 字段 | 类型 | 说明 |
|---|---|---|
product_id |
BIGINT | 商品唯一标识,作为主键 |
product_name |
VARCHAR(255) | 商品名称,用于展示 |
category_id |
BIGINT | 分类 ID,支持多级分类 |
category_name |
VARCHAR(255) | 分类名称,冗余存储便于查询 |
brand |
VARCHAR(255) | 品牌名称,用于品牌维度分析 |
cost_price |
DECIMAL(10,2) | 成本价,用于利润分析 |
retail_price |
DECIMAL(10,2) | 建议零售价,用于定价分析 |
2.2.2 门店维度表(dim_store)
-- =====================================================
-- 维度表:门店维度(dim_store)
-- =====================================================
DROP TABLE IF EXISTS dim_store;
CREATE TABLE dim_store (
store_id BIGINT PRIMARY KEY COMMENT '门店唯一 ID',
store_name VARCHAR(255) NOT NULL COMMENT '门店名称',
province VARCHAR(100) NULL COMMENT '所在省份',
city VARCHAR(100) NULL COMMENT '所在城市',
address VARCHAR(255) NULL COMMENT '门店详细地址',
open_date DATE NULL COMMENT '开业日期'
) COMMENT='门店维度表';
2.2.3 时间维度表(dim_date)
-- =====================================================
-- 维度表:时间维度(dim_date)
-- =====================================================
DROP TABLE IF EXISTS dim_date (
date_id DATE PRIMARY KEY COMMENT '日期ID',
year INT NOT NULL,
quarter INT NOT NULL,
month INT NOT NULL,
day INT NOT NULL,
weekday INT NOT NULL
) COMMENT='时间维度表';
时间维度表的重要性: 时间维度是数据分析中最常用的维度之一。通过预先生成时间维度表,可以大大简化按年、季度、月、周进行聚合查询的复杂度。
2.2.4 客户维度表(dim_customer)
-- =====================================================
-- 维度表:客户维度(dim_customer)
-- =====================================================
DROP TABLE IF EXISTS dim_customer;
CREATE TABLE dim_customer (
customer_id BIGINT PRIMARY KEY COMMENT '客户ID',
customer_name VARCHAR(255) NOT NULL,
gender VARCHAR(10) NULL,
age INT NULL,
city VARCHAR(100) NULL,
province VARCHAR(100) NULL
) COMMENT='客户维度表';
2.3 事实表设计
事实表(Fact Table)用于存储业务过程中的度量数据(如销售额、数量等),是数据分析的核心。
2.3.1 销售事实表(fact_sales)
-- =====================================================
-- 事实表:销售事实表(fact_sales)
-- =====================================================
DROP TABLE IF EXISTS fact_sales;
CREATE TABLE fact_sales (
sales_id BIGINT PRIMARY KEY COMMENT '销售记录ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
store_id BIGINT NOT NULL COMMENT '门店ID',
customer_id BIGINT NULL COMMENT '客户ID',
date_id DATE NOT NULL COMMENT '销售日期',
quantity INT NOT NULL COMMENT '销售数量',
sales_amount DECIMAL(10,2) NOT NULL COMMENT '销售金额',
discount DECIMAL(10,2) NULL COMMENT '折扣金额',
FOREIGN KEY (product_id) REFERENCES dim_product(product_id),
FOREIGN KEY (store_id) REFERENCES dim_store(store_id),
FOREIGN KEY (customer_id) REFERENCES dim_customer(customer_id),
FOREIGN KEY (date_id) REFERENCES dim_date(date_id)
) COMMENT='销售事实表';
销售事实表的结构特点:
| 字段 | 类型 | 角色 | 说明 |
|---|---|---|---|
sales_id |
BIGINT | 代理键 | 销售记录唯一标识 |
product_id |
BIGINT | 外键 | 关联商品维度 |
store_id |
BIGINT | 外键 | 关联门店维度 |
customer_id |
BIGINT | 外键 | 关联客户维度 |
date_id |
DATE | 外键 | 关联时间维度 |
quantity |
INT | 度量 | 销售数量 |
sales_amount |
DECIMAL(10,2) | 度量 | 销售金额 |
discount |
DECIMAL(10,2) | 度量 | 折扣金额 |
2.3.2 库存事实表(fact_inventory)
-- =====================================================
-- 事实表:库存事实表(fact_inventory)
-- =====================================================
DROP TABLE IF EXISTS fact_inventory;
CREATE TABLE fact_inventory (
inventory_id BIGINT PRIMARY KEY COMMENT '库存记录ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
store_id BIGINT NOT NULL COMMENT '门店ID',
date_id DATE NOT NULL COMMENT '库存日期',
quantity INT NOT NULL COMMENT '库存数量',
FOREIGN KEY (product_id) REFERENCES dim_product(product_id),
FOREIGN KEY (store_id) REFERENCES dim_store(store_id),
FOREIGN KEY (date_id) REFERENCES dim_date(date_id)
) COMMENT='库存事实表';
2.4 数据模型关系图
┌──────────────┐
│ dim_date │
│ (时间维度) │
└──────┬───────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌────────▼──────┐ ┌───────▼───────┐ ┌─────▼────────┐
│ dim_product │ │ dim_store │ │ dim_customer │
│ (商品维度) │ │ (门店维度) │ │ (客户维度) │
└───────┬───────┘ └───────┬───────┘ └──────┬───────┘
│ │ │
│ ┌─────────────┼──────────────────┘
│ │ │
│ │ ┌────────▼────────┐
│ │ │ fact_sales │
│ │ │ (销售事实表) │
│ │ └─────────────────┘
│ │
│ │ ┌─────────────────┐
└────┼───▶│ fact_inventory │
│ │ (库存事实表) │
│ └─────────────────┘
│
星型模型设计原则: 事实表位于中心,通过外键关联各个维度表。这种设计使得聚合查询高效且直观,非常适合 OLAP(联机分析处理)场景。
三、工程搭建
3.1 子模块 POM 配置
在父项目 carl-ai-agent 下创建子模块 ai-bi-helper,其 pom.xml 配置如下:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.carl</groupId>
<artifactId>carl-ai-agent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>ai-bi-helper</artifactId>
<packaging>jar</packaging>
<name>ai-bi-helper</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mysql.version>8.0.32</mysql.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Spring Boot JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.0</version>
</dependency>
<!-- Spring AI Tika 文档读取器 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring AI OpenAI 自动配置 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-openai</artifactId>
</dependency>
<!-- Spring AI Chat Client 自动配置 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>
<!-- Spring AI Redis 向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
<!-- Spring AI RAG -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
<!-- Spring AI Alibaba Graph Core -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-graph-core</artifactId>
</dependency>
</dependencies>
</project>
依赖说明:
| 依赖 | 用途 |
|---|---|
spring-boot-starter-web |
Web 服务基础框架 |
spring-boot-starter-mail |
邮件发送功能 |
spring-boot-starter-jdbc |
JDBC 数据库访问 |
mysql-connector-java |
MySQL 数据库驱动 |
redisson |
Redis 客户端,用于 Graph 状态持久化 |
spring-ai-tika-document-reader |
文档解析,支持多种格式上传 |
spring-ai-autoconfigure-model-openai |
OpenAI 模型自动配置 |
spring-ai-starter-vector-store-redis |
Redis 向量数据库支持 |
spring-ai-rag |
RAG(检索增强生成)功能 |
spring-ai-alibaba-graph-core |
AI 工作流图引擎核心 |
四、项目配置
4.1 application.yml
spring:
mail:
host: smtp.qq.com
port: 587
username: 1525761478@qq.com
password: efwvvempkglygcjg
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
application:
name: ai-bi-helper
data:
redis:
host: 127.0.0.1
port: 6379
database: 0
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/bi-helper?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
ai:
openai:
api-key: sk-e836ecd744a14caca23c9d06b9bd1f1f
base-url: https://dashscope.aliyuncs.com/compatible-mode
embedding:
options:
model: text-embedding-v4
chat:
options:
model: qwen3-max
temperature: 0.6
vectorstore:
redis:
initialize-schema: true
index-name: custom-index
prefix: custom-prefix
server:
port: 8877
logging:
file:
path: ./logs
level:
root: info
com.carl.ai.bi.helper: info
配置要点说明:
邮件配置:
| 配置项 | 说明 |
|---|---|
host |
SMTP 服务器地址,这里使用 QQ 邮箱 |
port |
SMTP 端口,587 为 TLS 加密端口 |
auth |
开启身份验证 |
starttls |
开启 TLS 加密传输 |
数据源配置:
| 配置项 | 说明 |
|---|---|
driver-class-name |
MySQL 8.0 驱动类 |
url |
数据库连接地址,bi-helper 为数据库名 |
username/password |
数据库认证信息 |
AI 模型配置:
| 配置项 | 说明 |
|---|---|
api-key |
阿里云 DashScope API Key |
base-url |
阿里云兼容模式接口地址 |
embedding.model |
嵌入模型 text-embedding-v4,用于文档向量化 |
chat.model |
聊天模型 qwen3-max,用于 SQL 生成 |
temperature |
温度参数 0.6,平衡创造性和确定性 |
向量数据库配置:
| 配置项 | 说明 |
|---|---|
initialize-schema |
自动初始化向量索引结构 |
index-name |
向量索引名称 |
prefix |
Redis Key 前缀 |
五、向量数据库与文档上传(RAG 技术实现)
5.1 RAG 技术概述
RAG(Retrieval-Augmented Generation,检索增强生成)是本系统的核心技术之一。其工作原理是:
- 文档上传与向量化:将数据库表结构文档上传,通过 Embedding 模型转换为向量,存储到向量数据库
- 查询时检索:当用户提出问题时,系统将问题也转换为向量,从向量数据库中检索最相关的表结构信息
- 上下文增强:将检索到的表结构信息作为上下文,提供给大模型生成 SQL
为什么需要 RAG? 大模型本身不知道企业具体的表结构,通过 RAG 技术,我们可以让大模型在生成 SQL 时"看到"正确的表结构信息,从而提高 SQL 的准确率。
5.2 启动 Redis 向量数据库
使用 Docker 启动 redis-stack-server,它内置了 RedisSearch 模块,支持向量搜索:
docker run --name redis-stack -d -p 6379:6379 redis/redis-stack-server
redis-stack-server是 Redis 的增强版本,内置 RediSearch、RedisJSON、RedisVector 等模块,支持向量相似度搜索,适合作为轻量级向量数据库使用。
5.3 文档上传接口
@RestController
@RequestMapping(value = "/document")
@AllArgsConstructor
public class DocumentController {
@Autowired
private VectorStore vectorStore;
@GetMapping("/upload")
public R upload(MultipartFile file) {
// 1. 使用 Tika 读取文档内容
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
List<Document> documents = documentReader.get();
// 2. 使用标题分割器对文档进行分块
HeadingTextSplitter headingTextSplitter = new HeadingTextSplitter();
List<Document> split = headingTextSplitter.split(documents);
// 3. 将分块后的文档存入向量数据库
vectorStore.add(split);
return R.success();
}
}
接口工作流程:
用户上传文档 ──▶ TikaDocumentReader 解析 ──▶ HeadingTextSplitter 分割 ──▶ VectorStore 存储
│
▼
文本块 ──▶ Embedding 模型 ──▶ 向量 ──▶ Redis
| 步骤 | 组件 | 作用 |
|---|---|---|
| 文档解析 | TikaDocumentReader |
支持 Word、PDF、TXT 等多种格式 |
| 文本分割 | HeadingTextSplitter |
按标题层级分割,保留语义完整性 |
| 向量存储 | VectorStore |
自动调用 Embedding 模型并存储到 Redis |
5.4 文档模板示例
上传的文档需要包含完整的表结构说明,以下是一个标准模板:
企业智能 BI 数据库表结构说明文档
1. 文档目的
本文件用于描述企业智能 BI 系统使用的数据库表结构,包括事实表、维度表及其字段说明,供后续 RAG 系统及 AI SQL Agent 使用。
2. 维度表结构
2.1 商品维度(dim_product)
字段名 | 类型 | 主键 | 描述
product_id | bigint | YES | 商品唯一 ID
product_name | varchar(255) | | 商品名称
category_id | bigint | | 商品分类 ID
category_name | varchar(255) | | 商品分类名称
brand | varchar(255) | | 品牌
cost_price | decimal(10,2) | | 成本价
retail_price | decimal(10,2) | | 建议零售价
2.2 门店维度(dim_store)
字段名 | 类型 | 主键 | 描述
store_id | bigint | YES | 门店唯一 ID
store_name | varchar(255) | | 门店名称
province | varchar(100) | | 所在省份
city | varchar(100) | | 所在城市
address | varchar(255) | | 门店详细地址
open_date | date | | 开业日期
(其他维度表和事实表结构类似,详见完整文档)
六、自定义文本分割器
文本分割(Text Splitting)是 RAG 系统的关键环节。分割质量直接影响向量检索的召回准确度。本节实现两种自定义分割器。
6.1 HeadingTextSplitter — 标题分割器
基于文档标题层级进行分割,适用于结构化的 Word 文档。
public class HeadingTextSplitter extends TextSplitter {
// 匹配 Word 文档的所有数字编号标题:1、1.1、2.3.4 等
private static final Pattern HEADING_PATTERN =
Pattern.compile("^\\d+(?:\\.\\d+)*\\s+.*$", Pattern.MULTILINE);
@Override
protected List<String> splitText(String text) {
List<String> blocks = new ArrayList<>();
if (text == null || text.trim().isEmpty()) {
return blocks;
}
Matcher matcher = HEADING_PATTERN.matcher(text);
List<Integer> starts = new ArrayList<>();
// 1. 找出所有标题的位置
while (matcher.find()) {
starts.add(matcher.start());
}
// 2. 如果没有找到标题,整块返回
if (starts.isEmpty()) {
blocks.add(text.trim());
return blocks;
}
// 3. 处理标题前的内容(文档前言部分)
int firstStart = starts.get(0);
if (firstStart > 0) {
String preamble = text.substring(0, firstStart).trim();
if (!preamble.isEmpty()) {
blocks.add(preamble);
}
}
// 4. 按标题分割文档
for (int i = 0; i < starts.size(); i++) {
int start = starts.get(i);
int end = (i + 1 < starts.size()) ? starts.get(i + 1) : text.length();
String block = text.substring(start, end).trim();
if (!block.isEmpty()) {
blocks.add(block);
}
}
return blocks;
}
}
分割原理:
输入文本: 分割结果:
1. 概述 ["1. 概述\n这是概述内容",
这是概述内容 "1.1 子标题\n子标题内容",
1.1 子标题 "2. 总结\n总结内容"]
子标题内容
2. 总结
总结内容
正则表达式说明:
| 模式 | 含义 |
|---|---|
^ |
行首 |
\d+ |
一个或多个数字 |
(?:\.\d+)* |
零个或多个 .数字 组合(如 .1、.2.3) |
\s+ |
一个或多个空白字符 |
.*$ |
任意字符到行尾 |
Pattern.MULTILINE |
多行模式,^ 和 $ 匹配每行开头和结尾 |
6.2 ChineseTextSpliter — 通用中文文档分割器
基于中文标点符号进行句子级分割,适用于通用的中文文本内容。
public class ChineseTextSpliter extends TextSplitter {
// 匹配中文句子结束标点:。!?;
private static final Pattern SENTENCE_SPLIT_PATTERN =
Pattern.compile("(?<=[。!?;])");
private final int chunkSize; // 每个块的最大字符数
private final int chunkOverlap; // 相邻块之间的重叠字符数
public ChineseTextSpliter(int chunkSize, int chunkOverlap) {
this.chunkSize = chunkSize;
this.chunkOverlap = chunkOverlap;
}
@Override
protected List<String> splitText(String text) {
List<String> segments = new ArrayList<>();
if (text == null || text.isBlank()) {
return segments;
}
// 1. 按中文标点符号分割为句子
String[] sentences = SENTENCE_SPLIT_PATTERN.split(text);
StringBuilder currentChunk = new StringBuilder();
// 2. 按 chunkSize 组合句子
for (String sentence : sentences) {
if (currentChunk.length() + sentence.length() > chunkSize) {
// 当前块已满,保存
segments.add(currentChunk.toString().trim());
// 保留重叠部分,确保语义连续性
String chunk = currentChunk.toString();
if (chunk.length() > chunkOverlap) {
currentChunk = new StringBuilder(
chunk.substring(chunk.length() - chunkOverlap));
} else {
currentChunk = new StringBuilder();
}
}
currentChunk.append(sentence);
}
// 3. 处理最后一块
if (currentChunk.length() > 0) {
segments.add(currentChunk.toString().trim());
}
return segments;
}
}
参数说明:
| 参数 | 说明 | 建议值 |
|---|---|---|
chunkSize |
每个文本块的最大字符数 | 500-1000 |
chunkOverlap |
相邻块之间的重叠字符数 | 50-100 |
分割效果示意:
原文:sentence1。sentence2!sentence3。sentence4;sentence5。
└─────────────────────────────────────┘ chunkSize
└─────────────┘ overlap
为什么需要重叠(overlap)? 重叠可以确保跨块的语义信息不会丢失。例如,如果一个完整的句子恰好被分割在两个块中,重叠区域可以保证检索时至少有一个块包含完整上下文。
七、SQL 生成节点开发
7.1 GenSqlNode — SQL 生成节点
GenSqlNode 是整个系统的核心节点,负责将用户的自然语言问题转换为可执行的 SQL 语句。
@Slf4j
public class GenSqlNode implements NodeAction {
private final ChatClient.Builder chatClientBuilder;
private final VectorStore vectorStore;
public GenSqlNode(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClientBuilder = chatClientBuilder;
this.vectorStore = vectorStore;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
String userInput = state.value("userInput", "");
// 1. 构建查询重写器
RewriteQueryTransformer queryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
// 2. 构建 RAG 检索增强顾问
RetrievalAugmentationAdvisor retrievalAugmentationAdvisor =
RetrievalAugmentationAdvisor.builder()
.queryTransformers(queryTransformer)
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.build();
// 3. 构建 ChatClient 并配置系统提示词
ChatClient chatClient = chatClientBuilder.build();
Flux<String> content = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.system("""
你是一名熟练的 SQL 专家,负责根据企业数据表结构生成 SQL 查询。用户将以自然语言提出数据需求。
严格基于用户提供的数据库表结构和表之间的关系来分析。
你有能力访问企业数据库表结构和表之间的关系(这些信息通过向量数据库检索得到)。请严格遵循以下规则:
1. 仅生成可执行 SQL,不输出任何与 SQL 无关的文字或解释。
2. 每个查询字段必须使用 AS 指定一个中文别名,中文别名应尽量简短、清晰、描述字段含义。例如:`user_name AS '用户名'`
3. 在生成 SQL 前,首先理解用户需求和检索到的表结构信息。
4. 根据表结构和关系选择合适的表和字段,生成可执行 SQL。
5. 输出 SQL 时,禁止使用 markdown 格式 ```sql 来输出。
6. 如果存在多种实现方式,优先选择最简洁、性能较优的写法。
7. 不要输出与 SQL 无关的文本或解释。
8. 不可凭空虚构数据,若数据不足,请返回空字符串。
""")
.user(userInput)
.stream().content();
// 4. 收集流式响应
StringBuilder sb = new StringBuilder();
content.doOnNext(co -> sb.append(co))
.doOnError(Throwable::printStackTrace)
.blockLast();
String ragResult = sb.toString();
log.info("ragResult=[{}]", ragResult);
return Map.of("ragResult", ragResult);
}
}
7.2 节点工作流程
用户输入 ──▶ RewriteQueryTransformer 重写查询 ──▶ VectorStore 检索相关表结构
│
▼
相关表结构上下文
│
用户输入 + 表结构上下文 ──▶ ChatClient (LLM) ──▶ 生成 SQL
│
▼
ragResult 存入 State
7.3 核心组件解析
7.3.1 RewriteQueryTransformer
RewriteQueryTransformer 用于优化用户的原始查询,使其更适合向量检索。例如:
| 原始查询 | 重写后的查询 |
|---|---|
| “帮我查一下” | “查询销售数据和门店信息” |
| “上个月的情况” | “2025年1月销售统计” |
7.3.2 VectorStoreDocumentRetriever
从 Redis 向量数据库中检索与查询最相关的表结构文档片段。
7.3.3 ContextualQueryAugmenter
将检索到的表结构信息与原始查询合并,形成增强后的完整提示词。
7.4 System Prompt 设计要点
System Prompt 是指导大模型生成 SQL 的核心指令,包含以下关键约束:
| 规则编号 | 规则内容 | 目的 |
|---|---|---|
| 1 | 仅生成 SQL,不输出其他文字 | 确保输出纯净,便于后续解析 |
| 2 | 字段必须使用 AS 指定中文别名 | 提升报表可读性 |
| 3 | 理解需求和表结构后生成 | 确保 SQL 符合实际表结构 |
| 4 | 选择合适的表和字段 | 避免关联错误 |
| 5 | 禁止使用 markdown 格式 | 避免解析失败 |
| 6 | 优先选择简洁、性能优的写法 | 提升查询效率 |
| 7 | 不输出无关文本 | 保持输出纯净 |
| 8 | 不可虚构数据 | 确保数据准确性 |
八、SQL 执行节点与自动化报表生成
8.1 ExecSqlAndCreateExcelNode — SQL 执行与 Excel 生成节点
该节点负责执行 Graph 生成的 SQL 查询,并将结果转换为 Excel 报表文件。
@Slf4j
public class ExecSqlAndCreateExcelNode implements NodeAction {
private final JdbcTemplate jdbcTemplate;
public ExecSqlAndCreateExcelNode(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
// 1. 从 State 中获取生成的 SQL
String sql = state.value("ragResult", "");
// 2. 执行 SQL 查询
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
log.info("rows=[{}]", JSONUtil.toJsonStr(rows));
// 3. 生成 Excel 文件
File excelFile = generateExcelFile(rows);
return Map.of("excelFile", excelFile);
}
}
8.2 generateExcelFile — Excel 文件生成方法
private File generateExcelFile(List<Map<String, Object>> rows) throws Exception {
if (rows == null || rows.isEmpty()) {
throw new RuntimeException("SQL 查询结果为空,无法生成 Excel");
}
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("result");
// 1. 创建表头
Map<String, Object> firstRow = rows.get(0);
List<String> columns = new ArrayList<>(firstRow.keySet());
Row header = sheet.createRow(0);
for (int i = 0; i < columns.size(); i++) {
header.createCell(i).setCellValue(columns.get(i));
}
// 2. 填充数据行
for (int r = 0; r < rows.size(); r++) {
Row row = sheet.createRow(r + 1);
Map<String, Object> data = rows.get(r);
for (int c = 0; c < columns.size(); c++) {
Object value = data.get(columns.get(c));
row.createCell(c).setCellValue(value == null ? "" : value.toString());
}
}
// 3. 写入临时文件
File file = File.createTempFile("report_", ".xlsx");
try (FileOutputStream out = new FileOutputStream(file)) {
workbook.write(out);
}
return file;
}
Excel 生成流程:
SQL 查询结果(List<Map<String, Object>>)
│
▼
┌─────────────────────┐
│ 1. 提取列名(表头) │
│ 从第一行 Map 的 │
│ keySet 获取 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ 2. 创建表头行 │
│ 第 0 行写入列名 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ 3. 逐行写入数据 │
│ 遍历每行 Map 的 │
│ value 写入单元格 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ 4. 保存为临时文件 │
│ report_xxx.xlsx │
└─────────────────────┘
技术要点: 使用 Apache POI 的
XSSFWorkbook类创建.xlsx格式的 Excel 文件,支持大数据量。createTempFile方法在系统临时目录创建文件,程序结束后可由系统清理。
九、邮件发送节点开发
9.1 SendEmailNode — 邮件发送节点
public class SendEmailNode implements NodeAction {
private final EmailService emailService;
public SendEmailNode(EmailService emailService) {
this.emailService = emailService;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
// 1. 从 State 中获取 Excel 文件
File excelFile = state.value("excelFile", File.class).get();
// 2. 发送邮件(带附件)
emailService.sendEmailWithAttachment("xxx@qq.com", excelFile);
return Map.of();
}
}
9.2 邮件发送流程
Excel 文件
│
▼
┌──────────────────────┐
│ EmailService │
│ │
│ 1. 构建 MimeMessage │
│ 2. 添加收件人地址 │
│ 3. 设置邮件主题 │
│ 4. 添加正文内容 │
│ 5. 添加 Excel 附件 │
│ 6. 发送邮件 │
└──────────────────────┘
十、图结构定义与配置
10.1 GraphConfig — 图配置类
Graph 配置类负责定义整个 AI 工作流的节点和边,以及状态持久化配置。
@Configuration
public class GraphConfig {
@Resource
private VectorStore vectorStore;
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private EmailService emailService;
@Resource
private RedissonClient redissonClient;
@Bean
public CompiledGraph stateGraph(ChatClient.Builder chatClientBuilder)
throws GraphStateException {
// 1. 定义 State 的 Key 更新策略
KeyStrategyFactory keyStrategyFactory = () -> Map.of(
"userInput", new ReplaceStrategy(),
"ragResult", new ReplaceStrategy()
);
// 2. 创建状态图
StateGraph helperGraph = new StateGraph("biHelperGraph", keyStrategyFactory);
// 3. 添加节点
helperGraph.addNode("genSqlNode",
AsyncNodeAction.node_async(
new GenSqlNode(chatClientBuilder, vectorStore)));
helperGraph.addNode("execSqlAndCreateExcelNode",
AsyncNodeAction.node_async(
new ExecSqlAndCreateExcelNode(jdbcTemplate)));
helperGraph.addNode("sendEmailNode",
AsyncNodeAction.node_async(
new SendEmailNode(emailService)));
// 4. 定义边的关系(工作流顺序)
helperGraph.addEdge(StateGraph.START, "genSqlNode");
helperGraph.addEdge("genSqlNode", "execSqlAndCreateExcelNode");
helperGraph.addEdge("execSqlAndCreateExcelNode", "sendEmailNode");
helperGraph.addEdge("sendEmailNode", StateGraph.END);
// 5. 配置状态持久化(使用 Redis)
SaverConfig saverConfig = SaverConfig.builder()
.register(SaverEnum.REDIS.getValue(), new RedisSaver(redissonClient))
.build();
// 6. 编译图
return helperGraph.compile(CompileConfig.builder()
.saverConfig(saverConfig)
.build());
}
}
10.2 图的执行流程
┌──────────────┐
│ START │
└──────┬───────┘
│
▼
┌──────────────┐
│ genSqlNode │ ◀── 生成 SQL
│ (SQL生成节点) │
└──────┬───────┘
│
▼
┌─────────────────────────────┐
│ execSqlAndCreateExcelNode │ ◀── 执行SQL + 生成Excel
│ (SQL执行与报表生成节点) │
└─────────────┬───────────────┘
│
▼
┌──────────────┐
│ sendEmailNode │ ◀── 发送邮件
│ (邮件发送节点) │
└──────┬───────┘
│
▼
┌──────────────┐
│ END │
└──────────────┘
10.3 状态持久化说明
SaverConfig saverConfig = SaverConfig.builder()
.register(SaverEnum.REDIS.getValue(), new RedisSaver(redissonClient))
.build();
使用 Redis 作为 Graph 状态的持久化存储,支持以下功能:
| 功能 | 说明 |
|---|---|
| 断点续传 | 工作流执行中断后可以从上次位置恢复 |
| 状态回溯 | 可以查看历史执行状态和中间结果 |
| 并发控制 | 通过 Redis 锁机制保证状态一致性 |
10.4 Redisson 配置
@Configuration
public class RedissonConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Value("${spring.data.redis.database}")
private int database;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 使用 JSON 序列化
config.setCodec(new JsonJacksonCodec());
// 配置单节点模式
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setDatabase(database);
return Redisson.create(config);
}
}
配置说明:
| 配置项 | 说明 |
|---|---|
JsonJacksonCodec |
使用 JSON 格式进行序列化,便于调试和查看 |
useSingleServer() |
单节点 Redis 模式 |
setAddress() |
Redis 服务器地址 |
setDatabase() |
使用的数据库编号 |
十一、业务代码与测试
11.1 TestController — 测试控制器
@RestController
@AllArgsConstructor
@RequestMapping("/test")
public class TestController {
@Resource
private CompiledGraph stateGraph;
@GetMapping("/test1")
public R<Map<String, Object>> test1() {
// 1. 构建运行配置
RunnableConfig runnableConfig = RunnableConfig.builder()
.threadId(IdUtil.simpleUUID())
.build();
// 2. 调用 Graph,传入用户输入
Optional<OverAllState> overAllState = stateGraph.call(
Map.of("userInput",
"请帮我统计每个门店在 2025 年 1 月份的销售总金额和销售商品数量。"),
runnableConfig);
// 3. 获取执行结果
Map<String, Object> stringObjectMap =
overAllState.map(OverAllState::data).orElse(Map.of());
return R.success(stringObjectMap);
}
}
11.2 调用流程解析
HTTP GET /test/test1
│
▼
┌─────────────────────────────┐
│ 1. 生成唯一 threadId │
│ 用于标识本次对话会话 │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 2. stateGraph.call() │
│ 传入 userInput 和配置 │
│ 触发完整工作流执行 │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 3. 工作流执行 │
│ genSqlNode ──▶ │
│ execSqlAndCreateExcelNode │
│ ──▶ sendEmailNode │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 4. 返回执行结果 │
│ 包含 ragResult 等状态数据 │
└─────────────────────────────┘
11.3 测试示例
接口调用示例:
curl http://localhost:8877/test/test1
预期执行效果:
- SQL 生成:Graph 根据 RAG 检索到的表结构,生成如下 SQL:
SELECT
ds.store_name AS '门店名称',
SUM(fs.sales_amount) AS '销售总金额',
SUM(fs.quantity) AS '销售商品数量'
FROM fact_sales fs
JOIN dim_store ds ON fs.store_id = ds.store_id
JOIN dim_date dd ON fs.date_id = dd.date_id
WHERE dd.year = 2025 AND dd.month = 1
GROUP BY ds.store_name
- SQL 执行:JdbcTemplate 执行 SQL 并获取结果集
- Excel 生成:将结果集写入
report_xxx.xlsx - 邮件发送:将 Excel 文件作为附件发送到指定邮箱
十二、常用 SQL 查询示例
以下 SQL 示例展示了 BI 系统中常见的分析场景,可作为系统测试用例:
12.1 类别销售统计
-- 统计 2024 年各产品类别的销售总金额,并按金额从高到低排序
SELECT
p.category AS 产品类别,
SUM(s.total_amount) AS 销售总金额
FROM Sales_Orders s
JOIN Products p ON s.product_id = p.product_id
WHERE YEAR(s.order_date) = 2024
GROUP BY p.category
ORDER BY 销售总金额 DESC;
12.2 客户消费分析
-- 查询某个客户 2024 年的订单数量及消费总额
SELECT
c.customer_name AS 客户名称,
COUNT(s.order_id) AS 订单数量,
SUM(s.total_amount) AS 消费总额
FROM Sales_Orders s
JOIN Customers c ON s.customer_id = c.customer_id
WHERE c.customer_id = 1001 AND YEAR(s.order_date) = 2024
GROUP BY c.customer_name;
12.3 热销产品排行
-- 查询销量最高的前 10 个产品及其所属类别
SELECT
p.product_name AS 产品名称,
p.category AS 类别,
SUM(s.quantity) AS 销量
FROM Sales_Orders s
JOIN Products p ON s.product_id = p.product_id
GROUP BY p.product_name, p.category
ORDER BY 销量 DESC
LIMIT 10;
12.4 月度销售趋势
-- 计算今年每个月的销售总额,并按月份升序排列
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS 月份,
SUM(total_amount) AS 月销售额
FROM Sales_Orders
WHERE YEAR(order_date) = YEAR(CURDATE())
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY DATE_FORMAT(order_date, '%Y-%m');
12.5 大额订单查询
-- 查询订单金额超过 10,000 的大额订单
SELECT
s.order_id AS 订单ID,
c.customer_name AS 客户名称,
p.product_name AS 产品名称,
s.total_amount AS 订单金额,
s.order_date AS 下单日期
FROM Sales_Orders s
JOIN Customers c ON s.customer_id = c.customer_id
JOIN Products p ON s.product_id = p.product_id
WHERE s.total_amount > 10000
ORDER BY s.total_amount DESC;
12.6 客户复购分析
-- 统计每个客户的复购次数(下单次数 >= 2)
SELECT
c.customer_name AS 客户名称,
COUNT(s.order_id) AS 订单次数
FROM Sales_Orders s
JOIN Customers c ON s.customer_id = c.customer_id
GROUP BY c.customer_name
HAVING COUNT(s.order_id) >= 2
ORDER BY 订单次数 DESC;
12.7 销量趋势分析(CTE 使用)
-- 查询近 30 天销量下降的产品
WITH last30 AS (
SELECT product_id, SUM(quantity) AS qty
FROM Sales_Orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY product_id
),
prev30 AS (
SELECT product_id, SUM(quantity) AS qty
FROM Sales_Orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
AND order_date < DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY product_id
)
SELECT
p.product_name AS 产品名称,
last30.qty AS 最近30天销量,
prev30.qty AS 前30天销量,
(last30.qty - prev30.qty) AS 变化
FROM last30
JOIN prev30 ON last30.product_id = prev30.product_id
JOIN Products p ON p.product_id = last30.product_id
WHERE last30.qty < prev30.qty
ORDER BY 变化 ASC;
12.8 地区销售排名
-- 查看每个地区的客户数及该地区今年销售额排名
SELECT
c.region AS 地区,
COUNT(DISTINCT c.customer_id) AS 客户数量,
SUM(s.total_amount) AS 销售额
FROM Customers c
JOIN Sales_Orders s ON c.customer_id = s.customer_id
WHERE YEAR(s.order_date) = YEAR(CURDATE())
GROUP BY c.region
ORDER BY 销售额 DESC;
12.9 移动平均分析
-- 查询每天 GMV 的 7 日移动平均值
SELECT
stat_date AS 日期,
total_amount AS 当日GMV,
AVG(total_amount) OVER (
ORDER BY stat_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS GMV_7日移动平均
FROM Sales_Daily_Stats
ORDER BY stat_date;
十三、系统总结与扩展方向
13.1 系统核心能力总结
| 能力 | 实现方式 | 效果 |
|---|---|---|
| 自然语言转 SQL | Spring AI + RAG + LLM | 业务人员无需懂 SQL |
| 智能表结构匹配 | 向量数据库检索 | 自动找到正确的表和字段 |
| 自动化报表 | Apache POI + 邮件 | 一键生成并发送报表 |
| 工作流编排 | Spring AI Alibaba Graph | 灵活的节点编排和持久化 |
13.2 可扩展方向
- SQL 评估与自纠正:增加 SQL 评估节点,对生成的 SQL 进行语法和逻辑校验,发现问题后循环修正
- 多数据源支持:扩展支持 Oracle、PostgreSQL、ClickHouse 等多种数据库
- 可视化图表:在 Excel 基础上增加 ECharts 等可视化图表生成
- 权限控制:增加数据权限管控,不同角色只能访问授权的数据
- 对话式交互:支持多轮对话,用户可以基于上次结果继续追问
- 数据洞察:增加 LLM 对查询结果的智能分析和洞察生成功能
更多推荐



所有评论(0)