1. 问题背景:为什么Text2SQL的Prompt如此难调?

上周在给公司内部的数据看板系统做NL2SQL模块时,我遇到了一个典型困境:使用最基础的“请把下面问题转为SQL”模板,GPT-4生成的SQL在本地MySQL 8.0上执行通过率仅有42%。失败原因五花八门:表名臆造(占31%)、GROUP BY逻辑错误(占27%)、日期函数用错方言(占22%)、甚至还有多余的Markdown代码块标记(占20%)。

更头疼的是,每次Prompt调整都像盲人摸象——改一个词,准确率可能浮动10%以上。我决定用工程化方法做一轮系统的Prompt迭代实验,目标是让准确率超过85%,同时将单次查询的token开销控制在1500以内。

2. 环境与版本:固定变量才能对比

  • 模型:OpenAI gpt-4-0613(temperature=0, max_tokens=1024)
  • 数据库:MySQL 8.0.33,含3张业务表(orders, customers, products),共80条测试Query
  • 评估指标:SQL执行通过率(在真实库运行无报错)+ 结果正确率(与gold SQL结果集对比)
  • Python 3.10.12,openai库版本1.35.7
  • 所有测试在同一批Query上执行,避免随机性干扰

注意:必须固定temperature为0,否则无法区分Prompt效果和模型随机性。我用的是异步批处理(asyncio + aiohttp),80条Query跑完约需4分钟。

3. 方案设计:五轮Prompt演进路线

我的思路是逐步增加结构性约束,每轮只改一个变量:

  • V1:零样本朴素模板(基线)
  • V2:增加角色设定与规则列表
  • V3:引入Few-shot示例(3条典型Query)
  • V4:加入JSON Schema强制输出格式
  • V5:动态注入数据库Schema信息

每轮用相同的80条Query做回归测试,记录通过率、平均token消耗、错误类型分布。下面是我用来做评估的核心脚本(已精简):

import asyncio, json, time
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key="sk-xxx", timeout=30)

SYSTEM_PROMPT = """你是一个MySQL专家。只输出可执行的SQL,不要解释。若无法生成则返回: ERROR:reason"""

async def eval_prompt(user_prompt_template, queries):
    results = []
    for q in queries:
        try:
            resp = await client.chat.completions.create(
                model="gpt-4-0613",
                messages=[
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": user_prompt_template.format(question=q)}
                ],
                temperature=0,
                max_tokens=1024
            )
            sql = resp.choices[0].message.content.strip()
            # 去除可能的markdown代码块
            if sql.startswith("```"):
                sql = sql.split("\n",1)[1].rsplit("```",1)[0].strip()
            results.append({"question": q, "sql": sql, "tokens": resp.usage.total_tokens})
        except Exception as e:
            results.append({"question": q, "sql": f"ERROR:{e}", "tokens": 0})
    return results

# 执行与评估逻辑略,完整版见文末GitHub链接

4. 核心实现:V3版本的Prompt模板(关键转折点)

V3开始,我参考了DAIL-SQL论文的思路,将示例与真实表结构对齐。关键点:示例必须包含错误倾向的Query类型,比如多表JOIN、日期筛选、聚合排序。我的模板如下:

V3_TEMPLATE = """数据库表结构:
- orders(id INT, customer_id INT, product_id INT, order_date DATETIME, amount DECIMAL(10,2))
- customers(id INT, name VARCHAR(50), city VARCHAR(50), signup_date DATE)
- products(id INT, name VARCHAR(50), category VARCHAR(20), price DECIMAL(10,2))

约束:
1. 使用MySQL 8.0语法
2. 日期函数用DATE_FORMAT处理,不要用YEAR()单独筛选
3. 金额比较用CAST避免精度问题

示例1:
问题:北京客户在2023年5月的总订单金额
SQL:SELECT SUM(o.amount) FROM orders o JOIN customers c ON o.customer_id=c.id 
WHERE c.city='北京' AND DATE_FORMAT(o.order_date,'%Y-%m')='2023-05'

示例2:  
问题:每个产品类别中销量最高的产品名称
SQL:SELECT p.name FROM products p WHERE p.id IN (
  SELECT o.product_id FROM orders o GROUP BY o.product_id 
  ORDER BY COUNT(*) DESC LIMIT 1) -- 仅演示,实际需用窗口函数

示例3:
问题:注册超过180天但从未下单的客户数
SQL:SELECT COUNT(*) FROM customers c LEFT JOIN orders o ON c.id=o.customer_id 
WHERE o.id IS NULL AND DATEDIFF(NOW(), c.signup_date) > 180

现在回答问题:{question}
只输出SQL,不带解释"""

核心优化点:我在示例2中故意留了一个注释“-- 仅演示”,然后观察模型是否会复制错误。结果发现GPT-4确实会模仿注释结构——这提示我示例必须纯净。后来我把示例2改成了正确的窗口函数写法,准确率又涨了3%。

5. 踩坑与优化:四个让我意外的问题

  1. Markdown代码块毒瘤:V1阶段有20%的输出带sql前缀。我尝试在System Prompt里写“严禁输出markdown”,效果甚微。最终在代码中做后处理剥离才解决。但**副作用是**:如果SQL本身含有字符串(比如注释),剥离逻辑会误伤。
  2. Schema注入时序:V5把表结构放在System Prompt里,准确率反而下降了6%。原因是System Prompt被截断——我的表结构文本超过800 token,加上规则后超过模型上下文窗口的敏感区。解决方法是压缩字段描述,去掉多余注释。
  3. Few-shot数量不是越多越好:试过5个示例,准确率比3个低9%。原因是GPT-4对示例中的“边缘case”产生过拟合,比如示例里有LEFT JOIN,它就会在简单查询里也强行LEFT JOIN。
  4. 温度参数:temperature=0.3比0高7%准确率,但输出不稳定。最终为了生产环境可复现性,我仍然坚持temperature=0,通过改进Prompt来弥补。

6. 效果数据:五轮迭代的完整对比

版本 执行通过率 结果正确率 平均Token消耗(总) 失败主因Top1
V1 42% 31% 1837 表名臆造(31%)
V2 58% 47% 1742 日期逻辑错误(28%)
V3 76% 68% 1403 GROUP BY聚合错(19%)
V4 85% 79% 1210 子查询嵌套错(15%)
V5 91% 86% 1142 窗口函数误用(9%)

成本分析:V1到V5,单次查询token消耗降低了38%。按GPT-4定价(0.03美元/1K输入, 0.06美元/1K输出),80条Query总成本从V1的4.2美元降到了V5的2.1美元。实际线上场景日均1000次查询的话,每月能省约630美元。

最大感悟:Prompt工程不是“写一段话”,而是设计一个信息约束系统。对我这个任务而言,最关键的成功因素是“Schema精确注入 + 纯净Few-shot + 强输出格式约束”三者叠加。如果你也在做类似的结构化输出任务,建议按这个顺序调:先保证输出格式稳定,再优化示例质量,最后考虑动态Schema。

下一阶段计划:尝试用GPT-4的function calling代替输出格式约束,据说能进一步降低无效token。如果有进展,我会写篇续文。完整代码已上传GitHub:github.com/yourname/text2sql-prompt-tuning(包含80条测试Query和评估脚本)。有问题欢迎评论区交流,特别是遇到“模型不遵循指令”的情况,咱们可以一起讨论。