面试被问到 Text2SQL,我用 DeepSeek 自己实现了一个

157 阅读9分钟

面试被问到 Text2SQL,我用 DeepSeek 自己实现了一个

面试官问我:"如果让你用 AI 实现自然语言查数据库,你会怎么做?" 我当时答得不太好,回来研究了一下,发现核心原理并不复杂。记录一下。

背景

前段时间面一家后端岗,面试官问了我一个开放题:"现在 AI 这么火,如果让你做一个自然语言查数据库的功能,你怎么设计?"

我支支吾吾说了一些,面试官追问:"那 Schema 怎么传给大模型?提示词怎么设计?多表 JOIN 怎么处理?"

答得不好,挂了。

回来之后我决定把这个东西搞明白。花了一天时间,用 DeepSeek API + SQLite + Python 实现了一个 Text2SQL 的小 demo。过程中把 SQL 的 JOIN 语法也重新过了一遍,顺便整理成这篇文章。


一、SQL JOIN 复习:面试高频考点

面试问 SQL,JOIN 是必考的。我之前一直搞混 INNER JOIN 和 LEFT JOIN 的区别,这次算是彻底弄清楚了。

两张测试表

employees(员工表):
| id | name  | department | salary |
|----|-------|------------|--------|
| 1  | 张三  | 销售       | 50000  |
| 2  | 李四  | 工程       | 75000  |
| 3  | 王五  | 销售       | 60000  |

departments(部门表):
| id | name | manager |
|----|------|---------|
| 1  | 销售 | 赵经理  |
| 2  | 工程 | 钱经理  |
| 3  | 市场 | 孙经理  |

注意:市场部没有员工。这个差异是理解三种 JOIN 的关键。

INNER JOIN(内连接)

SELECT e.name, d.manager
FROM employees e
INNER JOIN departments d ON e.department = d.name;
| name | manager |
|------|---------|
| 张三 | 赵经理  |
| 李四 | 钱经理  |
| 王五 | 赵经理  |

只返回两边都能匹配上的。 市场部没有员工,所以孙经理不会出现。

面试记忆法:INNER JOIN = 交集,只取两边都有的。

LEFT JOIN(左连接)

SELECT e.name, d.manager
FROM employees e
LEFT JOIN departments d ON e.department = d.name;
| name | manager |
|------|---------|
| 张三 | 赵经理  |
| 李四 | 钱经理  |
| 王五 | 赵经理  |

这个例子里结果跟 INNER JOIN 一样,因为所有员工都能匹配到部门。但如果有个员工的 department 字段是"财务部",而 departments 表里没有"财务部",LEFT JOIN 会保留这个员工,manager 显示 NULL。

面试记忆法:LEFT JOIN 以左表为主,左表一条不少。

RIGHT JOIN(右连接)

SELECT e.name, d.manager
FROM employees e
RIGHT JOIN departments d ON e.department = d.name;
| name | manager |
|------|---------|
| 张三 | 赵经理  |
| 李四 | 钱经理  |
| 王五 | 赵经理  |
| NULL | 孙经理  |

市场部没有员工,但 RIGHT JOIN 以右表(departments)为主,所以孙经理会保留,name 显示 NULL。

面试记忆法:RIGHT JOIN 以右表为主,右表一条不少。

顺便提一句:SQLite 不支持 RIGHT JOIN,MySQL 和 PostgreSQL 支持。实际开发中很少用 RIGHT JOIN,一般把表顺序换一下用 LEFT JOIN 就行。

面试速查表

连接类型保留规则面试怎么答
INNER JOIN只保留两边都能匹配的"取交集"
LEFT JOIN左表全保留,右表补 NULL"以左表为主"
RIGHT JOIN右表全保留,左表补 NULL"以右表为主"

GROUP BY 聚合

SELECT department, COUNT(*) AS 人数, AVG(salary) AS 平均工资
FROM employees
GROUP BY department;
| department | 人数 | 平均工资 |
|------------|------|---------|
| 工程       | 1    | 75000   |
| 销售       | 2    | 55000   |

聚合函数:COUNT()SUM()AVG()MAX()MIN()。这几个面试必问,记住就行。


二、Text2SQL 原理:面试怎么答

面试官问"Text2SQL 怎么做",其实就三步:

  1. 提取数据库 Schema(表结构)
  2. 构造提示词,把 Schema 和用户问题一起发给大模型
  3. 执行大模型返回的 SQL,返回结果
用户输入:"工程部门员工的姓名和工资是多少?"
    ↓
提示词 = Schema + 用户问题
    ↓
大模型(DeepSeek / ChatGPT / Claude)
    ↓
输出 SQLSELECT name, salary FROM employees WHERE department = '工程';
    ↓
执行 SQL → 返回结果

面试的时候把这个流程说清楚,再加一句"核心是提示词工程",基本就过了。


三、动手实现

面试过了还不够,自己得能写出来。下面是我实现的完整代码。

环境

pip install openai

DeepSeek 兼容 OpenAI 接口,所以直接用 openai 库。

import os
import sqlite3
from openai import OpenAI

client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

建库

conn = sqlite3.connect("text.db")
cursor = conn.cursor()

cursor.execute("""
CREATE TABLE IF NOT EXISTS employees (
    id INTEGER PRIMARY KEY,
    name TEXT,
    department TEXT,
    salary INTEGER
)
""")

sample_data = [
    (1, "张三", "销售", 50000),
    (2, "李四", "工程", 75000),
    (3, "王五", "销售", 60000),
    (4, "赵六", "工程", 80000),
    (5, "钱七", "市场", 55000)
]
cursor.executemany("INSERT INTO employees VALUES(?,?,?,?)", sample_data)
conn.commit()

提取 Schema

def get_schema(table_name):
    schema = cursor.execute(f"PRAGMA table_info({table_name})").fetchall()
    return f"CREATE TABLE {table_name} (\n" + \
           "\n".join([f"  {col[1]} {col[2]}" for col in schema]) + "\n);"

这个函数做的事情就是把表结构拼成 CREATE TABLE ... 的格式。面试时可以说"用 PRAGMA table_info 动态提取表结构"。

调用大模型

def ask_deepseek(query, schema):
    prompt = f"""
这是一个数据库的 Schema:
{schema}

根据这个 Schema,请输出一个 SQL 查询来回答以下问题。
只输出 SQL 查询语句本身,不要使用任何 markdown 格式,
不要包含反引号、代码块标记或额外说明。

问题:{query}
"""
    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        max_tokens=2048,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

提示词设计要点:

  1. 给 Schema —— 不给 Schema 的话 AI 根本不知道你数据库长什么样
  2. 约束输出格式 —— "只输出 SQL,不要 markdown",不然 AI 返回 sql ... 直接报错
  3. 问题要具体 —— "查销售部门工资大于5万的员工" 比 "查点数据" 靠谱得多

安全执行

def safe_execute(sql):
    dangerous = ["DROP", "TRUNCATE", "ALTER"]
    if any(kw in sql.upper() for kw in dangerous):
        raise ValueError(f"危险操作被拦截: {sql}")
    try:
        return cursor.execute(sql).fetchall()
    except Exception as e:
        return f"SQL 执行错误: {e}"

面试时如果被问到"AI 生成的 SQL 安全吗",就说"加了关键词拦截,生产环境建议做白名单,只允许 SELECT"。

跑一下

question = "工程部门员工的姓名和工资是多少?"
sql_query = ask_deepseek(question, get_schema("employees"))
print(f"AI 生成的 SQL: {sql_query}")

results = safe_execute(sql_query)
for row in results:
    print(row)

# AI 生成的 SQL: SELECT name, salary FROM employees WHERE department = '工程';
# ('李四', 75000)
# ('赵六', 80000)

四、多表 JOIN:面试官最爱追问的

单表查询太简单了,面试官肯定会追问"多表怎么处理"。

# 建第二张表
cursor.execute("""
CREATE TABLE IF NOT EXISTS departments (
    id INTEGER PRIMARY KEY,
    name TEXT,
    manager TEXT
)
""")

sample_departments = [
    (1, "销售", "赵经理"),
    (2, "工程", "钱经理"),
    (3, "市场", "孙经理")
]
cursor.executemany("INSERT INTO departments VALUES(?,?,?)", sample_departments)
conn.commit()
# 把两张表的 Schema 都告诉 AI
def get_full_schema(table_names):
    schema_str = ""
    for table in table_names:
        schema = cursor.execute(f"PRAGMA table_info({table})").fetchall()
        schema_str += f"CREATE TABLE {table} (\n" + \
                      "\n".join([f"  {col[1]} {col[2]}" for col in schema]) + \
                      "\n);\n\n"
    return schema_str

schema_str = get_full_schema(["employees", "departments"])

问一个需要 JOIN 的问题:

question = "列出每个部门的员工人数和平均工资,没有员工的部门也要显示"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)

AI 生成的 SQL:

SELECT d.name AS department,
       COUNT(e.id) AS employee_count,
       AVG(e.salary) AS average_salary
FROM departments d
LEFT JOIN employees e ON d.name = e.department
GROUP BY d.id, d.name;

结果:

| department | employee_count | average_salary |
|------------|---------------|----------------|
| 销售       | 2             | 55000          |
| 工程       | 2             | 77500          |
| 市场       | 0             | NULL           |

面试官问"AI 怎么知道用 LEFT JOIN"——因为提示词里说了"没有员工的部门也要显示",AI 理解了这个语义。


五、踩过的坑

坑 1:AI 返回 markdown 格式

```sql
SELECT name FROM employees;
```

直接 cursor.execute() 报错。解决:提示词里加"不要使用任何 markdown 格式"。

坑 2:中文匹配失败

问"销售部门的员工",AI 生成 WHERE department = '销售',结果返回空。后来发现是 SQLite 中文编码的问题,建库时统一 UTF-8 就好了。

坑 3:AI 搞错表关系

两张表没有外键约束,AI 错误地用了 ON e.id = d.id。解决:在 Schema 里加注释说明关联关系:

schema_str = """
CREATE TABLE employees (
    id INTEGER,
    name TEXT,
    department TEXT,  -- 关联 departments.name
    salary INTEGER
);
CREATE TABLE departments (
    id INTEGER,
    name TEXT,
    manager TEXT
);
"""

加注释真的有用。 测了几次,加了注释之后 AI 生成的 JOIN 条件基本都对了。


六、进阶:提示词工程技巧

Few-shot 示例

在提示词里给几个例子,AI 输出会稳定很多。面试时可以说"用 Few-shot 提升生成准确率":

prompt = f"""
你是一个 SQL 专家。请根据以下 Schema 和示例,生成正确的 SQL。

## Schema
{schema}

## 示例
问题:工资最高的员工是谁?
SQL:SELECT name FROM employees ORDER BY salary DESC LIMIT 1;

问题:每个部门有多少人?
SQL:SELECT department, COUNT(*) FROM employees GROUP BY department;

## 规则
1. 只输出 SQL,不要任何解释
2. 优先使用 LEFT JOIN 确保数据不丢失

## 问题
{query}
"""

多轮对话

还可以做上下文记忆,支持追问:

messages = [
    {"role": "system", "content": f"你是 SQL 专家。数据库 Schema:\n{schema}"}
]

def chat(query):
    messages.append({"role": "user", "content": query})
    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        messages=messages
    )
    answer = response.choices[0].message.content
    messages.append({"role": "assistant", "content": answer})
    return answer

chat("列出所有员工")
chat("只要工程部门的")      # 自动加 WHERE
chat("按工资从高到低排序")   # 自动加 ORDER BY

七、方案对比

面试官可能会问"为什么不直接用现成的框架":

方案优点缺点
自己写提示词(本文)灵活、可控、学习原理需要手动维护 Schema
LangChain SQL Agent开箱即用黑盒、依赖重
vanna.ai专用框架、支持微调学习成本高
Chat2DB有 UI 界面定制性差

面试时可以说:"学习阶段自己写理解原理,生产环境可以考虑 LangChain 或 vanna.ai。"


八、局限性(面试加分项)

面试时主动说出局限性,会加分:

  • 复杂查询:多层嵌套子查询、窗口函数,AI 容易出错
  • 歧义问题:"最近的订单"——最近是 7 天还是 30 天?AI 不知道
  • 安全性:生成的 SQL 可能有注入风险
  • 性能:AI 生成的 SQL 不一定是最优执行计划

最佳实践

  1. Schema 加注释
  2. 提示词加 Few-shot 示例
  3. 执行前用 EXPLAIN 检查
  4. 权限控制:只允许 SELECT

总结

环节技术方案
数据库SQLite
大模型DeepSeek API
核心技术提示词工程 + Schema 注入
支持操作SELECT / INSERT / UPDATE / DELETE / JOIN

做完这个 demo 之后,我再回答面试官那个问题就顺畅多了。核心就是把 Schema 和用户问题一起发给大模型,剩下的就是提示词工程的细节了。

代码不多,核心逻辑几十行,感兴趣的同学可以自己跑一下。有问题评论区聊。