首页
看点啥
插画图片
首页 看点啥 第 03 章 — Schema 检索(应对大库)

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

2026-07-14 0

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

上一章的痛点

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

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

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

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

核心原理:语义检索

思路和 RAG 一样:

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

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

层级设计

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

 复制代码L2  SQL 生成 ← 只拿到相关的 K 张表

L1  SchemaRetriever(本章)← 向量召回
      ▲
     SchemaProvider(第 02 章)

构建可嵌入的表文档

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

retriever.py

 复制代码"""L1: Schema 检索 —— 大库场景只召回相关表。"""
from dataclasses import dataclass
import anthropic
from schema import Table, extract_schema, render_schemaclient = 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 追加:

 复制代码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 也能演示效果(生产不要用,仅用于本地体验流程):

 复制代码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

 复制代码"""v3: 带 Schema 检索的问数器。"""
import sqlite3
import anthropic
from schema import extract_schema, render_schema
from retriever import SchemaRetriever  # 或 KeywordRetrieverclient = 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 太小漏表,太大回到噪声问题。生产要按库规模和评估集调。

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

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

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

本章小结

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

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

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

请思考:


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

 复制代码项目地址:https://github.com/qiuqiuqiu123/text2sql-agent.git 
如果你也在做 AI Agent 编排相关的事情,欢迎交流。 
微信公众号「夜猫子agent工坊」 
感谢小木API的赞助:https://xiaomuai.cn
喜欢(0)

上一篇

远程MCP实战:高德地图 Chrome DevTools File System 三大 Server 联动打造 AI 超级 Agent

远程MCP实战:高德地图 Chrome DevTools File System 三大 Server 联动打造 AI 超级 Agent

下一篇

从零搭建MCP Server:让LLM 拥有工具调用能力

从零搭建MCP Server:让LLM 拥有工具调用能力
猜你喜欢