text2SQL
在智能体业务场景中,“让 Agent 直接生成 SQL 去查库"几乎是每个人最先想到的做法——它简单、直接,不需要任何额外建设。但在实际项目中,并不建议这么做:模型生成的 SQL 常常"看着对、实际错”,幻觉会编造不存在的表和字段,超长上下文会稀释模型的注意力,业务口径不清则会产生语法完全正确、数字却错误的查询结果,而这类错误恰恰最难被察觉。那么,不直接生成 SQL,还有什么更可靠的做法?本文从这一痛点出发,介绍规避风险的三种主流方案:基于专用模型的微调方案、智能体工具调用方案和智能体 RAG 检索方案,最后结合业界实测数据给出对比与选型建议,希望对您有参考价值~
概述
在我们的实际智能体业务场景中,让智能体自己去查询数据库、再基于查询结果做分析和报表,几乎是绕不开的需求。而"直接生成 SQL"之所以不被推荐,是因为这条路的风险不可控,主要体现在三个方面:
- 模型幻觉:大模型对数据库的真实结构没有感知,生成时可能编造不存在的表名、字段名或 join 关系。SQL 看着像模像样,一执行就报错;
- 上下文溢出:实际业务中库表数量庞大,把全部表结构塞进提示词,容易超出上下文窗口,或因信息过载造成全局注意力下降,导致生成的 SQL 越来越不精确;
- 业务口径缺失:SQL 只是语法,业务含义才是灵魂。“本月活跃用户"如何定义、“收入"算不算退款,这些口径若不固化,模型生成的 SQL 语法完全正确,数字却是错的。
更麻烦的是,前两种问题至少会报错、能被发现;而第三种问题的结果是"看似合理但错误”——它不会报错,却会悄悄进入报表和决策链路。这正是"直接生成 SQL"最危险的地方。
针对这些问题,业界逐渐形成了三种主流方案,从不同角度规避上述风险:
- 基于专用模型的方案:使用为 SQL 生成任务专项优化的模型,让模型在"更懂 SQL"的前提下直接生成语句;
- 基于智能体工具调用的方案:把查询动作收敛到预置工具中,模型只做意图理解与参数抽取,查询由工具确定性执行;
- 基于智能体 RAG 检索的方案:让模型结合检索到的表结构与历史成功案例生成 SQL,并通过记忆系统越用越准。
下面逐一展开。
专用模型方案

上文说不建议直接生成 SQL,指的是"让通用 Agent 裸写 SQL”;但如果一定要走生成路线,至少应该选择为 SQL 任务专项优化过的模型,把生成质量的上限拉高——Qwen-Text2SQL 就是其中之一。
Qwen-Text2SQL 是阿里巴巴开发并开源的文本到 SQL 转换模型,属于通义千问大模型系列的衍生模型。它在 Qwen 基础架构上针对 SQL 生成任务做了专项优化,开源后可通过 Hugging Face、ModelScope 等平台获取,供开发者使用和二次开发。其核心功能围绕"精准理解自然语言查询并生成可执行 SQL 语句"展开。
推理代码如下:
import torch
from modelscope import snapshot_download
from modelscope import AutoModelForCausalLM, AutoTokenizer
model_dir = snapshot_download("Dingawa/Qwen3.5-2B-Text2SQL")
tokenizer = AutoTokenizer.from_pretrained(model_dir)
model = AutoModelForCausalLM.from_pretrained(
model_dir,
torch_dtype=torch.bfloat16,
device_map="auto",
)
model.eval()
schema = """CREATE TABLE employees (
id INTEGER,
name TEXT,
department TEXT,
salary REAL
)"""
question = "What is the name of the employee with the highest salary?"
system_message = (
"You are a helpful assistant. You first think about the reasoning process "
"and then provide the user with the answer."
)
user_message = f"""You are a SQL query writing expert. Your task is to write a valid SQLite query for the user's question using the database schema below.
Database Schema:
{schema}
Use valid SQLite and the information above to answer the question.
Show your reasoning in exactly one <think> </think> block. Return the final answer as JSON in exactly one <answer> </answer> block using this format:
<think>
[reasoning process]
</think>
<answer>
{{
"sql": "SELECT ..."
}}
</answer>
Here's the user query:
{question}"""
messages = [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message},
]
# PPO 训练和正式评测使用了下面的固定 assistant 前缀。
prompt = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
)
prompt += (
"<|im_start|>assistant\n"
"Let me write the SQL query with reasoning.\n"
"<think>\n"
)
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.inference_mode():
output_ids = model.generate(
**inputs,
max_new_tokens=512,
do_sample=False,
)
completion_ids = output_ids[0, inputs["input_ids"].shape[1]:]
completion = tokenizer.decode(completion_ids, skip_special_tokens=True)
print("<think>\n" + completion)
可以看到,这种方案的思路仍然是"把全部表结构传给模型,让它生成 SQL"。Qwen3.5-2B 的默认上下文长度为 262,144 个 token,对于部门级项目来说基本够用;但对于大型项目,几百张表的结构全部塞进上下文,仍然会稍显吃力。
而且,从官方说明来看,模型的适用边界比想象中要窄:
使用限制:
- 模型主要在英文问题和 SQLite schema 上训练,未验证其他 SQL 方言;
- 模型可能生成语法错误、语义错误或与 schema 不一致的 SQL;
- BIRD 复杂查询仍是当前短板,PPO 后该任务没有得到提升;
- execution accuracy 依赖数据库内容和执行环境,不能只用字符串匹配衡量;
- 不应直接在生产数据库执行模型输出。请使用只读连接、SQL 白名单、执行超时和资源限制,并在执行前进行人工或程序校验。
该模型仅在英文问题与 SQLite schema 上训练,而我们的业务环境通常是 MySQL 和中文。如果直接用在业务 SQL 生成上,很容易出现与 schema 不一致的 SQL。因此,如果选择这条路线,我们需要基于中文问题 + MySQL 对该模型进行二次微调。
官方训练脚本:Text2SQL_Based_Qwen3.5-2B/code/src/ppo at main · Lanerawa/Text2SQL_Based_Qwen3.5-2B
智能体工具调用方案
专用模型方案的问题在于"SQL 完全由模型自由发挥",准确性上限取决于模型能力。工具调用方案则换了一个思路:模型不写 SQL,只做意图理解和参数抽取,查询由预置工具确定性执行,从机制上杜绝了模型"编造"查询的可能。
该方案的核心是:利用大模型的问题理解能力,通过对用户输入信息的抽取和理解,选择对应的工具(可以是 function call 或 MCP),传入合适的参数后获取工具输出。查询过程被完全交给工具,从而保证返回结果的可靠性和准确性;模型侧只需要抽取信息、拿到数据,再进行后续步骤即可。针对这个方案的说明,我们以 Microsoft MCP Server for Enterprise 为例。
Microsoft MCP Server for Enterprise 是微软基于 Model Context Protocol 推出的企业级远程 MCP 服务器,它将 AI 智能体与企业 Microsoft 365 / Entra 数据连接起来。它的核心思想并不复杂:
- 不针对每个 API 操作暴露一个工具——传统做法是"一个 Graph API 一个工具",工具数量会爆炸且难以维护;它只用 3 个工具覆盖完整工作流;
- 用 RAG + 少样本提示替代手工枚举——基于 500+ 条真实世界查询示例,让模型按需检索最匹配的 API 调用,而不是把全部可能性塞进上下文;
- 只读 + 服务端权限强制——模型拿到的始终是受限的查询通道,可靠性由服务端保证。
(此处插入架构示意图:意图解析 → 候选查询检索 → 查询选择与参数确定 → 工具执行 → 结果生成)
其核心是"意图理解 + 候选检索 + 受限执行",整个工作流程可拆解为以下关键步骤:
- 步骤 1:意图解析——LLM 分析当前问题和对话历史,提取用户意图(如"统计租户中的用户数量"),判断需要查询的数据对象;
- 步骤 2:候选查询检索——LLM 调用
microsoft_graph_suggest_queries工具,将问题转换为向量后在微软维护的 Graph API 查询示例库中做语义检索,返回与意图匹配的候选示例(如"count total number of users"、“count guest users”),每个候选对应一条具体的 Graph API 调用; - 步骤 3:查询选择与参数确定——LLM 评估返回的候选示例,选择最匹配的 API 调用(如
GET /users/$count),并确定所需的参数; - 步骤 4:属性补齐——若模型对目标实体的字段结构不熟悉,可调用
microsoft_graph_list_properties获取该实体的属性定义(相当于按需查询"表结构"),避免凭空猜测字段名; - 步骤 5:工具执行与结果回传——LLM 调用
microsoft_graph_get执行只读的 Graph API 请求,MCP 服务器返回 JSON 结果;若请求失败,LLM 根据错误信息调整查询后重试; - 步骤 6:权限与合规强制——所有请求在服务端强制执行用户角色、MCP 客户端 scopes 和 Graph 限流策略,模型无法越权访问;全部操作在同一 App ID 下运行,并可通过 Graph activity logs 审计;
- 步骤 7:自然语言结果生成——LLM 解读返回的 JSON 数据,组织为自然语言回答返回给用户。
整个方案只暴露 3 个工具:
| 工具 | 用途 |
|---|---|
microsoft_graph_suggest_queries |
检索与用户意图匹配的 Graph API 调用候选 |
microsoft_graph_get |
执行只读的 Graph API 请求(受用户角色和客户端 scopes 约束) |
microsoft_graph_list_properties |
获取特定实体的属性定义,帮助模型理解数据结构 |
配置接入时(以 GitHub Copilot CLI 为例),只需声明远程 MCP 端点,鉴权走 OAuth:
// ~/.copilot/mcp-config.json
"mcp-enterprise": {
"type": "http",
"url": "https://mcp.svc.cloud.microsoft/enterprise",
"headers": {},
"tools": ["*"],
"oauthClientId": "<注册应用的客户端ID>",
"oauthPublicClient": true
}
接入前需在企业租户中预配 MCP Server 服务主体(App ID 为 e8c77dc2-69b3-43f4-bc51-3213c9d915b4)并注册 MCP 客户端应用,再按需给客户端分配 scopes:
以"我们的租户中有多少用户?“为例,模型侧的实际调用过程如下:
# 模型的 Agent 循环过程
# 第 1 轮:LLM 识别到意图是"统计用户数量",先检索候选查询
microsoft_graph_suggest_queries(question="count the number of users in the tenant")
# → 返回候选:"count total number of users" → GET /users/$count
# "count guest users" → GET /users?$filter=userType eq 'Guest'&$count=true
# 第 2 轮:LLM 选择最匹配的候选,确定无需额外参数
microsoft_graph_get(query="GET /users/$count")
# → 返回:{"@odata.count": 10930}
# 第 3 轮:LLM 解读 JSON,生成自然语言回答
# → "租户中共有 10,930 个用户。"
需要说明的是,微软的方案将查询空间收敛到了其维护的 Graph API 示例库,模型不会凭空构造 API 请求。这种设计带来的关键特性是——要么给出正确结果,要么明确报错或拒绝回答,不会给出"看似合理但错误"的答案,这正是工具调用方案相比纯 SQL 生成方案最大的优势。
不过,这种优势是有代价的:方案的覆盖范围完全取决于预置工具与示例库的完备程度,示例库覆盖不到的查询会直接失败,不存在"模型自己想办法"的余地。
智能体 RAG 检索方案
工具调用方案解决了准确性问题,却牺牲了覆盖率。RAG 检索方案试图在两者之间取得平衡:模型仍然自己生成 SQL,但通过检索表结构和历史成功案例来"喂"给模型,让它站在已有经验的基础上作答。
该方案的核心是:利用通用大模型的强大能力,通过用户的输入结合 RAG 检索出与问题相关的表信息,构建 Prompt,再让模型生成对应的 SQL。针对这个方案的说明,我们以 vanna 库的 2.0 版本 为例。
Vanna 2.0 的核心思想并不复杂:
- 将工具连接到大型语言模型(LLM)——赋予你的 AI 代理运行 SQL 查询、生成图表或任何自定义功能的能力;
- 记住如何使用它们——工具记忆会从成功的互动中学习,随着时间不断提升;
- 强制执行权限——每个用户都有贯穿整个系统的特定访问控制。
每次生成成功的 SQL 都会被保存到工具记忆中。当用户提出类似问题时:
-
智能体会搜索过去的 SQL 查询和工具使用模式,然后将成功的结果传入到当前的对话中;
-
智能体无需进行微调即可学习项目业务的数据库模式、业务逻辑和查询模式。

Vanna 2.0 的核心是"Agent 自主决策 + 双层记忆系统”,整个工作流程可拆解为以下关键步骤: -
步骤 1:记忆预检索——用户提问后,在调用 LLM 之前,系统先从工具记忆库中搜索类似问题的历史成功案例,分两种情况:
- 有成功案例:若匹配到高度相似的历史记录,系统直接将历史成功的"问题 → SQL"作为参考上下文传入 LLM,LLM 参照历史路径快速生成 SQL,最大程度保证准确性和一致性;
- 无成功案例:若工具记忆库中未匹配到类似记录,系统转入 RAG 检索流程,从向量数据库中检索与当前问题相关的三类训练数据——DDL 表结构、业务文档说明、历史问答对,拼装为上下文传入 LLM,让 LLM 从零开始基于表结构和业务规则生成 SQL;
-
步骤 2:构建增强提示——系统将基础指令、可用工具列表、步骤 1 的检索结果(成功案例或向量库 RAG 结果)以及文本记忆(领域知识、字段说明等)统一拼装为增强型 System Prompt,让 LLM 同时掌握"怎么查"和"查什么";
-
步骤 3:LLM 自主决策——直接生成 SQL、生成图表、保存领域知识,或对简单问题直接给出文字回答;
-
步骤 4:工具执行与结果回传——框架执行 LLM 选择的工具(如连接数据库执行 SQL),将结果以 DataFrame 形式回传给 LLM,LLM 据此判断结果是否正确,决定继续修正 SQL 再执行、生成可视化图表,还是输出最终回答;
-
步骤 5:反思修正循环——若 SQL 执行报错或结果不符合预期,LLM 可根据错误信息自动反思并重新生成 SQL,再次调用
run_sql,循环迭代直到生成正确结果(最多 10 轮),无需人工介入即可完成自我修正; -
步骤 6:成功后自动存记忆——工具执行成功后,系统自动将"问题 + 工具名 + 参数"存入向量数据库的工具记忆库,下次遇到类似问题时直接走步骤 1 的有成功案例路径;LLM 也可主动调用
save_text_memory将领域知识(如字段含义、业务规则)存入向量数据库的文本记忆库,实现记忆的持续积累; -
步骤 7:流式返回结果——全程通过 SSE 流式输出 UI 组件,用户实时看到每一步进展(状态更新 → 工具执行 → 数据表格 → 图表 → 文字总结),而非等待全部完成后一次性返回。
当我们使用 Vanna 2.0 时,只需要配置 SQL 的连接和 LLM,然后调用即可,不需要手动训练、不需要预存 DDL:
from vanna import Agent, ToolRegistry
from vanna.integrations.anthropic import AnthropicLlmService
from vanna.integrations.postgres import PostgresRunner
from vanna.integrations.chromadb import ChromaAgentMemory
from vanna.tools import RunSqlTool
# 配置组件
llm_service = AnthropicLlmService(api_key="...")
sql_runner = PostgresRunner(connection_string="...")
agent_memory = ChromaAgentMemory(persist_directory="./chroma_memory")
# 注册工具(只需要 run_sql)
registry = ToolRegistry()
registry.register_local_tool(RunSqlTool(sql_runner), access_groups=[])
# 创建 Agent
agent = Agent(
llm_service=llm_service,
tool_registry=registry,
user_resolver=MyUserResolver(),
agent_memory=agent_memory,
)
# 启动服务,用户直接提问,无需预训练
Vanna 2.0 的 LLM 通过 Agent 循环自主探索数据库结构:第一次提问时,LLM 会主动调用 run_sql 工具查询 information_schema 获取表结构,再基于表结构生成业务 SQL,全程不需要人工干预:
# 2.0 的 LLM 自主探索过程(Agent 循环,最多 10 轮)
# 第 1 轮:LLM 不知道有哪些表,先查表结构
run_sql("SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'mingshan'")
# → 返回:st_rain 表有 stcd, drp, tm 等字段
# 第 2 轮:LLM 拿到表结构后,生成业务 SQL
run_sql("SELECT stcd, drp FROM st_rain WHERE drp > 100 AND MONTH(tm) = MONTH(CURDATE())")
# → 返回:15 行查询结果
# 第 3 轮:LLM 判断结果正确,输出总结并存记忆
save_question_tool_args(question="查询本月降水量超100mm的站点", tool_name="run_sql", args={"sql": "SELECT ..."})
# → 存入向量数据库,下次类似问题直接复用
虽然 Vanna 2.0 会自动从成功的结果互动中学习,即越用越好,但我们初次使用的时候也可以手动添加记忆以提升性能,确保它掌握适合你具体用例的正确知识。
手动添加问题-SQL
from vanna.capabilities.agent_memory import ToolMemory
from vanna.core.tool import ToolContext
# Create training examples
training_examples = [
ToolMemory(
question="What are our top 5 selling products?",
tool_name="run_sql",
args={
"sql": """
SELECT p.ProductName, SUM(od.Quantity) as TotalSold
FROM Products p
JOIN OrderDetails od ON p.ProductID = od.ProductID
GROUP BY p.ProductName
ORDER BY TotalSold DESC
LIMIT 5
"""
}
),
ToolMemory(
question="Show me customer retention rates",
tool_name="run_sql",
args={
"sql": """
SELECT
YEAR(OrderDate) as Year,
COUNT(DISTINCT CASE WHEN CustomerID IN (
SELECT DISTINCT CustomerID FROM Orders
WHERE YEAR(OrderDate) = YEAR(o.OrderDate) - 1
) THEN CustomerID END) * 100.0 / COUNT(DISTINCT CustomerID) as RetentionRate
FROM Orders o
GROUP BY YEAR(OrderDate)
"""
}
)
]
# Create a user context (you would get this from your authentication system)
from vanna.core.user.models import User
your_user = User(id="admin", email="admin@example.com", group_memberships=["admin"])
# Add to agent memory
for example in training_examples:
await agent.agent_memory.save_tool_usage(
question=example.question,
tool_name=example.tool_name,
args=example.args,
context=ToolContext(user=your_user),
success=True
)
手动添加文档
添加领域知识、业务规则和上下文,帮助智能体理解你的数据:
from vanna.core.tool import ToolContext
# Add business context as text memories
business_context = [
"""
Customer Segmentation Rules:
Customers are segmented as:
- VIP: Orders > $10,000 in past year
- Regular: Orders > $1,000 in past year
- New: First order within 30 days
- Inactive: No orders in 6+ months
Always use these definitions in customer analysis queries.
""",
"""
Product Categories and their business meaning:
- Electronics: High-margin, warranty required
- Clothing: Seasonal inventory, frequent discounts
- Books: Low overhead, long tail sales
- Home & Garden: High shipping costs, regional preferences
"""
]
# Store documentation in memory (using the same user context as above)
for context_text in business_context:
await agent.agent_memory.save_text_memory(
content=context_text,
context=ToolContext(user=your_user)
)
Vanna 2.0 的向量数据库(AgentMemory)不存 DDL、不存表结构,只存两类数据:
- 工具使用记忆:LLM 成功后自动存储,下次类似问题直接复用;
- 文本记忆:领域知识和字段说明,可预先注入或由 LLM 主动保存。
需要注意的是:Vanna 默认用的 embedding 模型是 BAAI/bge-small-en-v1.5,而我们的实际业务是中文场景,需要将其更换为合适的中文嵌入模型。
方案对比与总结
三种方案分别对应"模型微调"“工具收敛"“记忆增强"三条设计路线,它们之间的本质区别在于一个问题:SQL 由谁来写,准确性靠什么保证。
| 维度 | 专用模型方案 | 工具调用方案 | RAG 检索方案 |
|---|---|---|---|
| 代表实现 | Qwen-Text2SQL | Microsoft MCP Server for Enterprise | Vanna 2.0 |
| SQL 由谁写 | 模型直接生成 | 预置工具/API 执行,模型不写 SQL | 模型生成,参考历史案例与 RAG 上下文 |
| 准确性保障 | 模型专项优化,仍需微调 | 查询空间受限,服务端校验与权限强制 | 记忆复用 + 反思修正循环 |
| 覆盖范围 | 高,理论上无上限 | 低,取决于预置工具与示例库 | 高,且越用越准 |
| 失败模式 | 给出"看似合理但错误"的结果 | 明确报错或拒绝回答 | 修正循环兜底,仍可能出错 |
| 主要成本 | 微调数据与训练 | 持续维护工具/语义模型 | 维护向量库与记忆质量 |
关于准确性差异与失败模式的区别,业界已有比较扎实的实测佐证:
- dbt Labs 2026 年基准测试(Semantic Layer vs. Text-to-SQL,2026 年 4 月发布)显示:在语义层(工具调用方案的核心形态)覆盖的范围内,前沿模型(Claude Sonnet 4.6 / GPT-5.3 Codex)的准确率达到 100%;而同样的模型直接做 text-to-SQL,整体准确率只有 64.5%(2023 年同一基准下仅 32.7%)。把原本超出语义层覆盖范围的查询通过建模纳入覆盖后,语义层准确率保持在 98.2%~100%,text-to-SQL 则为 84.1%~90.0%;
- 该测试还点出了两种方案最本质的差别:text-to-SQL 的失败表现为"给出一个看似合理但错误的数字”,语义层则会明确报错或拒绝回答。对汇报、审计、看板这类场景而言,这种失败模式的差异比准确率数字本身更重要;
- Google 内部测试发现,Looker 的语义层将生成式 AI 自然语言查询的错误率降低了约三分之二;Snowflake Cortex Analyst 依托语义视图,准确率约为单次 GPT-4o 的两倍;
- 有分析指出,81.2% 的 text-to-SQL 失败发生在 schema 与语义层面而非语法层面——也就是说,多数问题不是"模型不会写 SQL”,而是"模型不知道业务口径",这恰恰是工具调用方案通过固化业务定义就能解决的问题。
综合来看,没有一种方案是银弹,它们解决的是不同层面的问题:
- 专用模型方案适合作为底座能力。模型能力直接决定生成质量的上限,适合探索性、一次性的即席查询,但需要在中文 + 业务方言上微调,且生成结果必须经过校验;
- 工具调用方案适合高频、确定性的查询。把业务口径固化进工具,准确性和权限都最可控,但需要持续投入维护工具库与示例库;
- RAG 检索方案兼顾覆盖与准确。用记忆沉淀业务知识,越用越准,适合长尾问题的兜底。
对我们这类对准确性敏感的政务、水利场景,推荐的落地方式是混合架构:高频、确定性的查询走工具调用方案(意图分类节点 + 工具节点,模型只做槽位抽取)来保证准确性;未被覆盖的问题回退到 RAG + SQL 生成方案保障覆盖率;同时,对模型直接生成的 SQL 保留只读连接、超时限制与人工复核,避免"看似合理但错误"的结果直接进入决策链路。两条路不是二选一,而是"预置工具兜准确率、生成 SQL 兜覆盖率"。
参考文章
[1] Qwen3.5-2B-Text2SQL - Hugging Face
[2] Text2SQL_Based_Qwen3.5-2B 官方训练脚本 - GitHub
[3] Microsoft MCP Server for Enterprise 文档 - Microsoft Learn
[4] Microsoft MCP Server for Enterprise - GitHub
[5] Vanna - GitHub
[6] Semantic Layer vs. Text-to-SQL: 2026 Benchmark Update - dbt Labs