问数助手写 SQL 跑偏,常因 Schema 给错而非模型差。改接 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end),把金仓几十张表全塞给 Agent 的老办法就可以丢掉,越权表也不再进上下文。再由 KFS MCP Server 的 search_schema 决定模型该看哪些 Schema。
这个组合解决两件事:Schema 检索的边界由 search_schema 控制,底层模型调用统一走它这一条 API 通道,模型 ID 从官网模型广场选,Token 消耗也能在控制台对账。下面按我实际踩通的链路一步步说。
1. 问数助手跑偏的根因:SQL 没写错,是 Schema 给错了
用户问“最近 30 天各渠道实付金额 Top5”,金仓库里可能同时躺着 ods_order_raw、dwd_trade_order_detail、dws_channel_trade_day、ads_channel_gmv、finance_payment_record、channel_dict。模型拿到全量 DDL 后,要同时判断:哪张表代表“订单”;“实付金额”是 amount、pay_amount、paid_amount 还是 net_gmv;“渠道”字段是订单表里的快照,还是要 JOIN 渠道维表;30 天按 created_at、paid_at 还是业务日期过滤;当前用户到底有没有权限看某张表。
这一连串问题已经不是“SQL 生成”,而是 Schema Linking 加业务语义理解再加权限判断。全量 DDL Prompt 在这里有三处硬伤。
第一处,上下文噪声大。几百张表时,同义表、历史表、临时表会制造大量错误候选,模型看到的信息越多,反而越容易被带偏。第二处,Token 成本高。几十张表的 DDL 动辄几千上万 Token,真正留给问题分析、SQL 修正和结果解释的上下文被挤掉。第三处,也是最容易被忽视的:Schema 本身是敏感信息。运营用户没有薪资表权限,就不该让 employee_salary 这种对象名先进模型上下文,然后再指望它“不要查”。
所以更合适的前置链路是:自然语言问题 → Schema 检索 → 权限裁剪 → Schema 压缩 → SQL 生成 → SQL 执行防护。KFS MCP Server 在这一步承担的不只是 SQL 执行代理,更是 Schema Retrieval Gateway。而 Agent 这一层用哪个模型通道,直接决定前两步的成本和稳定性。
2. 金仓元数据目录:从 DDL 文本升级成四层可检索资产
2.1 示例业务库
还是用一个容易复现的订单域:orders(channel_id, paid_amount, status, paid_at)、channels(channel_id, channel_name),再加一张运营角色不可见的 employee_salary(employee_id, salary)。后面所有实验都在这个基础上做。
2.2 四层元数据设计
把 SHOW CREATE TABLE 的文本直接做 Embedding,只能算第一版。DDL 只表达物理结构,不表达业务含义。一个可用的元数据目录我建议至少分四层。
L1 物理元数据:database、schema、table、column、type、主外键、索引、分区字段,回答“数据在哪、表怎么连”。L2 语义元数据:表中文名、字段中文名、业务描述、同义词、枚举说明、指标口径、时间字段语义。比如 paid_amount 的业务名是“实付金额”,同义词有“支付金额、成交金额、已支付金额”,口径是“订单支付成功后金额,不含取消订单”。用户说“销售额”时,纯字段匹配搜不到 paid_amount,语义层可以补上这层映射。
L3 使用元数据:最近 30 天查询频次、表间 JOIN 共现频率、高频过滤字段、已验证 SQL 样例、指标常用维度。orders 和 channels 在历史正确 SQL 里经常通过 channel_id 关联,那么两表同时被召回时,这条关系的重排分数就该提高。L4 安全元数据:数据分级、可访问角色、敏感字段、脱敏规则、行级过滤策略。权限裁剪必须发生在这个阶段,而不是等 SQL 生成后再处理。
金仓里可以落一张目录表,把上面四层打平成可检索的最小结构:
CREATE TABLE kfs_schema_catalog (
object_id BIGINT PRIMARY KEY,
db_name VARCHAR(128) NOT NULL,
schema_name VARCHAR(128) NOT NULL,
table_name VARCHAR(128) NOT NULL,
column_name VARCHAR(128),
object_type VARCHAR(16) NOT NULL,
biz_name VARCHAR(256),
description TEXT,
synonyms TEXT,
data_type VARCHAR(64),
sensitivity VARCHAR(32),
allowed_roles TEXT,
usage_score DECIMAL(10,4) DEFAULT 0,
embedding_text TEXT,
updated_at TIMESTAMP NOT NULL
);
这张表不是一次性建完就完事。它要跟着 DDL 变更同步更新,否则 Agent 看到的是过期 Schema,后续所有检索都是白做。
3. 复现“全量 DDL Prompt”:三类失败与一道隐藏账单
3.1 基线方案
基线做法很简单:系统提示词写成“下面是数据库完整 Schema:{{all_ddl}},请根据用户问题生成 SQL”。两三张表时效果看着还行,问题是不具备扩展性。我构造了 30 张左右订单、支付、渠道、财务近义表,又设计了 60 个自然语言问题,包含原名、中文业务名、同义词、隐式维度、跨表 JOIN 和无权限请求。
3.2 第一类失败:召回错误表
用户说“成交金额”,模型可能选中 finance_payment_record.payment_amount,但业务真正认的是 orders.paid_amount。SQL 语法完全正确,结果口径错了。这类问题 SQL Parser 根本发现不了。
3.3 第二类失败:JOIN 关系靠猜
只给表和字段、不给关系说明时,模型可能写出 JOIN channels c ON o.order_id = c.channel_id。字段类型可能一样,数据库能执行,结果却是完全错误的明细。
3.4 第三类失败:Schema 泄露
全量 DDL 进系统提示词后,即使执行层最后拒绝了 SELECT employee_id, salary FROM employee_salary,employee_salary 这个对象本身已经暴露给模型了。对普通运营用户,正确行为是 Schema 检索阶段就不返回这张表。
这里还有一道隐藏账单:一次全量 DDL 请求就可能烧掉几千 Token,若模型反复猜错、来回改 SQL,一个问题烧掉上万 Token 是常有的事。这也是我会在 4.4 节把模型通道切到 TaoToken 的原因——不是官方额度不够用,而是 Schema 没受控前,任何模型都容易在错误信息上浪费大量调用。
4. 把 Schema Retrieval 做成 KFS MCP Server 的 search_schema
4.1 为什么独立成 search_schema
我不建议让 Agent 自己连元数据库做向量检索。更好的做法是 KFS MCP Server 只暴露一个 search_schema(query, role, top_k),模型不知道底层是 Elasticsearch、pgvector 还是倒排索引,它只负责调用。核心代码里最重要的一行不是排序,而是先按角色过滤再检索:
def search_schema(query: str, role: str, top_k: int = 5):
visible = [item for item in CATALOG if role in item["allowed_roles"]]
ranked = sorted(visible, key=lambda item: hybrid_score(query, item), reverse=True)
return {"tables": ranked[:top_k], "relations": find_relations(ranked[:top_k])}
先过滤再检索,敏感对象根本不会进入日志、重排上下文或模型中间推理。
4.2 五阶段检索流水线
阶段一,Query 理解:从“最近 30 天各渠道实付金额 Top5”里抽出 metric=实付金额、dimension=渠道、time_range=最近30天、sort=DESC、limit=5。常见问数问题用规则加小模型就能完成,不必每次请大模型。
阶段二,粗召回:BM25 加 Embedding 混合,而不是纯向量。order_id、sku_id、channel_id 这些标识符对关键词检索很友好,“成交额、销售额、实付金额”这类语义匹配更适合向量。示例权重是 score_recall = 0.5 * bm25_score + 0.5 * vector_score,实际要在自己的问数集上调。
阶段三,关系扩展:命中了 orders 还不够,用户要按渠道名称分组,就需要补 channels。粗召回之后做图扩展,orders.channel_id → channels.channel_id,避免模型只拿到订单表以后自己猜维表。关系可以单独存一张表:
CREATE TABLE kfs_schema_relation (
left_table VARCHAR(128),
left_column VARCHAR(128),
right_table VARCHAR(128),
right_column VARCHAR(128),
relation_type VARCHAR(32),
confidence DECIMAL(5,4)
);
这里不光存物理外键,也存 DBA 或数据治理人员确认过的逻辑关系。
阶段四,重排:综合语义、词面、使用频率、关系强度、业务域几个维度打分。权限不要作为权重项参与加权,而是硬过滤:permission 为 false 直接丢弃。不然向量召回阶段敏感对象已经过了模型,再删只是亡羊补牢。
阶段五,Schema 压缩:召回了 5 张表,也不意味着把 5 张表完整 DDL 全给模型。对这个问题,只需输出 orders 的 channel_id、paid_amount、status、paid_at,channels 的 channel_id、channel_name,JOIN 条件 orders.channel_id = channels.channel_id,口径“销售额使用 paid_amount,仅统计 status='PAID'”。
4.3 提示词模板:先调用 search_schema,再写 SQL
最终 SQL Agent 的系统提示词不应该再出现固定全量 DDL,而是约束工具流程:
你是金仓库内的问数助手。接到问题后,第一步永远是调用 KFS MCP Server 的 search_schema。只能使用 search_schema 返回的表、字段、关系和指标口径;不能猜测未返回的表名或字段名;如果信息不足,继续调用 search_schema 或明确告诉用户缺少哪部分元数据,绝不编造。SQL 生成后交给只读执行工具或 DBA 在客户端执行,不要自行连接生产库。
Prompt 负责告诉模型“工作流程是什么”,KFS MCP Server 负责保证“边界真的存在”。
4.4 接入配置:Claude Code 的模型通道指到 TaoToken
有了上面的流程,最后一步是把 Agent 底层模型的调用地址指到 TaoToken。先在 TaoToken 创建 API Key(占位符 YOUR_API_KEY),并确认模型广场里现在可用的模型 ID;然后编辑 ~/.claude/settings.json 的 env 部分:
{
"env": {
"ANTHROPIC_BASE_URL": "https://taotoken.net/api",
"ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY",
"ANTHROPIC_MODEL": "MODEL_ID_FROM_TAOTOKEN"
}
}
ANTHROPIC_MODEL 的值不要照抄,去模型广场复制当前可用的模型 ID,我这里不替你写死。Base URL 只填 https://taotoken.net/api,末尾不要加 /v1。如果你在用 Codex,则在 ~/.codex/config.toml 里给 model_provider 配 base_url,也指向同一个地址。
这样问数助手仍然通过 KFS MCP Server 调 search_schema,Schema 检索逻辑一行不用改;变的只是模型通道,以及每次调用产生的 Token 账单落到 TaoToken 控制台。全量 DDL 那种“把整库丢给模型”的做法,在接入后会被 search_schema 的返回结构天然卡住:模型再也拿不到未返回的表名。
4.5 MCP 工具调用示例
用户问“最近 30 天各渠道实付金额 Top5”,Agent 先发起一次 search_schema:
{
"name": "search_schema",
"arguments": {
"query": "最近30天各渠道实付金额Top5",
"role": "ops",
"top_k": 5
}
}
返回结构大致是这样,注意里面已经带上了业务名、关系和口径信息:
{
"tables": [
{
"table": "orders",
"biz_name": "订单事实表",
"columns": [
["channel_id", "渠道编号"],
["paid_amount", "实付金额"],
["status", "订单状态"],
["paid_at", "支付时间"]
]
},
{
"table": "channels",
"biz_name": "渠道维表",
"columns": [
["channel_id", "渠道编号"],
["channel_name", "渠道名称"]
]
}
],
"relations": [
{
"left": "orders.channel_id",
"right": "channels.channel_id",
"type": "FK"
}
]
}
5. 结果对比:既要 SQL 正确率,也要 Schema Token 账单
5.1 检索层指标
Table Recall@K:标准答案需要 orders 和 channels,Top5 全包含才算命中。Column Recall:表对了但漏掉 paid_at,最近 30 天过滤仍无法完成。Relation Recall:需要 JOIN 的问题,要检查正确关系有没有进入 Schema Context。还有安全指标 Unauthorized Exposure Rate,即越权对象暴露次数除以 Schema 检索总次数,生产目标必须是 0。
5.2 SQL 端到端指标
只测检索和标准表名一致还不够,最终要看 SQL 是否可执行、执行是否准确、口径是否对、Schema 平均消耗多少 Token。下面是复现实验示意数据,不是生产成绩:
| 指标 | 全量 DDL Prompt | search_schema |
|---|---|---|
| 必要表召回率(Top5) | 不适用 | 92% |
| 平均 Schema Token | 9,600 | 1,280 |
| SQL 可执行率 | 83% | 96% |
| 执行正确率 | 71% | 89% |
| 越权 Schema 暴露 | 存在 | 0 |
最值得关注的是后两行:Token 降低是成本收益,SQL 执行正确率上升和越权暴露归零,才是 Schema Retrieval 愿意单独做成一层系统的根本原因。
6. 风险与复盘:Schema RAG 做不好,会把错误变得更隐蔽
6.1 召回错误后模型会自信地写错
全量 DDL 至少还让模型在几十张表里重新选择;Schema 检索一旦把真正目标表过滤掉,模型通常会从剩下的候选里挑一个“看起来最合理”的。系统必须允许 search_schema(query, top_k=5) 后信息不足,再 search_schema(rewrite_query, top_k=10),而不是强制“一次检索后必须生成 SQL”。
6.2 Embedding 不等于业务口径
向量模型可能觉得“支付金额”约等于“销售额”约等于“GMV”,但这几个指标在企业内部可能完全不是一个数。高价值指标不能只靠向量相似度,要在元数据层写明:支付GMV 对应 orders.paid_amount,过滤 status='PAID',退款要另行计算。必要时直接封装成指标工具,别让模型自由发挥。
6.3 权限过滤顺序错误
正确顺序是身份确认→权限过滤→检索→重排→返回。危险顺序是全库向量召回→把候选发给模型排序→最后删掉无权限表。后者已经构成 Schema 泄露。
6.4 工具注解不能替代授权
MCP 工具可以描述自己“只读”“无破坏性”,这些注解是风险语义,不是安全边界。即使工具名叫 search_schema_readonly,服务端仍要执行 user→role→domain→table→column 的确定性权限判断。
6.5 元数据会过期
表结构加了 paid_at,向量库里仍只有 created_at,Agent 会持续产生错误 SQL。元数据平台至少要有 DDL 变更同步、索引版本号、updated_at、Embedding 重建状态和 Schema Cache TTL。
6.6 复盘与验证
经过这轮改造,我更愿意把企业内部 Text-to-SQL 拆成三层:Schema Retrieval 回答“该让模型看到什么”,SQL Generation 回答“该写什么 SQL”,SQL Execution Guard 回答“这条 SQL 能不能执行”。KFS MCP Server 承载第一层和第三层,AI Agent 只负责中间的推理和工具编排,模型通道则统一从这一条 API 通道进出。
验证时我先记下控制台初始用量,再跑 10 个问数问题,每个问题都强制先 search_schema 再生成 SQL,最后回金仓客户端执行并把结果贴回对话。回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 刷新,这次调用的 Token 数和模型 ID 都记在账上。如果看到 401,先确认 YOUR_API_KEY 是否在官网正确创建;如果是模型相关报错,回到模型广场核对 ANTHROPIC_MODEL 是否复制完整;如果报地址 404,检查 Base URL 是不是多加了一个 /v1——https://taotoken.net/api 末尾不带 /v1。
真正可靠的问数助手,不应该让模型先看完整数据库再努力“克制自己”。在模型写下第一行 SQL 之前,只把正确、必要、当前角色有权使用的 Schema 放到它面前,这才是 search_schema 这一层存在的意义。而模型通道统一走 TaoToken,只是让这条链路在成本和可观测性上变得真正可控。




被折叠的 条评论
为什么被折叠?



