个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangChain Text-to-SQL:让不会写 SQL 的人查数据库](#LangChain Text-to-SQL:让不会写 SQL 的人查数据库)
-
- 一、先说风险
- 二、最简实现
- 三、关键:表结构怎么写
-
- [3.1 只给相关的表](#3.1 只给相关的表)
- [3.2 字段要有中文注释](#3.2 字段要有中文注释)
- [3.3 给出示例值](#3.3 给出示例值)
- [四、自动提取 schema(表多的时候)](#四、自动提取 schema(表多的时候))
- 五、用官方链:create_sql_query_chain
- 六、安全层(必做)
- [七、完整链路:生成 → 校验 → 执行 → 自然语言回答](#七、完整链路:生成 → 校验 → 执行 → 自然语言回答)
- 八、失败重试:把报错告诉模型
- [九、用 Agent 让它可以多步查询](#九、用 Agent 让它可以多步查询)
- 十、常见坑
- 十一、小结
LangChain Text-to-SQL:让不会写 SQL 的人查数据库
"上个月华东区销售额前三的产品是哪些?"------
业务同事每天都要问这类问题,每次都得找数据分析师写 SQL。
Text-to-SQL 让模型来写 SQL。但这件事风险很高 ,
这篇讲怎么做到"能用"且"不出事"。
一、先说风险
在讲怎么做之前,必须说清楚三条红线:
- 绝不能让模型执行任意 SQL(DROP / DELETE / UPDATE 都可能被生成);
- 绝不能把整个数据库暴露给它(只给它需要的表);
- 必须有行数和耗时限制(一条没加 LIMIT 的查询能拖垮生产库)。
这三条守不住,就别做这个功能。
二、最简实现
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
SCHEMA = """
表 orders: id, user_id, product_id, amount, status, created_at
表 products: id, name, category, price
表 users: id, name, city, vip_level
"""
prompt = ChatPromptTemplate.from_template("""
你是 SQL 专家。根据表结构把用户问题转成 SQL。
【表结构】
{schema}
【规则】
1. 只输出 SQL 语句,不要解释,不要 markdown 代码块
2. 只使用上面给出的表,不要假设其他表存在
3. 必须加 LIMIT 100
用户问题:{question}
""")
chain = prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0) | StrOutputParser()
sql = chain.invoke({"schema": SCHEMA, "question": "上个月销售额最高的3个产品"})
print(sql)
# SELECT p.name, SUM(o.amount) AS total
# FROM orders o JOIN products p ON o.product_id = p.id
# WHERE o.created_at >= '2024-09-01' AND o.created_at < '2024-10-01'
# GROUP BY p.id, p.name ORDER BY total DESC LIMIT 3
三、关键:表结构怎么写
模型只能看到你给它的 schema。写不好,SQL 就写不对。
3.1 只给相关的表
python
# ❌ 把 50 张表全塞进去
schema = get_all_tables_schema() # 上万 token,模型还会选错表
# ✅ 只给业务相关的 3~5 张
SCHEMA = """
表 orders(订单): id, user_id, product_id, amount(元), status, created_at
表 products(商品): id, name, category, price
"""
3.2 字段要有中文注释
sql
-- ❌ 模型不知道 status 里存的是什么
status TEXT
-- ✅
status TEXT -- 订单状态:pending待付款 / paid已付款 / shipped已发货 / done已完成
枚举值必须写出来 ,否则模型会猜 status = 'completed',
而实际值是 'done',查询结果为空。
3.3 给出示例值
python
SCHEMA = """
表 orders:
id 订单ID
user_id 用户ID
amount 订单金额(元,整数)
status 状态:pending/paid/shipped/done/cancelled
created_at 下单时间(格式 YYYY-MM-DD HH:MM:SS)
示例数据:
id=1001, user_id=88, amount=299, status='paid', created_at='2024-09-15 10:30:00'
"""
给几行示例数据,模型对格式的理解会准确很多 ------
这和 Few-shot 是一个道理。
四、自动提取 schema(表多的时候)
表多了手写不现实,可以从数据库读:
python
from langchain_community.utilities import SQLDatabase
db = SQLDatabase.from_uri("sqlite:///data/app.db")
print(db.get_usable_table_names())
# ['orders', 'products', 'users']
print(db.get_table_info(["orders", "products"]))
# CREATE TABLE orders (...)
# SELECT * FROM orders LIMIT 3;
# ...
SQLDatabase 会自动带上 SELECT * LIMIT 3 的示例数据,很贴心。
但自动提取的 schema 没有中文注释,建议手动补一层:
python
COMMENTS = {
"orders.status": "状态:pending/paid/shipped/done/cancelled",
"orders.amount": "金额(元)",
}
schema = db.get_table_info()
schema += "\n\n字段说明:\n" + "\n".join(
f"{k} -- {v}" for k, v in COMMENTS.items()
)
五、用官方链:create_sql_query_chain
python
from langchain_community.utilities import SQLDatabase
from langchain_openai import ChatOpenAI
from langchain.chains import create_sql_query_chain
db = SQLDatabase.from_uri("sqlite:///data/app.db")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = create_sql_query_chain(llm, db)
sql = chain.invoke({"question": "上个月有多少订单"})
它内部帮你处理了 schema 注入和 prompt 模板。
但它只生成 SQL,不执行------执行要自己做(这正好,方便加安全层)。
六、安全层(必做)
python
import re
import sqlite3
DANGEROUS = ["drop", "delete", "update", "insert", "alter",
"create", "truncate", "attach", "pragma", "grant"]
def safe_execute(sql: str, conn, max_rows=100, timeout=10):
# 1. 清洗
s = sql.strip().rstrip(";")
# 去掉可能的 markdown 代码块
s = re.sub(r"^```(?:sql)?|```$", "", s, flags=re.M).strip()
# 2. 只允许 SELECT / WITH
first_word = s.split()[0].upper()
if first_word not in ("SELECT", "WITH"):
return None, f"只允许 SELECT 查询,收到的是 {first_word}"
# 3. 危险关键词检查(按词边界匹配,避免误伤列名)
low = s.lower()
for kw in DANGEROUS:
if re.search(rf"\b{kw}\b", low):
return None, f"查询包含不允许的关键字:{kw}"
# 4. 强制加 LIMIT
if not re.search(r"\blimit\b", low):
s += f" LIMIT {max_rows}"
# 5. 执行(带超时)
try:
conn.execute(f"PRAGMA busy_timeout = {timeout * 1000}")
cur = conn.execute(s)
rows = cur.fetchmany(max_rows)
cols = [d[0] for d in cur.description]
return {"columns": cols, "rows": rows}, None
except Exception as e:
return None, f"执行失败:{e}"
五道关卡,缺一不可。
更稳妥的做法------用只读账号连接数据库:
python
db = SQLDatabase.from_uri("postgresql://readonly_user:xxx@host/db")
这样即使 SQL 层被绕过,数据库层还能兜住。
七、完整链路:生成 → 校验 → 执行 → 自然语言回答
python
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
def query_and_answer(question: str):
# 1. 生成 SQL
sql = chain.invoke({"question": question})
# 2. 安全执行
result, err = safe_execute(sql, conn)
if err:
return {"answer": f"查询失败:{err}", "sql": sql}
# 3. 让模型把结果说成人话
answer = (
ChatPromptTemplate.from_template("""
根据查询结果回答用户问题。只陈述数据里有的事实,不要推测。
用户问题:{question}
SQL:{sql}
查询结果(最多 100 行):{result}
用一两句话回答,关键数字要带上。数据为空就说"没有查到数据"。
""")
| llm
| StrOutputParser()
).invoke({"question": question, "sql": sql, "result": str(result)})
return {"answer": answer, "sql": sql, "data": result}
把 SQL 也返回给前端展示 ------
让用户能看到"系统是怎么查的",
既能建立信任,也方便发现错误。
八、失败重试:把报错告诉模型
模型写的 SQL 经常有语法错误或字段不存在。
把数据库报错返回给它,让它改:
python
FIX_PROMPT = ChatPromptTemplate.from_template("""
下面的 SQL 执行失败了,请根据错误信息修正它。
只输出修正后的 SQL,不要解释。
【表结构】
{schema}
【原始问题】
{question}
【失败的 SQL】
{sql}
【错误信息】
{error}
""")
def query_with_retry(question: str, max_attempts=3):
sql = chain.invoke({"question": question})
for i in range(max_attempts):
result, err = safe_execute(sql, conn)
if err is None:
return sql, result
if i == max_attempts - 1:
return sql, None
# 让模型根据报错修正
sql = (FIX_PROMPT | llm | StrOutputParser()).invoke({
"schema": SCHEMA, "question": question,
"sql": sql, "error": err,
})
return sql, None
实测这一招能修回大部分语法错误和字段名错误 ,
比单纯重新生成(不带错误信息)成功率高得多。
九、用 Agent 让它可以多步查询
复杂问题可能需要"先查有哪些表→再查数据":
python
from langchain_community.agent_toolkits import create_sql_agent
from langchain_community.utilities import SQLDatabase
db = SQLDatabase.from_uri("sqlite:///data/app.db")
agent = create_sql_agent(
ChatOpenAI(model="gpt-4o-mini", temperature=0),
db=db,
agent_type="openai-tools",
verbose=True,
)
agent.invoke({"input": "哪个城市的 VIP 用户平均消费最高?"})
但生产环境我更推荐自己写固定链,理由:
- Agent 会自己执行 SQL,安全层不好插;
- 步数不可控,成本不可控;
- 出错难排查。
只有在探索性数据分析场景才用 Agent ,
面向业务的固定查询用前面的固定链。
十、常见坑
| 现象 | 原因 | 解法 |
|---|---|---|
| 生成的 SQL 查不到数据 | 枚举值猜错 | schema 里写明枚举 |
| 字段名不存在 | schema 过时 | 从数据库自动生成 schema |
| 查询把库拖垮 | 没加 LIMIT / 没超时 | 安全层必做 |
| 日期算错 | 模型不知道"上个月"是哪天 | 在 prompt 里注入当前日期 |
| 数字类型混乱 | amount 是分还是元 | schema 里注明单位 |
| 中文表名/字段处理 | 引号问题 | schema 里给出正确的引用方式 |
"日期"那条特别容易中:
python
from datetime import datetime
prompt = prompt.partial(
today=datetime.now().strftime("%Y-%m-%d")
)
# prompt 里加一句:"今天是 {today}"
模型不知道"今天"是几号,必须告诉它。
十一、小结
- 三条红线:只允许 SELECT、只暴露必要的表、必须加 LIMIT 和超时;
- schema 三要素:只给相关表、字段名加中文注释和枚举值、附示例数据;
SQLDatabase能自动提取 schema(get_table_info),
但中文注释要手动补;- 安全层五关:清洗 markdown → 只允许 SELECT/WITH → 危险词检查
→ 强制 LIMIT → 超时 ,外加只读数据库账号; - 把报错返回给模型让它修正,比重新生成成功率高得多;
- prompt 里注入当前日期,否则"上个月"这类相对时间会算错;
- 生产推荐固定链,Agent 只在探索性场景用。
下一篇讲知识图谱 + RAG------处理"谁和谁什么关系"这类问题。