简介:这份分享PDF围绕阿里云百炼团队推出的对话型数据分析产品“析言GBI”展开,面向AI产品经理、数据分析师、大模型应用工程师,以及希望降低取数门槛的业务管理者。它首先点出传统数仓/BI模式下业务人员写SQL难、排期慢、精力被复杂报表消耗的痛点,进而讲解析言GBI如何以问答式交互将自然语言自动转换为SQL查询,完成智能语义理解、多代理任务编排、智能总结与图表绘制,最终把分析结果以自然语言和图形化形式返回给业务人员。内容还覆盖整体产品架构、工作链路、XiyanSQL关键技术攻关,以及真实业务场景中的实验结果和最佳实践,并介绍了由浙江大学副教授、阿里云飞天实验室大模型算法负责人Dr.罗智凌领衔的技术团队背景。这套方案强调智能性与交互性,能降低专业技能门槛、提高业务决策及时性,对构建企业级ChatBI产品或做技术选型都有直接参考价值。资料为单个PDF文件,大小约6.75MB,适合通勤或碎片时间学习;目前已有56人学习,可作为了解大模型数据分析落地路径的入门材料。
1. ChatBI是什么:让数据分析从“查数”变成“对话”
上周我碰到一个很真实的场景:业务同学想确认“上月华东区各品类销售额的排名和环比”,数据团队放下手头的活去写SQL,半小时后交出来一张表,业务看完补了一句“我要的是含税口径”。这种来回在大多数公司每天发生,问题不在SQL写得快不快,而在数据分析这件事本身的门槛太高。ChatBI就是把这个门槛拆掉的方向——用户直接用大白话提问,系统借助通义大模型把自然语言翻译成SQL,到数仓里取回数据,再组织成图表和文字回答。这篇文章按我落地同类方案的实际顺序,把链路拆开讲清楚:每一步怎么做、关键参数怎么设、哪些坑必踩。
2. 让一句话变成一张图表:ChatBI背后的四个接力环节
用户按下发送键之后,ChatBI内部并不是“大模型直接吐结果”这么简单。我习惯把它拆成四个环节:意图识别与问题改写、Schema检索、NL2SQL生成、结果组织与可视化。每个环节出问题,用户看到的现象都不一样——有的答非所问,有的SQL报错,有的数据对不上,有的图表选型奇怪。所以谈ChatBI落地,必须先把这个管线立起来,再去调每个环节。
2.1 自然语言到SQL:NL2SQL是把“人话”翻译成查询计划
NL2SQL(Natural Language to SQL)是ChatBI最核心的一棒。早期实现是模板槽填充:把“XX指标+XX维度+XX时间范围”套进规则槽里,命中就生成SQL,不命中就回复“暂时无法回答”。这套方案在维度少、句式固定的报表场景能跑,但一旦用户问“哪个区域的退货率比平均值高”,槽就填不上。
用通义大模型做NL2SQL之后,做法变成:把数据库Schema、业务口径和用户问题一起塞进提示词,让模型直接产出SQL。它的优势是对复杂条件嵌套、模糊表达、跨表关联的容忍度高很多。同样是上面那句话,模型能自行推断需要计算区域平均退货率再比较,而不是非要用户把条件拆成固化的槽。
实际落地时我一般要求模型输出纯SQL文本,再在后端用正则或SQL解析器把语句提取出来。这一步很关键,因为模型偶尔会在SQL前后夹带“以下是查询语句”这类解释文本,直接拿去执行必挂。
2.2 不是所有表都该推给模型:Schema检索与列裁剪
刚开始做原型时,最容易犯的错误是把整个数仓的建表语句都塞进提示词。一个中型公司的数仓有几百张表、上万字段,超出模型的上下文窗口不说,就算塞得下,模型也会被无关信息干扰,选错表、选错字段。
正确的做法是先做Schema检索:根据用户问题,从表清单里挑出可能相关的十几张表,再进一步裁到相关的字段。常见做法有两种。一是靠字段中文注释的向量召回,把注释和问题做相似度匹配,这个需要提前把元数据embedding化;二是更轻量的关键词匹配,用问题里的词直接命中表和字段的注释。两者结合在绝大多数场景够用。
我一般会在检索后把Schema信息按固定JSON结构拼给模型:表名、用途说明、核心字段名、字段类型、一句口径注释。控制在一到两千字以内,既保证模型有足够上下文,又不至于被噪声覆盖。
2.3 查询结果不等于答案:图表生成与结果组织
SQL执行成功后拿到的是二维数据,但用户要的是一个结论,不只是表格。图表生成这个环节决定用户体感:单值适合KPI卡片,时间序列适合折线图,维度对比适合柱状图,多维明细适合表格,占比适合饼图。
判断选什么图表,可以让模型在做NL2SQL的同时输出一个图表配置JSON,里面带上图类型、x轴字段、y轴字段、聚合方式。常见做法是在提示词里要求:除了SQL,额外输出一段JSON,形如{"chart_type": "bar", "x": "category", "y": "sales", "agg": "sum"}。后端解析这个JSON,再映射到ECharts或公司的图表组件。
结果组织还有一个常被忽略的点:如果查询结果为空,模型生成的自然语言总结要能区分“确实没有数据”和“这个组合本身无意义”,前者说明业务不活跃,后者可能是条件矛盾。这个语义差别,建议通过后置规则去判断,而不是让模型猜。
3. 让通义大模型听懂你的数仓:Schema预处理与提示词设计
很多团队把ChatBI失败归因于“大模型不会写SQL”,但实际是模型没拿到该有的信息。数仓里一张表的字段命名可能是拼音缩写、业务代号,模型没见过这些,自然给不出正确查询。所以这一章讲的不是模型能力,而是怎么把数仓的“常识”翻译给模型。
3.1 Schema信息给多细:列名、类型、注释与口径备注
给模型看的Schema和给开发看的建表语句是两码事。建表语句里有分区字段、存储格式、索引等大量噪声,模型用不上;它需要的是“这张表是干嘛的、有哪些字段、每个字段什么含义、有没有口径约束”。
我维护Schema文件时,每个字段至少包含四样:字段名、类型、中文注释、口径备注。口径备注特别重要,比如“order_amount”如果不注明是“含税订单金额,不含退款”,模型就会拿它当销售额去回答。对指标类字段,口径备注相当于给模型一本迷你字典。
文件组织上,建议按业务域拆分:交易域、用户域、售后域各一份。这样Schema检索阶段可以先按域粗筛,再按字段细筛,避免每次把全量元数据组装进提示词。
3.2 提示词模板:把约束写成“生成现场”
提示词模板是ChatBI稳定性的一大半。我的模板包含四个区块:角色定义、数据库Schema、业务口径规则、用户问题。角色定义固定一句话:“你是企业数据分析助手,只能根据给定Schema生成只读SQL”。业务口径规则里放的是“日期相对词以today为基准”“只允许SELECT”这类硬约束。
模板末尾会显式写出要求:不要解释、不要输出多余内容、如果问题与数据无关或缺少必需条件,输出I_CANNOT_ANSWER。这个“允许拒绝回答”的设计很实用,它给了模型一个合法出口,而不是逼它硬编一个SQL。
关于SQL方言,我会在模板里写清楚是MySQL、PostgreSQL还是Hive。不同方言的日期函数、分页语法差异很大,模型默认倾向标准SQL,一旦数仓是Hive或MaxCompute,很多生成语句会带方言不兼容的函数,需要在模板里点明并给一个方言示例。
3.3 Few-Shot不是越多越好:两个示例讲清一个业务口径
Few-Shot是纠正模型行为最直接的手段,但很多人误解为“示例越多越好”。我见过把几十个示例塞进提示词的,结果模型被示例里的细节带偏,出现各种过拟合式的奇怪行为。经验法则:每个业务域配两到四个示例,每个示例聚焦一种易错的口径。
比如“退货率”这个指标,如果不用示例,模型可能用退货订单量除以总订单量,而业务口径是退货件数除以发货件数。这时给一个示例,问题、标准SQL、图表JSON三段式写清楚,模型就能模仿这一口径。后续再遇到类似表述,也会按这个逻辑走。
少样本设计还要注意示例的多样性:不要四个示例都是“求最大值”类型,要覆盖时间条件、过滤条件、聚合方式、排序规则至少各一种。示例的价值在于定义行为边界,而不是覆盖所有问题。
4. 在业务系统里跑通ChatBI:最小实现与三个必调参数
这一章给一套可以照着抄的最小实现:用DashScope兼容的OpenAI接口调用通义大模型,把查询链路跑通。代码不多,但每行都有讲究,参数调不好,线上就是另一个故事。
4.1 用通义API搭起的最小查询链路
# chatbi_engine.py import os import re from openai import OpenAI client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", ) def build_prompt(question, schema_text, rules_text): return ( "你是企业数据分析助手。请根据数据库 Schema 生成只读 SQL。\n\n" f"数据库 Schema:\n{schema_text}\n\n" f"业务口径:\n{rules_text}\n\n" f"用户问题:{question}\n\n" "约束:\n" "1. 只允许 SELECT,禁止 INSERT/UPDATE/DELETE。\n" "2. 只能使用 Schema 中出现的表和字段,禁止发明不存在的表。\n" "3. 日期相对词(上月、本周等)以 today 为基准计算。\n" "4. 无法回答时只返回 I_CANNOT_ANSWER。" ) def extract_sql(text): match = re.search(r"SELECT.*?(?:;|$)", text, re.S | re.I) if not match: return None return match.group(0).strip().rstrip(";") def chatbi_query(question, schema_text, rules_text, temperature=0.1): prompt = build_prompt(question, schema_text, rules_text) resp = client.chat.completions.create( model="qwen-plus", messages=[{"role": "user", "content": prompt}], temperature=temperature, max_tokens=2048, timeout=30, ) raw = resp.choices[0].message.content sql = extract_sql(raw) if sql is None: return {"error": "模型未生成可执行 SQL", "raw": raw} return {"sql": sql, "raw": raw}这段代码的逻辑不复杂:build_prompt把Schema和规则拼成完整提示词,chatbi_query调用通义模型并取回文本,extract_sql用正则把其中第一个SELECT语句抠出来。有几个细节值得注意:base_url指向DashScope兼容模式的地址,这样可以直接用OpenAI的SDK对接;temperature默认设0.1,是为了让SQL生成尽量确定;提取SQL用正则而不是让模型直接返回JSON,是因为实测中纯文本输出更稳定。
4.2 三个必调参数:temperature、max_tokens与timeout
| 参数 | 默认值 | 作用 | 建议 |
|---|---|---|---|
| temperature | 0.1 | 控制输出随机性 | SQL生成固定在0.05到0.2,别超过0.3 |
| max_tokens | 2048 | 限制单次返回长度 | 按最长SQL估算,复杂JOIN给4096 |
| timeout | 30秒 | API响应上限 | 简单查询30秒够,宽表扫描给到60秒 |
temperature对SQL生成的影响最微妙。调高会让同一句提问在不同时间产生不同SQL,这在数据分析场景是致命的——用户上午问和下午问,结果对不上,就成了报表口径事故。我一般把SQL生成和图表JSON生成放在同一次调用里完成,温度统一用0.1,保证输出可复现。
max_tokens设太小的后果是SQL写到一半被截断,最后变成语法错误。排查时如果发现模型经常输出以“--”开头但不完整的SQL,先看是不是max_tokens不够。另外,timeout一定要配合后端异步任务使用,同步接口里设60秒意味着用户最多等一分钟,交互上要有loading提示和心理预期,否则会被误判为系统卡死。
4.3 多轮对话状态:把“省略主语的追问”接住
ChatBI和单次问答最大的区别在于多轮场景。用户问完“6月华东区咖啡品类销量TOP5”,紧接着问“那毛利率呢”,这里的“那”指代的是上一轮的筛选条件。如果每轮都把全部历史消息拼进提示词,模型容易被长上下文干扰,也容易丢失关键约束。
我采用的方案是维护一个结构化会话状态:解析出当前会话的指标、维度、时间、过滤条件,存成JSON。下一轮生成SQL时,把“用户新问题+会话状态”一起传给模型,并在提示词里写明“以下条件是用户之前的对话中已确定的,如果本轮问题未否定它们,请沿用”。这样“那毛利率呢”会带上6月、华东、咖啡品类这些前缀条件,生成正确的SQL。
# session_state.py session = { "metrics": [], # 指标列表,例如 ["毛利率"] "dims": ["region", "category"], # 维度,例如 华东/咖啡 "filters": {"month": "2025-06"}, # 时间等硬条件 "last_question": "6月华东区咖啡品类销量TOP5", } def augment_question(question, session): if "那" in question or "呢" in question: inherited = f"沿用以下条件:{session.get('filters')},维度范围{session.get('dims')}。" return inherited + "新问题:" + question return question这个设计的核心是“条件继承”,而不是把所有聊天记录扔给模型。条件继承出的提示词更短、更聚焦,模型翻车的概率会小很多。注意,当用户明确提出新条件时,旧条件要能被覆盖,会话状态更新时机要放在SQL生成成功之后,而不是用户发完消息就改。
5. ChatBI落地避坑指南:五个让我熬夜的翻车现场
这一章是全篇最心疼的部分,每条都是我或同事在真实项目中踩过的。按“现象、原因、解决”三段写,数据脱敏处理过,但问题本身一点没打折。
5.1 模型凭空“发明”字段,SQL跑出不存在的结果
现象:用户问“最近一周各门店坪效”,模型生成的SQL里出现了一个叫store_area的字段,而表里根本没有这一列。SQL执行直接报错。我一度怀疑是模型幻觉,反复调试提示词也没用。
原因:Schema文本里确实包含“门店面积”相关信息,但它在一个关联表的注释里,字段名不叫store_area。模型看到了语义相关描述,用自然语义“脑补”出了一个字段名,而不是直接引用原字段。
解决:在提示词规则里把“只能使用Schema中出现的表和字段,禁止发明不存在的表”加重语气,并且把字段名列表单独高亮展示。后置校验也加了一道:用数据库的information_schema把字段清单拉出来,SQL执行前先做字段名白名单匹配,命不中的直接拦截。模型没有“守规”的本能,要靠系统和规则去兜底。
5.2 忘了给查询加护栏,一个聚合拖垮了生产库
现象:内测时有个同事问“2023年至今每个订单的金额、运费、优惠、用户地址、支付方式”,模型生成了一个大宽表查询,扫了全量订单数据,直接把生产库的慢查询队列塞满,线上报表出现抖动。
原因:ChatBI只做了“生成SQL”和“执行SQL”,没做“SQL体检”。用户一句看似普通的问题,模型可能生成全表扫描、甚至有笛卡尔积的JOIN。开发环境的库和线上生产库的体量不是一个级别。
解决:所有查询强制走只读从库账号,账号没有INSERT、UPDATE权限,账号层面限制最大执行时间。同时给生成的SQL套一层外层查询,强制加LIMIT 500。更激进的做法是前置EXPLAIN,扫描行数超过阈值就拒绝执行,提示用户“你的请求涉及数据量过大,请缩小时间范围或增加筛选条件”。
5.3 提示词越加越长,答案反而从准确变成幻觉
现象:某次迭代后,同一批测试题的准确率从80%掉到65%。回查改动,发现为了让模型“更懂业务”,在提示词里追加了一长串“不要做A、不要做B、不要做C”的负面示例,结果模型越过守规矩,开始频繁输出I_CANNOT_ANSWER。
原因:负面示例太多,语义干扰淹没了正面指令。大模型在生成时会综合全部上下文,过多的“不要”等于一直在给模型示范错误行为,反而强化了这些模式的出现概率。
解决:把提示词里负面规则压缩到三条以内,把高频错误改成正面描述,例如“遇到时间模糊时,默认取最近完整自然月”而不是“不要不填时间”。负面示例最多留两个,其余移到Few-Shot的“错误对照”里。改完之后准确率回到82%,也稳定住了。
5.4 多轮对话断片:“那毛利率呢”被模型当成了新问题
现象:用户先问“6月华东区咖啡品类销量TOP5”,得到回答后追问“那毛利率呢”,系统返回的是全品类毛利率,没有华东区、没有6月、也没有咖啡品类。
原因:实现时把多轮对话简单当成“把最近N条消息拼进messages”,没有做条件继承。模型看到的是“那毛利率呢”这条孤立消息加一堆历史记录,它不理解“那”字指代的对象,把新问题当成了全新查询。
解决:采用4.3节的结构化会话状态方案,把上一轮的筛选条件解析出来后显式拼进提示词。并额外加了一步指代检测:问题里出现“那、它、这个、这些”等词时,强制从会话状态提取主语。这步逻辑简单,但对多轮体验的改善非常明显。
5.5 指标口径对不上:销售额在ChatBI和大屏上是两个数
现象:业务反馈ChatBI回答的“华东区销售额”和BI大屏上的数字差了一截。排查后发现,大屏的销售额是净额口径(扣除退款和优惠券分摊),ChatBI生成的SQL直接SUM订单金额,两份数据各说各话。
原因:没有在Schema里写口径备注。字段名叫order_amount,模型按字面意思理解成销售额,但它实际语义包含退款、含税、未拆分摊,和大屏的净额计算天然不一致。
解决:核心指标字段全部补上口径备注,例如“销售额=订单金额-退款金额-优惠券分摊,含税”。同时把高频易混淆指标放进业务口径规则区块,用一句完整定义替代字段级别的零散注释。这之后,ChatBI和数据仓库的指标中心才真正对上了。
6. 守住答案准确率:一个轻量回归测试集的做法
跑通ChatBI之后,真正的工作才开始。大模型更新、提示词调整、Schema变动,任何一环都可能让原本正确的答案悄悄变错。与其靠人工抽查碰运气,不如把测试集放进代码仓库,让每次改动都有据可查。
我建回归测试集的做法很简单:每个业务域维护一个JSON数组,记录“用户问题、期望SQL包含的关键字、期望出现的过滤条件、期望排序方式、禁止出现的操作”。跑回归时,把每个问题真实发给ChatBI,生成的SQL做三级校验:先解析确认是SELECT语句,再做关键字断言,最后检查筛选条件是否带全。
# test_accuracy.py import json from chatbi_engine import chatbi_query TEST_CASES = [ { "question": "6月华东区咖啡品类销量前5是哪些?", "must_contain": ["SELECT", "ORDER BY", "DESC"], "must_have": ["华东", "咖啡"], "must_not": ["DELETE", "UPDATE"], }, ] def run_regression(schema_text, rules_text): failed = [] for case in TEST_CASES: result = chatbi_query(case["question"], schema_text, rules_text) sql = result.get("sql", "") if not sql: failed.append((case["question"], "未生成SQL")) continue upper = sql.upper() for kw in case.get("must_contain", []): if kw not in upper: failed.append((case["question"], f"缺少关键字 {kw}")) break for cond in case.get("must_have", []): if cond not in sql: failed.append((case["question"], f"缺少筛选条件 {cond}")) break return failed这套测试集跑一次只要几分钟,但它挡住的问题能省下几十次线上事故。习惯上,我要求任何提示词改动、模型版本变更、Schema字段调整,都必须先跑一遍回归再合并。踩过的坑全部沉淀成新的测试用例——每翻车一次,就往用例集里补一条,让同样的错误没有第二次机会。
准确率这件事没有一劳永逸,只能靠机制去守。我个人还有个习惯:每周跑完回归后扫一遍新增失败用例,看看是模型行为漂移还是Schema变更引起的,再决定是调提示词还是补少样本示例。ChatBI能不能真的投用,取决于这个循环跑得多顺。希望这几条做法能帮你在自己的数据分析项目里少走弯路。
本文还有配套的精品资源,点击获取