← 返回文章列表

Dify 高级实验(02):对话式 BI 分析——如何让业务人员用自然语言直接查数?

1. 业务场景

先讲一个我们实际遇到的场景。

一家电商公司的经营分析会上,市场总监盯着大屏上的销售额曲线,随口问了一句:「上个月销量最高的产品是什么?」在座的运营和数据分析师面面相觑——这个数据得找技术同学写 SQL 才能查。于是需求被记进工单,排进技术团队的队列,快则半天、慢则两三天,答案才回到业务手里。而业务真正想要的,是像跟同事聊天一样问一句,立刻得到「笔记本电脑 B,卖了 131 万」这种带数字、带单位的回答。

我们第一次接这类需求时,第一反应也是「那就给业务开个查询平台,教他们写 SQL」。后来才发现这思路从根上就错了——业务要的不是 SQL,是答案;而「问题 → SQL → 答案」这条链路上,最难的翻译步骤,恰恰是 LLM 的看家本领。Dify 工作流把这条链路做成了可视化节点:问题进去,大白话答案出来,中间那段 SQL 由模型生成、由代码校验。

这不是个例。任何「业务要数据、技术写 SQL」的场景都是这个模式:销售要看区域业绩对比,运营要看转化率趋势,财务要看回款情况——每一个看似简单的问题,背后都是一次「提需求 → 排期 → 写 SQL → 返结果」的漫长等待。

2. 场景痛点

这个流程的痛点,在业务人员身上体现得最直接:

本质上,「说人话查数据」的能力被卡在技术排期上,是数据驱动决策最大的隐形瓶颈——问题不是没有数据,而是数据到业务手里这条路太慢、太脆。

3. 方案:为什么是这套对话式 BI 流水线

Dify 工作流里的「NL → SQL → 安全校验 → 模拟查询 → 自然语言回答」链路,把「写 SQL」这件事从技术手里交还给系统。

选它的理由,我们实际对比过:

这篇文章我们就用它搭一个「销售数据问答助手」:业务人员输入自然语言问题,系统自动生成 SQL、安全校验、模拟查询,再用大白话回答。

4. 整体架构

graph TD start["开始:user_query / query_type"] --> db["代码节点 cd_db:模拟内存销售数据库(表结构 + 示例数据)"] db --> sql["LLM lm_sql:自然语言 → SQL"] sql --> safe["代码节点 cd_safe:剥离围栏 + 安全校验 + 模拟查询"] safe --> ans["LLM lm_answer:查询结果 → 自然语言回答"] ans --> e1["结束 end:输出 answer"]

链路很清晰:入口收自然语言问题 → 生成 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.dumpsresults_text 字符串再引用(dify103_02 实测)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过