为什么安全校验不通过时不纠错,纠错又只纠一次?
简化版
因为两种失败的性质不同。安全校验不通过,说明写法本身不合规(查了白名单外的表、带危险关键字、写了注释),让模型再答一遍多半还是同一类毛病,白花一次调用;更重要的是,如果让模型「修到能过校验」,等于教它绕过安全规则。执行报错才是「写法合规、但在这个库里查不通」,有数据库报错作为精确反馈,才值得交给模型改一版。纠错只纠一次,是因为一次纠错能解决的主要是字段名、分组这类简单错误;一次没改好多半是理解层面的问题,继续重试成本高、收益低,还可能来回震荡。
详细版
失败分类与处理策略:
| 失败类型 | 典型原因 | 处理 |
|---|---|---|
| 安全校验失败 | 越权查表、危险关键字、注释、多语句 | 停在「校验失败」,记录原因,不纠错 |
| 执行报错 | 字段不存在、分组不完整、函数不兼容 | 纠错一次,重新校验执行 |
| 纠错后仍报错 | 理解偏差、元数据缺失 | 停止,展示最终报错,交给人处理 |
| 权限、超时、连接 | 环境问题 | 不纠错,提示用户或运维 |
重试的一般原则:只重试「换个做法可能成功」的失败,并且每次重试都要带上新的信息。同样的输入重试同样的步骤,结果大概率不变。
完整版教学
一、失败要先分类,再决定要不要重试
很多系统对失败的处理是「一律重试 N 次」,这在调用外部服务时可能合理(网络抖动),但在 Text-to-SQL 里很浪费。Text-to-SQL 的失败可以分成几类,每一类要回答一个问题:再让模型试一次,有没有新的信息能让它做对?
校验失败 新信息:只有「违反了哪条规则」
执行报错 新信息:数据库给出的具体错误位置
纠错后仍错 新信息:又一条报错,但模型已经看过一次同类信息
环境问题 新信息:与 SQL 写法无关
只有执行报错这一类,重试时带进去的信息足够具体,值得交给模型改。
二、校验失败为什么不纠错
有人会觉得:校验不过就把原因告诉模型,让它改到能过不就行了?这里有两层问题。
第一层是效率。 校验失败通常说明模型对「能查什么」的理解本身有偏差,比如问题涉及的数据本来就不在允许范围内。再让它答一遍,它大概率还会用同样的表,或者换一张同样不允许的表。
第二层是安全,更关键。 如果系统的设计是「校验失败 → 把失败原因告诉模型 → 让它改到通过」,就等于在教模型绕过安全规则:
第 1 次:select ... from salary_detail → 不允许查询表 salary_detail
第 2 次:select ... from hr.salary_detail → 换了写法试探
第 3 次:select ... from salary_detail_view → 继续试探
安全校验是边界,边界的正确反馈是「拒绝并告知用户」,而不是「提示怎么绕过去」。遇到校验失败,更合理的做法是告诉用户这个问题超出了当前数据集的范围。
记忆钩子:安全校验是边界,不是考题。边界不给提示,也不给重考。
三、纠错为什么只纠一次
执行报错交给模型纠错,第一次通常很有效:字段写错、别名用错、分组不完整,报错信息把问题指得很清楚。但如果纠错后还报错,情况就不同了:
| 情况 | 说明 | 再纠一次的期望 |
|---|---|---|
| 同一个错误 | 模型没理解报错,或上下文里缺少关键信息 | 很低 |
| 新的错误 | 改 A 引入了 B | 可能继续引入 C,出现震荡 |
| 能跑但结果为空 | 过滤条件被改坏 | 无报错可纠 |
每多纠一次就多一次模型调用和一次数据库执行,而成功率快速下降。所以设一个硬上限——自动纠错一次——把有限的调用预算花在最可能成功的那一次上。要继续尝试,应该从头重新生成 SQL,而不是在一条已经被改过的 SQL 上反复修补。
四、「换个做法」比「再试一次」有效
同一个思路可以推广到整个问数系统的降级设计:
固定口径工具失败 → 换成 Text-to-SQL 兜底再试一次(换了一种做法)
Text-to-SQL 执行报错 → 纠错一次(带着新信息)
都失败 → 停下,记录停在哪一步、为什么
这里没有任何「原样再试一次」的环节。原样重试适合网络超时这类偶发故障,不适合模型推理这类确定性的失败:同样的输入给同一个模型,结果大概率一样。
五、纠错要留原件
纠错会覆盖原来的 SQL。覆盖之前,把现场保存下来:
original_sql 纠错前的 SQL
original_error 数据库返回的原始报错
corrected_sql 模型改出来的 SQL
correction_reason 模型给出的修改理由
这样事后回看,能完整还原「最初写成什么样、因为什么报错、改成了什么」。如果允许用户再次纠错,应该要求先重新生成 SQL,并清掉上一轮的纠错痕迹,避免两轮的原件和修正混在一起。
六、用数据验证策略是否合理
「只纠一次」不是拍脑袋,可以用两个指标持续观察:
| 指标 | 计算 | 说明 |
|---|---|---|
| 纠错触发率 | 触发纠错的次数 ÷ 执行次数 | 高了说明生成环节的上下文或元数据有问题 |
| 纠错成功率 | 纠错后执行成功的次数 ÷ 纠错次数 | 如果很高,说明一次纠错的性价比很好 |
举个例子:100 次执行中有 15 次触发纠错,其中 12 次一次就改好,成功率 80%;剩下 3 次如果再纠一次,假设能再救回 1 次,就是多花 3 次模型调用换 1 次成功。这类数据能帮你判断上限设在 1 还是 2 更合适。触发率本身更值得盯:它高的时候,与其在纠错上加次数,不如回头补字段语义和表关系。
七、把规则写成代码,而不是写在 Prompt 里
「校验失败不纠错」「纠错只一次」这些规则,要由代码的状态机决定,而不是在 Prompt 里告诉模型「最多尝试一次」。模型不负责控制流程,它只负责在被调用时给出一个修正方案;调用几次、什么情况下调用,由代码根据记录的状态判断。这样规则可靠、可测试,也不会因为换了模型而失效。
八、常见误区与追问
- 误区:失败了就让模型多试几次,总能成功。 同样的输入重试,结果大概率不变;只有带着新信息、或者换一种做法的重试才有效。
- 误区:安全校验失败时把原因告诉模型,让它改到能通过。 这等于教模型绕过安全边界,校验失败应该拒绝并告知用户。
- 误区:纠错次数设得越多,成功率越高。 第二次以后的成功率快速下降,还可能在不同错误之间震荡,成本却线性增加。
- 误区:纠错直接覆盖原 SQL 就行。 要保留原 SQL 和原报错,否则无法回看纠错过程,也分不清两轮纠错的结果。
- 误区:重试次数写在 Prompt 里让模型自己控制。 流程控制要由代码的状态判断,模型只负责给出修正方案。
- 追问:纠错后仍然失败,用户想再试怎么办? 要求重新生成 SQL,从头走一遍,并清掉上一轮纠错痕迹,而不是在修改过的 SQL 上继续修补。
- 追问:怎么判断纠错上限设成一次是否合适? 看纠错触发率和纠错成功率,用数据评估多纠一次的收益和成本。
九、加强记忆
重试之前先分类,只重试「带着新信息或换个做法可能成功」的失败。安全校验失败不纠错:一是写法本身不合规,重答多半还是同样的问题;二是让模型改到能过等于教它绕过边界,边界不给提示也不给重考。执行报错有数据库的精确反馈,才值得纠错,而且只纠一次,第二次以后成功率骤降、成本线性增加还可能震荡,想再试就重新生成。纠错前保留原 SQL 和原报错,规则由代码的状态机控制而不是写在 Prompt 里,用纠错触发率和成功率验证策略,触发率高就回头补元数据。