前情回顾
欢迎来到RAG系列的一个全新领域!在过去的十六天里,我们已经成为了与非结构化文本(如文章、笔记、代码)对话的大师。我们的RAG系统可以阅读、理解、检索并总结海量的文本信息。
但是,如果你的老板扔给你一个Excel销售报表,然后问:“上个季度哪个产品的销售额同比增长最快?”
我们现有的RAG系统,瞬间就“失灵”了。
为什么?因为它被训练来理解语义,但表格数据中的洞察,往往来自于计算和聚合(如 SUM, AVG, GROUP BY, JOIN)。你不能通过“语义相似度”来计算总销售额。
为了让我们的RAG系统能处理表格、数据库这类结构化数据,我们必须彻底转变思路。我们不再是让LLM扮演一个“阅读理解专家”,而是要让它成为一名**“代码生成师”**。
这种全新的范式,就是Text-to-SQL。
范式转变:从“检索”到“生成SQL”
当面对结构化数据时,RAG的工作流发生了根本性的变化:
- 旧流程(文本RAG):
问题 -> 检索相似文本块 -> 结合上下文生成答案 - 新流程(SQL-RAG):
问题 -> 生成SQL查询语句 -> 执行SQL语句 -> 获取查询结果 -> (可选)将结果转换成自然语言答案
我们的核心任务,从“找到最相关的文本”,变成了“生成最准确的SQL代码”。
成功的关键:让LLM看懂“表结构”
要让LLM能写出正确的SQL,它必须先知道数据库里有什么,即数据库的模式(Schema)。你不能指望一个不知道“学生表”里有“年龄”字段的LLM,能写出查询所有学生平均年龄的SQL。
因此,在SQL-RAG中,我们送给LLM的最重要的“上下文”,不再是文档块,而是数据库的表结构信息(通常是CREATE TABLE语句)。
上手实战:从零构建一个迷你SQL-RAG机器人
我们将使用Python内置的sqlite3库,创建一个完全自包含的、能与数据库对话的RAG应用。
第0步:创建我们的“公司数据库”
我们先用代码创建一个简单的内存数据库,包含“员工”和“部门”两张表。
import sqlite3
# 创建一个内存中的SQLite数据库
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
# 创建部门表 (Departments)
cursor.execute('''
CREATE TABLE departments (
department_id INTEGER PRIMARY KEY,
department_name TEXT NOT NULL
);
''')
# 创建员工表 (Employees)
cursor.execute('''
CREATE TABLE employees (
employee_id INTEGER PRIMARY KEY,
employee_name TEXT NOT NULL,
department_id INTEGER,
salary INTEGER,
FOREIGN KEY (department_id) REFERENCES departments(department_id)
);
''')
# 插入一些示例数据
cursor.execute("INSERT INTO departments VALUES (1, '技术部'), (2, '市场部');")
cursor.execute("INSERT INTO employees VALUES (101, '张三', 1, 8000), (102, '李四', 1, 9500), (103, '王五', 2, 6000);")
conn.commit()
print("数据库和表示例数据创建成功!")
第1步:编写我们的“黄金Prompt”
这个Prompt是整个系统的核心,它需要清晰地指导LLM如何行动。
SQL_PROMPT_TEMPLATE = """
你是一个顶级的SQLite数据库专家。
请根据下面提供的数据库表结构和用户问题,生成一个精准、可执行的SQLite查询语句。
只返回SQL查询语句,不要添加任何额外的解释或注释。
---
数据库表结构:
{schema}
---
用户问题: {question}
---
生成的SQLite查询语句:
"""
第2步:实现完整的工作流
现在,我们把获取表结构、调用LLM、执行SQL、生成最终答案的流程串起来。
# 这是一个模拟的LLM调用函数
def call_llm(prompt):
print("\n--- 正在调用LLM ---")
print(f"Prompt: {prompt[:300]}...")
# 手动模拟LLM的回答
if "技术部所有员工" in prompt:
return "SELECT employee_name FROM employees e JOIN departments d ON e.department_id = d.department_id WHERE d.department_name = '技术部';"
elif "平均工资" in prompt:
return "SELECT d.department_name, AVG(e.salary) FROM employees e JOIN departments d ON e.department_id = d.department_id GROUP BY d.department_name;"
else:
return "SELECT '模拟SQL'"
# 辅助函数:获取数据库的表结构
def get_db_schema(connection):
cursor = connection.cursor()
cursor.execute("SELECT name, sql FROM sqlite_master WHERE type='table';")
schema = "\n".join([f"表名: {name}\n创建语句: {sql}" for name, sql in cursor.fetchall()])
return schema
# 辅助函数:执行SQL并获取结果
def execute_sql(connection, sql_query):
cursor = connection.cursor()
try:
cursor.execute(sql_query)
return cursor.fetchall()
except Exception as e:
return f"SQL执行错误: {e}"
# --- 主流程 ---
def run_sql_rag(question):
print(f"\n========== 开始处理问题: {question} ==========")
# 1. 获取表结构作为上下文
db_schema = get_db_schema(conn)
# 2. 构建Prompt
prompt = SQL_PROMPT_TEMPLATE.format(schema=db_schema, question=question)
# 3. 调用LLM生成SQL
generated_sql = call_llm(prompt)
print("\n--- LLM生成的SQL ---")
print(generated_sql)
# 4. 执行SQL
sql_result = execute_sql(conn, generated_sql)
print("\n--- SQL执行结果 ---")
print(sql_result)
# 5. (可选) 将结果再次交给LLM,生成自然语言答案
# final_prompt = f"根据查询结果'{sql_result}',请用自然语言回答问题'{question}'。"
# final_answer = call_llm(final_prompt)
# print(final_answer)
# --- 运行示例 ---
run_sql_rag("技术部所有员工的名字是什么?")
run_sql_rag("每个部门的平均工资是多少?")
# 关闭数据库连接
conn.close()
结果分析:
通过这个流程,我们的RAG系统成功地将自然语言问题(“每个部门的平均工资是多少?”)转换为了精确的SQL语句(SELECT ..., AVG(...) ... GROUP BY ...),并从数据库中查询到了准确的数字结果。这在传统的文本RAG中是无法想象的。
总结与预告
今日小结:
- 面对结构化数据(如表格),RAG的核心范式从“检索文本”转变为“生成并执行代码(如SQL)”。
- 在Text-to-SQL任务中,最重要的“上下文”是数据库的模式(Schema)。
- 完整流程包括:获取表结构 -> 生成SQL -> 执行SQL -> (可选)生成自然语言答案。
我们今天从零实现了一个迷你的SQL-RAG流程。但你可能已经发现,这其中还有很多可以优化的地方:如果数据库有上百张表,我们能把所有表的Schema都塞进Prompt吗?如果LLM生成的SQL有语法错误怎么办?
这些复杂的工程问题,手动处理起来会非常棘手。幸运的是,强大的AI框架已经为我们铺平了道路。
明天预告:RAG 每日一技(十八):手写SQL-RAG太累?LangChain的SQL智能体(Agent)前来救驾!
明天,我们将学习如何使用LangChain中专门为此类任务设计的SQL Agent。我们将看到,这个强大的智能体是如何自动选择相关表格、自我修正错误的SQL、并更智能地与数据库进行交互的,从而用短短几行代码,构建出一个远比我们今天手写的更强大、更鲁棒的数据库问答机器人。