# ChatBI
**Repository Path**: yfwork/chat-bi
## Basic Information
- **Project Name**: ChatBI
- **Description**: 一个chatBI应用的简单实现,前端用html简单演示,后端使用java+springboot实现。使用者只需修改数据库及openai便能快速接入你的项目中!!
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 3
- **Created**: 2025-07-11
- **Last Updated**: 2025-07-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# chatBI总结
# 目录说明
chatBIdemo:后端工程文件
images:md文件中的图片
chatBI调研历程:一步一步的干活
README:说明文档
前端.html:前端文件
---
## 效果展示图
点击文件夹上的html文件,然后询问关于自己数据库上的问题

# 一,前言
## 为什么做
公司是tob的业务,让我做个chatBI调研,然后就写了这个小项目,大体是实现了,但是还是有很多优化空间。
## chatBI是什么
传统商业智能(BI)工具长期面临两大挑战:**技术门槛过高**导致业务人员难以自主分析数据,**交互体验割裂**使得从数据查询到可视化呈现需要跨越多个专业系统。
ChatBI 项目应运而生,通过将自然语言处理(NLP)与 BI 系统深度融合,构建了一套"对话即分析"的智能解决方案。本系统突破性地实现了三大创新:
1. **零代码交互**:用户可通过自然语言直接查询数据,如输入"显示最近三个月各区域销售额趋势",系统自动生成可视化图表;
2. **动态元数据感知**:通过自动化元数据采集技术,实时感知数据结构变化,避免人工维护数据字典的繁琐;
3. **分析链路闭环**:集成数据查询、智能解读、可视化呈现于一体,5 秒内完成从问题提出到分析报告生成的全流程。
---
# 二,测试环境
1,语言:采用的java语言,springboot框架
2,ai接口:采用火山引擎
---
# 三,实现方案
## 展望流程图:


只实现了主要的功能。
---
## 开始构建项目
### 打开idea


### 目录结构:

### 添加依赖:
```xml
com.mysql
mysql-connector-j
runtime
com.alibaba
fastjson
1.2.76
com.volcengine
volcengine-java-sdk-ark-runtime
LATEST
```
### application.yml
**需要修改成你的配置**
```
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/redonline?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 1234
ai:
volc:
api-key: "你的火山秘钥"
base-url: "你的url"
model-id: "你的model-id"
system-prompt: "你是一个高级数据分析专家(ChatBI),请根据用户问题、数据库元数据和查询结果进行分析。" # 可选自定义
```
### 自动收集数据库信息代码
#### MetadataService : 执行数据库的搜集逻辑
```
package com.llj.chatbidemo.ai;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
//新版数据库元数据收集:只需有查询权限就可手机数据库信息
@Service
public class MetadataService {
@Autowired
private JdbcTemplate jdbcTemplate;
String dataBaseName = "redonline";
// 主入口方法
public JSONObject generateMetadata() throws SQLException {
JSONObject metadata = new JSONObject();
metadata.put("version", "1.0.0");
JSONArray tables = new JSONArray();
// 获取所有表名
List tableNames = getTableNames();
for (String tableName : tableNames) {
JSONObject tableInfo = new JSONObject();
tableInfo.put("physical_name", tableName);
tableInfo.put("technical_comment", getTableComment(tableName));
tableInfo.put("first_row_sample", getFirstRowSample(tableName));
tables.add(tableInfo);
}
metadata.put("tables", tables);
return metadata;
}
// 获取所有表名
private List getTableNames() throws SQLException {
List tables = new ArrayList<>();
try (Connection conn = jdbcTemplate.getDataSource().getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
ResultSet rs = metaData.getTables(dataBaseName, null, "%", new String[]{"TABLE"});
while (rs.next()) {
tables.add(rs.getString("TABLE_NAME"));
}
}
return tables;
}
// 获取表注释
private String getTableComment(String tableName) throws SQLException {
try (Connection conn = jdbcTemplate.getDataSource().getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
ResultSet rs = metaData.getTables(dataBaseName, null, tableName, new String[]{"TABLE"});
return rs.next() ? rs.getString("REMARKS") : "";
}
}
// 获取首行数据样本
private JSONObject getFirstRowSample(String tableName) {
String sql = "SELECT * FROM " + tableName + " LIMIT 1";
try {
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
ResultSetMetaData meta = rs.getMetaData();
JSONObject row = new JSONObject();
for (int i = 1; i <= meta.getColumnCount(); i++) {
row.put(meta.getColumnName(i), rs.getObject(i));
}
return row;
});
} catch (EmptyResultDataAccessException e) {
return new JSONObject(); // 空表返回空对象
}
}
}
```
#### MetadataHolder:调用数据库收集流程,并对外提供数据库信息
```
package com.llj.chatbidemo.config;
import com.alibaba.fastjson.JSONObject;
import com.llj.chatbidemo.ai.MetadataService;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
@Component
public class MetadataHolder {
private JSONObject metadata; // Fastjson类型
private final MetadataService service;
// 获取最新元数据(线程安全)
public JSONObject getMetadata() {
return metadata;
}
public MetadataHolder(MetadataService collector) throws SQLException {
this.service = collector;
refresh(); // 启动时立即加载
}
// 可选的定时刷新(例如每天凌晨更新)
//@Scheduled(cron = "0 0 0 * * ?")
public void refresh() throws SQLException {
JSONObject jsonNode = service.generateMetadata();
this.metadata = jsonNode;
}
}
```
### chatBI应用
#### ChatAITool: 封装好的ai工具类
```
package com.llj.chatbidemo.config;
import com.volcengine.ark.runtime.model.completion.chat.*;
import com.volcengine.ark.runtime.service.ArkService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
@Component
public class ChatAITool {
@Value("${ai.volc.api-key}")
private String apiKey;
@Value("${ai.volc.base-url}")
private String baseUrl;
@Value("${ai.volc.model-id}")
private String modelId;
@Value("${ai.volc.system-prompt}")
private String systemPrompt;
private ArkService arkService;
@PostConstruct
private void init() {
System.out.println(apiKey);
this.arkService = ArkService.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.build();
}
/**
* 标准问答(自动包含系统提示)
*/
public String askQuestion(String userQuestion) {
List messages = buildMessages(userQuestion);
return executeChatCompletion(messages);
}
/**
* 自定义消息列表问答
*/
public String askWithMessages(List customMessages) {
return executeChatCompletion(customMessages);
}
private List buildMessages(String userQuestion) {
List messages = new ArrayList<>();
messages.add(ChatMessage.builder()
.role(ChatMessageRole.SYSTEM)
.content(systemPrompt)
.build());
messages.add(ChatMessage.builder()
.role(ChatMessageRole.USER)
.content(userQuestion)
.build());
return messages;
}
private String executeChatCompletion(List messages) {
try {
ChatCompletionRequest request = ChatCompletionRequest.builder()
.model(modelId)
.messages(messages)
.build();
return (String) arkService.createChatCompletion(request)
.getChoices()
.stream()
.findFirst()
.map(choice -> choice.getMessage().getContent())
.orElseThrow(() -> new RuntimeException("AI响应为空"));
} catch (Exception e) {
throw new ChatException("AI服务调用失败", e);
}
}
public void shutdown() {
if (arkService != null) {
arkService.shutdownExecutor();
}
}
// 自定义异常类
public static class ChatException extends RuntimeException {
public ChatException(String message, Throwable cause) {
super(message, cause);
}
}
}
```
#### Result:返回结果封装
```
package com.llj.chatbidemo.config;
import java.io.Serializable;
/**
* 后端统一返回结果
* @param
*/
public class Result implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static Result success() {
Result result = new Result();
result.code = 1;
return result;
}
public static Result success(T object) {
Result result = new Result();
result.data = object;
result.code = 1;
return result;
}
public static Result error(int exCode,String msg) {
Result result = new Result();
result.msg = msg;
result.code = exCode;
return result;
}
}
```
#### ChatBIServiceImpl: chatBI的逻辑
```
package com.llj.chatbidemo.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.llj.chatbidemo.config.ChatAITool;
import com.llj.chatbidemo.config.MetadataHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class ChatBIServiceImpl {
@Autowired
private MetadataHolder metadataHolder;
@Autowired
private ChatAITool chatAITool; // 应能正常注入
@Autowired
private JdbcTemplate jdbcTemplate;
// 核心提示词模板(可根据业务扩展)
private static final String ANALYSIS_PROMPT = """
你是一个高级数据分析专家(ChatBI),请根据用户问题、数据库元数据和查询结果进行分析。
### 数据库元数据(JSON格式):
{metadata}
### 用户问题:
{question}
### 最新查询结果(JSON):
{result}
请按以下步骤思考:
1. 用1句话总结数据趋势
2. 指出2个关键数据点
3. 给出1个业务建议
4. 推荐1种可视化图表类型(柱状图、折线图等)
严格按照该JSON格式返回,不要输入其他多余的字符:
{
"summary": "...",
"key_points": ["...", "..."],
"recommendation": "...",
"chart_type": "..."
}
""";
// 自然语言转SQL
public String naturalLanguageToSQL(String question) {
String prompt = """
根据数据库结构和用户问题生成规范的MySQL查询,严格遵循以下协议:
### 数据库元数据:%s
### 用户问题:
%s
### 生成协议(必须满足):
1. 【语句结构】
✓ 必须包含完整子句:SELECT → FROM → [JOIN] → WHERE → ORDER BY → LIMIT
✓ SELECT必须显式列出字段(禁用SELECT *)
✓ FROM必须指定主表
2. 【表关联】
✓ 多表查询必须使用显式JOIN(INNER/LEFT)
✓ JOIN必须带ON条件
✓ 必须为所有表定义别名
3. 【字段规则】
✓ 至少包含1个业务描述字段
✓ 时间相关查询必须选择时间字段
✓ 统计类问题必须用聚合函数
4. 【条件处理】
✓ 用户问题中的过滤值必须转为WHERE条件
5. 【结果规范】
✓ 按业务逻辑排序(时间正序/倒序,数值大小等)
✓ LIMIT 10必须作为最后子句
✓ 字符串值用单引号包裹
### 输出要求:
- 只用标准SQL,不要用方言
- 包含LIMIT 10
- 表名用反引号包裹
- 输出必须为纯SQL语句,不要包含任何其他字符(如```sql等代码块符号)
- 输出只有SQL,不要解释
""".formatted(metadataHolder.getMetadata(), question);
try {
// 调用火山引擎AI
return chatAITool.askQuestion(prompt);
} catch (Exception e) {
throw new RuntimeException("SQL生成失败: " + e.getMessage());
}
}
//根据用户问题与数据结果进行数据分析
public String analyzeData(String question, String sql) {
// 执行SQL获取结果
String result = executeSQL(sql);
// 构造分析提示
String fullPrompt = ANALYSIS_PROMPT
.replace("{metadata}", metadataHolder.getMetadata().toJSONString())
.replace("{question}", question)
.replace("{result}", result);
try {
// 调用OpenAI API
return chatAITool.askQuestion(fullPrompt);
} catch (Exception e) {
throw new RuntimeException("Failed to call OpenAI API" + e.getMessage());
}
}
public String explainMetric(String metricName) {
String prompt = """
作为数据分析助手,请解释以下指标的业务含义:
数据库元数据:%s
指标名称:%s
要求:
1. 用非技术人员能理解的语言
2. 包含计算方式(如果有)
3. 说明正常值范围
4. 给出1个应用示例
""".formatted(metadataHolder.getMetadata(), metricName);
return chatAITool.askQuestion(prompt);
}
// 在原有 ChatBIService 中添加方法
public String generateEChartsConfig(String sql, String question) {
String echartsExample = """
{
"title": {
"text": "月度销售趋势 (示例)",
"left": "center"
},
"tooltip": {
"trigger": "axis"
},
"xAxis": {
"type": "category",
"data": ["1月", "2月", "3月", "4月", "5月", "6月"]
},
"yAxis": {
"type": "value"
},
"series": [{
"name": "销售额",
"type": "bar",
"data": [6500, 8200, 7200, 9500, 7300, 8800],
"itemStyle": {
"color": "#409eff",
"borderRadius": [4, 4, 0, 0]
},
"emphasis": {
"itemStyle": {
"shadowBlur": 10,
"shadowColor": "rgba(64, 158, 255, 0.5)"
}
}
}]
}""";
//String dataStr = (data instanceof String) ? (String) data : data.toString();
String EC_Prompt = """
根据用户提供的数据集和问题描述,生成符合ECharts规范的配置选项(option)。请严格遵循以下步骤:
### 数据集(JSON格式):
%s
### 用户问题:
%s
### 图表类型选择规则:
根据数据特征自动选择图表类型:
1. 趋势分析(时间序列) → 折线图
- 示例:每月预约量变化
2. 类别对比(离散数据) → 柱状图/条形图
- 示例:不同剧本的评分对比
3. 比例分布 → 饼图/环形图
- 示例:各状态场次占比
4. 相关性分析 → 散点图/热力图
- 示例:玩家数量与评分的关系
5. 地理数据 → 地图
- 示例:用户地域分布
### 生成规则:
1. 自动识别最佳图表类型(柱状图/折线图/饼图等)
2. 包含完整的配置结构(title/xAxis/yAxis/series等)
3. 数据预处理(排序/过滤/聚合)
4. 智能颜色编码(使用企业级配色方案)
5. 响应式布局支持(resizeable: true)
示例参考:
%s
要求:
- 输出纯净JSON,无额外文本
- 禁止在返回内容中出现js函数
- 严格遵循ECharts官方规范
- 对无法可视化的数据返回错误结构:
{ "error": true, "message": "原因说明" }
请直接输出配置JSON:
""".formatted(executeSQL(sql), question, echartsExample);
try {
String json = chatAITool.askQuestion(EC_Prompt);
return json;
} catch (Exception e) {
return "{ \"error\": true, \"message\": \"配置生成失败: " + e.getMessage() + "\" }";
}
}
//需要获取数据库元数据时调用
public JSONObject getMetadata() {
JSONObject meta = metadataHolder.getMetadata();
String tableName = meta.getJSONArray("tables")
.getJSONObject(0)
.getString("physical_name");
return meta;
}
//执行sql语句
public String executeSQL(String sql) {
// 执行SQL获取结果
String result = JSON.toJSONString(jdbcTemplate.queryForList(sql));
return result;
}
}
```
#### ChatBIController:接受前端的请求
```
package com.llj.chatbidemo.controller;
import com.alibaba.fastjson.JSON;
import com.llj.chatbidemo.config.ChatAITool;
import com.llj.chatbidemo.config.Result;
import com.llj.chatbidemo.service.ChatBIServiceImpl;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/chatbi")
@Tag(name = "ChatBI接口", description = "用于数据分析接口")
public class ChatBIController {
@Autowired
private ChatBIServiceImpl chatBIService;
@Autowired
private ChatAITool chatAITool; // 应能正常注入
@PostMapping("/ask")
public String ask(@RequestBody String question) {
return chatAITool.askQuestion(question);
}
// 自然语言转SQL
@PostMapping("/ask1")
@CrossOrigin
public Result askQuestion(@RequestParam("question") String question) {
//对用户问题进行NLP预处理,目前感觉没太必要
//String prompt = nlpUtils.preprocessInput(question);
//得到sql语句
String sql = chatBIService.naturalLanguageToSQL(question);
String analysis;
try {
//得到分析
analysis = chatBIService.analyzeData(question, sql);
} catch (BadSqlGrammarException e) {
System.out.println("SQL语法错误: " + e.getMessage());
return Result.error(2, "ai生成的SQL语法错误 sql:" + sql);
}
String echartsConfig;
try {
//得到echarts配置
echartsConfig = chatBIService.generateEChartsConfig(sql, question);
} catch (BadSqlGrammarException e) {
return Result.error(2, "ai生成图表失败,失败原因" +e.getMessage());
}
System.out.println("SQL: " + sql);
System.out.println(" Analysis: "+analysis);
System.out.println("EChartsConfig: " + echartsConfig);
return Result.success(Map.of(
"success", true,
"sql", sql,
"analysis", JSON.parse(analysis) // 按需解析
, "option", JSON.parse(echartsConfig)
));
}
// 指标解释服务
@GetMapping("/explain/{metric}")
public String explainMetric(@PathVariable String metric) {
return chatBIService.explainMetric(metric);
}
}
```
# 四,优缺点分析(基于实现方案的分析)
**优点:**
1. 轻量化,适合小型项目
2. 能够快速构建
3. 可替换成自己部署的大模型
**缺点:**
1. 图表生成不稳定,因为有些问题是没法图表展示的
2. 不适配大型项目:数据库中表,仅限280张以下,超过的话调用ai会返回token过长错误
---
# 五:展望(未来优化方向)
## 1,接入大型数据库
**数据集格式变更:**是否可以将数据库信息放到向量数据库中
**优化数据集:**在保证数据集能成功的让ai知道数据库的信息。简化数据库信息结构来减少token
## 2,提示词优化
**”分析“:**分析能否更具体一点。
**”建议“:**可以根据用户身份生成对应的建议
### 3,接入权限
**安全:** 防止普通用户获取重要数据库信息