用 AI 智能体连数据库:说一句人话自动查数出报表(Text2SQL 实战)
运营问「上个月哪个省卖得最好」,财务要「本季度各渠道 ROI」,老板随口一句「给我看 Top 10 客户」——这些需求天天有,但写 SQL 门槛高、等数据团队排期又慢。Text2SQL 让 AI 智能体把一句大白话直接变成 SQL、连上数据库查出结果、还能出图出表。本教程用 Python + 开源方案,手把手给 Agent 接上 MySQL / PostgreSQL,跑通一个「对话式查数」助手,并讲清防注入与权限边界。
先搞懂:Text2SQL Agent 是怎么干活的?
用一句话理解:它是「翻译官 + 执行员」——把你的大白话翻成 SQL,再替你去数据库里跑,最后用你能懂的话和图表讲回来。
| 环节 | 谁来做 | 关键点 |
|---|---|---|
| 理解问题 | LLM | 把「卖得最好」翻译成聚合+排序 |
| 生成 SQL | LLM | 必须拿到准确的表结构(schema) |
| 执行查询 | 数据库驱动 | 只读账户 + 超时限制 |
| 呈现结果 | Agent | 表格 / 图表 / 自然语言总结 |
成败的关键一步:LLM 必须知道你的表结构(schema)才能写出对的 SQL。所以第一步永远是「把表名、字段、类型喂给模型」,而不是让它瞎猜。
Step 1:准备数据库与一支只读账号
千万别用管理员账号让 Agent 连库。先建一个只读、限库、有超时的账户:
# PostgreSQL 示例:建只读角色
CREATE ROLE report_ro LOGIN PASSWORD '强密码';
GRANT CONNECT ON DATABASE sales TO report_ro;
GRANT USAGE ON SCHEMA public TO report_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO report_ro;
# MySQL 示例
CREATE USER 'report_ro'@'%' IDENTIFIED BY '强密码';
GRANT SELECT ON sales.* TO 'report_ro'@'%';
FLUSH PRIVILEGES;
# 记下连接串(后面用)
# postgresql://report_ro:密码@localhost:5432/sales
# mysql://report_ro:密码@localhost:3306/sales
sqlite3 demo.db 然后建几张表,连接串就是文件路径。本教程以 PostgreSQL 为例,MySQL / SQLite 思路完全一样。Step 2:装驱动与 LLM SDK
pip install openai sqlalchemy psycopg2-binary pandas
# 设好模型 Key(任意 OpenAI 兼容接口均可)
export OPENAI_API_KEY="sk-你的Key"
SQLAlchemy 的好处:它统一了不同数据库的连接方式。换库只改连接串,业务代码一行不用动——对 Agent 来说就是换把「钥匙」。
Step 3:把表结构喂给模型(最关键)
用 SQLAlchemy 自动读表结构,生成一段「数据字典」给 LLM,让它生成的 SQL 字段名不瞎编:
from sqlalchemy import create_engine, inspect
engine = create_engine("postgresql://report_ro:密码@localhost:5432/sales")
insp = inspect(engine)
def build_schema_prompt():
parts = []
for t in insp.get_table_names():
cols = insp.get_columns(t)
col_txt = ", ".join(f"{c['name']} {c['type']}" for c in cols)
parts.append(f"表 {t}({col_txt})")
return "数据库表结构:\n" + "\n".join(parts)
schema_prompt = build_schema_prompt()
print(schema_prompt) # 例:表 orders(id INTEGER, province VARCHAR, amount NUMERIC, created_at DATE)
selct 或不存在的列。生产环境可把字段的「业务含义」也写进去(如 province=省份)。Step 4:让 LLM 生成 SQL(带约束)
在提示词里明确「只生成 SELECT、只查已知表、不确定就问」,降低胡写风险:
import openai
SYS = f"""你是 Text2SQL 助手。规则:
- 只能生成 SELECT 查询,禁止 INSERT/UPDATE/DELETE/DROP
- 只能使用下面表结构里的表和字段
- 不臆造表或列名,信息不足就说明无法生成
- 只返回 SQL,不要解释
{schema_prompt}"""
def ask_sql(question: str) -> str:
r = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role":"system","content":SYS},
{"role":"user","content":question}
])
return r.choices[0].message.content.strip()
sql = ask_sql("上个月哪个省份销售额最高?")
print(sql)
# 可能输出:
# SELECT province, SUM(amount) AS total
# FROM orders WHERE created_at >= date_trunc('month', current_date - interval '1 month')
# GROUP BY province ORDER BY total DESC LIMIT 1;
为什么强调「只返回 SQL」?因为下一步要直接拿字符串去执行。如果模型夹带解释文字,执行会报错。用正则 re.search(r"SELECT[\s\S]+?;", sql, re.I) 兜底抽取更稳。
Step 5:执行查询并把结果讲成人话
用只读引擎执行、pandas 接结果,再让模型把表格讲成一句人话,并可选出图:
import pandas as pd, re
def run_query(sql: str):
# 安全兜底:只允许以 SELECT 开头
if not re.match(r"^\s*select", sql, re.I):
raise ValueError("只允许查询语句")
with engine.connect() as conn:
df = pd.read_sql(sql, conn)
return df
df = run_query(sql)
print(df)
# 让模型总结
summary = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role":"user",
"content": f"用一句中文总结这个结果:\n{df.to_string()}"}]
).choices[0].message.content
print(summary) # 例:上月广东省销售额最高,达 128 万元。
# 想出图?df.plot.bar() 存成图片发给用户即可
Step 6:加固——防注入、加限流、可审计
直接上线有风险,至少加上这三道保险:
# 1) 语句白名单:执行前强制校验以 SELECT 开头(见 Step 5)
# 2) 超时与行数上限:防止「SELECT *」拖垮库
conn.execution_options(timeout=10) # 10 秒超时
if len(df) > 5000: df = df.head(5000)
# 3) 审计日志:记谁问了什么、生成了什么 SQL、影响多少行
log.info({"user":u,"q":question,"sql":sql,"rows":len(df)})
# 进阶:用 MCP 把这套能力暴露给 Claude/Cursor
# 封装成 MCP Tool,业务侧 Agent 直接调,无需自己写上面代码
三条红线:① 永远用只读账号;② 永远在执行前校验语句类型;③ 永远记录日志。做到这三点,Text2SQL Agent 才是能放心给同事用的工具,而不是一颗随时删库的炸弹。
常见问题速查
| 你遇到的现象 | 大概率原因 & 解决 |
|---|---|
| 生成的 SQL 字段名错 | Step 3 的 schema 没给全,或字段含义没写 |
| 报权限 denied | 用了管理员账号但没授权,或 Agent 账户非只读 |
| 复杂问题答错 | 多表 JOIN 需把关系也写进 schema 提示词 |
| 查询卡死 | 没加超时 / 行数上限,给大表加 LIMIT |