AI 在 BI 前端中的应用:自然语言查询与智能图表推荐

AI1周前发布 beixibaobao
10 0 0

AI 在 BI 前端中的应用:自然语言查询与智能图表推荐

一、BI 前端的最后一公里:从 SQL 编辑器到自然语言对话

BI(商业智能)系统的核心用户群不是工程师,而是业务分析师、运营经理、市场主管。他们对数据的理解很深——知道要看哪个指标、对比哪个时间段、按哪个维度拆分——但他们不一定会写 SQL。

传统的解决方式是提供拖拽式查询构建器(将字段拖入"维度"或"度量"槽中)或可视化 SQL 编辑器。这些方案降低了门槛,但仍有学习成本:用户需要理解"维度"和"度量"的概念,需要知道某个字段是在数据库的哪张表里,需要在 50 个候选字段中找到自己需要的那个。

AI 在 BI 前端中的核心价值,是将交互模式从"拖拽 → 配置 → 生成图表"升级为"一句话描述需求 → AI 生成查询 → 推荐最佳可视化方式"。用户输入"上个月各地区的销售额趋势,按周汇总",系统自动理解意图、生成 SQL、执行查询、选择合适的图表类型并渲染。

二、NL2SQL 的工程实现:从自然语言到结构化查询的完整链路

2.1 Text-to-SQL 的三个核心挑战

自然语言转 SQL(Text-to-SQL)不是一个简单的翻译任务,它面临三个工程挑战:

  1. 模糊消歧。"销售额"对应的是 orders.total_amount 还是 orders.amount + orders.tax?"地区"是按 province 聚合还是按 region 聚合?这些模糊性需要结合 Schema 上下文消歧。
  2. SQL 正确性保证。AI 生成的 SQL 可能语法正确但逻辑错误(比如 SUMCOUNT 混用、JOIN 条件遗漏)。必须在执行前做语法和语义校验。
  3. 用户信任建立。用户不能盲目信任 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 生成,最后补充图表推荐和对话上下文管理。

© 版权声明

相关文章