RAG 每日一技(十七):不止聊文本,如何让RAG“看懂”表格数据?

119 阅读6分钟

前情回顾

欢迎来到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、并更智能地与数据库进行交互的,从而用短短几行代码,构建出一个远比我们今天手写的更强大、更鲁棒的数据库问答机器人。