简介:这是一份聚焦阿里云百炼析言GBI的PDF讲稿,主题为通义大模型加持的对话型数据分析ChatBI,适合关注大模型落地、NL2SQL、智能BI的企业数据团队与算法工程师。材料由大模型ChatBI算法负责人Dr.罗智凌分享,围绕业务人员问答式取数需求,系统讲解了析言GBI的产品架构、工作链路、多代理协作、智能总结与图表绘制,以及XiyanSQL如何将自然语言转为SQL查询,并给出实验结果与最佳实践样例,能帮助读者快速理解对话型数据分析从交互设计到后端查询的完整落地思路,以及关键技术攻关方向。包体为单个PDF文件,共6.75MB,图文并茂,覆盖背景分析、产品架构、XiyanSQL与实验样例等模块,适合作为团队内部学习或技术分享的参考资料。目前已有56人学习下载,对该主题感兴趣的数据分析师、BI工程师和大模型应用开发者尤其有参考价值。
1. ChatBI 不是给大模型加个对话框:先想清楚它解决了谁的什么问题
很多人第一次听到通义大模型加持的对话型数据分析 ChatBI 时,脑子里浮现的是"网页上多个对话框"。我一开始也这么想,直到把它接进公司数据仓库才反应过来:ChatBI 的本质不是把大模型塞进 BI 页面,而是让业务人员用一句人话,省掉"找数据同学、提工单、等排期"的漫长链路。它解决的问题很具体——销售想看本周新品动销,运营想对比活动前后的留存,财务想确认口径有没有更新。落到技术上就是三件事:听懂问题、生成 SQL、把查询结果讲成人话。适合的团队是那些数据已经进了数仓、但查询入口还停留在"人对人"阶段的组织。接下来把落地路径、模型参数、必踩的坑一次讲清楚。
2. 为什么"翻译成 SQL"比直接让模型读库更靠谱:先从三件事说起
2.1 对话型数据分析的真实链路只有三个动作:听清、查询、解释
不管产品页面长什么样,对话型数据分析在底层只有三个动作。第一个是理解用户的自然语言问题,把它转成对应的表、字段、筛选条件和聚合方式;第二个是用生成出来的查询语句去数据库里真正跑一遍;第三个是拿到查询结果后,把它组织成报表或自然语言结论。三个动作里,最容易让团队误判的是第一个,以为"理解"就是大模型做的一切。实际工程上,理解只是起点,后面两个动作如果不做控制和校验,整个 ChatBI 就会变成一场翻车现场。
为什么非要用 SQL 作为中间语言?因为数据库执行引擎本身就是为这类查询而生的。让大模型直接"读库",它没有能力处理几百万行数据的计数、去重、窗口聚合,而且权限没法控制。让模型生成 SQL,再由数仓去执行,性能、权限、审计都留在数据层,这才是生产环境能接受的架构。反过来也一样:如果你把每条业务问题都拿全量数据喂给模型做推理,成本会直接把项目拖死,延迟也扛不住。
2.2 选型:qwen-max 管难句子,qwen-plus 管高频问题,别让一个模型干所有事
这里就要说到通义大模型系列怎么选。常见做法是搭一个模型路由,不要把所有请求都丢给同一个模型。我在项目里用的策略是:把用户问题先做一次难度判断,关键词和问题模板能覆盖的简单问题走 qwen-plus 或 qwen-turbo,这类模型便宜、延迟低,能扛住高频长尾;涉及多表关联、复杂筛选、歧义消解的,再升级到 qwen-max,准确率明显高一截。
| 模型 | 定位 | 我一般用它处理 | 注意点 |
|---|---|---|---|
| qwen-turbo | 快但粗 | 意图分类、追问、闲聊 | 生成 SQL 谨慎用 |
| qwen-plus | 性价比主力 | 常见业务问题、单表聚合 | 多表 JOIN 偶尔会漏 |
| qwen-max | 高准确率 | 复杂多表、长文本、歧义消解 | 贵,建议只在路由到它时用 |
模型选型之外,请求参数更关键。生成 SQL 是确定性任务,和写文案完全是两套参数哲学。我一般用 temperature=0.1、top_p=0.8,把随机性压到最低;max_tokens 要给到 2048 以上,复杂 SQL 往往很长,默认值不够会直接截断导致语法残缺。另外,建议开超时重试,单次生成超过 30 秒就换模型重试一次,不要无限等待。
2.3 Schema 是模型唯一的地图:把库结构喂给通义的正确姿势
模型没有见过你的数据库,必须把结构告诉它。有人只给字段名,然后就抱怨模型不靠谱——字段名经常是 u_r_60 这种缩写,模型再强也猜不出含义。正确姿势是给它一份"建表语句风格"的描述,字段注释、枚举值、主外键关系都写进去。
def build_schema_prompt(table_metas: list[dict]) -> str: # 把元数据拼成模型熟悉的建表语句格式,比 JSON 描述更有效 lines = [] for meta in table_metas: column_defs = [] for col in meta["columns"]: # 注释缺失时也保留字段名,让模型结合上下文推断 comment = col.get("comment") or "无注释" column_defs.append(f" {col['name']} {col['type']} COMMENT '{comment}'") pk = meta.get("primary_key") # 主键信息能帮模型理解关联关系,比注释还重要 pk_line = f" PRIMARY KEY ({pk})" if pk else "" lines.append( "CREATE TABLE {table} (\n{cols}{pk_line}\n) COMMENT '{comment}';".format( table=meta["table_name"], cols=",\n".join(column_defs), pk_line=",\n" + pk_line if pk_line else "", comment=meta.get("comment", ""), ) ) return "\n\n".join(lines)逻辑说明:模型在预训练阶段见过大量建表语句,这种格式比 JSON 或纯文本描述更能激活它对"表结构"的理解。一个关键点是表不是越多越好,上下文窗口有限,塞满 50 张表会让模型注意力分散。常见做法是先做相关性召回,用表名和注释做关键词匹配,只挑最相关的 5 到 8 张表拼进 prompt,这个动作能显著提升 SQL 正确率。
3. 把 ChatBI 跑通的最小工程闭环:从 Schema 构建到图表生成
3.1 最小依赖与环境准备
落地一套 ChatBI,Python 环境足够了,我不建议一上来就上重框架。依赖方面很精简:dashscope 负责调用通义大模型,sqlalchemy 做数据库连接统一管理,pandas 做结果聚合和后续图表转换。
# 最小依赖安装,Python 3.10+ 环境已验证 pip install dashscope sqlalchemy pandas pymysql这里要强调一个安全底线:连接串一定用只读账号,配上资源组隔离。ChatBI 生成的 SQL 是机器产生的,没人能保证它 100% 不犯错,数据库账号如果带写权限,一条 DELETE 可能让整个项目收场。我把这条放在最前面,因为它比任何模型参数都重要。
3.2 Schema 自动注册与指标口径注入:模型不认识你的业务,你得帮它翻译
Schema 不能靠手工维护,数据库表结构一直在变。常见做法是从 information_schema 直接读取,自动生成建表语句文本,每次请求动态拼装。这一步的代码不复杂,核心是把列名、类型、注释读出来,再交给上一章的 build_schema_prompt。
from sqlalchemy import create_engine, text engine = create_engine("mysql+pymysql://readonly:密码@10.0.0.5:3306/analytics") def load_table_metas(table_names: list[str]) -> list[dict]: # 从 information_schema 扫描表结构,避免手工维护 sql = text(""" SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'analytics' AND TABLE_NAME IN :tables """) rows = engine.execute(sql, {"tables": tuple(table_names)}) grouped = {} for r in rows: grouped.setdefault(r.TABLE_NAME, {"columns": []}) grouped[r.TABLE_NAME]["columns"].append({ "name": r.COLUMN_NAME, "type": r.DATA_TYPE, "comment": r.COLUMN_COMMENT, }) # 主键信息单独查一次,补充到表元数据里 pk_sql = text(""" SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'analytics' AND CONSTRAINT_NAME = 'PRIMARY' """) for r in engine.execute(pk_sql): if r.TABLE_NAME in grouped: grouped[r.TABLE_NAME]["primary_key"] = r.COLUMN_NAME return [{"table_name": k, **v} for k, v in grouped.items()]逻辑说明:这段代码把数据库物理结构和模型需要的逻辑描述打通了。实际运行时我一般会加一层缓存,表结构每天刷新一次,而不是每次请求都查 information_schema,否则高频场景下这个查询本身就成了瓶颈。表名列表从哪里来?就是 2.3 里说的相关性召回结果。
指标口径注入是另一个独立动作。业务词"复购率""动销率"模型不懂,必须把口径定义好。我维护一个 JSON 指标字典,生成 prompt 时单独拼进去:
metric_rules = { "复购率": "90天内有两次及以上购买的用户数 / 总购买用户数,按自然日统计", "动销率": "有销量SKU数 / 在架SKU数,按子类目汇总", "GMV": "订单实际支付金额之和,剔除退款订单", } def build_metric_prompt(rules: dict) -> str: # 把指标口径转成模型能遵循的规则文本 lines = [] for k, v in rules.items(): lines.append(f"- {k}:{v}") return "业务指标口径严格定义如下,涉及这些词时必须按此定义查询:\n" + "\n".join(lines)这一步是生成正确 SQL 的关键。为什么指标字典要单独放而不是写死在 Schema 里?因为数据平台字段名往往是物理层的缩写,逻辑层和物理层的映射关系必须单独维护,否则换个业务负责人,口径变了就要改代码。
3.3 查询生成主流程:让通义大模型把问题变成一条可执行 SQL
核心函数是 text_to_sql。我推荐用 OpenAI 兼容接口方式调用通义大模型,这样可以复用团队已有的调用习惯,切换成本低。
import re from openai import OpenAI client = OpenAI( api_key="YOUR_DASHSCOPE_API_KEY", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", ) def text_to_sql(question: str, schema_text: str, metric_text: str) -> str: system = ( "你是一名资深数据分析师。用户会提业务问题," "你需要根据提供的建表信息和指标口径,生成一条可执行的SQL。" "只输出SQL本身,不要输出任何解释。" ) user_prompt = f""" ## 表结构 {schema_text} ## 指标口径 {metric_text} ## 问题 {question} 请返回 SELECT 开头的SQL。 """ resp = client.chat.completions.create( model="qwen-max" if len(question) > 30 else "qwen-plus", messages=[ {"role": "system", "content": system}, {"role": "user", "content": user_prompt}, ], temperature=0.1, top_p=0.8, max_tokens=2048, timeout=30, ) return extract_sql(resp.choices[0].message.content) def extract_sql(raw: str) -> str: # 防止模型在SQL前后加解释文字,只截取第一段完整SELECT语句 match = re.search(r"SELECT.*?;", raw, re.S | re.I) if match: return match.group(0).rstrip(";") # 没找到分号结尾时,尝试取 ```sql 代码块里的内容 match = re.search(r"```sql\s*(.*?)\s*```", raw, re.S | re.I) return match.group(1).rstrip(";") if match else raw.strip()逻辑说明:model 字段按问题长度做了粗粒度路由,超过 30 个字的老实走 qwen-max,短问题交给 qwen-plus 扛量。这个阈值不是拍脑袋定的,是我跑了两百条测试题后得到的经验值:短问题用 qwen-max 提升不明显,长问题用 qwen-plus 错误率明显上升。extract_sql 必须做,模型偶尔会在 SQL 前后加代码块标记或解释性文字,不截取直接执行必然报错。
3.4 SQL 安全校验与执行:白名单是底线,LIMIT 是保命符
生成完不能直接执行,要过一道白名单校验。这是 ChatBI 工程的惯例,没有这道关卡,系统就是裸奔。
SQL_BLACKLIST = re.compile( r"\b(insert|update|delete|drop|truncate|alter|create|grant|revoke)\b", re.I, ) def validate_sql(sql: str) -> str | None: # 1. 只允许查询语句 if not sql.strip().upper().startswith("SELECT"): return "只支持 SELECT 查询" # 2. 黑名单拦截危险操作,防止模型幻觉产出非查询语句 if SQL_BLACKLIST.search(sql): return "检测到非查询语句,已拦截" # 3. 强制带 LIMIT,防止大表全量扫描拖垮数仓 if "limit" not in sql.lower(): sql = sql.rstrip(";") + " LIMIT 100" return sql参数说明:黑名单校验只是兜底。真正的安全边界在数据层——只读账号、行级权限、资源组隔离三者缺一不可。LIMIT 100 是默认行为,既防模型犯错全表扫描,也防用户一次拖几十万行到前端。如果业务确实需要更多数据,让用户在前端显式选择导出,而不是放开 SQL 层的限制。校验通过后,用 SQLAlchemy 执行并返回 DataFrame,执行超时单独设 60 秒。
3.5 结果解释与图表生成:别把全量数据喂给模型
拿到 DataFrame 后,还有一步:让通义大模型用自然语言概括结论。这里有个技巧——传给模型做总结的数据不要是整个 DataFrame,而是先做 describe 或抽样,控制 token 量。让模型"看懂趋势"只需要前几行加统计值,不需要全量明细。
def summarize_result(df: pd.DataFrame, question: str) -> str: # 用统计摘要代替全量数据,控制token消耗 summary = df.head(5).to_markdown() + "\n行数: {rows}, 数值列均值: {means}".format( rows=len(df), means=df.select_dtypes(include="number").mean().round(2).to_dict(), ) resp = client.chat.completions.create( model="qwen-plus", messages=[ {"role": "system", "content": "你是数据分析助手,用一句话概括用户问题的答案,基于提供的表格摘要。"}, {"role": "user", "content": f"问题: {question}\n数据摘要:\n{summary}"}, ], temperature=0.3, ) return resp.choices[0].message.content这里的取舍很明确:自然语言结论追求的是"说人话",不是精确计算。精确数字永远以表格展示为准,文字结论只做解读。图表生成同理,按查询结果的行数和聚合粒度决定用折线图、柱状图还是表格,规则写在代码里,不要交给模型随机发挥。
4. 对话型数据分析常见问题与排查:5 个踩坑现场
4.1 漏 JOIN 导致数据膨胀十倍:模型只查了明细表
现象:用户问"7 月各品类订单量",返回结果比真实值大了快十倍。排查发现 SQL 只从订单明细表做了单表 GROUP BY,根本没关联品类维表,订单自带的类目标识是内部编码,展示出来全是乱码。
原因:Schema 召回阶段没把维表召回来,模型手里只有一张明细表,它只能用现有字段硬凑。这不是模型能力问题,是你给它的地图就不完整。
解决:召回逻辑改成优先按 JOIN 关系召回。在建表语句描述里明确标注外键关系,同时在 prompt 里加一句约束:"如果查询的展示字段需要维度名称,必须 JOIN 对应维表。"我在 3.2 的 build_schema_prompt 里专门留了 primary_key 字段,就是为了让模型理解表间关系。
4.2 "复购率"三个字,每个部门口径都不一样
现象:用户问"上季度复购率",模型按自然日口径算了两笔订单就算复购,业务方说不对,他们的复购周期是 90 天。
原因:大模型按字面理解复购,但业务指标是人为约定的口径,物理表里根本没有"复购"这个字段,必须靠规则定义。
解决:指标字典必须前置拦截。在 text_to_sql 的 prompt 里,指标口径要放在表结构前面,让模型优先遵循。另外做一层关键词匹配:如果问题命中了指标字典里的词,直接在 prompt 里强插该指标的完整定义。这个动作比让模型自己猜可靠得多——指标口径属于公司业务知识,不属于模型常识。
4.3 语义缓存键设计失误:同一个问题两次问出两种结果
现象:用户问"昨天销售额"和"昨天的销售额",一个命中缓存,一个没命中,而且后者重新查了一遍,数据还不一样。更离谱的是,有人早上问"本月销售额",下午再问同一个问题,返回的还是早上的旧数据。
原因:把用户原始文本当缓存键,文本稍微变一下就不命中;日期型问题没有把时间戳纳入键值,导致同一天内数据更新后缓存不失效。
解决:缓存键分两层。先让通义模型把问题规范化成标准形式,例如"昨天销售额"统一转成"近1天销售额#2025-01-15",再把规范化文本算哈希做键。同时缓存里带版本号,指标口径或数据表结构发生变化时递增版本,让旧缓存自动失效。
4.4 日期被模型写死:每月数据都不对
现象:用户说"这个月",模型直接生成WHERE date BETWEEN '2024-01-01' AND '2024-01-31',把年份固定住了,下个月再问还是查 2024 年 1 月。
原因:模型不知道"当前时间",它训练数据里的时间概念是静态的,无法感知运行时日期。
解决:prompt 里显示注入当前日期,让模型用相对日期表达式。做法是在 system prompt 里加一行"当前日期是 2025-03-18,用户说'这个月'指的是 2025 年 3 月"。更稳的做法是让模型生成相对日期条件,例如WHERE date >= DATE_FORMAT(CURDATE(), '%Y-%m-01'),由数据库计算,而不是让模型硬编码数字。这属于用工程手段补模型的先天短板。
4.5 高峰期一条查询等半分钟:前端直接超时
现象:qwen-plus 在并发上来后,单条 SQL 生成要十几秒,再叠加数仓查询的秒级延迟,前端 10 秒超时根本扛不住,用户看到一个转圈然后报错。
原因:两个超时混为一谈。模型生成慢和 SQL 执行慢是两码事,不能共用一个超时配置。另外没有任何并发控制,请求全堆到模型 API 上,互相挤占资源。
解决:分开设超时。模型生成超时 30 秒,超时降级到 qwen-turbo 重试一次;SQL 执行超时 60 秒,超时返回"查询复杂请缩小时间范围"。并发用信号量限制在 10 以内,超出直接返回"系统繁忙,请稍后再试",宁可拒绝也不要堆积。这里的心得是:调度策略比升级模型更管用,先保证系统不死,再谈准确率。
5. 进阶:给 ChatBI 装上"后悔药"——对话改写、语义缓存与准确率评测
多轮对话是 ChatBI 最容易忽视的进阶点。用户第一句问"华东区销售额",第二句问"那华南呢",如果每句话都独立生成 SQL,第二句没有上下文,模型根本不知道"那华南"指的是什么。常见做法是加一层对话改写:把历史对话压缩成一句话,再拼上当前问题重新提问。具体实现时,让通义模型把"历史问题 + 当前追问"合并成"华南区的销售额是多少",再做后续处理。这个前置改写能显著提升多轮场景的准确率。
语义缓存要做到生产可用,必须解决"同样的问题换个说法"的匹配问题。做法是缓存键设计成三段式:规范化问题文本 + 指标口径版本号 + 数据日期。规范化文本由模型生成,口径版本号在指标字典修改时递增,数据日期让隔天查询自动失效。这样同一用户问"昨天卖了多少"和"昨天的 GMV",命中同一个缓存,响应时间从十几秒降到几百毫秒,体验完全不一样。
最后是准确率评测闭环。模型更新、prompt 调整、Schema 变化,任何一个动作都可能让 SQL 生成质量波动。我维护一个 50 题左右的评测集,每道题包含业务问题、期望 SQL 要点(不是完整 SQL,而是关键表和聚合字段)、预期结果。每次改动后自动跑一遍,人工只看差异项。这套评测集已经救了我很多次,有一次换模型版本后整体准确率没变,但细分发现日期类问题全部翻车,靠评测集才在灰度前拦下来。做这个方向最深的教训就是:大模型的输出是概率性的,黑匣子不可控,只有用工程手段把每个环节的可变性压住,ChatBI 才敢真正交给业务用。希望帮到你。
本文还有配套的精品资源,点击获取