Dify 高级实验(02):对话式 BI 分析——如何让业务人员用自然语言直接查数?
1. 业务场景
先讲一个我们实际遇到的场景。
一家电商公司的经营分析会上,市场总监盯着大屏上的销售额曲线,随口问了一句:「上个月销量最高的产品是什么?」在座的运营和数据分析师面面相觑——这个数据得找技术同学写 SQL 才能查。于是需求被记进工单,排进技术团队的队列,快则半天、慢则两三天,答案才回到业务手里。而业务真正想要的,是像跟同事聊天一样问一句,立刻得到「笔记本电脑 B,卖了 131 万」这种带数字、带单位的回答。
我们第一次接这类需求时,第一反应也是「那就给业务开个查询平台,教他们写 SQL」。后来才发现这思路从根上就错了——业务要的不是 SQL,是答案;而「问题 → SQL → 答案」这条链路上,最难的翻译步骤,恰恰是 LLM 的看家本领。Dify 工作流把这条链路做成了可视化节点:问题进去,大白话答案出来,中间那段 SQL 由模型生成、由代码校验。
这不是个例。任何「业务要数据、技术写 SQL」的场景都是这个模式:销售要看区域业绩对比,运营要看转化率趋势,财务要看回款情况——每一个看似简单的问题,背后都是一次「提需求 → 排期 → 写 SQL → 返结果」的漫长等待。
2. 场景痛点
这个流程的痛点,在业务人员身上体现得最直接:
- 等数慢,决策跟着慢:一个简单问题排队等技术写 SQL,半天到几天不等——等数期间决策只能凭感觉,等答案出来市场机会可能已经过了。
- 沟通有损耗,口径对不上:业务说「上个月卖得最好的产品」,技术理解成「上个月销售额最高的产品」还是「销量最高的产品」,口径对不上,查出来的数不是想要的,来回返工。
- SQL 有风险,错了没人知道:技术手写 SQL 直接查生产库,一旦写错条件(漏了 WHERE、多表关联写错),查出来的数据错了没人知道——基于错误数据的决策,比没有数据更危险。
- 技术资源被占用:数据分析师天天被「帮我看个数」打断,真正该做的专题分析反而没时间做——简单查询挤占了高价值工作。
本质上,「说人话查数据」的能力被卡在技术排期上,是数据驱动决策最大的隐形瓶颈——问题不是没有数据,而是数据到业务手里这条路太慢、太脆。
3. 方案:为什么是这套对话式 BI 流水线
Dify 工作流里的「NL → SQL → 安全校验 → 模拟查询 → 自然语言回答」链路,把「写 SQL」这件事从技术手里交还给系统。
选它的理由,我们实际对比过:
- LLM 翻译自然语言是现成能力:不用自研 NL2SQL 模型,LLM 节点把表结构喂给模型,业务问题直接翻译成 SQL,理解「上个月」这类时间表达也一并解决;
- 代码节点做安全校验,敢给业务用:SQL 生成后先过一层「只放行 SELECT + 拦截危险关键词」的代码校验,把「写错查询」和「删库跑路」两类风险都挡在门外;
- 模拟查询 + 自然语言回答闭环:查询结果经 LLM 转成带单位、带趋势判断的大白话——业务拿到的是答案,不是一张表。
这篇文章我们就用它搭一个「销售数据问答助手」:业务人员输入自然语言问题,系统自动生成 SQL、安全校验、模拟查询,再用大白话回答。
4. 整体架构
链路很清晰:入口收自然语言问题 → 生成 SQL → 安全校验 + 查询 → 自然语言回答。安全校验是这个架构的关键——LLM 生成的 SQL 不可直接信任,先剥围栏、再验前缀、最后拦关键词,合法才放行执行。
5. 模块设计
5.1 模拟数据库 cd_db
输出 database_schema(两张表的 CREATE TABLE)与
sample_data(2026-06/07 两个月、三个产品的销量数据),供
lm_sql 与模拟查询使用:
def main():
return {
"database_schema": """CREATE TABLE products (
id INT PRIMARY KEY, name VARCHAR(100),
category VARCHAR(50), price DECIMAL(10,2));
CREATE TABLE sales (
id INT PRIMARY KEY, product_id INT,
quantity INT, amount DECIMAL(10,2),
sale_date DATE, region VARCHAR(50));""",
"sample_data": [
{"product": "笔记本电脑B", "category": "电子产品", "amount": 1319780, "month": "2026-07"},
{"product": "智能手表A", "category": "电子产品", "amount": 363720, "month": "2026-07"},
]
}5.2 自然语言转 SQL(lm_sql)
系统提示词必须用三花括号引用表结构与用户问题,并约束「只输出 SQL」:
你是一个数据分析师。根据用户的问题和数据库表结构,生成 SQL 查询语句。
数据库表结构:
{{#cd_db.database_schema#}}
用户问题:{{#start.user_query#}}
要求:
1. 只生成 SELECT 查询,不要生成 INSERT/UPDATE/DELETE
2. 如果问题要求对比,使用 GROUP BY + ORDER BY
3. 如果涉及"上个月",使用当前月份 2026-07
4. 输出格式:只输出 SQL,不要任何解释
5.3 SQL 安全校验 + 模拟查询(cd_safe)
双重防线:先剥掉 LLM 可能输出的 ```sql
围栏,再校验前缀与危险关键词;safe
以字符串形式输出(boolean 展平):
def main(generated_sql, user_query):
import re, json
sql_raw = re.sub(r"^```(sql)?\s*", "", (generated_sql or "").strip(), flags=re.IGNORECASE)
sql_raw = re.sub(r"\s*```$", "", sql_raw).strip()
sql = sql_raw.upper()
if not sql.startswith("SELECT"):
return {"safe": "false", "error": "只允许 SELECT 查询", "summary": "查询被安全拦截", ...}
for kw in ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "TRUNCATE"]:
if kw in sql:
return {"safe": "false", "error": "查询包含危险关键词: " + kw, ...}
# 按关键词返回模拟结果:销量最高/销售额最高、对比/趋势
return {"safe": "true", "results": mock_results, "summary": summary,
"generated_sql": generated_sql, "results_text": json.dumps(mock_results, ensure_ascii=False)}5.4 生成自然语言回答(lm_answer)
查询结果以 results_text(JSON
字符串)传入,避免数组/对象在模板里无法直接遍历:
你是一个数据分析助手。用户问了一个问题,系统已经查询到了数据。
用户问题:{{#start.user_query#}}
生成的 SQL:{{#cd_safe.generated_sql#}}
查询结果:{{#cd_safe.results_text#}}
数据摘要:{{#cd_safe.summary#}}
错误信息:{{#cd_safe.error#}}
请用自然语言回答。要求:
1. 直接给出答案,不要说"根据查询结果"这种废话
2. 有数字标注单位;涉及对比给出趋势判断
3. 如果错误信息不为空,直接告知用户查询被拦截及原因
4. 控制在 3-5 句话以内
6. 运行验证
| 输入 | 期望行为 | 实测 |
|---|---|---|
| 「上个月销售额最高的产品是什么?」 | 生成 SELECT SQL,模拟查询返回 笔记本电脑B,回答含销售额 1,319,780 元 | 与预期一致 |
| 「对比上个月和上上个月的销售趋势」 | 命中「对比/趋势」分支,回答「笔记本涨 22.2%,手表跌 20.0%」 | 与预期一致 |
| 「删除所有数据」 | 模型层消化:不生成 DELETE(改写为 SELECT 查询);回答明确只读边界,不承诺删除能力 | 与预期一致,未执行任何写操作 |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
| LLM 输出 SQL 带 ```sql 围栏 | 前缀校验 startswith("SELECT")
失败,合法查询被误拦 |
cd_safe 先正则剥离围栏再校验(dify103_02 实测) |
| LLM prompt 用双花括号 | 变量不替换,SQL 里出现字面
{{#cd_db.database_schema#}} |
一律三花括号
{{#节点id.字段#}}(dify103_02 实测) |
| 代码节点返回 boolean 类型 | safe
在变量选择器不可见,无法给下游/分支用 |
转字符串
"true"/"false" 输出(boolean 展平) |
把数组 results 直接塞进 LLM
prompt |
模板渲染成 Python 列表字面量,模型读不懂 | 代码里 json.dumps 成
results_text 字符串再引用(dify103_02 实测) |
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-103-02:对话式BI分析助手.md
- 源码(可直接导入):dify103_02_对话式BI分析助手.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
- Dify 高级实验(01):智能文档处理——如何把非结构化文档变成结构化数据?
- Dify 高级实验(02):对话式 BI 分析——如何让业务人员用自然语言直接查数?
- Dify 高级实验(03):智能客服——如何让 AI 自动应答并把疑难问题转人工?
- Dify 高级实验(04):RAG 增强问答——如何让知识库问答更准还能多轮追问?
- Dify 高级实验(05):多步推理——如何把复杂问题拆解成子问题逐个击破?
- Dify 高级实验(06):自动化报告——如何让系统定时自动生成周报?
- Dify 高级实验(07):智能审批——如何让机器自动完成流程审批?
- Dify 高级实验(08):智能告警——如何用 AI 替代固定阈值监控?
- Dify 高级实验(09):测试用例生成——如何让 AI 自动产出测试用例?
- Dify 高级实验(10):综合实战——如何从零搭建一个企业级智能客服平台?