☰
如何让 LLM 通过反思提升 SQL 正确率:以 Gemini + sqlite 为例,配 TaoToken 统一 Key 跑通 Agent Reflection
2026/9/25 14:02:17 网站建设 项目流程

1. 为什么 Gemini 写 sqlite SQL 总在“看起来对”的地方翻车

你让 Gemini 写一句 sqlite SQL,它给你一段语法完全合法、跑起来也不报错的查询,但结果就是不对。比如问“NBA 球员体重的中位数是多少”,它给你SELECT WT FROM nba_roster WHERE WT != '' ORDER BY REPLACE(WT, ' lbs', '') LIMIT 1 OFFSET (...) / 2,执行没报错,返回的却是一个带lbs后缀的字符串,根本不是数值中位数。这就是 LLM 生成 SQL 最典型的失败模式:语法正确、语义错误、静默返回幻觉结果。

Agent Reflection(智能体反思)要解决的就是这件事。它的核心思路特别朴素:先让模型出第一版 SQL,拿去真库执行,把执行结果或报错原样回灌给模型,让它自己判断“这版查询有没有回答原问题”,然后重写。这跟人写 SQL 的习惯一模一样——先写一版,跑一下看结果对不对,不对就改。区别在于,反思闭环把这个“跑一下、看一眼、改一版”的循环自动化了,而且可以重复多轮。

这篇面向的是已经在用 Gemini 或类似模型做 Text-to-SQL、但被首轮正确率折磨的开发者。我会用 sqlite 的nba_roster.db做例子,把 Agent Reflection 的闭环拆成可复制的代码,同时用 TaoToken 统一 Key 管理模型调用,避免在多个 API Key 和 base_url 之间来回切换。目标很明确:把首轮正确率提升到可观测、可复现的稳定水平,而不是靠运气。

2. TaoToken 前置:统一 Key 与 base_url 的配置骨架

在写反思逻辑之前,先把模型调用的入口统一掉。我试过在代码里硬编码 Gemini 的 API Key 和 base_url,一旦要换模型或加一个备用模型,就得改多处。TaoToken 的作用是提供一个统一的 OpenAI 兼容入口,你只需要维护一个 Key 和一个 base_url,模型名通过参数切换。

官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (注意 API 地址不加 UTM 参数)。你需要先去控制台创建一个 API Key,然后把它写进环境变量或配置文件。

2.1 config.toml 骨架

如果你用 Python 项目,推荐把配置放在config.toml里,用tomllib(Python 3.11+)或tomli读取:

[llm] base_url = "https://taotoken.net/api" api_key = "sk-your-taotoken-key" model = "gemini-2.0-flash" max_tokens = 512 temperature = 0.0 [reflection] max_rounds = 3 execute_before_reflect = true

temperature = 0.0很关键。反思场景要的是稳定复现,不是创意发散。温度调高会让模型在重写 SQL 时引入不必要的“发挥”,反而降低收敛速度。

2.2 settings.json 骨架

如果你用 Node.js 或需要跨语言共享配置,settings.json版本如下:

{ "llm": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-your-taotoken-key", "model": "gemini-2.0-flash", "maxTokens": 512, "temperature": 0.0 }, "reflection": { "maxRounds": 3, "executeBeforeReflect": true } }

两个配置文件的字段一一对应,选你项目顺手的那份即可。Key 不要提交到 Git,用.env或本地覆盖文件管理。

2.3 读取配置并初始化客户端

import tomllib import openai with open("config.toml", "rb") as f: cfg = tomllib.load(f) client = openai.OpenAI( api_key=cfg["llm"]["api_key"], base_url=cfg["llm"]["base_url"], ) MODEL = cfg["llm"]["model"] MAX_ROUNDS = cfg["reflection"]["max_rounds"]

到这里,模型调用的入口就统一了。后面无论跑首轮生成还是反思重写,都走同一个client,换模型只改config.toml里的model字段。

3. 可复制配置:sqlite 建表、schema 注入与首轮 SQL 生成

反思闭环要能跑起来,前提是有一个真实的 sqlite 库和一份清晰的 schema 描述。schema 描述的质量直接决定首轮 SQL 的准确率,这一步不能省。

3.1 建表与造数据

import sqlite3 conn = sqlite3.connect("./nba_roster.db") cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS nba_roster ( Team TEXT, NAME TEXT, Jersey TEXT, POS TEXT, AGE INTEGER, HT TEXT, WT TEXT, COLLEGE TEXT, SALARY TEXT ) """) cur.executemany( "INSERT INTO nba_roster VALUES (?,?,?,?,?,?,?,?,?)", [ ("Toronto Raptors", "Otto Porter Jr.", "0", "PF", 22, "6' 7\"", "232 lbs", "Michigan", "$9,945,830"), ("Boston Celtics", "Jayson Tatum", "0", "SF", 25, "6' 8\"", "210 lbs", "Duke", "$32,600,060"), ("Denver Nuggets", "Nikola Jokic", "15", "C", 28, "6' 11\"", "284 lbs", "--", "$47,607,350"), ], ) conn.commit()

注意WT字段是TEXT类型,值形如"232 lbs",这就是后面中位数查询翻车的根源——模型需要先剥离单位再转数值。

3.2 schema 描述函数

def get_schema(): return """ 0|Team|TEXT eg. "Toronto Raptors" 1|NAME|TEXT eg. "Otto Porter Jr." 2|Jersey|TEXT eg. "0" and when null has a value "NA" 3|POS|TEXT eg. "PF" 4|AGE|INT eg. "22" in years 5|HT|TEXT eg. `6' 7"` or `6' 10"` 6|WT|TEXT eg. "232 lbs" and when null has a value "--" 7|COLLEGE|TEXT eg. "Michigan" and when null has a value "--" 8|SALARY|TEXT eg. "$9,945,830" and when null has a value "--" """

把每个字段的类型、示例值、空值表示都写清楚,模型生成 SQL 时才有依据。这一步偷懒,反思阶段就要花更多轮次去补。

3.3 首轮 SQL 生成

def make_message(user, system=""): messages = [] if system: messages.append({"role": "system", "content": system}) messages.append({"role": "user", "content": user}) return messages def get_llm_sql(messages): resp = client.chat.completions.create( model=MODEL, messages=messages, max_tokens=cfg["llm"]["max_tokens"], temperature=cfg["llm"]["temperature"], ) text = resp.choices[0].message.content if "```sqlite" in text: return text.replace("```sqlite\n", "").replace("\n```", "").strip() if "```sql" in text: return text.replace("```sql\n", "").replace("\n```", "").strip() return text.strip() question = "What is the median weight in the NBA?" system = f"""You are an NBA analyst with 15 years of experience writing complex SQL queries. Consider the nba_roster table with the following schema: {get_schema()} Write a sqlite query to answer the following question. Follow instructions exactly.""" messages = make_message(question, system) sql_v1 = get_llm_sql(messages) print("首轮 SQL:", sql_v1)

首轮输出大概率是类似SELECT WT FROM nba_roster WHERE WT != '' ORDER BY REPLACE(WT, ' lbs', '') LIMIT 1 OFFSET (...) / 2的查询。语法没问题,但WT是带单位的字符串,排序和取中位数的逻辑都不对。

4. 验证请求:执行、报错捕获与反思回灌

反思闭环的关键动作是“执行 + 回灌”。执行结果无论是报错还是错误数据,都要作为上下文喂回模型。

4.1 执行并捕获结果

import pandas as pd def execute_sql(sql): try: df = pd.read_sql(sql, con=conn) return df, None except Exception as e: return None, str(e) df_v1, err_v1 = execute_sql(sql_v1) print("首轮执行结果:") print(df_v1 if err_v1 is None else err_v1)

首轮执行不报错,但返回的是一个带lbs的字符串,不是数值中位数。这就是需要反思的信号:执行成功不等于答案正确。

4.2 构造反思消息并重写

def reflect_and_rewrite(question, sql, result, error, system): if error: feedback = f"Query: {sql}\nExecution error: {error}\nThis query failed. Write a corrected sqlite query." else: feedback = ( f"Question: {question}\n" f"Query: {sql}\n" f"Execution result: {result.to_string()}\n" f"This result does not correctly answer the question. " f"Write a corrected sqlite query that returns the numeric median weight." ) messages = make_message(feedback, system) return get_llm_sql(messages) sql_v2 = reflect_and_rewrite(question, sql_v1, df_v1, err_v1, system) print("反思后 SQL:", sql_v2)

反思提示词里要把原问题、上一版 SQL、执行结果(或报错)三样都带上。模型看到“返回的是字符串不是数值”这个事实,才会去写CAST(SUBSTR(WT, 1, INSTR(WT,' ')) AS INTEGER)这类转换逻辑。

4.3 多轮反思循环

def run_reflection_loop(question, system, max_rounds=3): messages = make_message(question, system) sql = get_llm_sql(messages) history = [] for i in range(max_rounds): df, err = execute_sql(sql) history.append({"round": i, "sql": sql, "error": err, "rows": None if df is None else len(df)}) if err is None and df is not None and len(df) > 0: # 这里可以加一个答案校验函数,判断是否真的回答了问题 pass sql = reflect_and_rewrite(question, sql, df, err, system) return sql, history final_sql, history = run_reflection_loop(question, system, MAX_ROUNDS) print("最终 SQL:", final_sql) df_final, err_final = execute_sql(final_sql) print(df_final if err_final is None else err_final)

max_rounds建议设 3。实测下来,大多数语义错误在 1 到 2 轮反思内就能收敛,设太多轮反而可能让模型在“过度修正”里绕圈。

5. 本篇常见错排查:反思不生效的六个坑

反思闭环跑不通,通常不是模型能力问题,而是工程细节没对齐。下面这几个坑我基本都踩过。

坑一:schema 描述太粗。只写字段名不写类型和示例值,模型不知道WT是带单位的字符串,首轮就会写出错误的排序逻辑。把示例值写进 schema,首轮正确率能明显提升。

坑二:反思提示词没带执行结果。只告诉模型“这版不对,重写”,模型没有判断依据,重写出来的 SQL 可能换汤不换药。必须把df.to_string()或报错信息原样带上。

坑三:temperature 没调低。反思阶段需要稳定收敛,温度高于 0.3 会让模型在重写时引入随机变化,多轮之间不收敛。设成 0.0。

坑四:把执行成功当成答案正确。这是最隐蔽的坑。pd.read_sql不报错,不代表结果回答了问题。需要在反思提示词里明确写出“结果没有正确回答问题”,让模型自己判断语义。

坑五:max_tokens 太小。反思消息里带了执行结果,上下文变长,如果max_tokens还是 100,模型可能截断输出,SQL 不完整。设成 512 以上。

坑六:base_url 或 Key 配错。如果客户端初始化时base_url写成了别的地址,或者 Key 失效,首轮请求就会失败,反思逻辑根本没机会跑。先用一个最简单的client.chat.completions.create验证连通性,再跑完整闭环。

排查顺序建议:先确认客户端能通,再确认 schema 描述完整,然后确认反思提示词带了执行结果,最后调 temperature 和 max_tokens。

6. 语义一致 CTA:把统一 Key 接进你的 Agent 工作流

反思闭环跑通之后,下一步是把它接进你日常的 Agent 工作流。如果你还在手动管理多个模型的 Key 和 base_url,建议先把入口统一到 TaoToken,再去控制台创建 API Key,然后按接入文档把base_url和 Key 写进config.toml或settings.json。

  • 需要创建和管理 Key:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
  • 需要看接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
  • 想先在网页里验证模型对话效果:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
  • 长期跑编码或 Agent 任务,考虑 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

如果你用的是 Claude Code 或 Anthropic 风格的调用,对应的接入入口在这里:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

把反思循环里的client换成统一入口之后,你可以在config.toml里一行切换模型,对比 Gemini 和其他模型在同一个 sqlite 任务上的首轮正确率和反思收敛轮数。这才是把“首轮正确率提升到可观测稳定水平”落地的关键——不是靠某一次调参,而是靠可复现的配置和可对比的模型入口。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询