AI 在 BI 前端中的应用:自然语言查询与智能图表推荐
AI 在 BI 前端中的应用:自然语言查询与智能图表推荐
一、BI 前端的最后一公里:从 SQL 编辑器到自然语言对话
BI(商业智能)系统的核心用户群不是工程师,而是业务分析师、运营经理、市场主管。他们对数据的理解很深——知道要看哪个指标、对比哪个时间段、按哪个维度拆分——但他们不一定会写 SQL。
传统的解决方式是提供拖拽式查询构建器(将字段拖入"维度"或"度量"槽中)或可视化 SQL 编辑器。这些方案降低了门槛,但仍有学习成本:用户需要理解"维度"和"度量"的概念,需要知道某个字段是在数据库的哪张表里,需要在 50 个候选字段中找到自己需要的那个。
AI 在 BI 前端中的核心价值,是将交互模式从"拖拽 → 配置 → 生成图表"升级为"一句话描述需求 → AI 生成查询 → 推荐最佳可视化方式"。用户输入"上个月各地区的销售额趋势,按周汇总",系统自动理解意图、生成 SQL、执行查询、选择合适的图表类型并渲染。
二、NL2SQL 的工程实现:从自然语言到结构化查询的完整链路
2.1 Text-to-SQL 的三个核心挑战
自然语言转 SQL(Text-to-SQL)不是一个简单的翻译任务,它面临三个工程挑战:
-
模糊消歧。"销售额"对应的是
orders.total_amount还是orders.amount + orders.tax?"地区"是按province聚合还是按region聚合?这些模糊性需要结合 Schema 上下文消歧。 -
SQL 正确性保证。AI 生成的 SQL 可能语法正确但逻辑错误(比如
SUM和COUNT混用、JOIN 条件遗漏)。必须在执行前做语法和语义校验。 - 用户信任建立。用户不能盲目信任 AI 生成的 SQL。系统必须展示生成的 SQL 原文并允许编辑,同时在下一次类似查询时学习用户的修正偏好。
2.2 基于 Schema 上下文的 NL2SQL 引擎
NL2SQL 引擎的核心架构包含三个模块:
- Schema 检索器:根据用户的自然语言查询,从数据库 Schema 中检索最相关的表和字段。使用向量相似度匹配,而非简单的关键词匹配。
- SQL 生成器:基于 Schema 上下文和用户意图,调用 LLM 生成 SQL。Prompt 中包含表结构定义、字段注释、示例数据行、以及 3~5 个 Few-Shot 示例。
- SQL 校验器:对生成的 SQL 做语法解析、表/字段存在性检查、注入攻击检测。不合格的 SQL 自动回退到 LLM 重新生成。
/**
* NL2SQL 引擎
* 将自然语言查询转化为可执行的 SQL
*/
interface NL2SQLRequest {
query: string; // 用户自然语言:"上个月各地区的销售额趋势"
dbSchema: TableSchema[]; // 数据库表结构(由前端从服务端获取并缓存)
conversationHistory?: { // 对话历史(用于上下文连续提问)
role: 'user' | 'assistant';
content: string;
}[];
}
interface TableSchema {
tableName: string;
description: string; // 表的中文描述
columns: ColumnSchema[];
sampleRows?: Record<string, unknown>[]; // 示例数据(帮助 LLM 理解数据分布)
}
interface ColumnSchema {
columnName: string;
dataType: string;
description: string;
isDimension: boolean; // 是否为维度字段(用于分组和筛选)
isMeasure: boolean; // 是否为度量字段(用于聚合计算)
enumValues?: string[]; // 枚举值(如状态字段的可能取值)
}
class NL2SQLEngine {
private schemaIndex: SchemaVectorIndex;
/**
* 执行完整的 Text-to-SQL 流程
*/
async textToSQL(request: NL2SQLRequest): Promise<{
sql: string;
explanation: string; // SQL 的中文解释
confidence: number; // 生成置信度 0~1
suggestedChart: string; // 推荐的图表类型
}> {
// 1. Schema 检索:找到相关的表和字段
const relevantSchema = await this.retrieveRelevantSchema(
request.query,
request.dbSchema
);
// 2. 构建 Few-Shot Prompt
const prompt = this.buildPrompt(request.query, relevantSchema, request.conversationHistory);
// 3. 调用 LLM 生成 SQL
const rawSQL = await this.callLLM(prompt);
// 4. SQL 校验
const validation = this.validateSQL(rawSQL, request.dbSchema);
if (!validation.valid) {
// 回退:将校验错误信息加入 Prompt,让 LLM 修正
return this.retryWithError(rawSQL, validation.errors!, request);
}
// 5. 提取解释和图表推荐
const explanation = this.extractExplanation(rawSQL);
const suggestedChart = this.suggestChartType(relevantSchema);
return {
sql: rawSQL,
explanation,
confidence: validation.confidence,
suggestedChart,
};
}
/**
* Schema 语义检索:找到与用户查询最相关的表和字段
*/
private async retrieveRelevantSchema(
query: string,
schema: TableSchema[]
): Promise<TableSchema[]> {
// 将用户查询向量化,与每个表的 description 做余弦相似度比对
const queryVector = await this.embed(query);
const scored = schema.map((table) => ({
table,
score: this.cosineSimilarity(
queryVector,
this.schemaIndex.getTableVector(table.tableName)
),
}));
// 只返回 Top 3 相关表(减少 Prompt Token 消耗)
return scored
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map((s) => s.table);
}
/**
* SQL 校验:语法 + 语义 + 安全性
*/
private validateSQL(
sql: string,
schema: TableSchema[]
): { valid: boolean; confidence: number; errors?: string[] } {
const errors: string[] = [];
// 1. 注入检测:禁止 DROP、DELETE、TRUNCATE 等危险操作
const dangerousPatterns = [
/bDROPb/i, /bDELETEb/i, /bTRUNCATEb/i,
/bALTERb/i, /bCREATEb/i, /bINSERTb/i,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(sql)) {
errors.push(`检测到危险操作:${pattern.source}`);
}
}
// 2. 表/字段存在性校验
const tableNames = schema.map((t) => t.tableName.toLowerCase());
const columnNames = new Set<string>();
for (const table of schema) {
for (const col of table.columns) {
columnNames.add(`${table.tableName}.${col.columnName}`.toLowerCase());
columnNames.add(col.columnName.toLowerCase());
}
}
// 简单检查 SQL 中引用的表名是否存在(实际应使用 SQL Parser 做精确分析)
for (const tableName of tableNames) {
if (!sql.toLowerCase().includes(tableName)) {
// 提示但不报错(可能使用了别名)
}
}
// 3. 置信度评估
let confidence = 0.8; // 基础置信度
if (errors.length > 0) confidence -= 0.3;
// SQL 长度过短或过长都降低置信度
if (sql.length < 20) confidence -= 0.2;
if (sql.length > 2000) confidence -= 0.1;
return {
valid: errors.length === 0,
confidence: Math.max(0, confidence),
errors: errors.length > 0 ? errors : undefined,
};
}
private buildPrompt(
query: string,
schema: TableSchema[],
history?: { role: string; content: string }[]
): string {
// 构建包含表结构、示例数据、Few-Shot 示例的完整 Prompt
const schemaDesc = schema
.map((t) => {
const cols = t.columns.map((c) => ` - ${c.columnName} (${c.dataType}): ${c.description}`).join('n');
return `表: ${t.tableName}n描述: ${t.description}n字段:n${cols}`;
})
.join('nn');
return `你是一个 SQL 生成助手。根据用户的自然语言查询和数据库结构,生成正确的 SQL 语句。
数据库结构:
${schemaDesc}
Few-Shot 示例:
用户:"北京地区今年的订单金额"
SQL:SELECT SUM(amount) FROM orders WHERE region = '北京' AND YEAR(created_at) = YEAR(NOW())
用户:"各品类上月销量 Top 10"
SQL:SELECT category, SUM(quantity) as total FROM orders WHERE MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) GROUP BY category ORDER BY total DESC LIMIT 10
用户查询:${query}
要求:
1. 只生成 SELECT 查询,不允许修改数据
2. 使用标准 SQL 语法
3. 仅在输出中返回 SQL 语句,不要有任何解释`;
}
private async callLLM(prompt: string): Promise<string> { return ''; }
private async retryWithError(sql: string, errors: string[], request: NL2SQLRequest): Promise<any> { return {}; }
private extractExplanation(sql: string): string { return ''; }
private suggestChartType(schema: TableSchema[]): string { return ''; }
private async embed(text: string): Promise<number[]> { return []; }
private cosineSimilarity(a: number[], b: number[]): number { return 0; }
}
// Schema 向量索引
interface SchemaVectorIndex {
getTableVector(tableName: string): number[];
}
2.3 对话式查询的上下文管理
BI 查询通常是对话式的连续提问。用户可能先问"上个月的销售额是多少",然后追问"按地区拆分呢",紧接着"只看华南地区"。系统需要维护一个对话上下文窗口,将后续问题与前面的查询结果关联。
实现要点:
- 维护一个查询上下文栈,记录每次查询的 SQL、返回的 Schema、当前应用的筛选条件。
- 追问时,将上下文栈中的上次 SQL 和筛选条件作为 LLM 的额外输入,让 LLM 理解"按地区拆分"是对前一查询的分组操作,"只看华南"是对前一查询的筛选操作。
- 上下文窗口的 Token 长度需控制。超过 5 轮对话后,将早期的上下文压缩为摘要而非保留完整 SQL。
三、智能图表推荐:从数据特征到可视化类型的自动映射
3.1 图表推荐的决策树
AI 生成的查询结果是一个结构化的数据表(行列矩阵)。如何自动选择最佳的可视化方式?决策基于三个维度的分析:
- 数据维度:结果中有几个维度字段(分类轴)、几个度量字段(数值轴)。1 维 + 1 度量 → 柱状图/饼图,2 维 + 1 度量 → 折线图/堆叠柱状图,3 维 + 1 度量 → 热力图/气泡图。
- 数据趋势:时间序列数据(维度为日期类型)优先推荐折线图。占比分析(度量总和为 100%)优先推荐饼图/环形图。
- 数据分布:含大量离群值的数据推荐箱线图。双变量关系分析推荐散点图。
/**
* 图表类型推荐引擎
* 根据查询结果的数据特征自动选择最佳可视化方式
*/
interface QueryResult {
columns: { name: string; type: 'string' | 'number' | 'date' | 'boolean' }[];
rows: Record<string, unknown>[];
rowCount: number;
}
type ChartType = 'bar' | 'line' | 'pie' | 'scatter' | 'heatmap' | 'table' | 'funnel' | 'radar';
interface ChartRecommendation {
type: ChartType;
score: number; // 推荐得分 0~100
reason: string; // 推荐理由
config: Record<string, unknown>; // ECharts/AntV 配置预设
}
class ChartRecommender {
/**
* 根据查询结果推荐 Top 3 图表类型
*/
recommend(result: QueryResult): ChartRecommendation[] {
const dimensions = result.columns.filter((c) => c.type !== 'number');
const measures = result.columns.filter((c) => c.type === 'number');
const hasDateColumn = result.columns.some((c) => c.type === 'date');
const candidates: ChartRecommendation[] = [];
// 规则 1:1 维度 + 1 度量 → 柱状图/饼图
if (dimensions.length === 1 && measures.length === 1) {
if (hasDateColumn) {
candidates.push({ type: 'line', score: 95, reason: '时间序列数据推荐折线图', config: {} });
}
candidates.push({ type: 'bar', score: 88, reason: '单维度数据推荐柱状图', config: {} });
// 饼图仅适用于少量分类(< 8 个)
const categories = new Set(result.rows.map((r) => r[dimensions[0].name]));
if (categories.size <= 8) {
candidates.push({ type: 'pie', score: 75, reason: '少量分类适合饼图', config: {} });
}
}
// 规则 2:2 维度 + 1 度量 → 堆叠柱状图/分组柱状图
if (dimensions.length >= 2 && measures.length === 1) {
candidates.push({ type: 'bar', score: 85, reason: '多维度推荐堆叠柱状图', config: { stack: true } });
if (result.rowCount > 20) {
candidates.push({ type: 'heatmap', score: 70, reason: '大量数据推荐热力图', config: {} });
}
}
// 规则 3:2 度量 → 散点图(分析相关性)
if (measures.length === 2 && dimensions.length <= 1) {
candidates.push({ type: 'scatter', score: 80, reason: '双度量推荐散点图分析相关性', config: {} });
}
// 规则 4:无度量 → 纯数据展示 → 表格
if (measures.length === 0) {
candidates.push({ type: 'table', score: 100, reason: '无度量数据推荐表格', config: {} });
}
// 降序排列,返回 Top 3
return candidates.sort((a, b) => b.score - a.score).slice(0, 3);
}
}
3.2 图表配置的 AI 微调
基本推荐框架给出一组合适的图表类型,但具体配置仍需要微调。例如柱状图是横向还是纵向、Y 轴是否从零开始、颜色映射是顺序色还是分类色。这些可以通过 LLM 根据数据的业务语义做二级推荐:
- "销售额对比" → Y 轴从零开始(否则视觉比例失真)
- "转化率趋势" → Y 轴不强制从零(变化幅度小,从零开始看不出波动)
- "各地区占比" → 使用分类色板(不同地区用不同颜色)
- "时间序列" → 使用顺序色板(按时间从浅到深)
四、AI 在 BI 中的边界与风险
4.1 SQL 生成的不可靠性
LLM 生成的 SQL 存在"幻觉"问题。对于复杂查询(多表 JOIN + 子查询 + 窗口函数),LLM 可能在 3%~8% 的情况下生成语法正确但语义错误的 SQL。因此必须在执行前做 SQL 校验,并且将生成的 SQL 原文展示给用户确认。
4.2 数据安全的两层防护
NL2SQL 的查询权限必须受控:
- 表级权限:数据库中包含敏感信息(用户手机号、身份证号)的表不能出现在 Schema 检索结果中。
- 行级权限:销售经理只能看到自己团队的销售数据,即使他输入"全公司的销售额"。行级权限在 SQL 生成后由中间件自动注入 WHERE 条件。
4.3 图表推荐的局限
图表推荐引擎不能替代数据分析师的判断。对于探索性分析("随便看看数据"),推荐引擎的准确率很高。但对于特定业务场景的定制化需求("我要用桑基图展示用户流转路径"),推荐引擎无法识别这类非标准化图表类型。
五、总结
AI 在 BI 前端中的应用核心在于两个能力:NL2SQL(自然语言转查询)和智能图表推荐(数据特征到可视化的映射)。
NL2SQL 的工程实现需要 Schema 语义检索(找到相关表和字段)、Few-Shot Prompt 工程、SQL 校验(语法 + 语义 + 安全性)三层保障。对话式查询需要维护上下文栈来理解连续提问的意图继承。
图表推荐基于数据维度的决策树分析(维度数、度量数、是否时间序列),但应保持可覆盖性——用户可以手动选择推荐列表之外的图表类型。
落地建议:先从 Schema 检索和 SQL 校验两个基础设施做起(不依赖 LLM 即可验证 Schema 检索的准确率),然后接入 LLM 做 SQL 生成,最后补充图表推荐和对话上下文管理。