基于Dify工作流的Text2SQL风控分析方案:让分析师用大白话查数
风控分析师每天最常干的一件事就是追着数据团队要数。风控策略调参要看近30天通过率趋势反欺诈规则上线要追踪命中率贷后催收要拆解不同逾期阶段的人群画像——这些需求看起来千奇百怪落到数据库上无非就是几十张表的条件组合与聚合统计。可问题就出在分析师不懂SQL开发懂SQL却不懂业务中间隔着一个“取数排队”的损耗层提需求、排期、写数、验数、返工轻则半天重则两三天。我做这套东西的出发点特别朴素——能不能让风控分析师直接用大白话提问系统自动翻译成SQL并跑出结果于是就有了基于Dify工作流搭建的Text2SQL风控策略分析方案。这个方案不是简单接一个大模型API就完事它是在Dify里编排了一条完整的工作流先把用户的自然语言问题做意图识别和参数抽取再结合风控库表的Schema信息和Few-shot样本生成SQL执行后把结果集封装成业务口径的解释文案。整套流程跑下来最大的感受是Text2SQL的难点根本不在“写SQL”这一步而在工程化链条上的每一个细节——表结构怎么暴露、示例怎么选、错了怎么兜底、口径怎么校验。这篇文章就把我踩过的坑和最终跑通的方案完整拆一遍给正在做类似事情的同学一个可复用的参考。1. 风控查数的血压时刻与Text2SQL的边界先看几个真实的业务提问你就能明白风控场景下Text2SQL和通用问答的区别在哪。分析师常问的问题大概是这些节奏“近7天新户的首次借款通过率是多少按渠道拆分”“Model_X评分低于600分的用户里有多少人发生过逾期30天以上”“昨天存量客户支用笔数环比上周一变化了多少”。这些问题看着简单但落到SQL里全是坑新户怎么定义、首次借款是取首笔放款还是首笔申请、逾期30天是按M1口径还是逾期天数≥30、环比是自然周还是工作日对齐。每一个口径都会影响最终数字而分析师问的时候默认“你应该懂”。这就是风控场景Text2SQL的第一个特殊性业务口径的隐含知识远多于通用场景。通用Text2SQL只要能把“找出所有价格大于100的商品”翻成WHERE price 100就及格了但风控里“新户”“存量”“M1”“支用率”这些词背后是一整套业务定义有的定义甚至在不同业务线之间还不一样。如果你直接把整张表结构塞给大模型让它自由发挥它大概率会生成语法正确但业务语义完全跑偏的SQL。所以我从一开始就没打算做一个“万能问答机器人”而是给这套系统划了三条明确的边界只回答与风控策略分析相关的查数场景包括通过率、拒绝率、逾期率、额度使用率、件均、笔均、分布统计等。其他问题一律走兜底回复让用户去找数据团队。口径由配置驱动核心业务概念新户、存量、M1、M2、支用等在配置中心里显式定义Text2SQL生成SQL时强制注入这些定义不让模型自行发挥。允许查不到不允许乱给数。当模型生成的SQL无法确定是否符合业务口径时系统宁可返回“无法确认请补充条件”也不能输出一个错的数字让分析师拿去做决策。这三条边界直接决定了后面的架构选型和Prompt设计。Dify工作流最大的价值正好在这里它让我能把“意图识别—参数抽取—SQL生成—SQL执行—结果解释”拆成独立节点每个节点都有自己的职责和兜底逻辑而不是一个大Prompt包打天下。这一点后面展开说。1.1 为什么是Dify而不是裸调API有人可能会问直接用LangChain或者裸调大模型API自己写编排逻辑不也一样能做吗我的回答是能做但工程成本完全不在一个量级。Dify这类工具带来的核心价值有三个恰好都打在Text2SQL的痛点上。第一是可视化调试。Text2SQL的成功率不是写一次Prompt就完事的需要反复调试看模型在哪些问题上理解偏了、哪条Few-shot示例反而起了反作用、某个表的字段描述是不是有歧义。Dify的工作流界面可以单节点运行、看中间变量、对比不同模型的输出这种调试体验比手写代码循环调参舒服太多。第二是可配置的分支逻辑。查数场景天然需要多路分支问题里有没有时间条件、有没有渠道维度、是需要明细还是只要汇总、SQL执行报错了该怎么处理。Dify的条件分支节点把这些逻辑可视化之后业务同学也能看懂这条链路是怎么走的后期维护迭代的门槛低很多。第三是监控和日志。线上跑起来之后每个节点的输入输出、Token消耗、执行耗时都有记录。风控场景对数据安全要求高我在Dify上层还套了一层审计逻辑每次查询都会记录操作人、问题原文、生成的SQL、返回的结果摘要这个在合规审计时是刚需。1.2 风控场景对Text2SQL的能力要求矩阵在设计整个方案前我梳理了一张需求清单明确风控策略分析场景对Text2SQL的特别要求。这张表我建议你也做一份因为很多项目做着做着就跑偏回头对照这张表就能快速发现问题能力维度通用Text2SQL要求风控策略分析要求查询类型以明细查询为主聚合统计占80%以上且带条件过滤和多维分组口径理解基于字段名理解基于业务定义新户/存量/逾期口径需要外部注入时间表达简单日期过滤“近7天”“环比”“同比”“周一”等相对时间自动解析错误容忍语法正确即可数字必须可解释口径必须能追溯宁可拒绝不可错答权限控制按用户控制按数据域控制敏感字段手机号、身份证默认脱敏或禁用有了这张矩阵后面所有技术选型和Prompt设计都有了依据。2. 工作流整体设计输入归一化到结果解释的五级管道实际跑通后的Dify工作流结构并不复杂核心是一条五级管道输入归一化 → 信息检索 → SQL生成 → 执行与修正 → 结果解释。每一级都对应一到两个Dify节点节点之间的数据传递用变量完成。这里我先给整体结构后面分别拆关键节点的实现细节。工作流的入口是一个简单的对话节点承接用户输入。第一件事不是直接去生成SQL而是做输入归一化。这一步的产出是一个结构化的“查询意图对象”包含四个关键字段query_type查询类型枚举值为trend趋势、distribution分布、ratio比率、comparison对比、detail明细。table_target目标表或目标数据集从知识库和表注册中心里匹配。conditions抽取出的过滤条件列表如时间范围、渠道、用户分层、评分区间。metrics需要计算的指标如通过率、件均、笔数、占比。这个归一化步骤极其重要。它把大模型从“每次都要理解自然语言”的压力中解放出来后续节点拿到的都是结构化数据Prompt可以写得更聚焦。我在实际调试中发现有了这层之后SQL生成的准确率直接从60%级别提到了85%级别后面再叠加Few-shot和Schema约束能稳定到90%以上。第二级是信息检索。根据归一化结果里的table_target字段从表注册中心拉取对应的Schema信息、业务口径描述、常用查询模板和相关的Few-shot示例。这一步在Dify里用知识库检索节点实现底层向量检索基于表名和表描述的语义匹配。这一级的设计思路是“只给模型够用的信息”不把整个库的表结构全塞进去既省Token又避免了无关表的干扰。第三级是SQL生成。这一级是重头戏也是Prompt工程最多的部分。它接收第二级带来的Schema上下文和第一级的结构化意图生成初步SQL。我用的模型是DeepSeek-V3兼顾效果和成本如果你的场景对延迟更敏感可以考虑更小尺寸的模型但SQL生成这个环节建议还是用能力足够强的模型因为后续修正的成本远高于一次生成的成本。第四级是执行与修正。生成的SQL先在测试环境或只读从库执行如果报错则把错误信息回传让模型结合Schema重新修正最多重试两次。如果执行成功但返回的行数异常比如超过1万行或聚合结果只有一行且全是0会触发规则检查决定是否需要追问用户。另外还有一道“风控口径校验”我把它做成了独立的校验函数插在SQL执行前这个细节在第五节专门讲。第五级是结果解释。SQL执行结果是一个DataFrame或JSON直接甩给用户肯定不行尤其是风控分析师要拿这个数字去做策略判断。结果解释节点的作用是把查询结果的关键指标用自然语言表达出来并附上“口径说明”——说明这个数字是按什么逻辑算出来的、包含了哪些条件、排除了哪些人群。这一步看着简单实际是赢得业务信任的关键。2.1 输入归一化节点的Prompt设计输入归一化节点本质上是一个信息抽取任务Prompt设计很吃细节。我最终采用的Prompt结构是这样的你是风控数据分析助手。请从用户的自然语言问题中抽取查询意图输出JSON格式结果。 用户问题{query} 抽取规则 1. query_type只能是这些枚举值之一trend、distribution、ratio、comparison、detail。如果无法确定输出unknown。 2. table_target必须是表注册中心里存在的表名或数据集名。如果无法匹配输出unknown。 3. conditions是一个数组每个元素包含field、operator、value三个字段。时间条件统一为date_range类型value使用最近N天或起始日期_结束日期格式。 4. metrics是一个数组描述需要计算的指标名称尽量使用业务术语原文。 只输出JSON不要解释。这里有一个细节conditions里的时间条件统一用自然语言范围描述而不是直接翻译成具体日期。因为同一句“近7天”在不同时间点运行对应的日期区间是不一样的过早转换成具体日期反而会丢失灵活性。真正的日期计算放在SQL生成阶段由系统时间函数处理。有一次我在调试时发现分析师问“两个渠道的通过率差多少”归一化节点把query_type识别成了ratio但实际上是comparison对比两个对象的同一指标。后来在Prompt里加了一条规则“当问题中出现了两个及以上对比对象时query_type优先判定为comparison”。加了这条之后这类问题的准确率才上去。2.2 为什么需要独立的“表注册中心”这是我在实践中最想强调的一点不要把数据库信息直接暴露给模型一定要在中间加一层表注册中心。表注册中心本质上是一份由人工维护的元数据清单记录每张表的以下内容表名和业务名称如f_loan_apply_d对应的业务名称是“借款申请日表”核心字段清单每个字段包含字段名、业务含义、枚举值说明、是否敏感字段常用的Join关系比如申请表和放款表通过apply_no关联该表常用的查询模板比如“按渠道查通过率”这个需求对应的典型SQL是什么权限标签哪个角色可以查这张表有了表注册中心Text2SQL的流程从“模型自由选择表”变成了“模型从候选列表里选表”自由度降低但准确率大幅提升。它还有一个额外好处敏感字段可以在注册中心里直接标记禁用SQL生成阶段会强制要求SELECT子句不得包含标记为敏感字段的列这是风控合规的底线要求。我在初始版本里跳过了这一步直接把所有表的DDL拼进Prompt结果模型经常选错表——明明要查放款表却关联了申请表的字段输出看似合法但逻辑荒谬。加了表注册中心之后这类错误几乎绝迹了。3. Text2SQL提示词工程Schema瘦身与Few-shot构建细节Text2SQL的Prompt工程和普通对话类Prompt有一个本质差别稳定性和可重复性优先创造性次之。同一个问题今天问和明天问结果必须一致换个说法问同一个事结果也必须一致。这就要求Prompt里的约束信息必须“硬”——不是建议模型怎么做而是强制模型怎么做。先看Schema瘦身。一张典型的风控宽表可能有四五十个字段但一个具体查询用到的通常不超过十个。如果把这些字段全塞进Prompt模型反而会被无关字段干扰尤其是有些字段名长得像但含义完全不同比如overdue_days和overdue_cnt模型选错一个就全盘皆输。我的做法是把Schema信息拆成三层第一层核心字段白名单由表注册中心根据query_type动态生成。比如trend类型查询只保留时间字段、渠道字段、指标字段detail类型查询才加上用户ID等明细字段。第二层字段描述每个字段配一句“人话”解释比如is_first_loan→ “用户是否首次借款1是、0否”。第三层枚举值约束对类型字段给出合法值范围比如approve_status→ “取值范围0待审核、1通过、2拒绝、3人工复核”。这三层Schema拼成上下文后模型生成SQL时基本不会“发明”不存在的字段。下面是一个实际使用的Schema片段示例表名f_loan_apply_d借款申请日表 业务说明每行代表一条借款申请记录按申请日期分区 核心字段 - apply_date申请日期DATE类型格式YYYY-MM-DD - channel_code渠道代码VARCHAR枚举值app_h5/app_android/app_ios/offline - is_first_loan是否首借TINYINT1是0否 - apply_amt申请金额DECIMAL(18,2)单位元 - approve_status审批状态TINYINT0待审核/1通过/2拒绝/3人工复核 - final_rate最终审批利率DECIMAL(9,4)单位% - user_score用户风控评分INT范围300-850值越大风险越低 敏感字段user_mobile、id_card_no禁止出现在SELECT子句3.1 Few-shot示例要喂对案例而不是喂一堆案例Few-shot对Text2SQL的准确率影响巨大但这里的“量”不是越多越好关键是“覆盖面”。我维护了一个Few-shot样本库目前只有24条但每条都经过精心挑选确保覆盖最常用的查询模式。选择样本的原则有三条常见性、边界性、口径性。常见性优先通过率、拒绝率、逾期率这三个指标相关的查询各配2条样本因为这是分析师问得最多的。边界性其次专门收录那些容易写错的查询类型比如“环比昨天”的时间窗口怎么表达、按渠道分组后还要按用户分层再分组的多重聚合怎么写。口径性最关键每条样本都要体现一个明确的业务口径并且口径说明要写在SQL上方的注释里。模型会学习这种“先说明口径再写SQL”的模式生成时也会自觉带出口径声明。举一条典型的Few-shot样本问题近30天各渠道新户的申请通过率是多少 口径说明新户定义为首次借款申请用户通过率审批通过申请数/总申请数。 SQL: SELECT channel_code, COUNT(*) AS total_cnt, SUM(CASE WHEN approve_status 1 THEN 1 ELSE 0 END) AS approved_cnt, SUM(CASE WHEN approve_status 1 THEN 1 ELSE 0 END) / COUNT(*) AS approve_rate FROM f_loan_apply_d WHERE apply_date DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND is_first_loan 1 GROUP BY channel_code;你注意看这里我特意用了SUM(CASE WHEN ...)而不是SUM(IF(...))不是因为这个写法更优雅而是因为模型对大模型更通用的SQL语法理解更好。实际踩坑中发现有些模型对IF函数在聚合中的处理容易出问题换成CASE WHEN后准确率明显提升。3.2 让模型学会“先算总量再算比率”风控分析里有一类高频问题特别容易写错SQL比率类统计。比如“近7天M1逾期率是多少”新手模型很可能会直接写SUM(overdue_flag) / COUNT(*)但正确逻辑往往需要分步计算——先算分母观察期内的放款本金余额或应还笔数再算分子落入M1的逾期金额或笔数最后才得到比率。我的处理办法是在Few-shot库里专门维护了一组“两步SQL”样本并且在这种场景下强制要求模型生成子查询或CTE结构。Prompt里有一段显式约束注意当查询涉及比率、占比、转化率时请优先使用子查询或者CTE先分别计算出分子和分母再做除法。禁止直接在SELECT子句里用聚合函数嵌套聚合函数。这个约束的初版是“建议”语气效果一般。后来改成“禁止”语气准确率又有几个点的提升。说明对大模型下指令时负面约束往往比正面建议更有力——模型对“不做什么”的执行确定性高于“应该怎么做”。4. SQL修正与风控口径校验把错误拦截在查询执行前Text2SQL生成之后直接扔给数据库执行是高风险动作。风控数据库通常是核心业务库的从库一个全表扫描的SQL就能把CPU打满一个少了WHERE条件的误操作可能拉出全量用户数据。因此我的方案在SQL执行前加了两道检查语法与安全性检查以及业务口径校验。语法与安全性检查用SQLGlot实现这是一款纯Python实现的SQL解析器对SQL语法的解析能力比正则强得多。我把校验规则写成了一套配置核心检查项包括SELECT子句中是否包含敏感字段注册中心里标记为禁用的列是否包含DELETE、UPDATE、DROP、TRUNCATE等非查询语句是否包含全表扫描的特征没有WHERE条件或WHERE条件里没有时间约束聚合查询是否有合理的GROUP BY防止全表聚合成一行造成误解LIMIT是否有上限明细查询强制限制返回行数这一层检查通过后SQL才算“可以执行”。但“可以执行”不等于“查得对”所以还有第二道口径校验。这道校验的核心思路是从生成的SQL里反向解析出“口径指纹”然后和注册中心里的标准口径做比对。4.1 口径校验的“分段比对”策略我实现口径校验的方式比较轻量不追求完全理解SQL语义只做分段比对。把SQL拆成四个片段——查询目标片段、时间条件片段、过滤条件片段、分组维度片段每个片段分别提取关键词然后和用户问题里的业务词做交叉校验。举个例子用户问“近30天各渠道首借用户的放款成功率”从归一化节点拿到的conditions里明确有is_first_loan 1。那么SQL里必须出现对is_first_loan的过滤或包含逻辑。如果没有校验直接拦截让模型重新生成并给出提示“用户问题中包含首借用户条件请检查SQL是否遗漏该过滤条件”。这套策略实现起来不难但对拦截“模型想当然丢掉条件”这类高频错误效果明显。我统计过加入口径校验后生成的SQL首次执行成功率变化不大但“生成正确但口径跑偏”的比例从之前的21%降到了6%左右。这个提升对风控场景意义重大因为口径跑偏是分析师最痛的问题——数字差一点策略结论可能完全翻转。4.2 SQL执行错误的三轮修正机制即便有前面的防线SQL在执行阶段依然可能报错字段名拼错、类型不匹配、函数用错版本。我的做法是给模型最多三轮修正机会每轮带上错误信息重新生成。第一轮修正把数据库的原始错误信息直接拼进Prompt同时附上对应表的Schema。大多数语法错误在这一轮就能解决。第二轮修正如果第一轮失败检查是否存在表名或字段名混淆。这个阶段把表注册中心里相似字段名的对照表注入Prompt比如“apply_amt是申请金额loan_amt是放款金额请确认你用的是否正确”。第三轮修正还失败就直接放弃回复用户“当前条件下无法生成可执行的SQL请联系数据团队手动处理”。这个兜底逻辑特别重要它让系统有了“承认不会”的能力避免了无限重试消耗资源。实际线上数据是三轮修正后SQL最终执行成功率在95%以上剩下不到5%的失败主要集中在新上线的表结构未注册、或者用户问题过于模糊这两个场景。4.3 执行模式选择直连数据库的配套工程我在Dify里做了两种执行模式自动执行和人工确认执行。默认是人工确认执行工作流先生成SQL和预期结果描述推给用户确认后才真正跑数。这个设计可能让人觉得多此一举但在风控场景意义重大。一方面业务分析师希望看到数字但更希望确认“这个数字是按我想要的口径算的”。生成SQL后先给他看SQL改写成的白话解释比直接给数字更能建立信任。另一方面人工确认也天然形成了操作审计链——每次查询都有用户确认识别出了数据问题能追踪到具体环节。实际用下来人工确认模式并没有想象中那么影响体验。因为SQL生成阶段已经把口径描述写得比较清楚了分析师瞄一眼发现没问题就点了确认整个过程只多花十几秒。磨合期过后一部分信任度高的高频查询可以在工作流里配置白名单对特定用户和特定查询类型跳过确认直接执行。5. 结果解释与多轮追问让分析师拿到的不只是一堆数字Text2SQL跑出结果只是前半程后半程是结果解释。我发现这类系统上线后用户反馈最集中的不是查询结果错了而是**“我不知道这个数字是怎么算出来的”**。所以结果解释节点承担着比“把数字翻译成人话”更重的职责它需要让分析师判断“这个数字能不能用”。我定义的结果解释模板包含四块内容结果摘要用一句话说清楚核心结论比如“近30天各渠道新户通过率线上渠道平均为42.3%其中自有App最高为46.8%”。口径说明简要说明计算口径比如“新户定义为首次借款申请用户通过率审批通过申请数/申请总数”。数据公告说明该数据的统计时间范围、数据更新延迟T1还是实时、数据源表名让分析师对数据新鲜度心里有数。异常提示如果本次查询结果与历史均值偏差超过一定阈值比如通过率突然下降15个百分点给出警告。这一节我最初做得非常简单只有“结果摘要”一项。后来业务同学反馈说虽然数字是对的但他们拿着数字去复盘时总是被问“这个数是实时的还是隔天的”“包含人工复核的吗”反复确认很消耗信任。加了口径说明和数据公告之后这类追问大幅减少。5.1 多轮追问的意图继承实际使用中分析师很少一句话问完就结束通常会用多轮追问来逐步收窄范围。比如第一句问“近30天新户通过率是多少”看到结果后第二句可能接着问“那只看额度在5万以上的呢”。多轮场景对Text2SQL最大的挑战在于意图继承。第二句里没有“新户”“近30天”这些条件但如果系统把它当作全新问题处理生成的SQL就会把上一轮的关键条件丢掉。Dify工作流的处理方式是把对话历史变量传入归一化节点让节点先判断当前问题是独立问题还是追问。我在归一化节点的Prompt里加了一段追问处理逻辑如果用户的问题是追问例如“那如果只看额度5万以上呢”“去掉线下渠道呢”“男性用户呢”请先继承上一轮查询的所有条件再在当前条件基础上修改。上一轮查询条件如下 {conversation_history}这个逻辑上线后多轮追问场景的准确率从55%直接跳到82%。剩下18%的失败大多来自用户追问时把条件关系搞复杂了比如“既不看首借也不看线下渠道”这种双重否定模型容易绕进去。这类情况暂时没有更好的解法需要用户在下一轮用更清晰的表达补充。5.2 结果解释中的SQL改写技术为了让分析师确认SQL符合预期结果解释节点还有一个隐藏功能把SQL翻译成白话。这个翻译不是针对结果的解释而是对SQL逻辑本身的复述。我用的方法是让模型把SQL语句逐句改写为业务语言的逻辑步骤比如原始SQL片段 WHERE apply_date DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND is_first_loan 1 白话翻译 本次查询的过滤条件为申请日期在最近30天内不含今天且用户为首借用户。这个翻译能力由一个独立的小模型节点承担因为任务简单不需要主模型的成本。实际操作中这个功能意外成为了整个系统的“信任神器”——分析师看到SQL被翻译成自己问的话信任感立刻建立起来。6. 三个真实业务案例的全链路追踪理论和架构讲了这么多还是用三个实际的业务问题走一遍全流程你会更清楚这套工作流每个节点到底做了什么。6.1 案例一趋势类查询——“近90天每周的放款金额趋势”归一化节点抽取的结果query_typetrendtable_targetf_loan_apply_dconditions[{date_range: 最近90天}]metrics[放款金额]。信息检索节点拉到这张表的Schema和两条趋势类Few-shot样本。SQL生成节点产出的SQL里时间条件用了DATE_SUB(CURDATE(), INTERVAL 90 DAY)分组方式用了YEARWEEK(apply_date, 1)——这里有个细节YEARWEEK的第二参数1表示周一为一周的开始符合业务上周一作为统计起始日的习惯。这个细节模型从Few-shot里学到了。口径校验通过后SQL在执行节点跑出结果结果解释节点自动生成描述“近90天放款金额整体呈现波动上升趋势第10周达到峰值1200万元最近两周略有回落。”同时附上口径说明“放款金额按放款成功日期统计包含所有渠道与产品”。这是我调出来的最稳的一类查询准确率超过98%。趋势类查询的结构相对固定少了许多“口径歧义”的坑。6.2 案例二对比类查询——“Model_X评分600分以下的用户支用率和600分以上的差多少”这个查询的难度一下子高了很多。归一化节点识别到“和……比”的结构判定query_typecomparison并且识别出两个对比分组评分600分以下、600分以上。这里的临界值600被抽取成condition但condition的描述是“用户评分≤600”还是“用户评分600”直接影响边界用户的归属。SQL生成阶段Prompt里注入的Schema约束中有一条专门处理这类区间边界“当用户问题中出现‘600分以下’时默认包含600分本身即score 600‘600分以上’默认不包含600分即score 600。如业务有特殊要求请在问题中说明。”这个默认规则是跟业务方对齐过的写死在Prompt里模型不会自由发挥。生成的SQL用CASE WHEN user_score 600 THEN low_score ELSE high_score END AS score_group对用户分组再分别计算支用率。结果解释节点输出时额外提醒了一句“600分为边界值统计在低分组”。这个细节处理到位后业务方愿意把这类查询交给系统做因为他们知道边界不会含糊。6.3 案例三带陷阱的比率查询——“近30天首借用户的M1逾期率是多少”这个问题看似和案例一差不多实际上处处是坑。M1逾期率的正确计算不止涉及申请数据还涉及还款计划表和逾期记录表的关联。直接单查一张表算出来的M1口径上通常是不完整的。第一版生成的SQL就踩坑了模型用f_loan_apply_d里的is_m1_overdue字段直接算比率但这个字段在表里的定义是“截至当前是否发生M1逾期”和“近30天新增放款中落入M1的比例”不是一回事。口径校验阶段系统发现用户问题里的“近30天”与SQL里的观察期没有形成对应关系——SQL对所有人计算了M1状态而不是对近30天放款的人计算。校验拦截了这条SQL进入修正环节。修正时注入了一个提示“M1逾期率计算建议使用子查询先筛选出近30天放款的用户集合再关联逾期记录表统计该集合中发生M1逾期的人数最后计算比率——分子是逾期用户数分母是放款用户数。”模型依此重新生成了SQL。执行后结果解释给出的口径说明也让业务方一眼看懂了计算逻辑。这个案例是整套工作流最复杂的场景也最能说明“为什么不能只靠模型裸跑”——如果没有口径校验拦截第一版错误SQL业务拿到一个“看似合法实则错得离谱”的数字后面的策略判断都会建立在错误基础上。7. 性能、成本与鲁棒性的调优实录最后这部分聊三个上线后必须面对的现实问题响应速度、Token成本和系统鲁棒性。 Text2SQL系统的技术指标再好这几个问题不解决业务方就是不爱用。7.1 响应时间哪里慢怎么破延迟链路主要消耗在四个环节信息检索向量搜索、SQL生成主模型推理、执行校验SQLGlot、结果解释小模型推理。实测下来主模型的推理时间占总耗时的70%以上。我做的优化有三个层面。第一是Schema瘦身前置信息检索阶段就把字段数量从四五十个压到十个以内Prompt变短之后推理速度明显加快。第二是结果解释模型降级从DeepSeek-V3降到更小的模型因为解释任务的复杂度远低于SQL生成。第三是缓存高频查询对完全相同的自然语言问题直接缓存上一次的SQL和结果响应时间从秒级降到毫秒级。风控分析师群体相对固定高频查询的复用率相当可观。优化后体感简单查询约4秒返回复杂查询约8秒。对分析师来说这个速度可以接受——毕竟相比提需求等半天8秒已经是质的飞跃。7.2 Token成本控制思路Text2SQL场景的Token消耗大头在Prompt中的Schema和Few-shot部分。这部分每次查询都要带上属于固定开销。我算过一笔账一套完整的Prompt大约2500到4000个Token其中Schema和Few-shot占了七成。控制成本的关键是把不变的上下文压到极限。我的做法是Schema严格按当前查询类型裁剪Few-shot只保留当前query_type相关的样本全局共享的系统提示词尽量简短。另外由于风控分析师的查询模式相对固定维护一个“高频查询模式”缓存池命中的直接走缓存不调模型也能省下不少Token。7.3 系统鲁棒性模型输出的不确定性怎么对冲Text2SQL本质上是生成式任务模型的输出天然带有随机性。同一个问题温度参数设0的时候基本稳定但一旦业务口径略有变化模型仍可能生成不同的SQL写法。我的处理思路是确定性优先推理时温度设为0同时在Dify工作流里用多路投票做交叉验证——对复杂查询同时让模型生成两次SQL逻辑等价则通过不等价则进入人工确认模式。二次生成的成本并不高因为复杂查询占总查询量的比例本来就小但换来的确定性收益很大。上线两个月因为模型“临时抽风”导致的误报问题一次都没再出现。另外提醒一个容易忽略的细节数据库是在变化的新表、新字段、字段含义调整都会影响Text2SQL的效果。我维护了一个每周巡检的定时任务自动对比表注册中心和数据库实际表结构发现不一致就告警提示人工更新。没有这个巡检系统上线三个月后准确率必然开始滑坡。我在这个项目里走过最大的弯路就是一开始把所有希望都寄托在“模型能力足够强”上指望大模型看图说话就能写出完美SQL。实际做下来才明白Text2SQL能落地多少完全取决于工程化的精细程度——口径定义是否明确、Schema是否裁剪得当、错误拦截是否够快、结果是否可解释。这几件事做好了即使模型不是最顶尖的整体的可用性也能达到实战要求。Dify工作流在整个方案里更像是把这些环节黏合起来的底座让每一步都能被观测、被调试、被改进。如果你也在做类似的场景建议先别急着调模型把一个查询从提问到拿到数字的全链路走一遍你会发现真正值得花时间的都在Prompt之外。