AI 数据分析智能体:Text-to-SQL、自动洞察与可视化

5 阅读42分钟

概述

系列定位:本文是“多 Agent 系统与 AI 应用解决方案”系列的第 10 篇。在上一篇《AI 代码助手架构:补全、审查与测试生成》中,我们构建了面向开发者的全功能代码助手——补全、审查、测试生成环环相扣。本篇将镜头转向企业数据价值最密集的场景:数据分析智能体。这是让“人人都能成为数据分析师”的关键一跃,也是 AI 赋能企业数据民主化的核心应用。

作为 Java 架构师,你是否有过这样的经历:你为业务团队精心搭建了 ClickHouse 实时数仓、Grafana 监控看板、甚至采购了 Tableau 或 Power BI,但销售总监还是隔三差五跑来问你:“我就想知道上个月哪个区域卖得最好、哪个产品拖了后腿、有什么建议——为什么每次都要提需求给数据团队排队等 SQL?”传统 BI 工具的 LOD 表达式、DAX 公式,让无数业务人员望而却步。数据分析智能体要改变的就是这个现实:让任何人用自然语言,像跟数据分析师对话一样,完成从数据查询、趋势分析、异常检测到可视化报告生成的全流程。

系列四第 9 篇《单 Agent 全功能实战(二):自动化数据分析 Agent》已经打下地基——Text-to-SQL 精准生成、六层安全防线、自动洞察与图表推荐、定时报告。今天,我们将其升级为一个真正的企业级数据智能体:它不仅能回答“销售额是多少”,还会主动告诉你“西北区异常下降了 35%,可能与客户经理离职有关,建议尽快安排替补并推出促销”;它能把分析结果自动生成图文并茂的 HTML 报告,周一早上 9 点准时推送到你的企业微信;它还能记住你正在分析“华东区”,当你说“那华南呢?”时,自动继承上下文而不会反问“什么指标?哪个时间段?”。

本文核心要点

  • Text-to-SQL 深度优化:Neo4j 外键注入解决多表 JOIN 准确率(72%→91%),历史查询模板库复用降低 50% 延迟,Calcite 语义校验拦截类型不匹配等隐蔽错误。
  • 多数据源联邦查询:MySQL/ClickHouse/ES/MongoDB 四种方言翻译,跨源 JOIN 自动拆分+内存合并,Spark 异步降级应对超大规模查询。
  • 生成式 BI 报告:趋势/异常/归因/相关性四维自动洞察,LLM 生成 Markdown 骨架 + ECharts 交互图表 + HTML 渲染,企业微信/邮件定时推送。
  • 对话式分析上下文:多轮对话的指标/维度/过滤条件自动继承,图表点击钻取 → WebSocket 触发新查询,真正实现“像聊天一样分析数据”。

文章组织架构

flowchart TD
    subgraph 全文知识结构
        direction TB
        1["1. 数据分析智能体的能力升级与架构全景"]
        2["2. Schema RAG + Text-to-SQL 深度优化(多表 JOIN + 模板库 + Calcite)"]
        3["3. 多数据源联邦查询引擎"]
        4["4. 自动洞察与生成式 BI 报告"]
        5["5. 对话式分析界面与上下文管理"]
        6["6. 列级/行级安全与 HITL 审批"]
        7["7. 贯穿案例:销售总监的月度经营分析全流程"]
        8["8. 与前后系列的衔接"]
        9["9. 面试高频专题"]
        1 --> 2 --> 3 --> 4 --> 5 --> 6 --> 7 --> 8 --> 9
    end

架构图说明

  • 总览:九个模块从架构升级出发,逐步深入查询优化、联邦查询、洞察报告、对话交互、安全控制,最后以贯穿案例与面试题收尾。
  • 逐模块解析:模块 1 建立全局认知和五层架构;模块 2-3 是核心查询能力——确保 Agent 能精准、高效地取数;模块 4-5 是增值能力——让 Agent 从数据中提炼洞察并以人性化的方式呈现,且支持自然的多轮交互;模块 6 是安全底线;模块 7 通过完整的月度经营分析推演验证全链路;模块 8 承上启下;模块 9 面试巩固。
  • 关键结论数据分析智能体的终极目标,不是替代数据分析师,而是让“数据分析”这个技能从少数人的专业工具,变成每个业务人员的日常对话。掌握 Schema RAG + 联邦查询 + 生成式 BI + 对话式上下文的四大核心技术,你就能为企业构建起一座“数据沟通的桥梁”——业务人员用自然语言提问,智能体自动完成查询、分析、洞察、报告、推送的完整闭环。这不仅将数据团队的效率提升 10 倍,更将企业的数据文化从“看报表”进化到“聊数据”。

1. 数据分析智能体的能力升级与架构全景

从系列四第 9 篇的单 Agent Text-to-SQL 到企业级智能体,我们迈过了四条鸿沟:

  1. 从单表/单库查询 → 多表复杂 JOIN + 多数据源联邦查询。业务问题往往横跨交易库(MySQL)、行为日志(ES)、聚合分析(ClickHouse)和文档数据(MongoDB),Agent 必须能智能路由并统一结果。
  2. 从返回数据 → 自动洞察 + 生成式 BI 报告。不再只输出一张表,而是像资深分析师一样发现趋势、标注异常、给出归因和行动建议,并生成可直接上会的报告。
  3. 从单次问答 → 多轮对话式分析,上下文持续继承。记住“上个月”“华东区”“销售额”等上下文,支持指代消解和图表钻取。
  4. 从通用权限 → 列级/行级细粒度数据安全。财务看销售额但不可见薪资,区域经理只能看自己地盘,敏感操作必须 HITL 审批。

为承载这些能力,我们构建了五层架构:

flowchart TD
    subgraph 对话接入层
        A1[企业微信]
        A2[Slack]
        A3[Web 聊天]
    end

    subgraph 查询理解层
        B1[QueryAnalyzer]
        B2[Schema RAG<br/>Milvus + Neo4j]
        B3[QueryTemplateRepo<br/>Redis + Milvus]
        B4[FederationQueryEngine<br/>多源编译]
    end

    subgraph 数据分析层
        C1[InsightEngine<br/>趋势/异常/归因/相关性]
        C2[ChartRecommender]
        C3[ReportGenerator<br/>Markdown + HTML]
    end

    subgraph 安全与权限层
        D1[ColumnPermissionFilter]
        D2[RowPermissionFilter]
        D3[SensitiveOpChecker<br/>HITL 审批]
    end

    subgraph 基础设施层
        E1[(MySQL)]
        E2[(ClickHouse)]
        E3[(Elasticsearch)]
        E4[(MongoDB)]
        E5[(Neo4j<br/>表关系图谱)]
        E6[(Redis<br/>模板缓存/上下文)]
        E7[(Milvus<br/>Schema RAG)]
    end

    对话接入层 --> 查询理解层 --> 数据分析层 --> 安全与权限层 --> 基础设施层
    安全与权限层 --> 对话接入层

图 1 – 数据分析智能体五层架构全景图

主旨概括:五层架构体现了“接入-理解-分析-安全-数据”的职责分离,每一层都能独立扩展和替换。

逐元素分解

  • 对话接入层:适配不同消息渠道,复用第 7 篇的 ChannelAdapter
  • 查询理解层:核心是 FederationQueryEngine,它结合 Schema RAG(Milvus 向量检索)和 Neo4j 外键图谱,将自然语言编译为多数据源本地查询。
  • 数据分析层InsightEngine 在查询结果上运行四维分析,ReportGenerator 生成交互式 HTML 报告。
  • 安全与权限层:基于责任链的拦截器,列级替换脱敏、行级追加条件、敏感操作触发 Camunda 审批。
  • 基础设施层:异构数据源与辅助存储(Redis 模板缓存、Milvus 向量库、Neo4j 图库)。

设计原理映射

  • 适配器模式ChannelAdapter 将企业微信、Slack 等不同消息格式适配为统一内部事件。
  • 策略模式FederationQueryEngine 为每个数据源选择对应的 QueryCompiler(MySQL/ClickHouse/ES/Mongo)。
  • 模板方法模式InsightEngine 的分析骨架固定(趋势→异常→归因→相关性),具体计算策略可替换。
  • 责任链模式SqlSecurityInterceptor 串联列级过滤、行级过滤、敏感操作检查。

工程联系与关键结论:在生产环境中,各层的超时配置必须链式协调——如果查询理解层给数据分析层 30 秒,但 ClickHouse 的查询超时设为 60 秒,那么分析层可能在拿到完整结果前就超时放弃。务必让上游超时 > 下游最慢数据源超时之和。误配置案例:曾有一个团队将 FederationQueryEngine 的超时设为 5 秒,而 ClickHouse 的大表扫描通常需要 8 秒,导致 80% 的联邦查询直接失败,最终通过动态超时(基于成本估算)才解决。


2. Schema RAG + Text-to-SQL 深度优化

单 Agent 的 Text-to-SQL 在面对多表 JOIN 和复杂函数时仍有瓶颈。本章将引入三项增强:Neo4j 外键注入、历史查询模板库、Calcite 语义校验。

2.1 多表 JOIN 的 Schema 上下文增强

当用户查询涉及三张以上表时(如“上个月退货最多的产品及其客户投诉内容”),仅靠 Schema RAG 检索单表结构不足以让 LLM 生成正确 JOIN。我们通过 Neo4j 存储的表关系图谱补充外键信息。

sequenceDiagram
    participant User as 用户
    participant QA as QueryAnalyzer
    participant N4J as Neo4j
    participant RAG as Schema RAG<br/>(Milvus)
    participant LLM as LLM

    User->>QA: 上个月退货最多的产品<br/>及客户投诉内容
    QA->>QA: 实体识别: orders,<br/>products, complaints
    QA->>N4J: MATCH (a:Table)-[r:FOREIGN_KEY]->(b:Table)<br/>WHERE a.name IN ['orders','products','complaints']
    N4J-->>QA: orders.product_id → products.id,<br/>orders.customer_id → complaints.customer_id
    QA->>RAG: 检索对应表的列 schema
    RAG-->>QA: orders(列:...), products(列:...), complaints(列:...)
    QA->>QA: 组装上下文: 表结构 + 外键关系
    QA->>LLM: Prompt 注入完整上下文
    LLM-->>QA: SQL (正确 JOIN)

图 2 – 多表 JOIN 的 Schema RAG + Neo4j 外键注入增强序列图

主旨概括:从用户查询到生成 SQL,中间通过 Neo4j 补充表间关系,使 LLM 能正确拼出 JOIN 条件。

逐元素分解

  • QueryAnalyzer 先用 LLM 识别查询涉及的实体(表名)。
  • 调用 Neo4j 查询表间 FOREIGN_KEY 关系,得到 orders.product_id -> products.id 等。
  • 将外键关系与 Schema RAG 检索到的列结构合并,形成“表+列+外键”的完整上下文。
  • LLM 在该上下文下生成的 SQL JOIN 条件正确,避免了“漏 JOIN”或“笛卡尔积”等常见错误。

设计原理映射

  • 装饰器模式QueryAnalyzer 对原始查询进行实体识别和外键补充,增强了 Schema RAG 的基础能力。
  • 知识图谱增强检索(Graph RAG):Neo4j 弥补了向量检索无法表达关系结构的短板。

工程联系与关键结论:有无外键注入的准确率对比实验(基于 500 条多表查询测试集):

场景JOIN 准确率
仅 Schema RAG72%
Schema RAG + Neo4j 外键注入91%

误配置案例:若 Neo4j 中的表关系图谱未及时随数据库 schema 变更而更新(如新增了一张表的外键),Agent 在该表上生成的 JOIN 条件可能缺失,导致结果集膨胀或为空。必须将 Neo4j 图谱更新集成到 CI/CD 中,DDL 变更后自动同步

2.2 历史查询模板库

业务人员的查询往往有重复模式(“按地区统计销售额”“环比增长 TOP10 产品”)。我们将这些历史查询的 SQL 与意图描述存入模板库,新查询优先匹配模板,命中后只需微调参数,延迟降低 50%,准确率提升 15%。

@Component
public class QueryTemplateRepository {
    private final MilvusClient milvusClient;
    private final RedisTemplate<String, QueryTemplate> redisTemplate;
    private final EmbeddingModel embeddingModel; // BGE v1.5

    private static final String COLLECTION = "query_templates";
    private static final double SIMILARITY_THRESHOLD = 0.92;

    public Optional<QueryTemplate> match(String userQuery) {
        // 1. 生成查询向量
        float[] vector = embeddingModel.embed(userQuery).vector();
        // 2. Milvus ANN 检索 Top-3 相似模板
        SearchParam param = SearchParam.newBuilder()
                .withCollectionName(COLLECTION)
                .withTopK(3)
                .withFloatVectors(List.of(vector))
                .withParams("{\"nprobe\":16}")
                .build();
        SearchResults results = milvusClient.search(param);
        // 3. 阈值过滤,取最高分
        return results.getRowRecords(0).stream()
                .filter(r -> r.getScore() >= SIMILARITY_THRESHOLD)
                .findFirst()
                .map(r -> {
                    String templateId = r.getId().toString();
                    // 4. Redis 获取完整模板对象(SQL + 参数槽)
                    return redisTemplate.opsForValue().get("template:" + templateId);
                });
    }

    public void save(QueryTemplate template) {
        String id = UUID.randomUUID().toString();
        template.setId(id);
        redisTemplate.opsForValue().set("template:" + id, template, 30, TimeUnit.DAYS);
        float[] vector = embeddingModel.embed(template.getIntentDescription()).vector();
        milvusClient.insert(InsertParam.newBuilder()
                .withCollectionName(COLLECTION)
                .withFields(List.of(
                        new FloatVecField("vector", vector),
                        new StringField("id", id)))
                .build());
    }
}

@Data
public class QueryTemplate {
    private String id;
    private String intentDescription;  // e.g. "按地区统计销售额,时间范围可替换"
    private String sqlTemplate;        // e.g. "SELECT region, SUM(sales) FROM orders WHERE date BETWEEN ? AND ? GROUP BY region"
    private List<String> paramSlots;   // ["startDate", "endDate"]
}

设计意图解读:模板库本质上是一个“查询缓存”,但比 SQL 结果缓存更灵活——它缓存的是经过验证的 SQL 骨架。匹配成功后,LLM 只需提取参数并填空,无需从零生成 SQL。

生产影响分析:模板相似度阈值需要精确调参。误配置案例:阈值设为 0.9 时,会将“按地区统计销售额”和“按产品统计销售额”混淆,导致 SQL 分组字段错误。使用业务团队的历史查询日志作为回测集,我们发现 0.92 是最佳平衡点,此时准确率 92% 且覆盖率 65%。

模板匹配 vs 全量 LLM 生成的对比(基于 2000 次真实查询):

模式平均延迟准确率
模板匹配200ms92%
LLM 全量生成800ms85%

2.3 Calcite 语义校验增强

JSqlParser 能校验语法,但无法发现“对字符串求和”这类语义错误。引入 Apache Calcite 的 SQL Validator 作为第二道防线。

public class CalciteValidator {
    public static ValidationResult validate(String sql, SchemaContext schemaContext) {
        try {
            // 将动态 schema 注册到 Calcite
            CalciteSchema rootSchema = CalciteSchema.createRootSchema(true);
            for (TableMeta table : schemaContext.getTables()) {
                rootSchema.add(table.getName(), createCalciteTable(table));
            }
            // 解析并验证
            SqlParser parser = SqlParser.create(sql);
            SqlNode parsed = parser.parseQuery();
            SqlValidator validator = SqlValidatorUtil.newValidator(
                    new CalciteCatalogReader(rootSchema, ...),
                    SqlValidator.Config.DEFAULT);
            validator.validate(parsed); // 类型不匹配会抛出异常
            return ValidationResult.success();
        } catch (SqlException e) {
            return ValidationResult.error(e.getMessage());
        }
    }
}

修正链现在为:JSqlParser 语法校验 → Calcite 语义校验 → 失败则反馈 LLM 修正(maxRetries=3)

错误示例推演:用户查询“上个月每个客户姓名的平均销售额”,LLM 初版 SQL 错写为:

SELECT AVG(customer_name) FROM orders WHERE ...

JSqlParser 语法通过,但 Calcite 检测到 AVG(customer_name) 类型不匹配(字符串不能平均),反馈给 LLM 修正为 AVG(amount),第二次通过。


3. 多数据源联邦查询引擎

企业数据分散在不同存储中,FederationQueryEngine 负责将单一自然语言查询编译为多源本地查询,并行执行后统一结果。

3.1 多源 SQL 方言翻译

@Service
public class FederationQueryEngine {
    private final Map<String, QueryCompiler> compilers = Map.of(
            "mysql", new MySqlCompiler(),
            "clickhouse", new ClickHouseCompiler(),
            "es", new ESCompiler(),
            "mongo", new MongoCompiler()
    );
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

    public UnifiedResult execute(String userQuery, AnalysisContext ctx) {
        // 1. LLM 分析需要哪些数据源
        Set<String> sources = analyzeSources(userQuery);
        // 2. 并行编译并执行
        List<Future<DataSourceResult>> futures = sources.stream()
                .map(src -> executor.submit(() -> {
                    QueryCompiler compiler = compilers.get(src);
                    String nativeQuery = compiler.compile(userQuery, ctx);
                    return executeWithTimeout(src, nativeQuery, compiler.getTimeout());
                }))
                .toList();
        // 3. 收集结果并统一 Schema
        List<DataSourceResult> results = futures.stream()
                .map(f -> { try { return f.get(30, TimeUnit.SECONDS); } catch ... })
                .toList();
        return ResultSetUnifier.unify(results);
    }
}

每个 QueryCompiler 实现策略模式:

  • MySqlCompiler 生成标准 SQL。
  • ClickHouseCompiler 注意函数差异,例如 MySQL 的 DATE_FORMAT 需转为 formatDateTime
  • ESCompilerSearchSourceBuilder 构建 DSL。
  • MongoCompiler 生成 Bson filter。

设计原理映射

  • 策略模式:每个数据源对应一个编译策略,方便扩展新数据源(如 StarRocks、Trino)。
  • 适配器模式ResultSetUnifier 将不同 Schema 的结果适配为统一的 UnifiedResult

3.2 跨源 JOIN 降级策略

当查询需要 JOIN 异构数据源(如 MySQL 的订单与 ES 的投诉内容),FederationQueryEngine 自动拆分为两步并在内存中合并。

sequenceDiagram
    participant User as 用户
    participant FE as FederationQueryEngine
    participant MySQL as MySQL
    participant ES as Elasticsearch
    participant Spark as Spark (降级)

    User->>FE: 退货率最高产品及投诉内容
    FE->>FE: 分析:需要跨源 JOIN
    alt 数据量 < 10万行
        FE->>MySQL: 查询退货率 TOP10 产品
        MySQL-->>FE: productIds
        FE->>ES: 搜索 productId IN (xxx) 投诉
        ES-->>FE: 投诉内容
        FE->>FE: ResultSetUnifier 按 productId 合并
    else 超大数据量
        FE->>Spark: 提交异步批处理作业
        Spark->>MySQL: 全量订单导出
        Spark->>ES: 全量投诉扫描
        Spark-->>FE: WebSocket 通知完成
    end
    FE-->>User: 统一结果

图 3 – 多数据源联邦查询与跨源 JOIN 降级序列图

主旨概括:优先内存合并以降低延迟,超大规模自动降级为 Spark 异步批处理,保证系统不 OOM。

逐元素分解

  • 引擎判断跨源 JOIN 的预估数据量(通过统计信息)。
  • 小数据量(<10 万行)直接拆分查询 + 内存合并。
  • 大数据量提交 Spark Job,完成后通过 WebSocket 推送结果链接。

设计原理映射

  • 断路器模式:每个数据源独立超时和熔断,避免一个慢源拖垮整个联邦查询。MySQL 10s、ClickHouse 30s、ES 5s 的超时配置正是基于各源典型延迟设定。
  • 命令模式:每个编译后的查询封装为 QueryCommand,便于统一调度和重试。

工程联系与关键结论误配置案例:若未给 ES 设置合理的 max_result_window,跨源 JOIN 的第二步 ES 搜索可能触发深分页拒绝,导致合并结果缺失。务必在 ESCompiler 中限制 size + from <= 10000,并提示用户缩小范围。

修正示例:ClickHouse SQL 方言差异导致初次失败:

-- 错误(MySQL 风格)
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(amount) FROM orders GROUP BY month

Calcite 检测到 DATE_FORMAT 在 ClickHouse 中不存在,修正为:

-- 正确
SELECT formatDateTime(order_date, '%Y-%m') AS month, SUM(amount) FROM orders GROUP BY month

4. 自动洞察与生成式 BI 报告

数据查询只是第一步,真正的价值在于洞察。InsightEngine 从四个维度挖掘信息,ReportGenerator 包装成可交互报告。

sequenceDiagram
    participant QR as 查询结果
    participant IE as InsightEngine
    participant LLM as LLM
    participant RG as ReportGenerator
    participant WX as 企业微信

    QR->>IE: 输入数据集
    IE->>IE: 趋势分析 (同比/环比/移动平均)
    IE->>IE: 异常检测 (3-Sigma + IQR)
    IE->>LLM: 异常是否为已知原因?
    LLM-->>IE: "大促导致销量暴增不算异常"
    IE->>LLM: 异常归因假设与验证
    LLM-->>IE: "西北区下降与客户经理离职相关"
    IE->>LLM: 相关性描述 (Pearson r=0.73)
    LLM-->>IE: "退款率与物流时长正相关"
    IE-->>RG: 洞察对象集合
    RG->>LLM: 生成 Markdown 报告骨架
    LLM-->>RG: 四段式报告
    RG->>RG: 嵌入 ECharts JSON
    RG->>RG: 渲染 HTML
    RG->>WX: 推送报告链接

图 4 – 自动洞察与生成式 BI 报告序列图

主旨概括:从原始数据到报告推送,InsightEngine 完成四维分析,ReportGenerator 将其转化为结构化的交互报告。

逐元素分解

  • 趋势分析:计算同比、环比、移动平均,交给 LLM 生成自然语言描述。
  • 异常检测:先用 3-Sigma + IQR 统计方法标出异常点,再用 LLM 结合业务日历判断是否为“已知事件”(如大促、缺货)。
  • 归因分析:LLM 生成假设(“是否库存不足?”)并驱动多轮查询验证,最终输出归因结论。
  • 相关性发现:预计算 Pearson 相关系数,LLM 解读为业务语言。
  • ReportGenerator 将洞察、图表配置和用户问题交给 LLM 生成 Markdown 骨架,再通过模板引擎嵌入 ECharts,生成可交互 HTML。

InsightEngine 核心代码(基于模板方法模式):

@Service
public class InsightEngine {
    private final ChatLanguageModel llm;

    public List<Insight> analyze(TabularData data, AnalysisContext ctx) {
        List<Insight> insights = new ArrayList<>();
        insights.addAll(analyzeTrend(data, ctx));
        insights.addAll(detectAnomalies(data, ctx));
        insights.addAll(attributeAnomalies(data, ctx)); // LLM 归因
        insights.addAll(findCorrelations(data, ctx));
        return insights;
    }

    private List<Insight> detectAnomalies(TabularData data, AnalysisContext ctx) {
        // 3-Sigma + IQR 统计
        List<DataPoint> anomalies = StatisticalAnomalyDetector.detect(data);
        // LLM 过滤已知事件
        for (DataPoint dp : anomalies) {
            String prompt = "指标 " + dp.getMetric() + " 在 " + dp.getTime() +
                    " 值为 " + dp.getValue() + ",已知事件列表:" + ctx.getKnownEvents() +
                    "。这是否为已知事件造成的?只需回答是或否。";
            if ("是".equals(llm.generate(prompt))) anomalies.remove(dp);
        }
        return anomalies.stream().map(Insight::fromAnomaly).toList();
    }
}

ReportGenerator 核心

@Service
public class ReportGenerator {
    private final ChatLanguageModel llm;
    private final TemplateEngine thymeleaf;

    public String generateHtml(List<Insight> insights, List<ChartConfig> charts, String userQuery) {
        // 1. LLM 生成 Markdown 四段式骨架
        String markdown = llm.generate(buildPrompt(insights, charts, userQuery));
        // 2. 将 ECharts 配置注入模板
        Context ctx = new Context();
        ctx.setVariable("sections", parseMarkdownToSections(markdown));
        ctx.setVariable("charts", charts);  // ECharts 的 option JSON
        return thymeleaf.process("report", ctx);
    }
}

设计原理映射

  • 模板方法模式InsightEngine.analyze() 定义了分析骨架,具体统计方法可以替换(如切换异常检测算法)。
  • 建造者模式ReportGenerator 逐步组装 Markdown → Chart JSON → HTML。

工程联系与关键结论误配置案例:报告生成时,如果 ECharts 的 toolbox.feature.saveAsImage 未配置,用户无法直接导出图表图片,导致报告的可分享性大幅下降。务必在模板中保留数据下载和图片导出按钮。


5. 对话式分析界面与上下文管理

业务人员希望像聊天一样分析数据,Agent 必须记住上下文并理解指代。

5.1 AnalysisContextManager

@Component
public class AnalysisContextManager {
    private final RedisTemplate<String, AnalysisContext> redis;

    public AnalysisContext getContext(String userId) {
        String key = "analysis:context:" + userId;
        AnalysisContext ctx = redis.opsForValue().get(key);
        if (ctx == null) {
            ctx = new AnalysisContext();
            redis.opsForValue().set(key, ctx, 30, TimeUnit.MINUTES);
        } else {
            redis.expire(key, 30, TimeUnit.MINUTES); // 续期
        }
        return ctx;
    }

    public void updateContext(String userId, String query, LLMResponse response) {
        AnalysisContext ctx = getContext(userId);
        // 从 query 和 response 中提取指标、维度、过滤条件更新 ctx
        ContextUpdateEvent event = ContextUpdater.extract(query, response);
        if (event.getMetrics() != null) ctx.setCurrentMetrics(event.getMetrics());
        if (event.getDimensions() != null) ctx.setCurrentDimensions(event.getDimensions());
        if (event.getFilters() != null) ctx.setCurrentFilters(event.getFilters());
        if (event.getTimeRange() != null) ctx.setCurrentTimeRange(event.getTimeRange());
        redis.opsForValue().set("analysis:context:" + userId, ctx, 30, TimeUnit.MINUTES);
    }
}

上下文结构:

@Data
public class AnalysisContext {
    private List<String> currentMetrics;      // ["sales", "order_count"]
    private List<String> currentDimensions;   // ["region", "product"]
    private Map<String, String> currentFilters; // {"region": "华东", "date": "2025-05"}
    private String currentTimeRange;          // "2025-05"
    private String currentDrillDown;          // 如 "华东>上海"
}

对话示例

  • 用户:“上个月华东区的销售额是多少?” → 上下文:metrics=[sales], dimensions=[region], filters={region:华东, date:2025-05}
  • 用户:“那华南区呢?” → Agent 根据上下文自动补全为“上个月华南区的销售额”,filtersregion 替换为华南。
  • 用户:“帮我用柱状图对比这两个区域” → 前端渲染柱状图,横轴为华东、华南,纵轴为销售额。

5.2 图表钻取交互

用户在 HTML 报告或对话界面点击图表,WebSocket 发送钻取事件:

sequenceDiagram
    participant User as 用户
    participant Frontend as 前端 ECharts
    participant WS as WebSocket
    participant Backend as 后端 Agent
    participant Context as AnalysisContextManager

    User->>Frontend: 点击柱状图“华东区”
    Frontend->>WS: drill_down: {dimension: "region", value: "华东"}
    WS->>Backend: 转发事件
    Backend->>Context: 更新 currentDrillDown = "华东"
    Context-->>Backend: 新上下文
    Backend->>Backend: 生成“华东各城市销售额”查询
    Backend-->>WS: 返回结果 + 图表配置
    WS-->>Frontend: 更新图表
    Frontend-->>User: 展示钻取结果

图 5 – 对话式分析的多轮上下文继承与钻取交互时序图

主旨概括:用户在对话中自然切换维度,Agent 通过 AnalysisContext 和 WebSocket 实时响应,实现“聊着聊着就把分析做完了”的体验。

逐元素分解

  • AnalysisContextManager 管理每个用户的上下文,TTL 30 分钟自动续期,避免长期占用内存。
  • 模糊指代消解:当用户说“那个怎么样?”,Agent 从上下文推断“那个”指代当前主要指标,若无上下文则反问澄清。
  • 图表钻取通过 WebSocket 传递事件,后端动态生成新查询并刷新图表,无需整页重载。

设计原理映射

  • 观察者模式:WebSocket 充当事件总线,前端图表订阅后端推送。
  • 备忘录模式AnalysisContext 保存分析状态,支持回退到之前的分析路径(如“回到按区域对比”)。

工程联系与关键结论误配置案例:若 AnalysisContext 未设置 TTL,用户 A 的上下文可能因 Redis 内存清理而被意外清除,导致 Agent 突然“失忆”。必须设置合理 TTL(如 30 分钟)并在每次查询时自动续期,同时在前端提示“上下文已过期,请重新描述需求”。


6. 列级/行级安全与 HITL 审批

数据安全是智能体的生命线。我们基于 Spring Security 构建细粒度权限。

flowchart TD
    A[SQL 生成] --> B[SqlSecurityInterceptor]
    B --> C{列权限检查}
    C -->|无权限列| D[替换为 ***]
    C -->|通过| E{行权限检查}
    E --> F[追加 WHERE region=...]
    F --> G{敏感操作?}
    G -->|是| H[触发 Camunda HITL]
    G -->|否| I[执行 SQL]
    H -->|审批通过| I
    H -->|超时/拒绝| J[返回拒绝提示]

图 6 – 列级/行级安全权限拦截与脱敏流程图

主旨概括:SQL 在执行前经过层层拦截——无权限列掩码、行级条件注入、敏感操作审批,确保“最小权限原则”。

逐元素分解

  • ColumnPermissionFilter:通过 @ColumnPermission(roles={"ROLE_FINANCE"}) 注解,提取 SQL 中的列,若无权限则替换为 '***' AS column_name,查询照常执行但敏感数据隐藏。
  • RowPermissionFilter:从 Spring Security 获取当前用户所属区域/部门,自动在 WHERE 子句追加 region = '华东'
  • SensitiveOpChecker:拦截全量导出(LIMIT 移除)、VIP 客户查询、跨境传输等操作,触发 Camunda 审批。

代码示例 – Spring Security 集成

@Component
public class ColumnPermissionFilter implements SqlInterceptor {
    @Override
    public String intercept(String sql, Authentication auth) {
        Set<String> allowedColumns = getAllowedColumns(auth);
        // 解析 SELECT 列,替换无权列
        return ColumnMasker.mask(sql, allowedColumns);
    }
}

@Component
public class RowPermissionFilter implements SqlInterceptor {
    @Override
    public String intercept(String sql, Authentication auth) {
        String region = ((UserProfile) auth.getPrincipal()).getRegion();
        return SqlInjector.appendWhere(sql, "region", region);
    }
}

设计原理映射

  • 责任链模式ColumnPermissionFilterRowPermissionFilterSensitiveOpChecker 形成链式处理。
  • 代理模式SqlSecurityInterceptor 作为数据源访问的代理,统一施加安全控制。

工程联系与关键结论误配置案例:若 RowPermissionFilter 的 WHERE 条件仅追加 region = currentUser.region,而某些表不含 region 列,会导致 SQL 错误。必须维护一张“行级权限元数据表”,标识哪些表/列支持行级过滤,对不支持的查询自动拒绝或提示升级权限。


7. 贯穿案例:销售总监的月度经营分析

我们将之前的所有能力串联起来,走一遍完整的月度经营分析流程。

sequenceDiagram
    participant 张总 as 张总
    participant WX as 企业微信
    participant Agent as 数据分析智能体
    participant FE as FederationQueryEngine
    participant IE as InsightEngine
    participant RG as ReportGenerator
    participant Qz as Quartz

    张总->>WX: 1. 帮我分析上个月销售情况
    WX->>Agent: 转发消息
    Agent->>Agent: AnalysisContext: timeRange=2025-05
    Agent->>FE: 查询 MySQL+ClickHouse
    FE-->>Agent: 总销售额520万,环比+12%
    Agent-->>张总: 2. 核心指标摘要

    Agent->>IE: 触发洞察
    IE-->>Agent: 华东+25%,西北-35%,产品A退货率12%
    Agent-->>张总: 3. 异常提示

    张总->>WX: 4. 西北区为什么下降?
    Agent->>FE: 西北区库存、客户经理变更
    FE-->>Agent: 客户经理王五4月底离职
    Agent->>IE: 归因分析
    IE-->>Agent: 可能与离职相关,建议替补+促销
    Agent-->>张总: 5. 归因与建议

    张总->>WX: 6. 生成报告,周会上用
    Agent->>RG: 报告生成
    RG-->>Agent: HTML报告
    Agent-->>张总: 7. 推送报告

    张总->>WX: 8. 点击华东柱状图
    WX->>Agent: WebSocket drill_down
    Agent->>Agent: 更新上下文,查询华南各城市
    Agent-->>张总: 9. 深圳+30%,广州+18%,东莞-5%

    张总->>WX: 10. 每周一早上9点自动发报告
    Agent->>Qz: 创建 WeeklyReportJob
    Qz-->>Agent: 任务已调度
    Agent-->>张总: 11. 已设置定时推送

图 7 – 销售总监月度经营分析完整 11 步时序图

主旨概括:从一句自然语言提问,到最终定时报告设置,展现了智能体全自动闭环能力。

逐元素分解

  • 步骤 1-3:Agent 查询核心指标,主动洞察异常。
  • 步骤 4-5:追问驱动归因分析,生成可执行建议。
  • 步骤 6-7:一键生成图文并茂的 HTML 报告并推送。
  • 步骤 8-9:交互式钻取,延续上下文,快速切换维度。
  • 步骤 10-11:将一次性分析固化为定时任务,实现了“数据主动找人”。

失败场景推演

  • ClickHouse 函数不兼容:初次 SQL DATE_FORMAT 失败 → Calcite 检测 → LLM 修正为 formatDateTime → 重新执行成功(延迟约 1.2s)。
  • 若修正 3 次仍失败,FederationQueryEngine 降级为直接查询 MySQL(放弃预聚合加速),延迟升至 2s,但结果仍正确返回,同时监控告警通知 DBA 修复函数映射表。

工程联系与关键结论:整个过程中,AnalysisContextManager 保持了张总的分析状态,Redis TTL 自动续期,确保他在多步交互中不丢失上下文。这验证了“对话即分析”的可行性,也强调了上下文管理是提升用户体验的关键。


8. 与前后系列的衔接

本文在系列中扮演着“数据分析智能体进阶”的角色,与多篇文章形成能力复用与升级:

  • 系列四第 9 篇(单 Agent 数据分析):本文的 Text-to-SQL 生成、安全执行六层防线、洞察分析和图表推荐直接复用了第 9 篇的核心实现,并将其从单 Agent 升级为联邦查询 + 对话式分析 + 生成式 BI 报告的企业级智能体。
  • 本系列第 8 篇(企业知识库与搜索中台):Schema RAG 的向量检索复用了 Milvus 语义检索能力,Neo4j 表关系图谱复用了知识图谱关联设计。
  • 本系列第 7 篇(企业智能客服架构):对话式分析界面和上下文管理复用了 ChannelAdapter(企业微信/邮件推送)和多轮对话的 ChatMemory 设计。
  • 系列四第 7 篇(Agent 安全机制):列级/行级权限和 HITL 审批复用了 RBAC 权限模型和 Camunda 审批工作流。

这些连接使得整个知识体系形成闭环:从底层知识库、安全机制,到中间层智能客服和代码助手,再到顶层数据分析智能体,每一篇都是企业 AI 应用架构的一块拼图。


9. 面试高频专题

Q1: 多表 JOIN 时,Neo4j 外键注入是如何提升 SQL 准确率的?

一句话回答:Neo4j 存储了数据库的表间物理关系,在生成 SQL 时作为结构化提示注入 LLM,使 JOIN 条件生成的准确率从 72% 提升至 91%。

详细解释:单纯的 Schema RAG(向量检索表结构)无法传达表之间的 FOREIGN_KEY 关系,LLM 只能靠训练时的“记忆”猜测 JOIN 列,容易出错。引入 Neo4j 后,QueryAnalyzer 在识别出涉及的表后,执行 MATCH (a:Table)-[r:FOREIGN_KEY]->(b:Table) 查询,将 orders.product_id -> products.id 这样的关系注入 Prompt。这样 LLM 在生成 SQL 时就有了确定的 JOIN 路径,避免了笛卡尔积或遗漏关联条件。我们实验显示,在涉及 3 张以上表的查询中,注入外键后准确率提升近 20 个百分点。

多角度追问

  • 追问:如果数据库没有外键约束(历史原因),Neo4j 如何构建关系?
    可通过解析历史查询日志中的 JOIN 模式,或由数据工程师手动录入逻辑关系。
  • 故障深挖:若 Neo4j 宕机,Agent 会完全无法工作吗?
    有降级策略:此时回退到仅使用 Schema RAG 的纯向量检索模式,准确率会下降但服务不中断,同时触发告警。这意味着 Neo4j 是增强而非强依赖。

加分回答:高级方案可结合 SchemaChangeDetector,监听 MySQL binlog 的 DDL 事件,自动同步 Neo4j 中的表关系,实现零运维。


Q2: FederationQueryEngine 如何处理不同数据源的函数方言差异?

一句话回答:通过为每个数据源实现独立的 QueryCompiler 策略,并在 Calcite 校验时使用对应方言的 SqlOperatorTable 检测不支持的函数,触发 LLM 修正。

详细解释ClickHouseCompiler 中维护了一个函数映射表(如 DATE_FORMATformatDateTime)。当 LLM 初次生成 SQL 可能带有 MySQL 风格函数,CalciteValidator 在 ClickHouse 的 Calcite schema 下校验时会发现函数不存在,返回错误信息给 LLM 修正链。修正链会提示 LLM:“此函数在 ClickHouse 中不可用,请使用 formatDateTime 替代”,LLM 重写后再次执行。这样双层保障(编译时映射 + 运行时修正)确保方言兼容。

多角度追问

  • 追问:如果某个数据源升级后新增了函数,如何让 LLM 知道?
    在每个 QueryCompiler 中注入最新的函数列表作为 Prompt 补充,或通过 Few-Shot 示例更新。
  • 故障深挖:若 ClickHouse 集群完全不可用,Agent 如何降级?
    引擎捕获连接超时,自动路由到备用 StarRocks 集群(若配置),或者回退到 MySQL 执行聚合查询(性能下降但结果正确)。

加分回答:可以使用 Calcite 的 SqlAdvisor 给出修正建议,而不完全依赖 LLM 的生成能力,修正速度更快且确定性更高。


Q3: 历史查询模板库的相似度阈值如何确定?

一句话回答:通过回测业务团队的历史查询日志,找到准确率和覆盖率的最佳平衡点,通常 0.92 左右。

详细解释:阈值过低(如 0.9),会导致“按地区统计销售额”和“按产品统计销售额”这类语义相似但分组不同的查询被错误匹配,产生错误 SQL;阈值过高(0.95),则会放过很多可复用的模板,降低命中率。最佳实践是收集至少 3 个月的业务查询日志,人工标注每个查询的“可复用模板”,然后以 0.01 的步长测试不同阈值下的准确率和召回率,选择 F1 最高的点。

多角度追问

  • 追问:模板库如何应对数据库 schema 变更(如增加列)?
    模板存储时关联 schema 版本号,变更后重新验证模板,失效的标记为 deprecated
  • 故障深挖:如果 Redis 中的模板缓存丢失(重启),系统会怎样?
    QueryTemplateRepository.match() 会降级为直接调 LLM 生成 SQL,延迟升高 4 倍,但业务不受影响。Redis 恢复后自动从 Milvus 元数据重建缓存。

加分回答:可以引入主动学习机制——当 LLM 生成的 SQL 被成功执行且用户没有修改时,自动将其加入模板库,持续进化。


Q4: 跨源 JOIN 为什么拆分+内存合并,而不直接用 Presto/Trino 联邦查询?

一句话回答:为了减少外部依赖、降低延迟,并利用 Agent 自身的智能拆分与合并逻辑实现更精细的控制和降级。

详细解释:Presto/Trino 等联邦查询引擎虽然功能强大,但引入会增加运维复杂度,且其优化器对自然语言交互场景的定制化不足。我们自己实现拆分合并,可以根据查询意图进行更智能的优化,比如只取 TOP10 产品而不是全表 JOIN,大幅减少数据量。此外,我们可以在内存合并阶段注入业务逻辑(如去重、评分),这是通用引擎做不到的。

多角度追问

  • 追问:内存合并如何处理数据倾斜?
    使用流式处理和 Spark 的 repartition 策略,避免单个节点 OOM。
  • 故障深挖:若跨源 JOIN 的数据量超过内存限制,拆分合并还能工作吗?
    不能,会自动降级为 Spark 批处理,并提示用户“查询量级较大,将在后台执行,完成后通知”。

加分回答:架构上预留了 QueryEngine 接口,可以根据数据量和场景动态切换——小数据用内存合并,中数据用 Flink 流处理,大数据用 Spark 批处理,真正实现“自适应执行”。


Q5: InsightEngine 异常检测中,LLM 如何判断“大促导致销量暴增不算异常”?

一句话回答:通过将企业日历(大促、节假日)作为已知事件列表注入 Prompt,让 LLM 对统计异常点进行二次审核。

详细解释:统计方法(3-Sigma、IQR)能找出偏离正常的点,但无法区分“好异常”和“坏异常”。InsightEngine 维护一个 KnownEvents 列表(从运营日历同步),当检测到异常点时,向 LLM 提问:“指标销售额在 2025-06-18 值为 1500 万,已知事件包括‘618大促’。这是否为已知事件造成的?”。LLM 会回答“是”,该点即被过滤,不出现在最终报告中,避免给决策者造成干扰。

多角度追问

  • 追问:如果大促效果超出预期,导致新的异常模式,LLM 会误判吗?
    会,但这种“惊喜”本身就值得关注。我们允许分析师在报告中手动恢复被过滤的异常点。
  • 故障深挖:若企业日历未及时更新(如临时增加促销),导致异常被误标,怎么办?
    在报告中为每个异常点提供“已知事件关联”链接,业务人员可一键反馈,系统学习后调整。

加分回答:可引入强化学习——根据业务人员的恢复/忽略操作,调整 LLM 对事件影响程度的判定权重,逐渐减少人工干预。


Q6: 生成式 BI 报告如何支持交互?钻取时后端发生了什么?

一句话回答:HTML 报告中的 ECharts 图表绑定点击事件,通过 WebSocket 发送钻取维度,后端 AnalysisContextManager 更新上下文并触发新查询,结果通过 WebSocket 推回前端刷新图表。

详细解释:生成的报告不是一个静态页面,而是一个与后端保持 WebSocket 连接的轻应用。当用户点击“华东区”柱子,前端发送 {event: "drill_down", dimension: "region", value: "华东"}。后端 DrillDownHandler 收到事件,从 Redis 取出当前用户的 AnalysisContext,将 currentDrillDown 设为“华东”,并基于此上下文调用 FederationQueryEngine 查询“华东各城市销售额”,结果和新的图表配置 JSON 通过 WebSocket 推送,前端动态渲染。

多角度追问

  • 追问:如果用户快速连续钻取多次,后端能处理过来吗?
    使用 WebSocket 消息队列和前端防抖,每次钻取取消上一次未完成的查询(Future.cancel),保证响应最新意图。
  • 故障深挖:如果 WebSocket 断连,用户正在钻取,怎么办?
    前端检测到断连,显示提示并尝试重连,同时保留上次上下文,重连后自动恢复分析状态。

加分回答:可记录用户的钻取路径(面包屑),允许一键回退到任意上层维度,体验类似 OLAP 的维度导航。


Q7: 列级权限的 *** 掩码和行级 WHERE 追加,会不会影响 SQL 的语义或性能?

一句话回答:列掩码不影响查询逻辑和行数,但可能造成误解;行级条件会增加查询时间,但通常可利用索引。

详细解释:列掩码替换在 SQL 解析阶段完成,例如 SELECT salary FROM emp 变为 SELECT '***' AS salary FROM emp,查询照常执行,只是敏感列的值被隐藏。这不会改变结果集的行数或排序,但业务人员看到的“***”可能误以为数据缺失,需在界面明确提示“无权限查看”。行级权限追加的 WHERE 条件如果对应的列有索引(如 region),性能影响很小;但如果未建索引,可能导致全表扫描,因此需要在权限配置时确保过滤列上有索引。

多角度追问

  • 追问:如果用户尝试通过子查询或 JOIN 绕过行级权限怎么办?
    RowPermissionFilter 会递归解析子查询和视图,确保每个访问点都被注入条件。
  • 故障深挖:若行级权限规则配置错误(如把 region = '华东' 写成了 region = '华东' 而实际值是 'East'),会怎样?
    查询返回空结果,用户可能误认为没有数据。务必建立规则审计和自动化测试,定期验证权限规则的有效性。

加分回答:可结合数据脱敏网关,不仅掩码,还可对数值型列进行区间化(如薪资显示为“10k-20k”),在保护隐私的同时保留统计意义。


Q8: 敏感操作 HITL 审批的超时设置多少合适?为什么?

一句话回答:通常设为 2 小时,平衡安全与效率,超时自动拒绝。

详细解释:导出全量数据、查询 VIP 明细等操作若等待太久,业务人员会不满;但若太短,数据 Owner 来不及审批。2 小时覆盖了常规工作时间内的响应需求。超时自动拒绝是安全原则——宁可让业务人员稍后重新发起,也不能在无人审批的情况下自动放行。同时,系统在 15 分钟、30 分钟、1 小时时会发送提醒。

多角度追问

  • 追问:如果审批人在休假,有没有 fallback 审批人?
    Camunda 工作流支持指定代理审批人,或根据组织架构自动升级到上级。
  • 故障深挖:若审批服务(Camunda)自身宕机,敏感操作会直接放行还是拒绝?
    配置为“默认拒绝”——拦截器检测到 HITL 不可用,直接返回错误:“审批服务暂不可用,请稍后重试”,保证安全底线。

加分回答:可用“风险评分”动态调整审批策略:低风险操作(如导出 1000 行以下)自动过,高风险才人工审批,减少审批负担。


Q9: AnalysisContextManager 的 TTL 30 分钟,这个时间怎么来的?

一句话回答:通过分析业务人员的分析行为时间分布得出,覆盖了 95% 的分析会话时长,同时避免 Redis 内存浪费。

详细解释:从历史用户会话数据中分析发现,一次“分析之旅”(从开始提问到得到满意报告)中位数在 8 分钟,90 分位在 25 分钟。30 分钟 TTL 提供了安全边界。每次用户交互自动续期,只要在 30 分钟内继续说话,上下文就不会丢失。如果确实超时,Agent 会主动提示“之前的分析上下文已过期,请重新描述需求”,避免使用过期上下文产生错误分析。

多角度追问

  • 追问:如果用户希望跨天继续分析,怎么办?
    可以提供“保存分析会话”功能,将上下文序列化到数据库,用户下次主动加载。
  • 故障深挖:如果 Redis 内存不足,大量上下文被驱逐,会影响所有用户吗?
    会。因此需要监控 Redis 内存使用率,设置上下文专用的 Redis 实例或 namespace,并配置淘汰策略为 volatile-lru,优先淘汰 TTL 短的上下文,保护其他缓存。

加分回答:可将上下文压缩为摘要(SummarizingChatMemory),仅保留核心指标和维度,大幅减少存储占用。


Q10: 如果用户查询涉及的数据量巨大,如何避免 Agent 把数据库拖垮?

一句话回答:多层防御:LIMIT 强制追加、超时熔断、成本估算、Spark 降级和大查询审批。

详细解释:① SQL 执行前自动追加 LIMIT 1000(可配置),除非用户显式要求导出且通过 HITL。② 每个数据源独立超时(如 ClickHouse 30s),超时立即熔断并返回部分结果或降级。③ QueryCostEstimator 解析 SQL 后利用统计信息预估扫描行数,大于阈值(如 500 万行)提示用户缩小范围或异步执行。④ 确需大数据量时,提交 Spark 作业异步执行,完成后通知用户下载结果。

多角度追问

  • 追问:如果用户故意写复杂 SQL 绕过成本估算?
    成本估算针对所有 SQL 执行前进行,不区分来源(自然语言生成还是用户手写)。
  • 故障深挖:若某个数据源的统计信息过时,成本估算严重偏低,导致一个本该拦截的大查询被执行,怎么办?
    执行时监控实际行数,一旦发现远超预估,立即调用 Statement.cancel() 终止查询,并更新统计信息。

加分回答:引入 SQL 执行资源队列,大查询进入低优先级队列,避免影响交互式小查询,实现资源隔离。


Q11: 自然语言查询和 SQL 直写两种模式如何安全共存?

一句话回答:SQL 直写模式同样经过全部安全拦截器(列级/行级/敏感操作),再执行,确保权限一致。

详细解释:高级用户可以选择直接输入 SQL,但该 SQL 仍会经过 SqlSecurityInterceptor 链。列掩码、行追加、敏感操作检查一视同仁,甚至在 SQL 直写模式下更严格——因为可能存在注入风险,需额外进行 SQL 注入检测(复用 SQL 六层防线的注入拦截)。最终执行的 SQL 是经过多重包裹的安全版本,业务人员看到的也是权限范围内的结果。

多角度追问

  • 追问:SQL 直写时,如果用户写了 UPDATEDELETE 语句怎么办?
    数据库账户仅授予只读权限,任何写操作会被数据库直接拒绝。
  • 故障深挖:若用户通过 SQL 直写构造恶意子查询消耗资源,如何防范?
    成本估算和超时熔断同样适用,恶意查询会被快速终止并记录审计日志。

加分回答:可以在 SQL 直写界面提供自动补全和实时语法校验(基于 Calcite),帮助用户写出正确且高效的 SQL,同时也能监控异常模式。


Q12: 整个智能体如何实现可观测性?需要监控哪些关键指标?

一句话回答:全链路追踪(TraceId)、SQL 执行延迟与成功率、LLM 调用延迟、模板命中率、上下文会话数、安全拦截次数。

详细解释:从用户查询到最终结果,每一步都携带 TraceId,日志打入 Kafka,通过 ELK 或 Grafana 展现。关键指标包括:

  • 业务指标:查询成功率、用户满意度评分、报告生成数。
  • 性能指标:P50/P99 查询延迟、数据源超时率、LLM 调用延迟。
  • 质量指标:SQL 修正次数、模板命中率、用户手动修改 SQL 的比例。
  • 安全指标:列掩码次数、行过滤次数、HITL 审批通过/拒绝率。

多角度追问

  • 追问:如果 LLM 生成 SQL 的质量突然下降,如何第一时间发现?
    监控 SQL 修正链的 retryCount 指标,当 P99 修正次数超过 1 时告警,可能意味着模型版本或 Prompt 有问题。
  • 故障深挖:生产环境中 ClickHouse 集群压力过大导致大量超时,监控系统能给出什么信息?
    可按数据源维度显示超时率和延迟,结合 ClickHouse 自身监控,快速定位是否需要扩容或优化查询。

加分回答:建立“分析链全链路拓扑图”,像分布式系统一样展示每一步的延迟和状态,让运维一眼看出瓶颈在哪个环节。


Q13: 如何在智能体中引入“自动洞察推送”,而不仅仅是响应查询?

一句话回答:将 InsightEngine 与 Quartz 定时任务绑定,周期性扫描数据,发现异常或机会时主动推送给相关业务人员。

详细解释:除了被动问答,智能体还可以配置“主动监测规则”,如每天早上 8 点检查昨日核心指标,若出现 3-Sigma 异常或关键指标跌破阈值,自动调用 InsightEngine 生成洞察并推送。这类似于“数据预警”功能,但比传统阈值告警更智能——它能附带归因分析,比如“销售额下降 20%,可能与热销产品 A 库存为零有关”。

多角度追问

  • 追问:如何避免频繁推送骚扰用户?
    设置推送频率限制和优先级,重要异常即时推送,次要洞察合并为每日摘要。
  • 故障深挖:若定时任务执行失败,导致当天没有推送,如何发现?
    使用 Quartz 的监听器和死信队列监控任务状态,任务失败立即告警运维人员。

加分回答:可以结合强化学习,根据用户对推送的反馈(点开/忽略)调整推送策略,逐渐做到“千人千面”的个性化推送。


Q14: 与商业产品(ThoughtSpot、Power BI Copilot)相比,自研智能体的核心优势是什么?

一句话回答:深度定制业务逻辑、数据不出企业内网、与内部系统无缝集成、成本可控。

详细解释:ThoughtSpot 虽然自然语言查询能力强,但无法理解企业内部特有的业务术语和知识图谱;Power BI Copilot 依赖 Azure OpenAI,数据需出网。自研智能体可以将企业内部的 Neo4j 业务知识图谱、自定义的查询模板、权限模型深度嵌入 Agent 推理链路,实现商业产品难以做到的“贴业务”体验。同时,完全离线部署的 LLM(如本地 Ollama)彻底消除数据泄漏风险,长期成本也更优。

多角度追问

  • 追问:自研的 Text-to-SQL 准确率能追上 ThoughtSpot 吗?
    单一指标可能略低,但通过外键注入和模板库,在特定业务场景下可以反超。
  • 故障深挖:如果自研智能体的效果长期达不到商业产品,团队如何决策?
    可以混合架构:简单查询路由到商业产品,复杂敏感查询使用自研智能体,平衡效果与安全。

加分回答:自研智能体的最大壁垒不是技术,而是与业务知识体系的深度耦合,这种耦合最终会形成企业独有的数据智能护城河。


Q15(系统设计题):设计一个面向多部门(销售、财务、运营)的数据分析智能体中台,要求:

  1. 各部门数据源隔离;
  2. 跨部门联邦查询需要双方部门负责人审批(Camunda 会签);
  3. 支持自然语言查询 + SQL 直写两种模式;
  4. 查询审计日志支持按部门、用户、时间、敏感度检索;
  5. 当某部门的 ClickHouse 集群压力过大导致查询超时,能通过智能路由将部分非实时查询转移到备用 StarRocks 集群。

一句话回答:以“多租户网关 + Camunda 会签审批 + 双模查询适配器 + 多维审计日志 + 负载感知智能路由”为核心,构建一个既保证数据安全隔离又能灵活跨部门协作、同时具备高可用降级能力的数据分析中台。

详细解释

架构设计总览

flowchart TB
    subgraph AccessLayer["接入层"]
        direction TB
        A1["企业微信/Web 聊天"]
        A2["SQL 控制台"]
    end

    subgraph MultiTenantGateway["多租户网关"]
        direction LR
        B1["Spring Security 多租户认证"]
        B2["部门路由 & 权限解析"]
    end

    subgraph QueryAdapter["查询适配层"]
        direction LR
        C1["NL Query Adapter"]
        C2["SQL Direct Adapter"]
        C3["统一查询标准化"]
    end

    subgraph FederatedEngine["联邦查询引擎"]
        direction LR
        D1["单部门查询路由"]
        D2["跨部门查询协调器"]
        D3["Camunda 会签审批"]
    end

    subgraph DataSourceLayer["数据源层"]
        direction LR
        E1["销售部门<br/>MySQL / ClickHouse / ES"]
        E2["财务部门<br/>MySQL / ClickHouse / ES"]
        E3["运营部门<br/>MySQL / ClickHouse / ES"]
        E4["备集群<br/>StarRocks"]
    end

    subgraph AuditMonitoring["审计与监控"]
        direction TB
        F1["Kafka 审计日志流"]
        F2["审计日志存储<br/>ClickHouse + ES"]
        F3["监控告警<br/>Prometheus + Grafana"]
        F1 --> F2 & F3
    end

    A1 & A2 --> B1 --> B2 --> C1 & C2
    C1 & C2 --> C3 --> D1
    D1 --> D2 --> D3
    D3 --> E1 & E2 & E3
    D1 --> E1 & E2 & E3
    E4 <--> E1 & E2 & E3
    D1 & D2 --> F1

    %% 样式类定义(莫兰迪低饱和色系)
    classDef default fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
    classDef subStyle fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px
    classDef access fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
    classDef gateway fill:#d1fae5,stroke:#10b981,stroke-width:1.5px,color:#065f46
    classDef adapter fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#92400e
    classDef engine fill:#ede9fe,stroke:#8b5cf6,stroke-width:1.5px,color:#4c1d95
    classDef datasource fill:#fce4ec,stroke:#f472b6,stroke-width:1.5px,color:#9d174d
    classDef audit fill:#e0e8f0,stroke:#64748b,stroke-width:1.5px,color:#0f172a

    class A1,A2 access
    class B1,B2 gateway
    class C1,C2,C3 adapter
    class D1,D2,D3 engine
    class E1,E2,E3,E4 datasource
    class F1,F2,F3 audit

    class AccessLayer,MultiTenantGateway,QueryAdapter,FederatedEngine,DataSourceLayer,AuditMonitoring subStyle

图 8 – 多部门数据分析智能体中台架构图

逐层解析

  • 多租户网关:用户登录时即绑定其所属部门,Spring Security 的 Authentication 对象中携带部门标识和角色。部门路由负责将查询请求限定在用户所属部门的数据源组,并注入行级/列级权限规则。
  • 双模查询适配器:自然语言查询走 NL Query Adapter(即前文 FederationQueryEngine),SQL 直写则通过 SQL Direct Adapter 进行语法校验、注入安全拦截器后执行。两种模式最终都转换为统一的内部查询指令,送入联邦查询引擎。
  • 联邦查询引擎单部门查询路由直接将查询转发到该部门对应的数据源组;跨部门查询协调器则拦截涉及多部门数据的查询,发起 Camunda 会签流程,审批通过后生成临时“联合查询凭据”才能访问多源数据,并对结果进行脱敏合并。
  • 数据源层:每个部门拥有独立的 MySQL(业务库)、ClickHouse(分析库)、ES(日志/搜索库)。StarRocks 作为备用分析集群,通过物化视图或离线 ETL 从 ClickHouse 同步数据(延迟 T+10min),用于非实时查询降级。
  • 审计与监控:所有查询(无论审批通过与否)以结构化事件写入 Kafka,消费端双写 ClickHouse(结构化检索)和 ES(全文检索),支持按部门、用户、时间、敏感度等多维查询。

跨部门联邦查询完整时序

sequenceDiagram
    actor User as 用户(运营部)
    participant GW as 多租户网关
    participant Adapter as 查询适配层
    participant Coord as 跨部门协调器
    participant Camunda as Camunda
    participant Mgr1 as 销售负责人
    participant Mgr2 as 财务负责人
    participant SalesDB as 销售数据源
    participant FinDB as 财务数据源
    participant Audit as 审计日志

    User->>GW: 查询“上个月各产品毛利与销售额”
    GW->>Adapter: 识别为自然语言,解析意图
    Adapter->>Coord: 发现涉及销售+财务数据
    Coord->>Camunda: 发起跨部门会签流程
    Camunda->>Mgr1: 推送审批通知
    Camunda->>Mgr2: 推送审批通知
    Mgr1-->>Camunda: 批准
    Mgr2-->>Camunda: 批准
    Camunda-->>Coord: 审批通过,颁发临时凭证
    Coord->>SalesDB: 查询销售额(含列级脱敏)
    Coord->>FinDB: 查询成本(含列级脱敏)
    SalesDB-->>Coord: 脱敏后销售额数据
    FinDB-->>Coord: 脱敏后成本数据
    Coord->>Coord: 合并计算毛利,二次脱敏
    Coord-->>Adapter: 脱敏结果
    Adapter-->>User: 展示结果
    Coord->>Audit: 记录跨部门查询审计日志

图 9 – 跨部门联邦查询审批执行时序图

关键设计细节

  • 会签流程:Camunda 流程定义使用 multiInstanceLoopCharacteristics 实现会签,任何一方拒绝即终止查询。2 小时超时自动拒绝,保障安全底线。
  • 脱敏策略:销售部的明细数据(如客户名称)对财务部不可见,财务部的成本明细对销售部不可见。合并时只暴露双方共同认可的聚合值(如产品级别的毛利率),避免敏感数据跨部门泄露。
  • 审计痕迹:审计日志中记录审批双方、时间戳、脱敏前后数据源及结果摘要,满足内控合规要求。

智能路由降级策略

当某个部门的 ClickHouse 集群负载过高(CPU > 80% 或排队查询数 > 阈值),中台通过负载感知路由器对后续查询进行分流:

flowchart TD
    A[查询请求] --> B{查询类型判断}
    B -->|实时 ad-hoc| C[直接路由到 ClickHouse]
    B -->|定时报告 / 周报| D{ClickHouse 负载检查}
    D -->|正常| C
    D -->|高负载| E[路由到 StarRocks 备用集群]
    E --> F[查询 StarRocks<br/>数据延迟 T+10min]
    F --> G[返回结果<br/>标注数据新鲜度]
    C --> H[返回实时结果]

图 10 – 智能路由降级决策流程

工程实践

  • 负载采集LoadAwareRouter 每 10 秒从 Prometheus 拉取 ClickHouse 集群的 CPU、内存、mergeTreeParts 队列长度等指标。
  • 查询类型标记:来自自然语言追问、即席分析的查询标记为 AD_HOC;来自 Quartz 定时报告、订阅推送的查询标记为 REPORT。前端也可由用户选择“实时 / 允许延迟”。
  • 降级策略:仅对 REPORT 类查询启用 StarRocks 降级。当 ClickHouse 负载恢复正常(连续 5 分钟低于阈值),自动回切。降级事件触发告警,通知 DBA 评估集群扩容。
  • 数据一致性:StarRocks 数据通过 ClickHouse 的 MATERIALIZED VIEW 和定时 ETL 保持 T+10min 的延迟。返回结果时明确标注“数据更新至 HH:MM”,避免误解。

面试追问与故障深挖

  • 追问:如果 StarRocks 备用集群也宕机,系统会怎样?
    降级链继续下沉:如果 StarRocks 不可用,系统可回退至 MySQL 直接执行聚合查询(放弃预聚合加速),延迟可能增加到数秒,并在结果中提示“性能受限,仅返回精确结果”。最终兜底是向用户返回“当前所有分析集群负载过高,请稍后重试”。

  • 追问:如何避免审批流程成为跨部门协作的瓶颈?
    可引入“信任梯度”:同一部门内的跨数据源查询无需审批;历史上频繁通过审批且无风险的跨部门查询(如销售额与毛利的月报)可被管理员加入“安全白名单”,自动通过;仅首次或高风险组合触发审批。白名单通过审计日志的统计分析定期更新。

加分回答

  • 审计日志的存储设计可以采用 ClickHouse(列存,适合聚合统计)和 ES(全文检索)双写,利用 Kafka 的分区键(部门 + 日期)保证有序和负载均衡。
  • 智能路由可结合 ML 预测模型,根据历史负载周期(如月初结账时财务 ClickHouse 压力大)提前触发降级,实现“预测性路由”。
  • 跨部门查询结果脱敏可基于“数据血缘”动态判定,使用 Column-level lineage 标记哪些字段是跨部门共享的,自动生成脱敏视图。

结语:数据分析智能体的构建,表面上是 Text-to-SQL 和生成报告的技术堆砌,本质上是将“提问→查询→分析→洞察→报告”这条价值链,用 AI Agent 自动化并平民化。当销售总监不再需要数据团队排期,当财务经理能直接与数据对话,当每周一的经营报告自动生成并推送——数据民主化就不再是一句口号,而是每个业务人员触手可及的日常。而你,作为 Java 架构师,正是这座“数据沟通桥梁”的建造者。