|
```
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])
```
|
|
|
|
|
|
|
|
|
|
|
|
**依据最大索引转向量:在最后一维上增加一个维度,将索引转化为一个浮点向量
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
|
|
|
|
|
|
**来源**: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。
|
|
| 方案 | 维度 | 最大长度 | 中文效果 | 许可 | 定位 |
|---|---|---|---|---|---|
| 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——内网无模型时保功能可用。
|
|
**官方原话**:"对于 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 的标准搭配),不能一半加一半不加。
|
|
把「自然语言问题 → 相关数据库表/示例」拆成检索问题,文本特点:
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`
|
|
> "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`
|