LangChain Text-to-SQL:让不会写 SQL 的人查数据库

个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

LangChain Text-to-SQL:让不会写 SQL 的人查数据库

"上个月华东区销售额前三的产品是哪些?"------

业务同事每天都要问这类问题,每次都得找数据分析师写 SQL。

Text-to-SQL 让模型来写 SQL。但这件事风险很高 ,

这篇讲怎么做到"能用"且"不出事"。

一、先说风险

在讲怎么做之前,必须说清楚三条红线:

  1. 绝不能让模型执行任意 SQL(DROP / DELETE / UPDATE 都可能被生成);
  2. 绝不能把整个数据库暴露给它(只给它需要的表);
  3. 必须有行数和耗时限制(一条没加 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------处理"谁和谁什么关系"这类问题。

相关推荐
Pniubi1 小时前
基于openclaw开发skill与自研agent中的skill开发区别
服务器·agent·skill·openclaw·龙虾
梦想不只是梦与想1 小时前
SQLAlchemy:查询与操作 (三)
数据库·python·sqlalchemy
问天_观心1 小时前
Mistral-7B QLoRA 实战(二):官方权重转换、4-bit NF4 加载与 Base Model 基线测试
人工智能·深度学习·学习·大模型·transformer
倔强的石头1061 小时前
Gemini架构解析_多模态大模型的设计思路
人工智能·大模型
Aric_Jones1 小时前
WAL / CDC / 复制槽 /位点
数据库·oracle
草莓熊Lotso1 小时前
【C++ 基于微服务的即时通讯系统】基建三件套:gflags + gtest + spdlog 源码编译安装与实战入门
linux·网络·数据库·c++·微服务·log4j
bksczm2 小时前
Redis基础篇之初识分布式
数据库·redis·分布式
for_ever_love__2 小时前
MySQL 索引类型全解:B+Tree / Hash / R-Tree / 全文 的底层差异,以及一次性建对的选型表
数据库·mysql·b+树·唯一索引·全文索引·索引类型·索引分类
逆风飞翔的小叔2 小时前
【AI智能体】Langchain 工具调用实战操作详解
langchain·langchain 工具调用·langchain 工具使用·langchain 工具详解·langchain 工具总结·langchain 工具·langchain tool