星云点击:星空遥控器
120.47M · 2026-02-04
作者:吴佳浩
撰稿时间:2026-2-1
最后更新:2026-2-3
测试模型: Qwen3-VL-Embedding-8B
为什么这个话题重要?
理解不同 Embedding 模型的向量提取机制,对于以下场景至关重要:
通过本文的分析,你将清楚地了解到:Qwen3-VL-Embedding 的向量提取方式是其 Decoder-only 架构的必然选择,而非一个可以随意替换的设计决策。这种理解将帮助你在实际应用中做出更明智的技术选型和实现决策。
常规 Embedding 模型的向量提取有两种主流策略:
但是,当我最近深入研究 Qwen3-VL-Embedding 的官方实现代码时,发现了一个令人意外的事实:它既不使用 CLS Token,也不使用 Mean Pooling。
这个发现促使我重新审视了不同 Embedding 模型在向量提取策略上的根本差异。事实证明,Qwen3-VL-Embedding 采用的是一种完全不同的方法——Last Token Pooling(最后有效 token 池化),这种选择并非偶然,而是由其底层架构特性所决定的。
本文将通过对比分析,深入探讨常规 Embedding 模型(如 BERT、Sentence-BERT)与 Qwen3-VL-Embedding 在向量提取机制上的本质区别。我们将从以下几个维度展开讨论:
核心议题:
flowchart TB
subgraph title1["BERT-like Embedding 模型 (双向编码器)"]
A1["输入文本"] --> B1["Tokenization"]
B1 --> C1["添加特殊标记<br/>(CLS) text (SEP)"]
C1 --> D1["Bi-directional Encoder<br/>双向注意力"]
D1 --> E1{"Pooling 策略"}
E1 -->|最常用| F1["CLS Token<br/>取 position 0"]
E1 -->|备选| F2["Mean Pooling<br/>平均所有有效 token"]
E1 -->|少用| F3["Max Pooling"]
F1 --> G1["L2 归一化"]
F2 --> G1
F3 --> G1
G1 --> H1["最终向量"]
end
style title1 fill:#f0f9ff,stroke:#1890ff,stroke-width:2px,stroke-dasharray: 5 5
style F1 fill:#e6f7ff,stroke:#1890ff,stroke-width:2px
style E1 fill:#fafafa,stroke:#d9d9d9
style D1 fill:#f9f0ff,stroke:#722ed1
flowchart TB
subgraph title2["Qwen3-VL-Embedding (因果解码器)"]
A2["多模态输入<br/>文本/图像/视频"] --> B2["Vision Encoder<br/>处理图像/视频"]
A2 --> B3["Tokenization<br/>处理文本"]
B2 --> C2["模态融合"]
B3 --> C2
C2 --> D2["Causal Decoder<br/>单向注意力"]
D2 --> E2["Last Token Pooling<br/>取最后有效 token"]
E2 --> F4["提取 hidden_state<br/>at last position"]
F4 --> G2["L2 归一化"]
G2 --> H2["最终向量"]
end
style title2 fill:#fff7e6,stroke:#fa8c16,stroke-width:2px,stroke-dasharray: 5 5
style E2 fill:#fff7e6,stroke:#fa8c16,stroke-width:2px
style D2 fill:#fff0f6,stroke:#eb2f96
style C2 fill:#f6ffed,stroke:#52c41a
| 维度 | 常规 Embedding 模型 (BERT/Sentence-BERT) | Qwen3-VL-Embedding |
|---|---|---|
| 架构类型 | Encoder-only (双向) | Decoder-only (单向) |
| 注意力机制 | Bi-directional Attention 每个token看到全局 | Causal Attention 只看左侧context |
| 特殊token | [CLS] 在开头 | 无专用CLS token |
| 默认Pooling | CLS Token (position 0) | Last Token (最后有效位置) |
| 支持模态 | 纯文本 (部分支持图像) | 文本 + 图像 + 视频 |
| 向量维度 | 通常 384/768/1024 | 根据模型配置 |
sequenceDiagram
participant Input as 输入文本
participant Tokenizer
participant Encoder as Bi-directional<br/>Encoder
participant CLS as [CLS] Token
participant Output as 输出向量
Input->>Tokenizer: "How are you?"
Tokenizer->>Encoder: [CLS] How are you [SEP]
Note over Encoder: 每个token都能看到<br/>全部其他tokens
Encoder->>CLS: 汇聚全局语义到[CLS]
CLS->>Output: 提取 position 0<br/>的 hidden state
Output->>Output: 归一化
代码示例:
# BERT-style embedding
def get_bert_embedding(text, model, tokenizer):
# 添加 [CLS] 和 [SEP]
inputs = tokenizer(text, return_tensors='pt')
# input_ids: [CLS, token1, token2, ..., SEP]
outputs = model(**inputs)
# 提取 [CLS] token (position 0)
cls_embedding = outputs.last_hidden_state[:, 0, :] # 第一个位置
# 归一化
embedding = F.normalize(cls_embedding, p=2, dim=-1)
return embedding
为什么用 CLS Token?
[CLS] 用于句子级任务[CLS] 能"看到"整个句子sequenceDiagram
participant Input as 多模态输入
participant Processor as Vision+Text<br/>Processor
participant Decoder as Causal<br/>Decoder
participant Last as Last Valid<br/>Token
participant Output as 输出向量
Input->>Processor: 文本 + 图像 + 视频
Processor->>Decoder: token1, token2, ..., tokenN
Note over Decoder: 单向注意力<br/>每个token只看左侧
Decoder->>Last: tokenN 看到了<br/>所有前面的信息
Last->>Output: 提取最后有效token<br/>的 hidden state
Output->>Output: L2 归一化
官方代码实现:
@staticmethod
def _pooling_last(hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
"""
从 hidden_state 中提取每个样本最后一个有效 token 的向量
Args:
hidden_state: [batch_size, seq_len, hidden_dim]
attention_mask: [batch_size, seq_len] # 1=有效token, 0=padding
Returns:
[batch_size, hidden_dim] # 每个样本的 embedding
"""
# 步骤1: 翻转 attention_mask (从右往左看)
flipped_tensor = attention_mask.flip(dims=[1])
# 例: [1,1,1,1,0,0] → [0,0,1,1,1,1]
# 步骤2: 找到第一个1的位置 (翻转后)
last_one_positions = flipped_tensor.argmax(dim=1)
# 例: [0,0,1,1,1,1] → argmax=2 (从右数第3个是最后一个1)
# 步骤3: 转换回原始索引
col = attention_mask.shape[1] - last_one_positions - 1
# 例: 6 - 2 - 1 = 3 (实际最后一个有效token在位置3)
# 步骤4: 提取对应位置的 hidden state
row = torch.arange(hidden_state.shape[0], device=hidden_state.device)
return hidden_state[row, col] # 关键:最后一个有效token
为什么用 Last Token?
graph LR
subgraph "BERT: Bi-directional Attention"
CLS1[CLS] -.双向.-> T1[Token1]
CLS1 -.双向.-> T2[Token2]
CLS1 -.双向.-> T3[Token3]
T1 -.双向.-> T2
T1 -.双向.-> T3
T2 -.双向.-> T3
CLS1 ==> E1[Embedding]
style CLS1 fill:#90EE90
end
subgraph "Qwen3-VL: Causal Attention"
T4[Token1] --> T5[Token2]
T5 --> T6[Token3]
T6 --> T7[TokenN]
T4 -.看不到.-> T5
T4 -.看不到.-> T6
T5 -.看不到.-> T6
T7 ==> E2[Embedding]
style T7 fill:#FFB6C1
end
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
# 编码文本
text = "What is machine learning?"
embedding = model.encode(text) # 自动使用 CLS token
print(embedding.shape) # (384,)
from qwen3_vl_embedder import Qwen3VLEmbedder
embedder = Qwen3VLEmbedder('Qwen/Qwen3-VL-Embedding')
# 编码文本
inputs = [{'text': "What is machine learning?"}]
embedding = embedder.process(inputs) # 自动使用 Last Token
print(embedding.shape) # (1, hidden_dim)
# 大多数 BERT-style 模型不支持多模态
# 需要使用 CLIP 等专门的多模态模型
from transformers import CLIPModel, CLIPProcessor
model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32')
processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')
# 分别编码文本和图像
text_inputs = processor(text=["a photo of a cat"], return_tensors="pt")
text_embedding = model.get_text_features(**text_inputs)
image_inputs = processor(images=image, return_tensors="pt")
image_embedding = model.get_image_features(**image_inputs)
# 原生支持文本+图像+视频混合输入
embedder = Qwen3VLEmbedder('Qwen/Qwen3-VL-Embedding')
# 单次调用处理多模态
inputs = [{
'text': "Describe this image",
'image': "/path/to/cat.jpg",
'instruction': "Generate a comprehensive embedding"
}]
embedding = embedder.process(inputs) # 融合所有模态信息
flowchart TD
subgraph "常规 Embedding 处理流程"
A1[输入文本] --> B1[Tokenization]
B1 --> C1["添加 [CLS] + text + [SEP]"]
C1 --> D1[Padding to max_length]
D1 --> E1[Encoder Forward]
E1 --> F1["提取 hidden_state[:, 0, :]"]
F1 --> G1[Normalize]
G1 --> H1[返回向量]
end
subgraph "Qwen3-VL-Embedding 处理流程"
A2[多模态输入] --> B2{输入类型}
B2 -->|图像| C2[Vision Encoder]
B2 -->|视频| C3[Video Processor<br/>采样帧]
B2 -->|文本| C4[Tokenization]
C2 --> D2[融合模态tokens]
C3 --> D2
C4 --> D2
D2 --> E2[Apply chat template<br/>添加系统prompt]
E2 --> F2[Causal Decoder Forward]
F2 --> G2["找到最后有效token位置<br/>(通过 attention_mask)"]
G2 --> H2["提取 hidden_state[row, col]"]
H2 --> I2[L2 Normalize]
I2 --> J2[返回向量]
end
style F1 fill:#90EE90
style H2 fill:#FFB6C1
| 特性 | 常规 Embedding | Qwen3-VL-Embedding |
|---|---|---|
| 处理速度 | 快速 | 中等(多模态处理更复杂) |
| 内存占用 | 低 | 中高(需处理视觉数据) |
| 适用场景 | 纯文本检索/分类 | 多模态检索/RAG/跨模态搜索 |
| 上下文长度 | 通常 512 tokens | 默认 8192 tokens |
| 灵活性 | 单一模态 | 文本+图像+视频混合 |
# 常规模型
embedding = hidden_state[:, 0, :] # 第一个位置 (CLS)
# Qwen3-VL
embedding = hidden_state[row, last_valid_col] # 最后有效位置
graph LR
A[CLS Token<br/>双向注意力] -->|同时看到| B[所有tokens]
C[Last Token<br/>因果注意力] -->|只看到| D[左侧所有tokens]
style A fill:#90EE90
style C fill:#FFB6C1
| 方面 | 常规模型 | Qwen3-VL |
|---|---|---|
| 目标 | 句子级表示学习 | 多模态理解 + 生成 |
| 预训练 | Masked LM | Causal LM |
| 架构选择 | 专门为编码设计 | 适配生成式模型 |
# ============ 常规 Embedding 模型 ============
from sentence_transformers import SentenceTransformer
import numpy as np
class RegularEmbedder:
def __init__(self, model_name='all-MiniLM-L6-v2'):
self.model = SentenceTransformer(model_name)
def encode(self, texts):
# 内部使用 CLS token pooling
return self.model.encode(texts, normalize_embeddings=True)
# ============ Qwen3-VL-Embedding ============
from qwen3_vl_embedder import Qwen3VLEmbedder
class MultimodalEmbedder:
def __init__(self, model_path):
self.embedder = Qwen3VLEmbedder(model_path)
def encode(self, inputs):
# 内部使用 Last Token pooling
return self.embedder.process(inputs, normalize=True)
# ============ 使用对比 ============
# 纯文本
text_embedder = RegularEmbedder()
text_emb = text_embedder.encode(["Hello world"])
# 多模态
mm_embedder = MultimodalEmbedder('Qwen/Qwen3-VL-Embedding')
mm_emb = mm_embedder.encode([{
'text': "Hello world",
'image': "cat.jpg" # 常规模型做不到
}])
| 问题 | 答案 |
|---|---|
| 核心区别是什么? | 常规用 CLS Token (首位),Qwen3-VL 用 Last Token (末位) |
| 为什么不同? | 架构不同:Encoder(双向) vs Decoder(单向) |
| 哪个更好? | 取决于任务:纯文本用常规,多模态用 Qwen3-VL |
| 可以互换吗? | 不建议,各自有优化场景 |
关键记忆点:
这是我最经在使用的模型分析,如果你也感兴趣的话可以看一下官方提供的脚本希望我的分享对你有帮助
参考官方的脚本地址:
Qwen/Qwen3-VL-Embedding-8B