大语言模型赋能实时OLAP:构建自然语言交互式数据分析系统
1. 项目概述当大语言模型遇上实时分析最近几年大语言模型LLM的火爆程度有目共睹它正在重塑我们与数字世界交互的方式。与此同时在数据驱动的业务决策领域实时在线分析处理OLAP系统比如我们团队深度使用的 Apache Doris一直是支撑海量数据即时查询、多维分析和报表生成的核心引擎。一个自然而然的问题就出现了能否让 LLM 的“大脑”直接连接并理解 OLAP 数据库的“数据海洋”让业务人员用最自然的语言就能获得过去需要复杂 SQL 和 BI 工具才能得到的洞察这正是“LLM-Powered OLAP”这个项目要回答的问题。简单来说这个项目探索的是如何将像 ChatGPT、Claude 这样的 LLM 与 Apache Doris 这类高性能 OLAP 数据库深度结合构建一个“会说话”的数据分析系统。用户不再需要记忆表结构、编写复杂的 SQL 语句只需要像提问一样用自然语言描述自己的分析需求系统就能自动理解意图、生成准确的查询、执行并返回清晰易懂的结果甚至能根据结果进行初步的解读和建议。这不仅仅是给 Doris 加了一个“聊天机器人”前端而是涉及到意图识别、语义映射、查询生成、结果解释与安全管控等一系列复杂挑战的深度集成。在腾讯内部我们基于 Apache Doris 构建了覆盖广告、游戏、金融科技等多个核心业务的统一实时数据仓库。每天处理着 PB 级的数据和数十亿次的查询。将 LLM 能力引入这个体系我们期望它能显著降低数据分析的门槛提升数据消费的效率和广度让更多非技术背景的产品、运营、市场同学能自主、即时地获取数据洞察从而加速业务决策闭环。接下来我就结合我们在腾讯的实践拆解一下这个项目的核心思路、关键技术细节以及我们趟过的一些“坑”。2. 核心架构设计与技术选型考量2.1 为什么是“增强”而非“替代”在项目启动初期我们内部有过激烈的讨论LLM 是否最终会取代 SQL 和传统的 BI 工具经过多轮 POC 和业务场景验证我们达成的共识是LLM 的核心价值在于“增强”而非“替代”。它扮演的是一个“智能翻译官”和“资深数据分析助手”的角色。对于成熟的、固定的报表和看板由数据工程师编写优化好的 SQL 和调度任务其稳定性、性能和成本是当前 LLM 生成式查询难以匹敌的。LLM-Powered OLAP 的主战场在于“即席查询”和“探索式分析”。例如产品经理突然想了解“过去一周新上线的某个功能在 30 岁以下用户群体中的次日留存率按省份和性别维度拆分对比”。这类需求多变、突发且往往涉及多张表的关联和复杂条件过滤。让产品经理自己写 SQL 不现实提需求给数据团队又可能排期很久。这时LLM 驱动的自然语言查询就能大显身手。因此我们的架构设计始终围绕一个核心原则LLM 负责理解与生成Doris 负责执行与计算两者各司其职通过一个健壮的中间层紧密协作。这个中间层需要处理语义理解、上下文管理、查询验证、结果后处理和安全审计。2.2 整体技术栈与组件拆解我们最终落地的架构主要包含以下几个核心组件自然语言交互前端可以是一个 Web 聊天界面、集成到内部协作工具如企业微信、钉钉的机器人或者一个 API 服务。我们选择了 API 优先的策略方便不同前端集成。LLM 服务与提示工程层这是系统的“大脑”。我们并没有绑定某一个特定的 LLM而是设计了一个抽象层可以对接 OpenAI GPT、Azure OpenAI、国内的主流大模型 API 以及我们内部自研的模型。提示工程是这里的重中之重我们为不同场景如简单查询、复杂关联、趋势分析、根因下钻设计了不同的提示模板。语义层与元数据管理这是连接自然语言和数据库 Schema 的“词典”和“地图”。它需要维护 Doris 集群中所有数据库、表、字段的名称、类型、注释、业务含义、关联关系等信息。我们扩展了 Doris 的 Information Schema并额外维护了一个业务语义知识库将“DAU”映射到具体的 SQL 表达式COUNT(DISTINCT user_id)将“营收”映射到SUM(amount)等。SQL 生成与优化器LLM 根据用户问题和语义层信息生成初步的 SQL。但这个 SQL 可能不完美如性能不佳、语法有小错误。我们引入了一个轻量级的 SQL 优化器对生成的 SQL 进行简单的重写例如确保SELECT的字段在GROUP BY或聚合函数中、添加必要的库表前缀并利用 Doris 的EXPLAIN功能进行初步的可行性检查。查询执行与安全网关所有生成的 SQL 都必须通过一个安全网关执行。这个网关负责权限校验用户是否有权查询这些表行级/列级安全策略是否满足、查询限流防止生成恶意或低效的查询耗尽集群资源、SQL 注入防护虽然 LLM 生成但仍需二次检查和结果采样/截断对于可能返回百万行结果集的查询自动限制返回行数。结果解释与可视化建议查询结果返回后系统不是简单抛出一堆数字。LLM 会再次被调用对结果进行“解读”生成一段简明的文字总结指出关键趋势、异常值甚至可以根据查询的意图建议合适的图表类型如折线图、柱状图、饼图并生成对应的前端图表配置 JSON。注意这个架构不是一蹴而就的。我们最初尝试了“端到端”的简单方案即用户问题直接丢给 LLMLLM 直接返回 SQL 和结果。这在实际中遇到了严重的问题生成的 SQL 错误率高、无法利用 Doris 特有的优化特性如物化视图、Rollup 表、且存在巨大的安全风险。因此引入强约束的语义层和安全网关是项目成功的关键。3. 核心实现细节与关键技术挑战3.1 提示工程如何让 LLM 成为合格的“DBA”让一个通用 LLM 理解特定企业的数据仓库 Schema 并写出正确的 Doris SQL需要精心设计的提示。我们的提示模板通常包含以下几个部分系统角色设定明确告诉 LLM “你是一个专业的 Apache Doris 数据库专家精通 SQL 编写和性能优化”。数据结构上下文以清晰格式如 Markdown 表格或 JSON Schema提供相关的库、表、字段信息包括字段名、类型、示例值和业务注释。查询规则与约束指定 Doris 的 SQL 方言特性例如不支持FULL OUTER JOINUPDATE语句有特定语法。要求使用CAST处理类型转换使用COALESCE处理空值。对于聚合查询必须明确所有非聚合字段都要出现在GROUP BY中。优先使用分区字段和建表时定义的 Bloom Filter、Bitmap 索引字段进行过滤。任务指令清晰描述用户问题并要求 LLM 输出格式化的 JSON包含generated_sql、sql_explanation用中文解释这个 SQL 在做什么以及confidence置信度0-1。少样本示例提供 2-3 个高质量的例子展示如何将类似业务问题转化为 SQL。一个简化的提示示例你是一个腾讯数据团队的 Apache Doris 专家。请根据以下表结构将用户的自然语言问题转化为高效、正确的 Doris SQL。 ## 表结构 **数据库ads** - 表user_behavior_daily - dt (DATE) COMMENT ‘分区字段日期’ - user_id (BIGINT) COMMENT ‘用户ID’ - age_group (VARCHAR) COMMENT ‘年龄段如 18-25’ - province (VARCHAR) COMMENT ‘省份’ - is_new (BOOLEAN) COMMENT ‘是否新用户’ - login_count (INT) COMMENT ‘当日登录次数’ - payment_amount (DECIMAL(16,2)) COMMENT ‘当日支付金额’ ## 规则 1. SQL 必须符合 Apache Doris 语法。 2. 查询必须包含分区字段 dt 的过滤条件以提高性能。 3. 使用 CAST 进行必要的类型转换。 4. 输出格式为 JSON: {generated_sql: ..., sql_explanation: ..., confidence: 0.9} ## 示例 用户问题“昨天的新用户登录总次数是多少” { generated_sql: SELECT SUM(login_count) AS total_logins FROM ads.user_behavior_daily WHERE dt DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY) AND is_new true, sql_explanation: 查询昨日dt为当前日期减一天且为新用户is_new为真的所有记录的登录次数总和。, confidence: 0.95 } ## 当前任务 用户问题“帮我查一下过去7天各个省份的30岁以下用户的平均支付金额并且按平均金额从高到低排序。”通过这样的结构化提示LLM 生成准确 SQL 的成功率从最初的不足 50% 提升到了 85% 以上。3.2 语义层构建业务知识的“固化”LLM 并不真正“理解”数据。它需要我们将业务知识“教”给它。我们构建的语义层主要做两件事物理元数据同步定期从 Doris 的information_schema同步表、列、分区、索引信息。同时我们强烈建议并推动业务方在建表时填写详细的COMMENT这为语义理解提供了宝贵信息。业务逻辑定义这是语义层的核心。我们创建了一个 YAML 配置文件也可用数据库存储来定义“业务指标”和“维度”。metrics: - name: dau description: 日活跃用户数 expression: COUNT(DISTINCT user_id) table: ads.user_behavior_daily applicable_dimensions: [dt, age_group, province, ...] - name: arpu description: 平均每用户收入 expression: SUM(payment_amount) / NULLIF(COUNT(DISTINCT user_id), 0) table: ads.user_behavior_daily applicable_dimensions: [dt, age_group, province, ...] dimensions: - name: province column: province table: ads.user_behavior_daily description: 用户所在省份 - name: user_age_group column: age_group table: ads.user_behavior_daily description: 用户年龄段当用户提问“过去一周的 DAU 趋势”时系统会先将“DAU”映射到预定义的COUNT(DISTINCT user_id)表达式并自动关联到dt维度进行分组和排序。这极大地提高了复杂查询生成的准确性和效率。3.3 查询安全与性能防护这是企业级应用无法回避的挑战。我们主要从以下几个层面进行防护权限继承与最小化原则LLM 查询引擎执行 SQL 时使用的数据库账号其权限继承自当前登录的用户。我们与公司的统一权限中心打通确保用户只能查询其被授权访问的表和字段。对于敏感字段如手机号、身份证号即使在语义层有定义如果用户无权限生成的 SQL 中也会自动排除或脱敏。SQL 审计与拦截安全网关会解析所有待执行的 SQL。禁止高危操作DROP,TRUNCATE,ALTER,GRANT等 DDL 和权限操作一律拦截。资源消耗预估通过EXPLAIN查看扫描行数、评估复杂度。对于预估扫描数据量过大如超过 10 亿行或涉及多张大表JOIN的查询会要求用户添加更具体的过滤条件或转由专业数据工程师处理。频率与并发限制每个用户有每分钟/每小时查询次数限制防止恶意刷接口。结果行数限制默认限制返回 1000 行。如果用户确实需要更多数据可以明确在问题中指定如“请导出前 10000 条记录”系统会在日志中记录并可能触发人工审核。实操心得安全策略必须“默认拒绝显式允许”。我们曾因为初期策略过松导致一个测试用户生成了一个CROSS JOIN两个十亿级大表的查询差点引发集群雪崩。事后我们加入了基于EXPLAIN的代价预估拦截并建立了慢查询和危险查询的实时告警机制。4. 系统集成与效果优化实践4.1 与现有数据开发生态集成在腾讯Apache Doris 并非孤立的系统它上游承接 Flink、Spark 等流批处理任务下游对接 BI 平台、报表系统和数据服务 API。LLM-Powered OLAP 需要平滑嵌入这个生态。与 BI 工具互补我们将自然语言查询能力以插件形式嵌入到内部的 BI 平台中。用户在看报表时如果对某个数据点有疑问可以直接在旁边提问“为什么这个省份的 ARPU 突然下降了” 系统会结合当前报表的上下文如当前筛选的日期范围、维度自动生成下钻分析查询寻找可能的原因如新用户占比变化、某款产品支付额下降等。对接数据目录我们与公司的数据治理平台打通LLM 在生成 SQL 前可以先查询数据目录了解数据的血缘、质量分数和热门程度优先选择质量高、更新及时的数据源。查询结果缓存对于常见的、计算成本高的查询如“今日核心大盘指标”我们利用 Doris 的查询结果缓存或物化视图。LLM 引擎在接收到问题后会先检查是否有可复用的缓存结果避免重复计算。4.2 效果评估与持续迭代如何衡量这个系统的成功我们定义了几个核心指标查询生成准确率随机抽样用户问题由资深 DBA 评估生成的 SQL 是否正确。目标 90%。用户问题解决率用户提出的问题中有多少比例是直接通过系统返回的结果得到了满意答案而无需转而求助于人工。这是我们最关注的业务指标。平均查询耗时从用户提问到获得解读后结果的总时间。我们优化目标是在 10 秒内完成大部分查询。用户活跃度与留存每周使用该功能的活跃用户数以及用户的重复使用率。为了持续提升效果我们建立了闭环迭代机制错误 SQL 收集与反馈所有执行失败或结果明显异常的查询都会被记录。我们会定期分析这些 case如果是提示工程或语义层的问题就优化模板和配置如果是 LLM 本身的理解问题则将这些 case 作为新的“负样本”加入提示的 Few-shot 学习中。用户反馈渠道在结果页面提供“这个结果有帮助吗”的反馈按钮并允许用户提交修正后的 SQL。这些人工修正的 SQL 是极其宝贵的训练数据。A/B 测试当我们尝试新的提示模板、新的 LLM 模型如从 GPT-3.5 升级到 GPT-4或新的功能时会进行小流量的 A/B 测试严格评估其对核心指标的影响。5. 遇到的典型问题与实战排查记录在实际落地过程中我们遇到了形形色色的问题这里分享几个有代表性的案例及其解决方法。5.1 问题一LLM “幻觉”导致 JOIN 关系错乱现象用户问“对比游戏 A 和游戏 B 的用户留存率”。我们有两张表game_a_events和game_b_events但 LLM 生成了一条试图将这两张表通过一个不存在的user_id字段JOIN起来的 SQL导致报错。根因分析LLM 在训练数据中见过很多对比分析的例子通常涉及JOIN或UNION。当它看到“对比”并且上下文中有多张表时它倾向于生成带JOIN的语句而忽略了物理上这些表可能并无直接关联或者关联逻辑复杂。解决方案在语义层明确定义表关系除了单表指标我们开始定义跨表的虚拟“数据集”。例如定义一个user_retention数据集其逻辑 SQL 已经预先写好UNION ALL来自不同游戏表的数据并打上game_name标签。当 LLM 遇到“对比”和“游戏”关键词时语义层优先引导它使用这个预定义的数据集视图而不是让 LLM 自由发挥去JOIN。优化提示词在提示中增加明确警告“如果问题涉及对比不同实体的数据请优先考虑使用UNION ALL或分别查询后由程序合并除非表结构明确显示存在可用的关联键。”后置校验在 SQL 优化器中加入简单的规则检查如果发现生成的 SQL 包含JOIN但ON条件中的字段在两边表中不存在或类型严重不匹配则触发高风险警告并尝试回退到更安全的查询模式如分别查询。5.2 问题二性能问题——LLM 生成了低效的 SQL现象一个简单的查询“查一下上海的用户数”却执行了很长时间。检查发现LLM 生成的 SQL 是SELECT COUNT(*) FROM users WHERE province LIKE ‘%上海%’没有利用到province字段的精确值索引。根因分析LLM 基于自然语言模式匹配LIKE ‘%上海%’似乎更符合“查一下上海”这种口语化表达。它不知道province字段是精确匹配的枚举值用效率更高。解决方案在语义层补充字段的查询特性在元数据中不仅记录字段名和类型还记录其“查询建议”。例如为province字段添加属性filter_style: ‘exact_match’并为该字段维护一个常见的值列表‘上海市’ ‘北京市’…。在提示中将这些信息告诉 LLM“province字段建议使用等值过滤常见值有…”SQL 重写优化器在安全网关中加入一个轻量的 SQL 重写模块。它会检测到WHERE column LIKE ‘%value%’这种模式并检查该字段是否有“精确匹配”建议。如果有且value完全匹配常见值列表中的某一项则自动重写为WHERE column ‘value’。同时对于日期范围查询自动优化为使用分区字段。结果反馈学习将执行慢的查询及其优化后的版本记录下来作为后续提示的示例教育 LLM 如何写出更高效的 SQL。5.3 问题三复杂嵌套问题的意图识别偏差现象用户问“上个月销售额最高的产品类别是什么以及它比前一个月增长了多少” LLM 有时会生成两个独立的查询无法在一个 SQL 中计算出环比增长。根因分析这是一个多步骤推理问题。LLM 可能将其识别为两个独立意图“找最大值”和“计算增长”特别是当提示模板更侧重于单意图问题时。解决方案意图分类路由我们引入了一个前置的“意图分类”微服务。它本身是一个轻量级模型或规则引擎先将用户问题分类为SIMPLE_QUERY简单查询、COMPARISON对比、TREND趋势、RANKING排名、COMPLEX_REASONING复杂推理等。对于COMPLEX_REASONING类问题系统会调用一个专门为多步骤问题设计的、更复杂的提示模板该模板会引导 LLM 进行子问题拆分并最终组合成一个或多个高效 SQL。分步执行与记忆对于极其复杂的问题系统不再强求一个 SQL 搞定。而是允许 LLM 生成一个执行计划先执行查询 A将中间结果暂存如存入一个临时表或变量再基于此结果执行查询 B。这需要系统具备多轮对话的上下文管理能力记住上一步的结果。提供分析函数示例在提示模板的示例部分加入使用窗口函数如LAG,RANK处理排名和环比计算的例子让 LLM 学会使用 Doris 的高级分析功能。6. 成本控制与规模化思考将 LLM 用于生产系统成本是不可忽视的因素。每次调用模型 API 都需要费用且处理长上下文包含大量元数据的成本更高。我们的成本控制策略元数据压缩与摘要不是每次都将完整的表结构 DDL 发送给 LLM。我们预先用一个小模型为每个核心表生成一个“自然语言摘要”例如“user_behavior_daily表按天分区记录用户每日登录、支付行为关键字段包括用户ID、日期、省份、年龄、支付金额等。” 在提示中优先使用摘要只有当 LLM 生成的 SQL 涉及细节时才在后续轮次中补充具体字段信息。查询结果总结的轻量化对于查询结果的文字总结我们根据场景选择不同成本的模型。对于简单的数值汇总使用便宜的模型如 GPT-3.5-Turbo对于需要洞察和推理的复杂分析才使用更强大的模型如 GPT-4。缓存机制语义缓存对用户问题进行语义编码如果历史上有一个非常相似的问题如“今日DAU” vs “今天日活”且数据已更新则直接返回缓存的 SQL 和结果无需调用 LLM。模板缓存对于高频问题模式如“X指标在Y时间段内的趋势”其生成的 SQL 结构高度相似。系统可以识别这种模式直接填充参数生成 SQL大幅减少 LLM 调用。异步处理与队列对于非实时性的、复杂的探索性问题系统将其放入队列异步处理用户可以稍后回来查看结果。这样我们可以利用空闲时段的计算资源并可能批量处理请求以优化成本。规模化思考当用户量和数据规模增长后单点提示工程和规则维护会变得笨重。我们正在探索的方向是微调专用小模型考虑使用开源模型如 Code Llama、SQLCoder在高质量的用户问题正确SQL配对数据上进行微调得到一个专精于 Doris SQL 生成的轻量级模型以降低对通用大模型 API 的依赖和成本。向量化元数据管理将表名、字段名、业务指标描述转换为向量当用户提问时先将问题向量化快速从向量数据库中检索出最相关的少数几个表再送入 LLM而不是每次都传入所有元数据从而减少上下文长度和成本。LLM-Powered OLAP 的旅程才刚刚开始。在腾讯的实践中我们深刻体会到它不是一个炫技的玩具而是一个需要扎实的工程化能力、深刻的数据业务理解和持续迭代优化才能创造真实价值的系统。它正在让我们的数据仓库从“只读的图书馆”转变为“能对话的智库”虽然前路仍有挑战但看到越来越多的业务同学能轻松获取数据洞察我们觉得这一切的努力都是值得的。未来我们期待与 Doris 社区和更多同行一起探索如何让数据分析变得更智能、更普惠。