Agent 的数据库 Schema 理解:怎么让 Agent 在 30 秒内理解一个有 200 张表的数据库

我最早干过一件蠢事:把公司生产库的建表语句全部导出,拼成一个将近 8 万 token 的巨型 Prompt,丢给 GPT-4,让它当 DBA。

AI technology illustration

结果可想而知。200 张表里跟问题真正相关的可能只有 7 张。模型在一大堆无关 DDL 里翻找,写出来的 SQL 要么 join 了八竿子打不着的表,要么对着完全无关的表瞎编字段名。更肉疼的是,这种玩法每问一次,光输入 token 就要烧掉一块多人民币——按 OpenAI 旗舰模型的定价算——回答还要等半天。

后来我想明白一件事:让 Agent 理解 200 张表的数据库,难点从来不是“把 schema 塞进上下文”,而是“把多余的 190 张表挡在门外”。

Schema 给得越多,SQL 写得越差

先看一组数字。一张 15 列的表,算上类型、注释、索引和外键,建表语句大概 200 个 token。200 张表就是三四千行、五六万 token;再加上视图和注释,奔着 8 万去了。

8 万 token 对今天的上下文窗口来说不是装不下,但模型对长上下文的利用效率远没有你想象的那么高。Lost in the Middle 这篇论文发现,相关文档被埋在长上下文中间时,模型的表现会明显变差。

Performance is often highest when relevant information occurs at the beginning or end of the input context, and significantly degrades when models must access relevant information in the middle. ——论文原文

翻译过来就是:你把关键表埋在 190 张无关表中间,等于主动给模型制造困难,差距最大能到 20 个百分点左右。更麻烦的是噪音——无关的表名会误导模型:用户问“退款”,模型看到一张 refund_log 就激动地 join 上去,却没发现真正的退款金额在 payment_records 里。CHESS 这篇论文把问题点得很透:在真实的大库上,先做 schema 过滤再写 SQL,比把全量 schema 塞进去更准BIRD 这个专门为难模型的 Text-to-SQL 基准,收集了 95 个真实数据库、1.2 万个问题,设计之初就加了“数据库很大,模型必须先定位相关表”这一关。

把 200 张表变成一张地图

想通这一点花了我挺长时间。直到有一天我意识到,这件事跟人看数据库完全一样——你让一个新数据分析师上手 200 张表的系统,会丢给他一份 1 万行的 DDL 吗?不会。你会先让他看数据字典和 ER 图:先看地图,用到哪张再点开哪张。

Agent 也需要这张地图。它不叫 ER 图,叫 schema 指纹:每张表用一两句人话说明“这张表是干嘛的、关键字段有哪些、跟谁有外键关系”。真正的技巧在于:如果表名是 t_2001_xxx 这种反人类命名,就让 LLM 先看每张表的 5 行样本数据,自己把描述写出来。用一次 LLM 调用把地图画好,之后 Agent 用地图导航,只在最后一步展开相关表的原始 DDL。

这一步做完,你手里就不再是 8 万 token 的 DDL,而是一份 200 行的目录——每张表一行,总共三四千 token。Vanna 这类开源 Text-to-SQL 框架,干的就是这件事:把 DDL、文档和样例问答提前向量化,查询时只捞相关的部分。

四种给 Agent 看 Schema 的方式

方式 输入量 问题 适用场景
全量 DDL 硬塞 5~8 万 token 噪音误导、中间丢失、烧钱 20 张表以内的小库
人工精简 DDL 1 万 token 左右 维护成本高,schema 一改就过期 基本不变的小库
每表一句话总结 3~6 千 token 丢了列级细节,join 靠猜 让 Agent 先做概览
检索剪枝 + 局部全量 2~4 千 token 依赖检索质量 百表级大库的正解

最后一种方案,正是“30 秒”的秘密:不是让 Agent 在 30 秒内读完 200 张表,而是让它在 30 秒内只看到需要的那十来张表

30 秒是怎么挤出来的

整个方案拆成离线和在线两段。离线阶段花几分钟,只做一次;在线阶段才是每次提问走的路径。

  1. 抽实体:从问题里抽出关键词和实体。“上季度下单超过 3 次但还没付款的客户”,实体就是下单、付款、客户。
  2. 检索候选表:用 embedding 相似度加关键词命中,从 200 张表里捞 top 8。注意要同时建表级和列级索引——用户可能提到具体字段,比如“发票号”。
  3. 沿外键扩一圈:检索到 orders,就顺带把 order_items、payments、users 拉进来。join 是 SQL 的老本行,别让模型漏了邻居表。
  4. 局部全量:只把筛出来的 10~15 张表的完整 DDL、字段注释和 3 行样本数据拼进 prompt。
# 离线:建地图
for table in db.tables:
    desc = llm.describe(table.sample_rows(5))   # LLM 看图说话
    fingerprint[table.name] = desc + table.fk
vector_db.add(fingerprint)                       # 表级 + 列级 embedding

# 在线:按图索骥
candidates = vector_db.search(question, k=8)
candidates += fk_neighbors(candidates)           # 外键邻居
prompt = build_full_ddl_prompt(candidates)       # 只给局部全量

四步走完,耗时一两秒。加上 SQL 生成和一次自我纠错,30 秒绰绰有余。这 30 秒里模型真正读到的只有两三千 token,而不是 8 万

这套思路在学术界已经是 Text-to-SQL 的标配。DIN-SQL(NeurIPS 2023)把流程拆成 schema linking、查询分解、SQL 生成、自我纠错四步,schema linking 排在第一位;后来的 CHESS 把过滤做得更细,在 BIRD 上达到了 73% 的执行准确率,靠的也是“先找相关的表和列,再写 SQL”。

想深入一点:CHESS 是怎么做 schema 过滤的?

CHESS 把过滤拆成两层:先用 entity retrieval 把问题里的表名、列名、字段值与 schema 元素做匹配打分,再做结构性剪枝,沿着外键把候选集扩充成一棵可 join 的子图,还会参考数据库统计信息。整条链路的目标只有一个:让最终生成器只面对一个十来张表的子 schema。

三个最常见的误解

上下文窗口加到 1M,不就不用筛了吗?

窗口再大,也没解决“该看哪张表”的导航问题。1M 输入意味着更高的成本和延迟,无关信息照样在 attention 里产生噪音。更别说 200 张表只是今天的大小,明年可能变成 500 张——你不会想每次提问都背一遍整个企业。

让 Agent 自己查 information_schema 不行吗?

可以,但那是临时抱佛脚。每次现查意味着多轮对话来回烧 token,而且 Agent 在库里的探索同样会被表名误导。离线把地图画好,是一次构建、无限次复用,还能把踩坑经验写进描述里。

表名和列名起得足够清楚不就行了?

不够。语义藏在列的值域里:status 字段到底是 0/1 还是 pending/paid,crated_at 是不是写错的 created_at——这些只有看了样本数据才知道。LLM 写描述时喂 5 行真实数据,比喂 50 行注释管用。

这套方案的边界在哪

说句公道话,检索剪枝不是银弹。它最大的软肋是描述质量决定检索上限:如果地图画得烂——比如把 order 表描述成“订单相关”,用户问“最近哪些客户有欠款”,就永远检索不到它。外键扩张也救不了靠业务逻辑隐式关联的表,两张表之间可能根本没有外键约束。

另一个边界是跨域聚合类问题。“各区域销售人员的绩效和库存周转率的关系”这种问题,相关表散落在五个业务域,top 8 的检索可能漏掉一两张。对策是把 K 调大,或者把“二次检索”做成 Agent 的一个工具调用,而不是赌一次检索就全对。

所以我的判断是:“理解 schema”这件事,最终会从 Agent 的即时推理,沉淀为数据库资产的长期建设——数据字典、语义层、指标定义。那些把地图画得最用心的团队,Agent 的表现也最好。30 秒不是模型的魔法,是你提前做的功课。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/601.html

(0)
上一篇 1天前
下一篇 1天前

相关推荐