第 03 章 — Schema 检索(应对大库)

第 03 章 --- Schema 检索(应对大库)

层级:L1 基础层 + L2 能力层 目标:当表数量爆炸时,只把"相关"的表注入 prompt,解决 token 超限与选表干扰。 本章产出:一个基于向量检索的 SchemaRetriever。 依赖:第 02 章的 schema.py

上一章的痛点

第 02 章能自动提取全库 Schema 了。但开放题已经点破:全库注入在大库上直接崩

  • 1200 张表 → 40 万 token → 超上限。
  • 就算塞得下,噪声表会干扰模型选表,准确率反而下降。
  • 每次请求都带全量 Schema,成本高、延迟大。

核心矛盾:用户一次提问通常只涉及 2~5 张表,却付了全库的代价。

解法:先召回相关的表,再注入。这是从"全量"到"检索"的关键跃迁。

核心原理:语义检索

思路和 RAG 一样:

复制代码
离线:把每张表的描述 → 向量 → 存进向量库
在线:用户问题 → 向量 → 检索最相似的 K 张表 → 只注入这 K 张

问题"上月各城市中止率"和表 drives(含 status/drive_date)、vehicles(含 city) 语义相近,会被召回;和 scenarios 关系弱,被过滤。

层级设计

检索器仍在 L1/L2 边界,它消费第 02 章的 Table,产出"被选中的子集":

markdown 复制代码
L2  SQL 生成 ← 只拿到相关的 K 张表
      ▲
L1  SchemaRetriever(本章)← 向量召回
      ▲
     SchemaProvider(第 02 章)

构建可嵌入的表文档

先把每张表压成一段用于检索的"文档"。检索文档和注入文本可以不同 ------ 检索用的可以更侧重语义关键词。

retriever.py

python 复制代码
"""L1: Schema 检索 ------ 大库场景只召回相关表。"""
from dataclasses import dataclass
import anthropic
from schema import Table, extract_schema, render_schema

client = anthropic.Anthropic()


def table_to_doc(t: Table) -> str:
    """把表压成一段适合语义检索的文档。"""
    col_names = ", ".join(c.name for c in t.columns)
    # 把样例值也拼进去,让"北京""中止"这类词能命中
    samples = []
    for c in t.columns:
        if c.sample_values:
            samples.extend(str(v) for v in c.sample_values)
    sample_text = ", ".join(samples[:20])
    return f"表 {t.name},字段:{col_names}。数据样例:{sample_text}"

向量化与检索

生产会用专门的 embedding 服务(如 Voyage、OpenAI embeddings)。这里为了聚焦原理,用一个可替换的 embedder 接口,并给一个基于 embedding API 的实现。

retriever.py 追加:

python 复制代码
import math


def embed(texts: list[str]) -> list[list[float]]:
    """把文本批量转成向量。生产可换任意 embedding 服务。

    这里用 Voyage/OpenAI 兼容接口示意;教学也可换成本地模型。
    """
    # 伪代码占位:实际调用你的 embedding 服务
    # resp = embed_client.embeddings.create(model=..., input=texts)
    # return [d.embedding for d in resp.data]
    raise NotImplementedError("接入你的 embedding 服务")


def cosine(a: list[float], b: list[float]) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb + 1e-9)


@dataclass(frozen=True)
class IndexedTable:
    table: Table
    vector: tuple[float, ...]


class SchemaRetriever:
    """离线建索引,在线按问题召回 top-k 表。"""

    def __init__(self, tables: tuple[Table, ...]):
        docs = [table_to_doc(t) for t in tables]
        vectors = embed(docs)
        self._index = tuple(
            IndexedTable(table=t, vector=tuple(v))
            for t, v in zip(tables, vectors)
        )

    def retrieve(self, question: str, k: int = 5) -> tuple[Table, ...]:
        qv = embed([question])[0]
        scored = sorted(
            self._index,
            key=lambda it: cosine(qv, list(it.vector)),
            reverse=True,
        )
        return tuple(it.table for it in scored[:k])

教学替代:没有 embedding 服务也能跑

如果你暂时没有 embedding API,用一个关键词重叠的 fallback 也能演示效果(生产不要用,仅用于本地体验流程):

python 复制代码
class KeywordRetriever:
    """零依赖的关键词召回 ------ 仅用于本地跑通流程,非生产方案。"""

    def __init__(self, tables: tuple[Table, ...]):
        self._tables = tables

    def retrieve(self, question: str, k: int = 5) -> tuple[Table, ...]:
        def score(t: Table) -> int:
            doc = table_to_doc(t)
            # 命中的字符越多分越高(极简,仅演示)
            return sum(1 for ch in set(question) if ch in doc)

        ranked = sorted(self._tables, key=score, reverse=True)
        return tuple(ranked[:k])

真实项目务必用向量检索 ------ 关键词召回处理不了"接管/脱离/里程"这类同义词。这正是语义检索的价值。

接入问数器

agent_v3.py

python 复制代码
"""v3: 带 Schema 检索的问数器。"""
import sqlite3
import anthropic
from schema import extract_schema, render_schema
from retriever import SchemaRetriever  # 或 KeywordRetriever

client = anthropic.Anthropic()

PROMPT = """你是 SQL 专家。下面是与问题最相关的表结构(已从大库中检索)。
若你发现缺少必要的表,请只回答:NEED_MORE_TABLES

数据库结构:
{schema}

规则:只输出 SQL 或 NEED_MORE_TABLES,不要解释。只允许 SELECT。

用户问题:{question}
"""


class QueryAgent:
    def __init__(self, db: str = "drive.db"):
        self.db = db
        tables = extract_schema(db)
        self.retriever = SchemaRetriever(tables)

    def ask(self, question: str, k: int = 5) -> list:
        relevant = self.retriever.retrieve(question, k=k)
        print(f"召回的表: {[t.name for t in relevant]}")
        schema_text = render_schema(relevant)

        resp = client.messages.create(
            model="claude-opus-4-8",
            max_tokens=500,
            messages=[{
                "role": "user",
                "content": PROMPT.format(schema=schema_text, question=question),
            }],
        )
        sql = resp.content[0].text.strip()
        if sql == "NEED_MORE_TABLES":
            # 兜底:召回不足时放大 k 或降级为全量
            print("召回不足,放大检索范围重试")
            return self.ask(question, k=k * 2)

        print(f"SQL: {sql}")
        conn = sqlite3.connect(self.db)
        try:
            return conn.execute(sql).fetchall()
        finally:
            conn.close()


if __name__ == "__main__":
    agent = QueryAgent()
    print(agent.ask("北京有多少次中止的路测?"))

注意两个生产级设计细节:

  1. 召回不足的兜底 :让模型能说"表不够"(NEED_MORE_TABLES),触发放大检索。这是从"盲目生成"到"知之为知之"的关键 ------ 第 06 章的自愈会大量用到这种"承认失败"的信号。
  2. k 是可调超参:k 太小漏表,太大回到噪声问题。生产要按库规模和评估集调。

召回质量的进阶技巧(生产必备)

单纯向量召回在生产往往不够,常见增强:

  • 列级召回:不只召回表,还召回具体列(宽表 200 列时,只给相关列)。
  • 关系补全 :召回了 interventions 但漏了 drives,用外键图自动把中间表补上,否则 JOIN 断链。
  • 混合检索:向量召回 + 关键词/BM25,再融合排序(术语精确匹配 + 语义泛化)。
  • 业务词典:"加权接管量→SUM(takeover_count*risk_weight)"这类映射,用同义词表把问题改写后再检索。

这些不必现在全实现,但要知道它们存在 ------ 它们是问数准确率从 60% 到 90% 的关键。

本章小结

  • 大库场景必须从"全量注入"转向"检索后注入"。
  • 检索文档里带上样例值,能让业务词命中相关表。
  • 让模型能表达"召回不足",是鲁棒性的重要信号。
  • 生产要做列级召回、关系补全、混合检索、业务词典。

【开放性问题 · 生产案例】

一家自动驾驶路测数据平台的问数系统在演示时准确率 90%,上线后掉到 55%。排查发现:

  1. 用户爱用行话:"把 MPI 拉一下""看看接管率" ------ 向量检索完全召回不到相关表,因为表名字段名都是英文技术命名(miles_per_interventiondisengagement_rate)。
  2. 有张"路测数据万能宽表"250 个字段,几乎每个问题都召回它,但模型在 250 列里选错列。
  3. 安全团队和运营团队对"接管"的定义不同(人工主动接管 vs 系统降级接管),同一个词该召回不同的表。

请思考:

  • 问题 1 该在检索前 还是检索后解决?"业务词典/问题改写"应该放在流程的哪一步?
  • 问题 2 说明"表级召回"粒度不够,你会怎么设计"列级召回"?宽表的列要不要也向量化?
  • 问题 3 触及了一个更深的问题:同一个词在不同角色/场景下语义不同。这还只是检索能解决的吗?(提示:这需要"上下文"------第 07 章多轮对话会带来用户角色信息)

下一章:04 --- Few-shot 与生成质量,解决"SQL 语义写不对"的准确率问题。

ruby 复制代码
项目地址:https://github.com/qiuqiuqiu123/text2sql-agent.git 
如果你也在做 AI Agent 编排相关的事情,欢迎交流。 
微信公众号「夜猫子agent工坊」 
感谢小木API的赞助:https://xiaomuai.cn
相关推荐
Artech1 小时前
[MAF预定义的AIContextProvider-05]CompactionProvider——采用多种策略压缩对话历史
ai·agent·maf·aicontextprovider·compactionprovider
前端双越老师1 小时前
AI 编程普及,初学者还要不要学习基础语法?
agent·ai编程·全栈
程序猿DD2 小时前
国内模型供应商缓存与可用性实测:谁更适合跑 Agent?
人工智能·缓存·agent
SoaringPigeon2 小时前
世界动作模型(WAM)综述:从 VLA 到具身智能的下一前沿
人工智能·机器人·自动驾驶
Mintimate11 小时前
从 Git 贡献记录到人设画像报告:AI Stats Agent 的一次全栈实践
github·边缘计算·agent
带刺的坐椅12 小时前
用 Solon AI ReActAgent 落地智能客服工单处理
java·ai·llm·agent·solon·react-agent
努力搬砖的咸鱼15 小时前
如何开始使用agent
ai·agent·ai编程
Jackson__16 小时前
为什么 Agent 越聊越慢?聊聊 Context(上下文)管理
前端·agent·ai编程
Esaka_Forever18 小时前
是什么把工作流(workflow)和智能体(agent)区分开来?
agent