ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Text2SQL 语义解析中的自适应 Few-Shot 召回:基于向量语义相似度动态组装黄金案例 Prompt

Text2SQL 语义解析中的自适应 Few-Shot 召回:基于向量语义相似度动态组装黄金案例 Prompt

在基于大语言模型(LLM)构建企业级 Text2SQL 系统的工程实践中,如何编写 Prompt 是决定最终生成质量的关键生命线。

在最初的技术探索中,团队往往习惯使用零样本提示(Zero-Shot)或静态少样本提示(Static Few-Shot):

  • Zero-Shot 的困境:仅提供 Schema 表结构,大模型在面对复杂的业务缩写、特殊方言函数(如 ClickHouse 的groupArray、DuckDB 的list_transform)以及特殊的连接谓词时,推理成功率通常只能在 60% 左右徘徊;
  • 静态 Few-Shot 的局限:在 Prompt 中固定写死 3 到 5 个示范案例(Examples)。当用户提问与这几个固定案例结构相似时,效果确实不错;但一旦用户的提问切换了主题(例如固定案例是查订单,而用户提问涉及退款风控与用户留存),固定案例不仅无法提供正向启发,反而会强行抢占宝贵的上下文窗口(Context Window),甚至诱导模型产生负迁移幻觉!

为了让大模型在面对任何千奇百怪的提问时都能拥有“恰如其分”的高质量参考范例,现代顶尖的 Text2SQL 架构普遍转向了自适应少样本动态召回系统(Adaptive Dynamic Few-Shot Retrieval)。

本文将深入拆解如何结合双塔语义向量嵌入(Dense Embedding)、SQL 骨架结构相似度(Structural Skeleton Similarity)与向量数据库(Vector DB),在数十毫秒内为当前提问动态组装出信噪比最高的黄金案例 Prompt。


一、 自适应动态召回的系统架构拓扑

动态 Few-Shot 召回的核心思想是:“千人千问,量身定制案例库”。

flowchart TD UserQuery[用户实时提问: '统计近7天退款率最高的前10个商品'] --> Embed[Query 向量化编码器: BGE-large / OpenAI Text-Embedding] subgraph 黄金案例资产知识库 (Gold Cases Repository) CaseLib[(向量数据库: Milvus / Qdrant<br/>含 2000+ 经人工校验的高难度精准 SQL 范例)] end Embed --> AnnSearch[近似最近邻向量检索 ANN Top-20] CaseLib --> AnnSearch AnnSearch --> Rerank[第二阶段: 领域结构重排器 (Reranker)] subgraph 重排考量维度 Rerank --> D1[1. 表名与实体覆盖重合度 Jaccard] Rerank --> D2[2. 意图类型匹配度: 聚合/排序/Top-N] Rerank --> D3[3. 多样性覆盖 MMR: 避免案例同质化] end Rerank --> TopK[精准挑选最相似的 Top-3 黄金范式] TopK --> Assembly[动态组装进系统 Prompt 上下文] Assembly --> LLM[送入大语言模型进行精准推理]

为什么不能仅靠纯文本向量相似度?

在 Text2SQL 场景中,仅靠语义相似度常常会“翻车”:
例如用户提问:“查询销量最高的用户”,纯文本向量模型可能会召回“查询退款最高的用户”的案例。虽然自然语言很像,但如果用户当前问题实际上需要用到深层窗口函数或多表 JOIN,而召回的案例却是一个简单的单表聚合,就无法提供核心的语法骨架指导。
因此,必须实施**“密集向量初筛(Dense Retrieval) + 语法结构与领域重排(Domain Reranking)”**的双阶段管道。


二、 黄金案例库的数据结构设计

案例库中存储的绝不是单纯的“问题-SQL”键值对,而是经过高度结构化标注的元数据单元:

{ "case_id": "gold_case_0089", "natural_language_question": "统计上个月各省份 VIP 客户的平均实付客单价", "canonical_sql": "SELECT u.province, round(avg(o.pay_amount), 2) AS avg_pay FROM fact_orders o JOIN dim_user u ON o.user_id = u.id WHERE u.vip_level >= 1 AND o.pay_time >= date_trunc('month', now() - INTERVAL 1 MONTH) AND o.pay_time < date_trunc('month', now()) GROUP BY u.province;", "involved_tables": ["fact_orders", "dim_user"], "sql_keywords": ["JOIN", "GROUP_BY", "AVG", "DATE_TRUNC"], "sql_complexity_score": 3, "embedding_vector": [0.0124, -0.0452, 0.0891, "... 共 1024 维"] }

三、 自适应召回与重排核心代码实现

以下展示基于 Python、向量检索与最大边际相关性(MMR / Maximal Marginal Relevance)多样性重排的动态 Few-Shot 组装器:

import numpy as np from typing import List, Dict, Any class DynamicFewShotRetriever: def __init__(self, vector_db_client, embedding_model, gold_cases_metadata: List[Dict]): self.vector_db = vector_db_client self.embedder = embedding_model self.cases = {c["case_id"]: c for c in gold_cases_metadata} def retrieve_adaptive_few_shots(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]: """ 动态检索与当前提问最相关、且最具结构多样性的 Top-K 黄金案例 """ # 步骤 1: 将用户提问编码为紧凑语义向量 query_vector = self.embedder.encode(query) # 步骤 2: 在向量数据库中执行 ANN 近似最近邻检索 (初筛候选集 Top-15) raw_candidates = self.vector_db.search(query_vector, limit=15) # 步骤 3: 基于最大边际相关性 (MMR) 进行二次多样性重排 # 目标: 既要与 Query 高度相似,又要让选出的 3 个案例涵盖不同的 SQL 语法模式 (避免 3 个全是一模一样的单表 SUM) selected_cases = self._maximal_marginal_relevance_rerank( query_vector, raw_candidates, target_k=top_k, lambda_param=0.7 ) return selected_cases def _maximal_marginal_relevance_rerank(self, query_vec: np.ndarray, candidates: List[Dict], target_k: int, lambda_param: float = 0.7) -> List[Dict]: """ MMR 算法实现: 权衡相关性与案例之间的多样性 """ selected = [] candidate_pool = [self.cases[c["id"]] for c in candidates] while len(selected) < target_k and len(candidate_pool) > 0: best_score = -float("inf") best_case = None for cand in candidate_pool: # 计算候选案例与 Query 的语义相似度 cand_vec = cand["embedding_vector"] sim_to_query = np.dot(query_vec, cand_vec) / (np.linalg.norm(query_vec) * np.linalg.norm(cand_vec)) # 计算候选案例与已经入选案例的最大相似度 (惩罚冗余) if len(selected) == 0: redundancy_penalty = 0.0 else: redundancy_penalty = max([ np.dot(cand_vec, s["embedding_vector"]) / (np.linalg.norm(cand_vec) * np.linalg.norm(s["embedding_vector"])) for s in selected ]) # MMR 综合打分公式 mmr_score = lambda_param * sim_to_query - (1 - lambda_param) * redundancy_penalty if mmr_score > best_score: best_score = mmr_score best_case = cand if best_case: selected.append(best_case) candidate_pool.remove(best_case) return selected

四、 动态 Prompt 组装范式

当提取出 Top-3 黄金案例后,组装器将其动态渲染进发送给大模型的 Prompt 模板中:

你是一位资深的精通 ClickHouse 与 StarRocks 的数据分析专家。 根据用户的问题和给定的数据表 Schema,生成标准执行 SQL。 【参考历史相似业务的最佳实践范例】: 以下是经过数仓架构委员会严格验证的相似问题黄金执行范例,请仔细参考其语法组织模式(包括 JOIN 拓扑与特殊函数使用范式): [范例 1]: 问题: "统计上个月华东大区退款率前 5 的店铺" SQL: ```sql SELECT shop_id, round(sum(refund_amt) / sum(pay_amt), 4) AS refund_rate FROM dwd_trade_orders WHERE region = 'EAST_CHINA' AND event_date >= subtractMonths(now(), 1) GROUP BY shop_id ORDER BY refund_rate DESC LIMIT 5;

SQL:

SELECT DISTINCT user_id FROM ( SELECT user_id, event_date, leadInFrame(event_date, 2) OVER (PARTITION BY user_id ORDER BY event_date) AS lead_date FROM dws_user_active_daily ) WHERE dateDiff('day', event_date, lead_date) = 2;

【当前待处理的用户提问】:
"{user_query}"

请依据上述黄金范例的设计思路,输出最优化、无歧义的 SQL:

--- ## 五、 生产实测收益与总结 在某金融级数仓的实战评测集中,对比不同 Few-Shot 策略的端到端表现: | 提示词策略 (Prompt Strategy) | 复杂 SQL 首次执行成功率 | 字段幻觉发生率 | 平均 Prompt Token 消耗 | | :--- | :--- | :--- | :--- | | **Zero-Shot (纯 Schema)** | 58.4% | 18.2% | **850 tokens (最少)** | | **固定 Static Few-Shot (5 案例)** | 72.1% | 11.5% | 2,400 tokens (严重浪费) | | **自适应动态 Few-Shot (Top-3 MMR)** | **91.6% (大幅跃升)** | **2.3% (极低)** | **1,350 tokens (高性价比)** | **核心启示**: 在 Text2SQL 的系统工程中,**给大模型喂一堆无关的冗余案例,等同于在它的工作记忆里倾倒垃圾**。 通过构建**基于稠密向量检索与 MMR 多样性重排的自适应动态 Few-Shot 机制**,系统能够精准命中与当前业务意图最契合的物理语法骨架,以极小的 Token 开销撬动最大化的生成精确度,让 Text2SQL 系统真正具备工业级的自适应进化能力。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进