ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

DeepSeek+Dify+达梦:自然语言查询数据库的NL2SQL落地实践

DeepSeek+Dify+达梦:自然语言查询数据库的NL2SQL落地实践 1. 项目背景与整体方案设计做一个输入一句话自动查出数据库里的数据的功能这个问题我琢磨了很久。起因是公司内部经常有业务部门提数据需求今天要个订单统计明天要个库存明细每次都得数据团队写SQL、跑查询、导表格再发过去一通操作下来大半天就没了。正好这两年DeepSeek这类大模型在自然语言理解和代码生成上表现相当能打Dify这类LLM应用开发平台又把模型接入、Agent编排和对外服务封装的门槛拉低了不少再加上项目本身跑在国产化环境业务库用的是达梦——于是就有了这套DeepSeek Dify 达梦的组合方案。这篇文章把整个实现过程、踩过的坑和优化心得完整整理出来给正在做数据查询服务、报表平台或者被查数据还得写SQL困扰的读者一个可以直接参考复现的落地路径。1.1 为什么需要自然语言查询数据库先说一个实际场景。业务部门真正想要的不是一张报表而是一个问题的答案。比如上个月华东区退货率最高的商品有哪些最近30天新注册用户的下单转化率是多少这类问题放在数据团队手上写SQL几分钟但排队等排期可能等到下周。如果能让业务人员直接用大白话提问系统自动生成SQL、执行查询、再把结果用自然语言回传整个过程从提需求-排期-取数-加工-反馈压缩成提问-回答效率提升是数量级的。自然语言查询数据库行业内一般叫NL2SQLNatural Language to SQL。这个方向其实很早就有人在研究早期用规则模板、序列到序列模型效果一直差强人意。真正让它变可用靠的是大语言模型的崛起——模型本身已经学会了SQL语法、常见表结构设计套路和大量业务查询表达只需要给它明确的表结构信息和约束规则它就能生成质量相当不错的SQL。DeepSeek在这个场景里优势很明显中文理解能力强代码生成稳定API价格相比同类模型友好很多而且支持私有化部署对数据敏感的企业项目来说非常关键。1.2 技术选型为什么是DeepSeek Dify 达梦选型的时候其实纠结过好几轮。最初考虑过直接用Python写一个脚本调DeepSeek的API拿到SQL后用JDBC执行达梦再让模型把结果翻译成自然语言。这个方案最轻量适合自己玩但真要给业务部门用还缺对话管理、权限控制、日志审计、API封装这些东西全自己写工作量不小。后来看到了Dify它是开源的LLM应用开发平台把模型管理、Prompt编排、知识库、工作流、Agent工具这些能力都做成了可视化配置社区版免费部署也简单于是果断转向Dify 自定义工具的架构。三个组件的职责划分是这样的DeepSeek负责理解用户自然语言生成SQL并把查询结果整理成读得懂的答案。它是整个系统的大脑。Dify负责应用编排和运行管理包括模型接入、会话上下文、智能体工具调用、日志追踪和API网关。它是躯干。达梦数据库负责存储业务数据执行最终SQL返回结果集。它是底座。这套组合还有一个额外的好处开发出来的应用不绑定具体数据库。如果哪天要把达梦换成其他主流关系型数据库只需要改查询服务里的连接配置和方言适配上层Prompt和Dify编排完全不用动。1.3 整体架构与数据流整个系统的数据流是这样的用户在Dify的对话界面输入自然语言问题DeepSeek根据预设的表结构信息生成SQLDify通过自定义工具把SQL发送给一个轻量的查询服务查询服务用JDBC连接达梦执行SQL并返回JSON结果最后DeepSeek把结果集转化成自然语言回答展示给用户。这里有个关键设计Dify本身并没有直接连达梦的官方插件所以我们需要在中间加一层查询服务。这个服务可以做得非常简单就是一个HTTP接口接收SQL返回执行结果。我第一版用FastAPI写的一共不到100行代码部署在应用服务器上只有内网可以访问安全风险可控。Dify侧通过自定义工具的方式接入这个HTTP接口整个过程不复杂后面我会把核心代码和配置完整贴出来。2. 环境准备与基础部署2.1 DeepSeek模型的两种接入方式接入DeepSeek有两条路调用官方API或者本地私有化部署。这个选择直接影响后面的网络架构和安全设计。方式一官方API。适合快速验证和中小规模使用。去DeepSeek开放平台注册账号创建API Key在Dify的模型供应商里选择DeepSeek并填入Key就能用整个过程不到十分钟。优势是零运维模型能力由官方持续更新劣势是数据要经过外部API对于数据安全要求极高的企业内网环境这一条可能直接不通过。方式二本地部署。适合对数据安全有硬性要求或者需要完全离线运行的项目。DeepSeek开源了多个尺寸的模型权重常见的有7B、16B、32B级别。7B模型大概需要16GB显存32B模型需要64GB以上显存部署时可以用vLLM或者llama.cpp做推理加速。成本方面一台双卡4090或者单卡A800的服务器基本就能跑起来对中小企业来说是可以接受的投入。本地部署的好处是数据完全不出内网响应延迟也更可控。我在实际项目里采用的是官方API验证方案先跑通整个流程后续如果客户要求数据不出域再平滑切换到本地部署。Dify里切换模型供应商非常简单不需要动应用逻辑这也是选Dify的加分项。2.2 Dify平台的部署要点Dify社区版推荐用Docker Compose方式部署官方提供了一键部署脚本对机器要求不高4核8G内存的服务器就能流畅跑起来。部署流程如下# 拉取项目代码建议锁定版本 git clone https://github.com/langgenius/dify.git cd dify git checkout 1.17.1 # 复制环境变量示例 cp .env.example .env # 启动服务 docker compose up -d启动完成后浏览器访问 http://服务器IP:80 就能进入Dify控制台。Dify本身依赖PostgreSQL、Redis和一些中间件Compose编排会自动拉起不需要手动安装。这里有一个非常容易踩的坑Dify拉取镜像失败。因为Docker Hub在国内网络环境下经常超时第一次部署时好几个镜像拉不下来卡了快一个小时。解决办法是在 /etc/docker/daemon.json 里配置镜像加速器然后再重启Docker服务亲测有效。配置完成后重新 docker compose pull 就能正常拉取。{ registry-mirrors: [https://docker.m.daocloud.io] }Dify的版本更新节奏比较快社区版1.10以后加入了多租户能力1.17.x系列在工作流和知识库流水线方面又有不少增强。生产环境建议锁版本部署不要每次更新都盲目升级避免功能变更影响业务。部署完成后记得第一时间在设置-模型供应商里接入DeepSeek或其他模型。2.3 达梦数据库准备与连接配置达梦数据库的安装不展开讲网上的教程很多。这里只说和本项目强相关的几步建业务库、建只读账号、确认JDBC驱动和连接参数。达梦的JDBC驱动包一般叫 DmJdbcDriver18.jar在数据库安装目录的 drivers/jdbc 下能找到。连接URL的格式是jdbc:dm://192.168.1.100:5236?schemaDMDB驱动类名是 dm.jdbc.driver.DmDriver默认端口5236。需要注意达梦对大小写敏感未加引号的表名和字段名会被自动转为大写这一点和Oracle很相似。这意味着大模型生成的SQL如果用了小写的表名和字段名直接执行会报无效的表名或视图名这是我们踩过的最大的一个坑后面会专门讲怎么解决。在给查询服务配置连接池时我用的是HikariCP配置里有一个关键项连接测试语句。MySQL可以用 SELECT 1达梦兼容Oracle方言写成 SELECT 1 FROM DUAL 更稳妥。还有一个容易忽略的参数是驱动类名HikariCP要求必须显式指定 driverClassName否则会报Failed to load driver class。spring: datasource: driver-class-name: dm.jdbc.driver.DmDriver jdbc-url: jdbc:dm://192.168.1.100:5236?schemaDMDB username: query_user password: xxxxxx hikari: connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 5 minimum-idle: 1权限方面一定要单独创建查询用账号只授予SELECT权限千万不要用DBA账号。这一点在涉及大模型自动生成SQL的场景里尤其重要——模型再聪明也可能生成 DELETE 或 UPDATE只有从权限层面锁死才能保证数据库安全。3. 核心实现在Dify中搭建自然语言查询应用3.1 应用类型选择与创建Dify里有多种应用类型本项目选择的是Agent应用。为什么不用普通聊天助手或工作流因为自然语言查询数据库天然需要工具调用能力用户输入问题后模型需要判断是否要查库、生成SQL、调用查询工具、根据结果回答这是一个典型的多步骤Agent行为。Agent类型允许模型自主决定是否调用以及何时调用工具对话体验最自然。创建一个Agent应用的步骤很简单进入Dify工作台点击创建空白应用选择Agent类型命名后进入编排界面。在编排界面左侧选择模型为DeepSeek中间区域编写系统提示词右侧配置工具下面详细拆解。3.2 核心Prompts设计与NL2SQL逻辑Agent效果好不好一半看模型一半看Prompt。我的System Prompt经过多轮迭代核心内容如下你是一名资深的数据分析师负责将用户的自然语言问题转换为SQL查询语句。 ## 数据库信息 数据库类型达梦数据库兼容Oracle语法 当前日期{{current_date}} ## 表结构信息 表 orders订单表 - order_id BIGINT 订单ID主键 - customer_name VARCHAR(64) 客户姓名 - region VARCHAR(32) 区域华东/华北/华南/西南等 - product_id BIGINT 商品ID关联products表 - order_amount DECIMAL(10,2) 订单金额元 - order_date DATE 下单日期 表 products商品表 - product_id BIGINT 商品ID主键 - product_name VARCHAR(128) 商品名称 - category VARCHAR(32) 商品分类 - unit_price DECIMAL(10,2) 单价元 ## 工作流程 1. 理解用户的查询意图 2. 根据表结构生成合法的SQL查询语句 3. 调用query_database工具执行查询 4. 将查询结果整理成通俗易懂的自然语言回答 ## 硬性规则 1. 只能执行SELECT查询禁止生成INSERT/UPDATE/DELETE等写操作SQL 2. 查询结果必须限制在100条以内防止返回数据量过大 3. 如果用户的问题涉及的表不存在于表结构信息中必须以数据表中暂无相关信息回答 4. 严禁捏造查询结果工具返回什么就回答什么 5. 所有表名、字段名在SQL中一律使用大写达梦数据库大小写规则 6. 涉及日期计算时以当前日期为准可以使用SYSDATE函数辅助计算这个Prompt里有两个细节值得细说。第一个是表结构信息这是模型生成正确SQL的基础我把业务表整理成这样的字段清单每个字段标注类型、含义和典型值模型生成SQL的准确率明显提高。如果表特别多可以只录入最常用的几张核心表避免上下文过长。第二个是当前日期我用Dify的变量功能在会话开始时动态注入这样模型在回答上月本周这类时间表达时能正确换算日期范围不会算错。3.3 连接达梦数据库工具与服务配置Dify没有达梦数据库的原生工具所以需要先在Dify的工具页面创建一个自定义工具指向我们自己写的查询服务。查询服务我用FastAPI实现核心代码大致如下from fastapi import FastAPI, Request import dmPython # 达梦的Python驱动需要单独安装 import json app FastAPI() app.post(/query) async def query_database(req: Request): data await req.json() sql data.get(sql, ) # 安全检查只允许SELECT if not sql.strip().upper().startswith(SELECT): return {error: Only SELECT statements are allowed} conn dmPython.connect(userquery_user, passwordxxxxxx, server192.168.1.100, port5236) try: cursor conn.cursor() cursor.execute(sql) columns [col[0] for col in cursor.description] rows cursor.fetchmany(100) result [dict(zip(columns, row)) for row in rows] return {columns: columns, rows: result, count: len(result)} finally: conn.close()这个服务有几个细节强制检查SQL前缀为SELECT防止SQL注入和误操作fetchmany(100)限制返回条数返回时带上列名方便模型理解结果含义。生产环境建议用连接池替换每次请求新建连接的方式性能会好很多。在Dify自定义工具配置里OpenAPI Schema填写Swagger生成的JSON或者手动编写关键是要声明 /query 接口的请求参数格式。配置完成后Agent会收到一个名为query_database的工具模型会在需要查库时自动调用。3.4 从单轮到多轮对话式查询优化一个容易被忽略的需求是多轮对话。业务人员经常会连续提问上个月华东区订单量多少那退货率呢和上上个月比呢——如果没有对话上下文管理第二句那退货率呢模型根本无法理解。在Agent应用里Dify会自动携带会话历史所以大模型能通过上下文推断出退货率指的是上一轮提到的订单数据。我只需要在Prompt里加一句注意结合对话历史理解用户的追问效果就出来了。但这里有个隐患多轮对话会让Token消耗快速增长而且上下文过长可能导致模型注意力分散。我建议在Dify的会话设置里把历史消息窗口限制在10轮以内既保留有效的上下文信息又控制成本开销。实测下来10轮对话以内的连带指代识别率挺高超过之后就开始出现理解偏差所以这个值是够用的。4. 核心环节实现细节与效果验证4.1 第一轮查询从提问到SQL的完整流程部署完成后的第一次测试我用的问题是上个月华东区销售额最高的3种商品是什么整个流程分几步走用户输入问题DeepSeek识别出这是一个需要查询数据库的问题根据表结构信息生成SQL调用query_database工具执行拿到结果后整理成自然语言回答。模型生成的SQL大致是这样SELECT p.product_name, SUM(o.order_amount) AS total_sales FROM orders o JOIN products p ON o.product_id p.product_id WHERE o.region 华东 AND o.order_date TRUNC(SYSDATE, MM) - INTERVAL 1 MONTH AND o.order_date TRUNC(SYSDATE, MM) GROUP BY p.product_name ORDER BY total_sales DESC FETCH FIRST 3 ROWS ONLY这里值得表扬一下DeepSeek的地方是它主动考虑了达梦的Oracle方言用了TRUNC和INTERVAL来做月份计算而不是写死日期这样每月运行都能自动对齐上个月的时间范围。执行结果正常返回Agent最终给出的回答是上个月华东区销售额最高的商品依次是XXX、XXX、XXX销售额分别为X元、X元、X元并且附带了查询范围是本月1号到30号的说明业务部门一看就懂。4.2 复杂自然语言处理与SQL生成技巧第一轮查询跑通之后我开始测试更刁钻的问题。比如按区域统计一下今年的客单价趋势——这个客单价涉及计算逻辑通常定义为总销售额除以订单数。模型对这个业务概念的理解取决于表结构里有没有明确说明Prompt里如果没有写模型可能生成错误的计算方式。我的办法是在表结构信息里补充业务口径说明。比如客单价 总销售额 / 订单数 退货订单 订单状态字段为已退货的订单把业务口径直接喂给模型生成SQL的准确率会大幅提升。另外对于不常见的查询表达我在Prompt底部加了一组Few-shot示例比如示例1 用户北京地区的平均订单金额是多少 SQLSELECT AVG(order_amount) FROM orders WHERE region 北京 示例2 用户每个商品的销量排名最高的前5个 SQLSELECT product_id, COUNT(*) AS sale_count FROM orders GROUP BY product_id ORDER BY sale_count DESC FETCH FIRST 5 ROWS ONLYFew-shot示例控制在3组以内就够了太多会挤占上下文长度效果提升不明显。经过这轮优化客单价同比环比TopN这类复杂需求基本都能正确转化为SQL。4.3 大小写问题与达梦方言适配前面提到的达梦大小写问题这里展开说。第一次测试时模型生成的SQL全是小写表名执行直接报错。排查了半天才发现达梦在默认配置下不带引号的标识符会自动转为大写存储而小写的表名根本找不到。解决思路有两个层面。第一个层面是在Prompt里硬性约束让模型生成SQL时表名和字段名全部使用大写。这个方法简单直接但模型偶尔会忘记不够保险。第二个层面是在查询服务里做一次大写转换因为字段内容里可能包含大小写敏感的字符串值不能无脑整体转大写而是通过解析SQL中的关键字只把表名和字段名转成大写这个实现稍复杂一些。我的最终方案是两个层面结合Prompt里约束大写查询服务里做了一层兜底处理——检测到SQL执行报无效的表名错误时自动尝试把代码中涉及的表名字段名转大写重试一次实际运行中兜底率不低但确实增加了一些代码复杂度。如果你只是做内部验证Prompt约束就够用了生产环境还是建议把查询服务里的自动修正逻辑加上。4.4 产品化封装与安全控制验证阶段跑通之后我给这个应用做了产品化封装。在Dify的访问API页面可以创建API Key调用Dify对外开放的接口这样外部的报表系统、企业微信机器人、Web页面都可以接入。接口调用方式很简单POST一个对话消息同步返回结果Dify已经把Agent内部的所有工具调用封装好了。安全控制做了三层。第一层是数据库账号权限只读账号 专用Schema从根源上杜绝危险的写操作。第二层是查询服务层面的SQL前缀校验同时限制了单次返回的最大行数和最大执行时间防止业务人员问出一个全表扫描导致数据库性能雪崩。第三层是Dify层面的访问控制API Key定期轮换同时做了操作日志留痕每次查询都能追溯到提问人、提问时间和生成的SQL。5. 常见问题与排查技巧实录5.1 Dify部署与镜像拉取问题这个坑几乎每个部署Dify的人都会遇到。docker compose up -d 执行后好几分钟没反应最后报错全是pull access denied或timeout。排查思路先确认不是镜像本身的问题github.com/langgenius/dify 仓库的镜像都在Docker Hub上国内直接访问不稳定。配置镜像加速器是最直接的解决方案配置完执行 systemctl restart docker然后重新拉取即可。如果加速器还是不行可以尝试使用代理或者从镜像站手动下载再docker load导入后者比较麻烦加速器解决90%的问题。5.2 达梦连接与驱动配置问题查询服务启动时报各种连接异常常见原因有三个。第一个是驱动类名写错达梦有两个驱动类老的叫 dm.jdbc.driver.DmDriver新版本也有类似的必须是全限定名不能省略包名。第二个是连接URL格式不对有的同学习惯套MySQL格式写成 jdbc:mysql://那肯定报错达梦的格式是 jdbc:dm://host:port。第三个是防火墙问题5236端口没开放从应用服务器telnet一下端口就能定位。HikariCP连接池还有一个特殊配置项connection-test-query。如果不配置HikariCP默认用JDBC4的 isValid() 方法验证连接达梦驱动对这个接口的支持不完善可能导致连接池把正常的连接判定为失效。显式配置 SELECT 1 FROM DUAL 可以避免这个问题这也是达梦社区里比较常见的处理方式。5.3 NL2SQL准确率问题的调优思路很多读者体验后反馈模型生成的SQL偶尔能用偶尔不能用怎么办。准确率问题是NL2SQL系统的常态我的优化思路按照优先级排序第一检查表结构信息是否完整准确字段描述是否清晰如果模型对字段含义模糊生成SQL时只能靠猜准确率必然低第二检查Prompt里的硬性规则是否覆盖了达梦方言和业务口径把会出错的模式直接写成禁止或必须第三增加Few-shot示例把业务方最常问的10类问题写进示例模型有样可依会稳定很多第四考虑在查询服务里增加SQL语法校验生成后先解析再执行语法错误直接抛给模型重试而不是把错误抛给用户。实测下来前三个优化做完常见问题的准确率已经从70%提升到90%以上。剩下的10%多属于长尾问题需要持续收集真实提问定期补充到示例库中模型的稳定度会随着标注数据的积累稳步上涨。6. 延伸思考与个人体会这套方案做完之后我自己最大的感触是NL2SQL下SQL生成的最后一公里问题难点真的不在于模型而在于对业务的理解。同一个字段在不同语境下含义可能完全不同同一个指标不同部门的统计口径可能还不一样。这些业务知识能不能有效传递到模型侧决定了这个功能是好用还是鸡肋。另外想提醒的是自然语言查询数据库并不是要取代数据分析师。它擅长处理的是已知问题、找答案这类标准化查询而真正需要分析师介入的是那种连问题都没想清楚的分析场景。所以这套系统的定位应该是把数据分析师从重复的提数工作中解放出来让他们去做更深层的分析和业务洞察。我个人的建议是如果你想在自己的项目里复现这套方案不要一上来就搞一大堆表的全量接入先挑一个业务域、两三张最核心的表把Prompt和表结构信息打磨好跑通一个完整的业务闭环。等业务方用起来、反馈真实问题时再逐步扩充表范围和示例库。这样迭代节奏最稳也能更早看到实际价值。
返回列表