site logo

Marico's space

从英文到 SQL:LLM 如何真正理解你的数据库模式

AI技术与应用 2026-05-21 17:37:01 10

你在聊天界面里输入"查一下上个月每个客户的总收入",一秒钟后,一条格式标准的 SQL 查询就出来了,针对的是你的 orderscustomers 表。感觉就像变魔术一样。

但这不是魔术。一旦你理解了背后的机制——尤其是模型如何读取和解析你的数据库结构——你就能明白为什么有些文本转 SQL 工具偶尔能准确转换,其他时候却错得离谱,以及怎样才能提高准确率。

这篇文章深入分析了 LLM 驱动的文本转 SQL 系统中模型理解数据库结构的内部机制,并为正在构建或集成这类工具的开发者提供实用建议。

第一个问题:LLM 根本不知道你的数据库长什么样

开箱即用的 LLM 对你的业务数据库一无所知。它不知道你有张 subscriptions 表,不知道 mrr 字段藏在 billing_events 表里,更不知道你 везде 都用小写下划线命名 user_id,偏偏有张历史遗留表用的是 UserID

这些信息必须每次都注入到模型提示词里。如何选择要包含哪些结构信息、以什么格式注入,这个过程叫结构链接(schema linking)。这是任何文本转 SQL 流程中最关键、也是最容易被搞砸的环节。

典型的结构注入是这样的:

-- Passed to the LLM in the system or user prompt: Table: users
Columns: id (integer, PK), email (text), created_at (timestamp), plan (text) Table: orders
Columns: id (integer, PK), user_id (integer, FK  users.id), amount_cents (integer), status (text), created_at (timestamp) Table: subscriptions
Columns: id (integer, PK), user_id (integer, FK  users.id), plan_id (integer), started_at (timestamp), cancelled_at (timestamp, nullable)

模型读完之后,会在内部构建对你数据模型的理解——有哪些表、各表有哪些字段、它们之间如何关联、期望的数据类型是什么。

第一步:结构解析——模型实际看到了什么

当用户问"上个月流失了哪些客户?"时,LLM 不会简单地在你结构里搜索"流失"这个关键词。它会根据可用字段推断"流失"可能是什么意思:大概率是过去 30 天内 cancelled_at 时间戳非空,然后通过 subscriptions 关联到 users

这种将问题中的语义意图与结构元素连接起来的推理过程,就叫结构链接。它包含三个子问题:

1. 表的选择:哪些表跟这个问题相关?对于有几百张表的大型数据库,模型必须筛选出少数有关联的表。把整个结构一股脑塞进提示词是不可行的——一个 300 张表的数据库轻松就能撑爆模型的上下文窗口。

2. 字段的选择:在这些表里,需要哪些字段?这里命名的影响巨大。amt 是歧义的。amount_cents 就清晰了。ts 毫无用处。created_at 很有帮助。

3. 连接的推断:相关的表之间如何关联?模型需要重建关系图——最好是从你提供的结构中明确的外键声明来还原,而不是靠猜测字段命名规律。

第二步:为什么字段名和表名比你想象的更重要

做个测试。用下面两种结构问 LLM:"上周注册了多少用户?"

结构 A:

Table: u
Columns: uid, em, ts, pl

结构 B:

Table: users
Columns: id, email, created_at, plan

结构 B 每次都赢。自然语言词汇组成的字段名(emailcreated_atplan)能干净地映射到模型认识的词。缩写破坏了这个映射。

IBM 等机构的研究表明,给模型丰富的元数据——包括表描述、字段定义、示例值——比直接丢给它原始 DDL,性能领先一大截。

实操建议:在命名规范上投入,给文本转 SQL 流程显式传入描述信息。 如果你被历史遗留的短字段名困住了,加上注释说明。

第三步:如何组织结构以最大化 LLM 理解效果

下面是一个结构良好的提示词示例,文本转 SQL 系统可能会用到:

You are a SQL expert. Use the following schema to answer the user's question. ### Schema Table: users
Description: One row per registered user.
Columns: - id (integer, primary key) - email (text) — the user's login email - created_at (timestamp) — when the account was created - plan (text) — current plan: 'free', 'pro', 'enterprise' Table: orders
Description: Purchases made by users.
Columns: - id (integer, primary key) - user_id (integer) — foreign key → users.id - amount_cents (integer) — order total in cents (divide by 100 for dollars) - status (text) — 'pending', 'completed', 'refunded' - created_at (timestamp) ### Question
Which users on the 'pro' plan have made more than 3 orders in the past 30 days?

注意这里除了字段名还包含了什么:

  • 表描述解释业务含义
  • 行内注释说明歧义字段(单位、枚举值)
  • 显式外键关系用自然语言写出

这就是平庸和可以上生产线的文本转 SQL 结果之间的差距。

第四步:大型结构问题——塞不下的时候怎么办

现实中的数据库远不止你想塞进单次提示词的几张表。把 200 张表的定义全塞进上下文窗口会造成两个问题:成本飙升,准确率下降。周围都是无关表时,模型关注结构细节的能力会显著下降。

解决方案是检索增强的结构选择。在调用 LLM 之前,用检索步骤过滤结构:

# Pseudocode for schema-aware RAG in a text-to-SQL pipeline

user_question = "show me monthly revenue by plan for the last 6 months" # 1. Embed the user question
question_embedding = embed(user_question) # 2. Search a vector index of table + column descriptions
relevant_tables = vector_search( index=schema_index, query=question_embedding, top_k=5
) # 3. Build a focused schema prompt with only relevant tables
schema_context = format_schema(relevant_tables) # 4. Call the LLM with a focused prompt
sql = llm.complete( system_prompt=f"Use this schema:\n{schema_context}", user_prompt=user_question
)

像亚马逊的 RASL(检索增强结构链接)这类系统就用这个方案处理数千张表的数据库,准确率比粗暴的全结构注入高出好几个档次。

核心洞见:提示词里放对的结构,永远比用更大的模型强。

第五步:外键是隐藏的王牌

JOIN 是文本转 SQL 模型最容易翻车的地方。模型可能正确识别出你需要 usersorders 两张表,然后因为猜错了连接字段而生成一条破碎的查询。

解法很简单:永远在结构提示词里包含外键关系。对比这两种写法:

模糊版:

Table: orders
Columns: id, user_id, amount_cents, created_at

显式版:

Table: orders
Columns: - id (integer, primary key) - user_id (integer) — foreign key → users.id; never null - amount_cents (integer) - created_at (timestamp)

当外键声明清楚后,模型可靠地生成正确的 JOIN:

-- Generated correctly when FK is explicit:
SELECT u.email, COUNT(o.id) AS order_count, SUM(o.amount_cents) / 100.0 AS total_revenue
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'completed' AND o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY u.email
ORDER BY total_revenue DESC;

没有 FK 提示,你就只能依赖模型猜测 orders 里的 user_id 对应 users 里的 id。它通常能猜对——但"通常"用在生产环境可不够。

开发者常犯的错误

直接传原始 DDL 不加注释。 你的 CREATE TABLE 语句只是个开始,那是给数据库引擎优化的,不是给 LLM 看的。加上注释和描述。

-- Don't just pass this:
CREATE TABLE ev ( id SERIAL PRIMARY KEY, uid INT, typ VARCHAR(50), ts TIMESTAMPTZ
); -- Also include:
-- Table: events (alias: ev)
-- Description: user activity events for product analytics
-- uid → users.id, typ = 'click' | 'page_view' | 'signup' | 'purchase'

每个查询都塞所有表。 token 膨胀会损害准确率。注入前按相关性过滤结构。

忽略枚举值。 如果某个字段(如 status)有固定的取值集合,列出来。模型需要知道 'completed' 是有效状态,而不是 'done''success'

不给示例数据。 即使每张表只给一行示例数据,对边缘情况的准确率提升也是显著的——特别是日期格式、单位约定、枚举拼写。

核心要点

文本转 SQL 工具从"让人眼前一亮"到"敢在生产环境跑"之间的差距,几乎总是结构质量的问题,而不是模型的问题。简短总结:

  • 使用描述性的表名和字段名,避免缩写
  • 在结构提示词里显式声明外键关系
  • 为歧义字段(单位、枚举、nullable 含义)提供字段描述
  • 大型数据库用向量搜索按相关性过滤结构
  • 为分类字段添加示例值或枚举列表
  • 永远不要假设模型会推断你没告诉它的东西

LLM 做的事比看起来复杂得多——把你的英文对应到你的结构、推断连接、一次生成语法正确的 SQL。给它正确的结构上下文,它给你的就是能实际跑的结果。

原文链接:https://dev.to/...