tpf.cn.Embedding
``` import sys sys.path.insert(0, r'D:\wks\aitpf\src') import tpf print(tpf.__file__) import os from openai import OpenAI from dotenv import load_dotenv, find_dotenv # 加载 .env 到环境变量,自动读取 OPENAI_API_KEY 和 OPENAI_BASE_URL _ = load_dotenv(find_dotenv(filename="../env.txt")) ``` DASHSCOPE_API_KEY="sk-a4511..." DASHSCOPE_URL_BEIJING="https://dashscope.aliyuncs.com/compatible-mode/v1" 文件默认三级加载 - 当前目录.env - 当前目录env.txt - /wks/app/conf/env.txt
``` from tpf.cn import sb # 默认 DASHSCOPE 配置 + qwen3.7-text-embedding vec = sb.embed("人,背道而驰,越刻意越心累") # 单条文本 -> List[float] type(vec),len(vec),vec[:3] (list, 1024, [0.015067178755998611, -0.01444337796419859, -0.026895392686128616]) vecs = sb.embed(["知足常乐", "知止不殆"]) # 批量 -> List[List[float]],顺序与输入一致 type(vecs),len(vecs),vecs[0][:3] (list, 2, [0.02194032073020935, 0.029327882453799248, 0.01076632272452116]) ``` ``` score = sb.similarity("知足常乐", "知止不殆") # 余弦相似度 score 0.5519020822701092 top = sb.most_similar("清心", ["少思", "寡欲", "返璞","归真"], top_k=2) top [(0, '少思', 0.5132988199340213), (3, '归真', 0.4444539469701941)] ```
- 切换模型 ``` # 切换模型 / 供应商参数,其余参数通过 **kwargs 透传 vec = sb.embed("文本", model="text-embedding-v4", dimensions=1024) type(vec),len(vec),vec[:3] (list, 1024, [-0.06052623689174652, -0.00662272097542882, 0.057755667716264725]) ```
torch nn.Embedding 举例
**依据最大索引转向量:在最后一维上增加一个维度,将索引转化为一个浮点向量 import torch from torch import nn embedding = nn.Embedding(num_embeddings=8, embedding_dim=3, padding_idx=0) index = torch.tensor([1,3,5,7]) print(embedding(index).shape) #torch.Size([4, 3]) import torch from torch import nn embedding = nn.Embedding(num_embeddings=8, embedding_dim=3, padding_idx=0) index = torch.tensor([[1,3,5,7],[1,3,0,0]]) index=index.permute(1,0) print(embedding(index).shape) #torch.Size([4, 2, 3]) manual_seed import torch from torch import nn # 固定随机种子(关键步骤) torch.manual_seed(42) # 可以是任意整数,但选定后要保持一致 embedding = nn.Embedding(num_embeddings=8, embedding_dim=3, padding_idx=0) # 此时 embedding 的权重是固定的 index = torch.tensor([1, 3, 5, 7]) embed = embedding(index) print(embed.shape) # torch.Size([4, 3]) embed torch.Size([4, 3]) tensor([[-2.1055, 0.6784, -1.2345], [-0.6866, -0.4934, 0.2415], [-0.2168, -0.3097, -0.3957], [-0.0631, -0.8286, 0.3309]], grad_fn=EmbeddingBackward0) 直接初始化 nn.Embedding 的权重(更精确控制) import torch from torch import nn # 自定义固定的初始化权重(示例:简单递增的值) fixed_weight = torch.tensor([ [0.0, 0.0, 0.0], # padding_idx=0 的向量(全零) [0.1, 0.1, 0.1], # index=1 [0.2, 0.2, 0.2], # index=2 [0.3, 0.3, 0.3], # index=3 [0.4, 0.4, 0.4], # index=4 [0.5, 0.5, 0.5], # index=5 [0.6, 0.6, 0.6], # index=6 [0.7, 0.7, 0.7], # index=7 ]) embedding = nn.Embedding(num_embeddings=8, embedding_dim=3, padding_idx=0) embedding.weight.data = fixed_weight # 直接赋值 index = torch.tensor([1, 3, 5, 7]) embed = embedding(index) print(embed.shape) # torch.Size([4, 3]) print(embed) 关键点说明 torch.manual_seed(seed) 设置随机种子后,nn.Embedding 的权重初始化会固定,但每次修改代码或运行环境变化时可能仍会不同。 直接赋值 weight.data 完全控制权重值,适合需要可重复实验的场景(例如论文复现)。 padding_idx 的影响 如果指定了 padding_idx(如本例的 0),对应索引的向量会始终为全零,不受随机种子影响。 mytab('ulembedding', 'tbembedding'); nn.Embedding 参数·num_embeddings 模板·对象·升维 随机性与有效性 num_embeddings nn.Embedding中num_embeddings理论上指索引的个数, 但这有个前提,索引编码从0开始,并且一个连一个, 不能跳跃,一般情况下也不会跳跃,但跳跃也没有关系 实际上num_embeddings指提供的索引中的最大值 比如,本例如果num_embeddings低于8将报错,而实际上的索引个数只有4个 正常情况下索引编码都是从0开始,也无跳跃 nn.Embedding模板类生成的对象 import torch from torch import nn embedding = nn.Embedding(num_embeddings=6,embedding_dim=3,padding_idx=0) batch_vec = torch.arange(start=0,end=6,step=1).reshape(2,3) embedding(batch_vec).shape # torch.Size([2, 3, 3]) 该对象将索引矩阵升了一维,所升的这一维就是索引所转向量的维度, pytorch的维度是这样的(dim=0,dim=1), 升的这一维默认放在更高的维度,就是(dim=0,dim=1,dim=2) 随机性与有效性:起点是随机的,终点是有效的(但不一定是最好的,) import torch from torch import nn embedding = nn.Embedding(num_embeddings=6,embedding_dim=3,padding_idx=0) batch_vec = torch.arange(start=0,end=6,step=1).reshape(2,3) embedding(batch_vec) tensor([[[ 0.0000, 0.0000, 0.0000], [-0.9986, 0.2995, -1.3050], [-0.8627, 0.4689, -0.1170]], [[-0.3458, 1.2517, -0.7372], [ 0.4307, -0.3824, 0.1640], [-1.2211, 1.0763, 1.4638]]], grad_fn=EmbeddingBackward0) AI所要训练的参数是在定义nn.Embedding模板类时初始化的 nn.Embedding模板类在初始化参数时,是随机的, 这样随机地将一个整数索引转换为n维浮点向量,有效吗? 其实它的思想跟神经网络层的思想是一样的,参数随机初始化, 在训练时梯度下降,逐步向不随机的方向,就是标签的方向靠近 随机当然是无效的,训练后才有效 mytab('ulem_showme', 'tbem_showme'); 索引Embedding 封装示例 IndexEmbed算法模型 ClsIndexEmbed算法模型 import numpy as np from tpf.nlp import IndexEmbed embed = IndexEmbed() index = np.array([1,2,3]) embed.embedding(index,embedding_dim=2) tensor([[ 0.7980, -0.0897], [ 0.0561, 0.2836], [-0.5602, 1.4158]]) embed.embedding(index,embedding_dim=5) tensor([[-0.8093, -2.0782, -1.1509, 0.5586, -3.9801], [-0.6414, -1.3621, 0.1242, -1.0882, 0.4275], [ 0.5396, -0.7445, 0.6825, 0.5504, -1.4078]]) embedding_dim 映射到6维及以上维度时,还需要指定 num_embeddings 或者说,需要更加灵活的映射关系时,指定embedding_dim,num_embeddings 本功能的关键是 - 记录参数文件,格式为 f"embed_{num_embeddings}_{embedding_dim}" - 第二次加载时,相同的索引会生成相同的向量 embed.embedding(index, embedding_dim=6, num_embeddings=100000) 指定文件前缀 import numpy as np from tpf.nlp import IndexEmbed embed = IndexEmbed(file_pre="/tmp/embed") index = np.array([1,2,3]) embed.embedding(index,embedding_dim=2) 生成文件 $ ls -ltrh /tmp/embed_* -rw-rw-r-- 1 llm llm 2.1K Jul 4 17:15 /tmp/embed_9_2 -rw-rw-r-- 1 llm llm 3.2K Jul 4 17:15 /tmp/embed_99_3 -rw-rw-r-- 1 llm llm 18K Jul 4 17:15 /tmp/embed_999_4 -rw-rw-r-- 1 llm llm 20M Jul 4 17:15 /tmp/embed_999999_5 -rw-rw-r-- 1 llm llm 2.3M Jul 4 17:15 /tmp/embed_100000_6 可以看到num_embeddings越大时,参数占用的空间也是越大的 file_pre为None则表示不记录文件 import numpy as np from tpf.nlp import IndexEmbed embed = IndexEmbed() index = np.array([1,2,3]) embed.embedding(index,embedding_dim=2) tensor([[ 0.7980, -0.0897], [ 0.0561, 0.2836], [-0.5602, 1.4158]]) 此时生成随机向量仍然是固定的,因为设置了随机种子,但这种固定没有存储为文件靠谱 - 可能重启服务或修改代码生成的随机向量就变了 添加forward方法 import numpy as np from tpf.nlp import IndexEmbed embed = IndexEmbed() index = np.array([1,2,3]) embed(index) tensor([[ 0.7980, -0.0897], [ 0.0561, 0.2836], [-0.5602, 1.4158]]) embed(index,embedding_dim=3) tensor([[-0.0369, -1.0962, 0.0523], [ 1.1494, 0.5489, 0.6164], [ 0.9435, -0.7968, -2.2954]]) embed(index,embedding_dim=3,num_embeddings=1000) tensor([[-0.0369, -1.0962, 0.0523], [ 1.1494, 0.5489, 0.6164], [ 0.9435, -0.7968, -2.2954]]) ---------------------------------------------------------------------------------- import numpy as np import pandas as pd import torch from torch import nn # 设置随机种子以便结果可复现 np.random.seed(42) columns = ['feature_value_1', 'is_feature_value_535', 'is_feature_value_536'] data = { col: np.random.randint(0, 10, size=5) for col in columns } df = pd.DataFrame(data) df from tpf.nlp import ClsIndexEmbed cls_dim_dict = { 'is_feature_value_535': 3, 'is_feature_value_536': 2 } cie = ClsIndexEmbed() df =cie(df,cls_dim_dict) df 指定文件前缀:如此索引编码永久固定** cie = ClsIndexEmbed(file_pre="/tmp/aa/embed") df =cie(df,cls_dim_dict) df
llm@ii:/opt/wks/aitpf$ ls -ltrh /tmp/aa/ total 20M -rw-rw-r-- 1 llm llm 2.1K Jul 9 16:15 embed_10_2 -rw-rw-r-- 1 llm llm 3.2K Jul 9 16:15 embed_100_3 -rw-rw-r-- 1 llm llm 18K Jul 9 16:15 embed_1000_4 -rw-rw-r-- 1 llm llm 20M Jul 9 16:15 embed_1000000_5
Hash 向量方案(nl2sql 工程兜底 EMBEDDING_PROVIDER=hash)
**来源**:nl2sql 统一检索底座默认 embedding 提供方(`vector_service._hash_embed`)。 一句话:文本拆 **字符 unigram + bigram** → 每个 gram 经 **md5 哈希**映射到 512 维空间的某个维度(带正负号累加)→ **L2 归一化**。无模型、无训练、零依赖、**确定性**(同文本永远同向量)。 | 步骤 | 做什么 | 为什么 | |---|---|---| | 1. 归一化 | 去空白、转小写 | 消除大小写/空格差异 | | 2. 切 gram | 单字 + 相邻两字 | 中文无天然分词,字级 n-gram 是最朴素"伪分词" | | 3. 哈希定位 | md5 前 8 字节取整 `% 512` 定维 | 无限 gram 空间压进固定维度(Hashing Trick) | | 4. 符号累加 | 第 9 字节奇偶定 ±1 | 正负抵消降低碰撞偏置(signed hashing) | | 5. L2 归一化 | 模长缩放到 1 | 此后内积 = 余弦相似度 | **本质**:词面(字面)袋子模型——两句话字重叠越多向量越接近,与语义无关。
与 nl2sql 生产实现完全一致(纯标准库): ```python import hashlib, math, re def hash_embed(text: str, dim: int = 512) -> list: """字符 unigram + bigram -> md5 signed hashing -> dim 维 -> L2 归一化""" v = [0.0] * dim t = re.sub(r"\s+", "", str(text or "")).lower() # 1. 归一化 if not t: return v grams = list(t) + [t[i:i+2] for i in range(len(t) - 1)] # 2. 单字+双字 for g in grams: h = hashlib.md5(g.encode("utf-8")).digest() idx = int.from_bytes(h[:8], "little") % dim # 3. 定维 v[idx] += 1.0 if h[8] & 1 else -1.0 # 4. 符号累加 norm = math.sqrt(sum(x*x for x in v)) # 5. L2 归一化 return [x / norm for x in v] if norm > 0 else v def cosine(a, b) -> float: return sum(x * y for x, y in zip(a, b)) ``` 配套 notebook(已执行):`jupyter/nlp_rag/hash-embedding-方案分析与执行测试.ipynb`
**实验1:相近 vs 无关问题对** ```text cos=+0.4857 | 查一下交易金额最大的10笔交易 <-> 查询交易金额排名前十的交易记录 cos=+0.6423 | 查一下交易金额最大的10笔交易 <-> 交易金额最高的10笔是哪些 cos=+0.2023 | 查询交易金额最大的10笔 <-> 统计每日交易总笔数 cos=+0.2335 | 查询账户余额 <-> 账户里还有多少钱 cos=-0.0836 | 查询账户余额 <-> 机构网点有哪些 (无关→接近 0/负) ``` **实验2:固有缺陷——误报与漏报** ```text [误报] cos=+0.7647 | 查询客户表的记录数 <-> 删除客户表的记录数 (一查一删,字面几乎同) [漏报] cos=+0.0778 | 存款余额是多少 <-> 账户里还剩多少钱 (语义同,字面远) ``` **实验3:模拟 NL2SQL 表检索(全部命中目标表)** ```text Q: 查一下交易金额最大的10笔交易 → test_transactions(+0.2846) ✓ Q: 账户余额合计是多少 → test_accounts(+0.2110) ✓ Q: 哪些机构是二级网点 → test_org_info(+0.3308) ✓ Q: 客户张三的证件号 → cust_info(+0.3476) ✓ ``` **实验5:性能**——100,000 条短文本向量化 10.38s(约 9,633 条/秒,纯 CPU 单线程)。
| 维度 | 表现 | |---|---| | 语义理解 | 无。同义不同词(存款余额/账户还有多少钱)召回差 | | 确定性 | 同文本永远同向量(可缓存、可复现、跨进程一致) | | 依赖 | 零依赖零模型,内网/离线开箱即用 | | 性能 | ~万条/秒/核量级,远超神经网络编码器 | | 中文效果 | 中文字级 bigram 提供一定"伪语义"(词重叠度) | | 适用 | 工程兜底、冷启动、别名冲突检测等**词面相似**即可的场景 | **在 nl2sql 中的角色**:`EMBEDDING_PROVIDER=hash` 是默认兜底——内网无 embedding 模型时, REQ-005/006/007 的"检索式选表/示例过滤"依然可用;语义质量升级路径 = 切 openai 或本地 bge。
常见中文 / 中英文 Embedding 方案(2026 视角)
| 方案 | 维度 | 最大长度 | 中文效果 | 许可 | 定位 | |---|---|---|---|---|---| | bge-base-zh-v1.5(BAAI) | 768 | 512 | 优 | MIT | 中文检索事实标准,nl2sql 已本地化 | | bge-m3(BAAI) | 1024 | 8192 | 优 | MIT | 多语+长文本+稠密/稀疏/ColBERT 三合一 | | text2vec-base-chinese | 768 | 128 | 良 | Apache-2.0 | 早期中文 Sentence-BERT 复刻 | | m3e-base(Moka) | 768 | 512 | 良 | Apache-2.0 | 中英+代码混合训练 | | gte-base-zh(阿里) | 768 | 512 | 优 | Apache-2.0 | C-MTEB 中文榜头部 | | multilingual-e5-base | 768 | 512 | 中 | MIT | 100+ 语通用,中文非最优 | | Qwen3-Embedding(0.6B/4B/8B) | 1024~ | 32k | 优 | Apache-2.0 | LLM 系嵌入,指令感知,SOTA 级 | | OpenAI text-embedding-3-small | 1536 | 8191 | 良 | API | 外部 API 免运维 | | 阿里 text-embedding-v3 | 1024 | 8192 | 优 | API | 国产 API 中文强 | 配套 notebook(代码就绪):`jupyter/nlp_rag/中文embedding方案概览-代码就绪.ipynb`
**bge-base-zh-v1.5**(已本地化 `D:\wks\data\models\bge-base-zh-v1.5`): ```python from sentence_transformers import SentenceTransformer model = SentenceTransformer(r"D:\wks\data\models\bge-base-zh-v1.5") # 768 维 # s2p 检索:query 加指令前缀,passage 不加(bge 官方建议) INSTRUCTION = "为这个句子生成表示以用于检索相关文章:" q = model.encode([INSTRUCTION + "查询交易金额最大的10笔交易"], normalize_embeddings=True) p = model.encode(["test_transactions 交易明细表 amount 交易金额"], normalize_embeddings=True) score = float(q[0] @ p[0]) # 归一化后内积=余弦 ``` **bge-m3**:多语 + 8192 长文本 + 三种表示——稠密向量(语义)、稀疏权重(词级关键词精确命中)、ColBERT(细粒度)。表结构长描述/表名列名精确词场景利器: ```python from FlagEmbedding import BGEM3FlagModel m3 = BGEM3FlagModel("BAAI/bge-m3", use_fp16=True) out = m3.encode(["查询交易金额最大的10笔交易"], return_dense=True, return_sparse=True, return_colbert_vecs=True) ```
**text2vec-base-chinese**(相似句匹配启蒙,128 token 太短): ```python from sentence_transformers import SentenceTransformer model = SentenceTransformer("shibing624/text2vec-base-chinese") ``` **m3e-base**(中英+代码混合训练):`SentenceTransformer("moka-ai/m3e-base")` **gte-base-zh**(C-MTEB 头部,与 bge 同档):`SentenceTransformer("thenlper/gte-base-zh")` **multilingual-e5-base**(多语通用,前缀协议):query 加 `"query: "`、passage 加 `"passage: "`。
**Qwen3-Embedding-0.6B**(LLM 嵌入,指令感知,32k 上下文): ```python import torch from transformers import AutoModel, AutoTokenizer tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-Embedding-0.6B", padding_side="left") model = AutoModel.from_pretrained("Qwen/Qwen3-Embedding-0.6B") def embed_qwen(texts, instruction=None): if instruction: # LLM embedding 核心优势:任务感知 texts = [f"Instruct: {instruction}\nQuery: {t}" for t in texts] batch = tok(texts, padding=True, truncation=True, max_length=8192, return_tensors="pt") with torch.no_grad(): out = model(**batch).last_hidden_state[:, -1] # 最后 token 表示 return torch.nn.functional.normalize(out, p=2, dim=-1) q = embed_qwen(["查询交易金额最大的10笔交易"], instruction="根据用户问题检索相关数据库表") ``` **API 类(nl2sql `EMBEDDING_PROVIDER=openai` 即此模式)**: ```python import requests resp = requests.post( "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings", headers={"Authorization": "Bearer sk-xxx"}, json={"model": "text-embedding-v3", "input": ["查询交易金额最大的10笔交易"]}, ) vec = resp.json()["data"][0]["embedding"] ```
1. **本地首选:bge-base-zh-v1.5**——已下载、中文检索强、MIT 许可、CPU 可跑; 2. **长文本/多语/关键词精确命中**:bge-m3(8192 + 稀疏向量); 3. **效果天花板**:Qwen3-Embedding-0.6B(指令感知"为选表而嵌入"),需 GPU/大内存; 4. **免运维**:OpenAI 兼容 API(nl2sql 已内置该通道,改 .env 切换); 5. **兜底**:hash——内网无模型时保功能可用。
BGE s2p 指令前缀:为什么要加"为这个句子生成表示以用于检索相关文章:"
**官方原话**:"对于 s2p(短查询到长段落)检索任务,每个短查询都应以指令开头(指令参见模型列表)。但段落不需要指令。" **为什么是"必须"的语气?根源在训练分布**: BGE 用对比学习训练检索能力(把 (query, 正例段落) 拉近、(query, 负例段落) 推远)。**训练时 query 侧永远带指令前缀、passage 侧永远不带**。也就是说,模型学到的"查询-文档相似度"的标定, 是针对**带指令的查询**校准的: ```text 训练: [指令 + query] <===> [裸段落] 推理: [指令 + query] <===> [裸段落] ← 与训练同分布 ✓ 推理: [裸 query] <===> [裸段落] ← 分布外(OOD),分数失真 ``` **指令不是被忽略的咒语**:Transformer 的自注意力让指令 token 与查询的每个 token 互相"看见", 指令会**改写查询中每个 token 的表示**——把整段查询从"普通文本"投影到"检索意图子空间"。 这相当于告诉编码器:"你现在做的是检索任务的查询侧,不是在做语义理解。" **这是业界的通用设计,不是 BGE 独有**: | 模型 | 前缀约定 | |---|---| | E5 系列 | `"query: "` / `"passage: "` | | bge-m3 | s2p 查询侧同样建议指令 | | Qwen3-Embedding | `"Instruct: {任务描述}\nQuery: {查询}"` | | OpenAI/阿里 API | 服务端已内建任务区分,用户无感 |
**可以不加**——向量照样产生,相似度照样算,绝大多数排序不会崩。 但有两个层面的代价: **1. 分布外(OOD)分数失真** 模型从没见过"裸查询做检索",检索分数的整体分布偏移:C-MTEB 检索榜上掉几个点。 长尾词、歧义问题(恰是选表失败的常见模式)受影响更明显。 **2. "查询→文档"的对齐改变(不只是常量偏移)** 直觉上"对每句查询统一加同一个前缀,排序应该不变"——**不完全对**。 如果指令是纯加性偏移,排序确实不变;但 self-attention 让指令与**查询内容**交互 (非线性),对不同查询的改写程度不同 → "查询→文档"的对齐会变 → 排序会变。 所以它不是"无关紧要的常量",是"训练时约定好的任务开关"。 **v1.5 的缓和**:官方发布说明原文——"发布 bge-*-v1.5 嵌入模型以缓解相似度分布问题, **并在无指令的情况下增强其检索能力**"。即 v1.5 专门做过无指令微调,裸查询也能用—— 这是"感觉没必要"成立的依据;但官方**未撤销**建议,因为带指令仍是上限更高的写法 (成本只有一个常量字符串,零开销)。 **对称任务(s2s)从来不需要指令**:官方建议只针对 s2p(查询↔段落不对称检索)。 问题-问题相似、句子-句子相似这类对称比较,加指令反而画蛇添足。
nl2sql 有两个集合,恰好一边一个性质: | 集合 | 文档形态 | 任务性质 | 是否加指令 | |---|---|---|---| | `nl2sql_tables`(表) | 表名+中文注释+标签+列摘要(**文档式**文本) | 问题 → 文档 = **s2p** | **建议加**(但边际收益小:600 表词面信号已很强) | | `nl2sql_examples`(示例) | 示例问题本身(**问题式**文本) | 问题 → 问题 = **s2s 对称** | **不应加**(会把查询推离"问题空间",与未加指令的示例错配) | **所以用户的直觉一半一半**: - 对**示例过滤(REQ-007)**——没必要加,且加了反而不利; - 对**表检索(REQ-005/006)**——加了无害且理论略优,不加也能跑(v1.5 已缓释)。 **官方为什么"多此一举"地建议?三个原因**: 1. **训练分布对齐**(避免 OOD,见 Tab1)——这是最硬的理; 2. **零成本换上限**:一个常量字符串换 C-MTEB 几个点,没有不写的理由; 3. **惯例与保守**:v1.0 时代差距大,v1.5 缩小差距后官方也只做"增强"而非"撤销建议"。 **nl2sql 落地建议**:把指令做成**可配置项**而非硬编码——query 编码侧统一按集合类型决定是否加: 表检索默认加、示例检索默认不加;通过 `.env`(如 `EMBEDDING_QUERY_INSTRUCTION`)开关。 这样语义升级(hash → bge)后两个集合各得其所。
**加 vs 不加 对照实验(本地 bge 可运行)**: ```python from sentence_transformers import SentenceTransformer model = SentenceTransformer(r"D:\wks\data\models\bge-base-zh-v1.5") q = "查一下交易金额最大的10笔" docs = [ "test_transactions 交易明细表 trans_no 交易编号 amount 交易金额", "test_accounts 账户信息表 account_no 账号 balance 余额", ] INST = "为这个句子生成表示以用于检索相关文章:" d = model.encode(docs, normalize_embeddings=True) q_with = model.encode([INST + q], normalize_embeddings=True)[0] q_without = model.encode([q], normalize_embeddings=True)[0] print("带指令 :", (q_with @ d.T).round(4)) # 与文档对齐更好 print("不带指令:", (q_without @ d.T).round(4)) # 也能排序,分数整体偏移 ``` **nl2sql 接入点示意(vector_service 的 query 编码侧,~6 行)**: ```python # 表检索(s2p):加指令;示例检索(s2s):不加 def embed_query_for_tables(text: str) -> list: return model.encode([INSTRUCTION + text], normalize_embeddings=True)[0].tolist() def embed_query_for_examples(text: str) -> list: return model.encode([text], normalize_embeddings=True)[0].tolist() # payload 灌入侧(表描述/示例问题)一律不加指令 ``` 记住铁律的另一面:**集合内文档与历史查询侧的处理必须一致**——同一集合的向量必须全部 按同一规则编码(灌入侧裸文本 + 检索侧加指令 是 bge 的标准搭配),不能一半加一半不加。
NL2SQL 场景预处理模型 Embedding 优缺点对比
把「自然语言问题 → 相关数据库表/示例」拆成检索问题,文本特点: 1. **短文本**:问题一般小于 50 字;表描述 = 表名+中文注释+列名摘要(100~500 字); 2. **中英混杂**:`trans_amount 交易金额` 这类"英文标识符+中文注释"混合体; 3. **领域专有词**:账户/机构/交易/贷款……语义靠领域对齐,不是通用常识; 4. **查询-文档不对称**:query 是口语化问句,文档是结构化元数据——属 **s2p(短查长段)** 检索; 5. **精度敏感**:选错表 → SQL 直接失败,宁可不选也不可选错(少而准 > 多而全)。 评估维度:**语义能力 / 中文对齐 / s2p 检索适配 / 池化可用性 / 指令感知 / 资源成本 / 许可**。 对比对象(llm_yuchuli 页面收录):bert·chinese / bart·chinese / bge-base-zh-v1.5 / ChatGLM3-6B / text2vec-base-chinese / 摘要生成·t5,另加 hash 作工程基线。
**bert·chinese**:mask 填充/NSP 训练,**非检索设计**;[CLS]/mean pooling 语义空间各向异性, 相似度分布拥挤,Top-K 表检索易混噪声。生态成熟、模型小,但需微调才可用(=text2vec/BGE 的由来)。 **bart·chinese**:编码器-解码器,强项是**生成**(摘要/改写);embedding 只能取 encoder 副业复用, 检索分层差。选它做嵌入不如 BGE,选它做生成不如更大 LLM。 **bge-base-zh-v1.5**:**专为检索设计**(对比学习+难负例+v1.5 修复相似度分布)。 s2p 指令前缀天然匹配"问题→表描述";base 级 CPU 可跑;MIT 许可;已本地化。 短板:512 token 上限(超长表描述需摘要/分块)。 **ChatGLM3-6B**:LLM 副业嵌入(取最后 token 表示)。语义上限高、可 prompt 注入任务指令; 但推理成本高 1~2 个数量级,且 LLM 语义≠检索分层——确需 LLM 嵌入应选专门调优的 Qwen3-Embedding。 **text2vec-base-chinese**:CoSENT 微调的中文 similarity BERT 第一代,比裸 BERT 好用, 但 128 token 硬伤 + 全面落后 BGE——历史价值 > 现值。 **摘要生成·t5**:text-to-text 生成模型,无 embedding 目标函数;与 BART 同病,不建议选型。
| 模型 | 语义 | 中文对齐 | s2p适配 | 长度 | 指令感知 | 成本 | NL2SQL 综合 | |---|---|---|---|---|---|---|---| | hash | ★ | ★★★词面 | ★★ | 不限 | 无 | ★★★★★ | ★★ 兜底 | | bert·chinese | ★★ | ★★ | ★ | 512 | 无 | ★★★★ | ★ | | bart·chinese | ★★ | ★★ | ★ | 512 | 无 | ★★★ | ★ | | text2vec-base | ★★★ | ★★★ | ★★ | 128 | 无 | ★★★★ | ★★ | | **bge-base-zh-v1.5** | ★★★★ | ★★★★★ | ★★★★★ | 512 | query指令 | ★★★★ | ★★★★★ | | ChatGLM3-6B | ★★★★ | ★★★★ | ★★★ | 8k | prompt | ★★ | ★★★ | | t5 | ★★ | ★★ | ★ | 512 | 无 | ★★★ | ★ | **结论**:生产主选 **bge-base-zh-v1.5**;兜底 **hash**;升级方向 **Qwen3-Embedding-0.6B** (指令感知、32k 上下文、调用形态不变可平替)。 配套 notebook:`jupyter/nlp_rag/预处理模型NL2SQL-embedding优缺点对比.ipynb`
为什么需要 Qdrant 向量库(与"几行 Python"的对账)+ bge 集成
> "bge 已经能转向量;向量相似性比较几行 python 就够了。Qdrant 连转向量的能力都没有, > 为什么还要用 Qdrant?" **结论**:embedding 模型解决"怎么把文本变成向量",Qdrant 解决"怎么在大量向量里 **又快又稳又带条件地**找到最近邻"。两者是互补不是竞争——Qdrant 本来就不做 embedding, 就像 MySQL 不做分词,但没人问"有了 jieba 为什么还要 MySQL"。 embedding 计算永远发生在**客户端**(应用进程),Qdrant 只接收向量负责存、索引、过滤、检索。 **什么时候确实不需要 Qdrant**(诚实边界):点数 < 1 万、一次性计算、无持久化要求、 无元数据过滤、单进程——几行 python/numpy 完全够用。
几行 python 的检索 = **暴力扫描**:每查询 O(N × D)。 - 100 万点 × 768 维 float32 ≈ 3GB 内存常驻,每查询 7.68 亿次乘加; - 并发一高就排队;多进程共享要自己搞共享内存。 **Qdrant 用 HNSW**(分层可导航小世界图):查询复杂度近似 **O(log N)**, 百万级点毫秒返回——代价是近似最近邻(ANN,牺牲一点召回换几个数量级速度)。 > 一句话:1 万点以内 python 凑合;10 万点以上 python 是单点故障来源。 另外 Qdrant 支持**标量/二进制量化**(内存与速度再优化一个数量级)、 稀疏+稠密混合检索(BM25 词面 + 语义融合)。
真实查询从来不是纯找相似,而是**带结构化条件的相似检索**: - 示例集合:必须 `is_enabled=true`;可选 `business_type ∈ [交易, 账户]`; - 表集合:必须 `db_id=当前业务库`;可选 `table_types` 命中 `[交易, 常用]` 任一(一表多标签)。 python 两条路都差:**先过滤再暴力**(条件宽松时退化全量扫描); **先检索再过滤**(Top-K 过滤完可能一条不剩)。 Qdrant 把过滤条件**下推到 HNSW 图遍历过程**(Filterable HNSW):遍历时跳过不满足 payload 的点但继续沿图扩展——**召回率与速度兼得**。前提:过滤字段建 payload index (nl2sql 已为 db_id/table_types/business_type/is_enabled/example_id 建索引)。 几行 python 做到同等效果 = 自己实现"倒排索引+图索引+联合遍历"——那已经不是几行,而是一个向量数据库内核。
- python 方案:进程重启 → 向量全量重算(上万条示例每次重启重算 embedding); - Qdrant:**磁盘存储引擎 + WAL**,写入即持久,重启秒级恢复; - **快照备份/恢复**是生产运维刚需——nl2sql 已在数据库管理页面集成快照→归档→恢复; - **CRUD 与增量**:point 级 upsert/delete、按 filter 批量删除——业务写钩子(示例增删改 → 同步点)都是单行 best-effort 调用; - REST 6333 / gRPC 6334 双协议,任何语言可接;collections 管理/points 浏览已整合进管理页面。
**职责划分**: ```text 文本 --(bge-base-zh-v1.5,客户端侧)--> 向量(768维) 向量 + payload --(Qdrant:存储/HNSW/过滤/检索)--> Top-K 结果 ``` Qdrant 从不关心向量怎么来的——它只接收向量。集成示例: ```python from sentence_transformers import SentenceTransformer from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchAny model = SentenceTransformer(r"D:\wks\data\models\bge-base-zh-v1.5") client = QdrantClient(url="http://127.0.0.1:6333") if not client.collection_exists("nl2sql_tables_bge"): client.create_collection("nl2sql_tables_bge", vectors_config=VectorParams(size=768, distance=Distance.COSINE)) # 注意:维度必须=768(bge);与 hash 的 512 不同 → 切换 embedding 方案须全量重建 vec = model.encode(["test_transactions 交易明细表 amount 交易金额"], normalize_embeddings=True)[0].tolist() client.upsert(collection_name="nl2sql_tables_bge", points=[PointStruct(id=1, vector=vec, payload={"table_name": "test_transactions", "table_types": ["交易", "常用"]})]) q = model.encode(["为这个句子生成表示以用于检索相关文章:查一下交易金额最大的10笔"], normalize_embeddings=True)[0].tolist() hits = client.query_points( collection_name="nl2sql_tables_bge", query=q, query_filter=Filter(must=[FieldCondition(key="table_types", match=MatchAny(any=["交易"]))]), limit=3, with_payload=True, ) ``` **nl2sql 接入注意三点**:① 维度切换=集合重建(ensure_collections 内建检测,之后跑全量 sync); ② query 侧加 bge s2p 指令前缀、passage 不加;③ CPU base 级每 query 几十 ms 可接受,同步侧批量编码。 分析文档:`jupyter/nlp_rag/Qdrant优势分析与bge-embedding集成.md`
参考