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

📅 2026/7/25 3:13:55 👁️ 阅读次数 📝 编程学习
AI 在 BI 前端中的应用:自然语言查询与智能图表推荐

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 = [ /\bDROP\b/i, /\bDELETE\b/i, /\bTRUNCATE\b/i, /\bALTER\b/i, /\bCREATE\b/i, /\bINSERT\b/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('\n\n'); 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 生成,最后补充图表推荐和对话上下文管理。