先交代一下背景。我最近做了一个“基于DeepSeek + Dify + 达梦数据库的自然语言查询”项目,业务人员直接在对话框里输入“上个月华东区销售额排名前十的客户是谁”,系统自动把这句话翻译成达梦数据库能执行的SQL,查完再把结果用大白话返回。这套东西在信创和国产化替代的背景下特别实用,因为数据库从Oracle、MySQL迁到达梦之后,很多传统BI工具和连接方式都要重新适配,业务人员又不会写SQL,自然语言查询就成了刚需。
这篇文章我把整个落地过程拆开讲,包含组件选型、达梦连接配置、Dify应用搭建、NL2SQL提示词设计、常见报错排查,以及安全控制这些关键环节。内容尽量做到可复现,你照着走一遍基本能跑通。
1. 方案整体设计与架构思路
1.1 三个组件的角色分工
这套方案里有三个核心角色,各管一摊:
DeepSeek:负责自然语言理解和文本转SQL,也就是把“上个月华东区销售额排名前十的客户”转成一条达梦数据库能执行的SELECT语句。它是整个系统的大脑,模型能力直接决定SQL生成质量。
Dify:负责应用编排,把对话管理、知识库检索、工具调用、日志审计这些环节串起来。你可以把Dify理解成一个带流程的“办公桌”,大模型、用户、数据库之间的所有交互都在这张桌子上完成。
达梦数据库:负责数据存储和SQL执行,是最终的数据源。查询结果从达梦返回后,再由DeepSeek整理成人类友好的回答。
用一个生活化的类比:DeepSeek是脑子,负责听懂人话和写SQL;Dify是办事大厅,负责接待、排队、协调各个窗口;达梦是档案室,真正存放和调取数据的地方。
1.2 为什么选这套组合
这个选型不是拍脑袋定的,我在实际落地前对比过几条路线。
第一,DeepSeek在中文理解和SQL生成上表现均衡。NL2SQL(自然语言转SQL)这个任务,模型要吃透两件事:理解用户的查询意图,同时知道目标数据库的表结构和方言特性。DeepSeek在这两方面的综合表现不错,尤其是对中文口语化问题的理解,比一些通用英文模型更稳。
第二,Dify社区版功能够用且支持私有化部署。它本身自带Agent、工作流、知识库、模型管理这些能力,省去了从零开发前端对话界面和编排引擎的工作量。更关键的是,Dify可以对接任意OpenAI兼容接口,这意味着DeepSeek无论是走官方API还是本地部署,都能直接接入。
第三,达梦兼容Oracle语法,SQL方言有迹可循。达梦DM8支持Oracle兼容模式,比如ROWNUM、DUAL表、NVL函数这些都认得,这对习惯了Oracle语法的开发者和模型来说都很友好。我在提示词里明确告诉模型“使用达梦方言,参考Oracle语法”,生成出来的SQL正确率会明显提高。
还有一点很实际,在一些对数据保密要求高的场景里,国外大模型API根本没法用,DeepSeek开源模型可以本地化部署,实现数据不出内网。这也是企业选型时非常看重的一点。
2. 环境准备:达梦、Dify、DeepSeek三件套
2.1 达梦数据库部署与基础配置
达梦数据库(DM8)安装本身不复杂,Windows环境和Linux环境都有对应的安装包,安装过程中的关键点有两个:一个是初始化实例时设置好页大小和字符集,另一个是记住实例端口,默认是5236。
以Linux环境为例,解压安装包之后执行DMInstall.bin进入图形化安装界面,选择“创建数据库实例”,按向导走就行。需要注意的是,实例初始化时字符集建议选UTF-8,不然后面存储中文会出现乱码或长度计算问题。页大小选默认的8K或16K都可以,如果后续要存大字段,建议选16K。
安装完成之后用disql登录,创建业务表和专门的查询用户。我的做法是单独建一个只读账号给NL2SQL系统用,绝不使用SYSDBA或业务超级账号:
-- 创建只读查询用户 CREATE USER NL_QUERY IDENTIFIED BY "YourStrongPass123"; -- 授予指定表的查询权限(按需授予,不要一把梭) GRANT SELECT ON SALES.ORDERS TO NL_QUERY; GRANT SELECT ON SALES.CUSTOMERS TO NL_QUERY; GRANT SELECT ON SALES.PRODUCTS TO NL_QUERY;这里为什么要单独建账号?因为大模型生成的SQL不可控因素多,即使有提示词约束,也有可能生成出越权查询或非预期内容。一个权限最小化的只读账号,能把风险限制在可控范围内。
2.2 Dify平台本地部署
Dify社区版推荐用Docker Compose方式部署,官方仓库里有完整的docker-compose.yaml。我的部署步骤大概是:
# 克隆代码并进入目录 git clone https://github.com/langgenius/dify.git cd dify/docker # 复制环境变量模板 cp .env.example .env # 启动服务 docker compose up -d启动完成后访问http://服务器IP:80,第一次打开会要求设置管理员账号密码,设置完进入Dify控制台。
这里有个容易踩的坑:Dify镜像拉取失败。默认的Docker Hub源在国内网络环境下经常超时,尤其是在内网或者云服务器上部署时。我的处理办法是在Docker的daemon.json里配置镜像加速源,然后用docker compose pull重新拉镜像。如果是完全隔离的内网环境,就得在一台能联网的机器上把镜像导出,再docker load进去,这个后面第6部分详细说。
Dify版本方面,我早期用过一个比较旧的版本,后面升级到1.17.x,工作流节点的能力和稳定性都有提升。不过升级前建议先备份数据库和docker目录里的volumes,避免升级失败导致应用配置丢失。
2.3 DeepSeek接入:API调用还是本地部署
DeepSeek有两种接法,按场景选:
第一种,官方API接入,最简单。去DeepSeek开放平台注册账号、创建API Key,然后在Dify后台的“设置-模型供应商”里找到DeepSeek,填入API Key就能用。这种方式的优点是模型效果最好、省去硬件投入,缺点是需要联网,数据要经过外部API。
第二种,本地化部署,数据不出内网。用Ollama或者vLLM把DeepSeek模型跑在自己的GPU服务器上,然后通过OpenAI兼容接口接入Dify。Ollama适合快速验证,一条命令就能跑起来:
# 用Ollama跑DeepSeek-R1蒸馏版,以14B为例 ollama run deepseek-r1:14b跑起来之后,Ollama默认监听11434端口,在Dify“模型供应商”里选择“OpenAI-API-compatible”,填入http://模型服务器IP:11434/v1作为Base URL即可。
我在做信创环境项目时推荐本地部署路线,因为很多单位明确要求数据不出机房。本地部署的代价是硬件成本和推理速度,14B模型至少需要一张24G显存的显卡,用vLLM部署的话响应速度能到几十毫秒级,体验基本可接受。
3. 达梦连接与数据准备的关键细节
3.1 JDBC驱动与连接串配置
要把Dify和达梦连通,最关键的第一步是拿到正确的JDBC驱动和连接串。达梦官方提供的驱动包是DmJdbcDriver18.jar,在达梦安装目录下的drivers/jdbc里可以找到。如果是Java项目,还要把驱动打包进自己的服务或API里。
达梦JDBC连接串的格式和MySQL不一样,不要想当然写成jdbc:mysql://。达梦的正确写法是:
jdbc:dm://127.0.0.1:5236?compatibleMode=oracle&schemaName=NL_QUERY几个参数说明一下:
jdbc:dm是达梦的协议前缀,写错连不上。5236是达梦默认端口。compatibleMode=oracle表示使用Oracle兼容模式,这使得SQL写法和Oracle更接近。schemaName指定默认模式(Schema),避免查表时要带模式名前缀。
我负责的项目里,Java后端连接达梦用的就是这套配置。如果你用Spring Boot,把驱动类名写对也非常关键,达梦的驱动类是dm.jdbc.driver.DmDriver,不是com.mysql.cj.jdbc.Driver。
3.2 HikariCP连接池适配达梦的配置
热词里提到“达梦 hikrcp 连接池配置”,这个确实是很多人在Spring Boot接达梦时卡住的地方。HikariCP默认对连接做校验时会执行SELECT 1,但达梦如果不做兼容处理,这条校验语句可能不正常返回。
我的经验是在HikariCP配置里把连接测试语句显式指定为SELECT 1 FROM DUAL。因为达梦在Oracle兼容模式下支持DUAL虚拟表,这样每次从连接池取连接时,HikariCP执行的探活SQL才能稳定通过。
一份可用的application.yml配置模板如下:
spring: datasource: driver-class-name: dm.jdbc.driver.DmDriver jdbc-url: jdbc:dm://127.0.0.1:5236?compatibleMode=oracle&schemaName=NL_QUERY username: NL_QUERY password: YourStrongPass123 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 connection-test-query: SELECT 1 FROM DUAL pool-name: DmHikariPool这段配置里有个容易忽略的点:Spring Boot 2.x之后官方推荐使用jdbc-url而不是url属性,如果你沿用旧项目的url写法,HikariCP可能因为无法识别而报错。
3.3 元数据准备:把表结构变成模型能看懂的内容
自然语言转SQL要成功,只靠大模型本身是不行的。模型不知道你数据库里有哪些表、每个字段是什么意思、状态字段用什么值表示。所以必须在搭建应用前,把表的元数据整理好,喂给模型。
我通常分三步准备:
第一步,从达梦系统表里导出核心表的字段结构。可以执行类似下面这条SQL:
SELECT T1.TABLE_NAME, T2.COLUMN_NAME, T2.DATA_TYPE, T2.DATA_LENGTH, T3.COMMENTS FROM ALL_TABLES T1 LEFT JOIN ALL_TAB_COLUMNS T2 ON T1.TABLE_NAME = T2.TABLE_NAME LEFT JOIN ALL_COL_COMMENTS T3 ON T1.TABLE_NAME = T3.TABLE_NAME AND T2.COLUMN_NAME = T3.COLUMN_NAME WHERE T1.OWNER = 'SALES' ORDER BY T1.TABLE_NAME, T2.COLUMN_ID;第二步,把字段注释补齐,特别是枚举字段的含义。比如订单表的STATUS字段,值0代表待支付、1代表已支付、2代表已取消,这种信息如果不告诉模型,模型生成的SQL只能查原始数字,业务解释还得靠人去猜。
第三步,整理几条典型查询样例。例如:
- 查询某个客户的全部订单:
SELECT * FROM ORDERS WHERE CUSTOMER_ID = 'C001'; - 查询近30天销售额前10的产品。
这些样例我会直接写进提示词,或者作为知识库内容。它们的作用相当于“Few-shot示范”,大幅提升模型生成SQL时的模仿能力,实测比纯粹描述表结构准确率高很多。
4. 在Dify中搭建自然语言查询应用
4.1 创建应用并接入DeepSeek模型
打开Dify控制台,在“应用”页面创建一个“聊天助手”类型的应用,然后进入应用配置页。先确认模型供应商里已经添加了DeepSeek,如果没添加,去“设置-模型供应商”里找到DeepSeek并填入API Key。
配置模型时有一个参数特别重要:把Temperature降到0。NL2SQL是强逻辑性、强确定性的任务,过高的Temperature会让模型每次生成不同的SQL,有的是对的,有的就会语法错误或逻辑不对。温度设为0或接近0,能够最大程度保证SQL生成的稳定性。
另外一个细节是打开“对话记忆”。用户可能连续提问“上个月华东区的销售情况”“那环比呢”,后者依赖前者的上下文,只有开启记忆,模型才能把“那”指代的对象衔接上。
4.2 提示词设计:让模型写出达梦方言SQL
提示词设计是这套系统的灵魂。很多人项目跑不通,不是模型不行,而是提示词没有明确约束。我的System Prompt经过反复迭代,目前保留了一个比较稳的版本:
你是一名达梦数据库(DM8)SQL专家,负责把用户的中文问题转换成可执行的SQL语句。 规则: 1. 只输出SQL,不要输出任何解释和额外文字。 2. 使用达梦数据库方言,支持ROWNUM分页、NVL空值处理、DUAL虚拟表。 3. 只允许SELECT语句,禁止生成INSERT、UPDATE、DELETE、DROP、ALTER等任何非查询语句。 4. 查询结果最多返回100行,超出限制时使用ROWNUM限制。 5. 如果用户的问题不明确或涉及到的表在可用表中不存在,请回复“信息不足,请补充查询条件”。 6. 查询所有字段时使用显式列名,不要使用SELECT *。 可用表结构: {context} 用户问题:{query}这个提示词有几个关键点:
- 明确“只输出SQL”,避免模型在SQL前后夹带解释文字,否则后面解析SQL并执行时会报错。
- 明确“只允许SELECT”,配合前端和API层的双重校验,把风险控制在只读范围内。
- 明确“可用表结构”,这是通过
{context}变量动态注入的,由知识检索或工作流节点把命中的表结构信息填进去。 - 明确“显式列名”,一方面避免
SELECT *返回大字段导致响应变慢,另一方面让结果集的JSON结构稳定,方便前端展示。
4.3 知识库建设:把数据字典和样例查询放进去
如果表的数量多、字段注释复杂,把所有表结构直接写进Prompt会占用大量Token,效果也不理想。我推荐把表结构文档做成Dify知识库,用工作流或Agent里的知识检索节点动态检索。
具体操作:在Dify里创建一个知识库,上传一份整理好的“数据字典.md”,内容包含每张表的表名、字段名、字段类型、字段注释、枚举值说明,以及两三条典型查询SQL。分段模式选“自动分段”即可,索引方式我建议选“高质量”模式,也就是向量检索,这样用户提问时能根据语义匹配到最相关的表结构片段。
这里有个坑要注意:DeepSeek模型本身不提供Embedding接口,所以知识库的嵌入模型要单独配置。如果允许联网,可以用OpenAI的text-embedding-3-small;如果是纯内网环境,我一般用Ollama部署一个bge-m3嵌入模型,再在Dify里添加Ollama供应商,把模型名设为bge-m3即可。
有了知识库以后,用户提问“上个月华东区销量”,知识检索节点能自动命中“订单表”“地区字段”这些片段,把它们拼到Prompt里,模型生成SQL的准确率会有一个肉眼可见的提升。
5. 从自然语言到SQL的核心实现
5.1 方案一:Agent模式加SQL执行工具
Dify的Agent模式适合快速验证。原理是给Agent挂一个“执行SQL”的工具,Agent通过LLM判断当前是否需要调用工具,需要时就调用工具并把结果返回给用户。
在Dify里,自定义工具可以通过“OpenAPI Schema”方式添加,也可以用代码节点写一个简易的HTTP接口。我的做法是写一个独立的SQL执行微服务,Dify通过HTTP请求节点或者自定义工具调用它。
SQL执行微服务的逻辑比较简单,核心代码如下(使用Python FastAPI):
from fastapi import FastAPI, Request import dmPython import re app = FastAPI() # 白名单校验:只允许SELECT开头的单条语句 def validate_sql(sql: str) -> bool: sql = sql.strip().rstrip(';') if not sql.lower().startswith('select'): return False # 禁止多语句执行,简单防御 if ';' in sql: return False return True @app.post("/query") async def exec_sql(req: Request): payload = await req.json() sql = payload.get("sql", "") if not validate_sql(sql): return {"error": "SQL未通过安全校验,仅允许单条SELECT语句"} conn = dmPython.connect(user="NL_QUERY", password="YourStrongPass123", server="127.0.0.1", port=5236) cursor = conn.cursor() cursor.execute(sql) columns = [d[0] for d in cursor.description] rows = cursor.fetchmany(100) conn.close() return { "columns": columns, "rows": rows, "row_count": len(rows) }这里有两个细节需要说明:
第一,我是用一个独立服务去连接达梦,而不是在Dify容器里直接装dmPython。因为Dify的代码节点运行在Dify容器内部,容器里默认没装达梦驱动,强行在容器里加依赖会让Dify镜像变得臃肿且难以维护。独立服务的方式更干净,还能在服务层统一做安全校验和日志记录。
第二,安全校验非常关键。这段代码虽然只有两层检查(必须以SELECT开头、不能包含分号),但足以挡住大部分风险SQL。实际项目中建议再加一层:用sqlparse解析SQL语法,进一步排除嵌套INSERT、写入语句等情况。
5.2 方案二:工作流模式做精准控制
Agent模式的好处是省事,但缺点是LLM的自主权比较大,有时候该调用工具时没调用,或者调用时机不对。在生产环境我更推荐用Dify的工作流模式,每一步都显式控制。
我在生产环境里使用的工作流结构如下:
- 开始节点:接收用户输入。
- 知识检索节点:根据用户问题检索表结构文档,返回相关的表字段信息。
- LLM节点:把用户问题 + 检索到的表结构信息 + System Prompt模板拼接,让DeepSeek生成SQL。
- 代码节点或HTTP请求节点:调用SQL执行微服务,传入上一步生成的SQL,拿到查询结果。
- LLM节点:把查询结果(列名 + 行数据)丢给DeepSeek,让它整理成用户能理解的自然语言回答。例如“2025年11月华东区销售额排名前10的客户是:客户A,4500万;客户B,3200万……”
- 结束节点:返回最终回答。
工作流的优势是每一环都可观测、可干预。如果发现SQL生成质量差,可以只调LLM节点的Prompt;如果发现执行慢,可以先查HTTP请求节点的耗时;每个环节的日志都留得清清楚楚。对于后续的运营维护来说,这个优势是决定性的。
5.3 结果返回与前端展示
前端展示这一块,我用Dify自带的WebApp做Demo足够,但正式项目通常是调用Dify的服务API,把对话能力嵌入到企业内部的系统里。
有一个经验值得分享:不要让大模型自由发挥地描述查询结果。如果查询结果是200行数据,模型想用文字概括出来会非常啰嗦且容易出错。我一般会让模型只做两件事:先用一句话总结查询到的核心结论,然后用Markdown表格把结果的主要内容展示出来。这样前端可以直接渲染表格,比让用户看一大段文字直观很多。
另外一定要限制结果集大小。在SQL执行服务里,fetchmany(100)这个限制不能去掉。否则用户一句“把所有订单都列出来”,数据库几百万行数据全查出来,整个链路都会卡死。达梦侧也可以设置查询超时时间,双保险。
6. 常见问题与排查实录
6.1 Dify镜像拉取失败问题
这个问题太常见了,Dify部署阶段十个有八个会卡在docker pull。
常规解法是在Docker配置里加镜像加速源,修改/etc/docker/daemon.json:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com" ] }改完执行systemctl restart docker重启Docker服务,再重新拉镜像。
如果在完全隔离的内网环境,镜像加速也没用,需要在一台能联网的机器上执行docker pull和docker save,把镜像打成tar包,拷贝进内网后用docker load导入。Dify依赖的镜像比较多,建议用docker compose pull先拉全了,再用一个脚本循环docker save所有镜像。
6.2 连接达梦报错:驱动类找不到或URL格式错误
Dify或者Java后端连接达梦时报ClassNotFoundException: dm.jdbc.driver.DmDriver,多半是DmJdbcDriver18.jar没放到正确位置。在Spring Boot里就是依赖没引入,在Dify代码节点里就是容器里没装驱动,我前面建议的独立微服务方案能绕开大部分这类问题。
还有一类报错是URL格式错误,检查一下你的连接串是不是写成了jdbc:dm://,而不是dm://或jdbc:mysql://。这个细节看着不起眼,但排查起来挺费时间,我一开始也在这上面栽过跟头。
另外要说一个达梦特有的小坑:字段名默认是大写。如果你建表时没有给字段加双引号,查出来的列名就是大写的,比如CUSTOMER_ID。Dify或前端代码如果按小写customer_id去解析,就会取不到值。解决方案是在SQL执行服务里统一把列名转成小写,或者让前端统一兼容大小写。
6.3 DeepSeek请求失败
Dify社区里经常有人报request extension preparation failed这个错,多半跟模型供应商配置有关。我之前排查过一例,原因是Base URL填错了或者API Key失效。DeepSeek官方的Base URL是https://api.deepseek.com,在Dify填模型供应商时要注意这个地址不能带多余的路径后缀。
另一个常见问题是超时和限流。DeepSeek API在高峰期响应会比较慢,Dify默认的请求超时时间如果太短,就会报超时错误。可以到模型供应商配置里把超时时间调大,比如调整到120秒。另外并发请求多的时候容易遇到429限流,我的处理方式是在上层加一个简单的重试逻辑,遇到429就等待几秒再试一次。
6.4 SQL生成质量不高的调优思路
如果你发现模型生成的SQL经常跑不对,不要急着换模型,先把下面几个点逐一检查:
第一,知识库和数据字典是否完整。模型不知道表结构就相当于人没有地图,每一条SQL都是瞎猜。把字段注释和枚举值含义补全,效果立竿见影。
第二,提示词里有没有给出可参考的样例。我在提示词里固定放了两条常见查询样例,模型会照着样例的格式来生成。这个方法比单纯写一堆规则更有效,因为大模型本质上是“模仿学习”。
第三,复杂查询试着拆解。比如“各区域销售额环比增长排名”,这个逻辑比较复杂,一步到位生成正确的SQL难度很高。我的建议是引导用户分步提问:“先查各区域本月销售额”“再查各区域上月销售额”“最后对比排名”。Dify里开启对话记忆后,这种多轮交互完全可行。
第四,多试几个模型参数。除了把Temperature调低,有些场景还需要调整Top_P和Max Tokens,因为SQL语句较长时默认的Token上限可能不够用,导致SQL被截断。
6.5 权限与安全控制
大模型直接连生产数据库,安全怎么强调都不为过。我在生产环境里做了四层防护,目前效果还算让人放心:
第一层,数据库账号隔离。达梦侧建立只读账号,只授予必要表的SELECT权限,没有任何写权限。
第二层,SQL内容校验。在SQL执行微服务里做规则判断,只接受单条以SELECT开头的语句,其他一律拒绝。这块还可以用sqlparse做语法解析,加上禁用关键词表,比如过滤INTO OUTFILE、SLEEP这类有风险的写法。
第三层,资源限制。查询结果集限制100行,查询超时时间限制30秒,防止慢查询拖垮数据库。
第四层,审计日志。所有通过NL2QS接口发送的SQL,连同用户提问一起记录下来,存到单独的日志表。一旦出现问题,可以回溯到具体的用户和具体提问。
7. 避坑心得与后续扩展
这套系统从搭建到上线我差不多踩了大半个月的坑,最后沉淀下来几条心得:
NL2SQL项目做得成不成功,模型只占一小部分,工程细节和数据质量才是大头。把数据字典整理清楚,把提示词反复调优,把安全控制做扎实,比单纯换一个大模型要重要得多。
搭建的时候建议先拿三五个真实业务问题跑通闭环,再逐步扩展表范围,不要一开始就把上百张表全部接入。范围越大,知识库的检索准确率就越容易下降,到时候排查都不知道从哪查起。先跑通一个销售域,验证效果,再慢慢加生产、库存、财务这些域。
还有一点,Dify版本升级要谨慎。社区版在1.10之后变化很大,多租户、工作流节点能力都有调整,升级前一定要备份数据和镜像,升级后在测试环境先跑一遍已有的工作流,确认没问题再切生产。我用久了就养成习惯:上线后固定一个版本,不在生产环境频繁升级。
这项目后续还有一些可以扩展的方向,比如给查询结果增加图表可视化,或者把语音输入接进来,用户可以语音问“上个月利润多少”,系统自动转文字再查库。不过这些都是锦上添花,核心的NL2SQL闭环一旦稳定了,加什么功能都不难。
最后再分享一个小技巧:给用户返回结果时,固定一个JSON结构,比如{"summary": "一句话总结", "table": {"columns": [...], "rows": [...]}},让模型严格按这个结构输出。这样前端和后端都不用去猜模型返回的格式,整个链路会顺畅非常多。这一点是我在实际项目里反复踩了格式解析的坑之后总结出来的,强烈建议你一开始就这样做。