3.4 Qwen3 Embedding 模型介绍#

在完成 Milvus 向量数据库的安装以后,下一步我们将开始介绍如何使用千问 Qwen3 Embedding 系列词嵌入模型来对文本进行向量化。下面先直接介绍其使用方法,然后再对其中的原理做一个简单地介绍,以便更好地设置相关参数获得合适的词向量结果。

3.4.1 DashScopeEmbeddings 使用示例#

在第2.4节内容中,我们已经简单介绍了通过 dashscope 原生 SDK、OpenAI SDK 以及 langchain_community 的方式来使用 Qwen3 Embedding 模型的使用方法。总结起来就是,直接使用 dashscope 原生 SDK 这种方式能够获得 Embedding 完整的能力, langchain_community 其次,OpenAI SDK 则最差只能使用基本功能。

之所以会有不同的途径来使用 Qwen3 Embedding 模型,本质原因还是我们在上一章提到的先入为主,以及为了适配其它集成框架,如 LangChain 生态。

在后续介绍中,我们也将使用 langchain_community.embeddings 模块中的 DashScopeEmbeddings 模块来完成文本的向量化过程,这是千问专门为适配 LangChain 生态所开发的。同时,整个向量化过程已经被 LangChain 高度集成,不用我们手动去转换每一篇文档,这也是使用 LangChain 开发 RAG 的便携之处。

通过 LangChain 访问 dashscope 访问方式如下:

1 def test_LangChain_dashscope_embedding(input_text):
2     from langchain_community.embeddings import DashScopeEmbeddings
3     embeddings = DashScopeEmbeddings(model="text-embedding-v4")
4     print("test_LangChain_dashscope_embedding")
5     query_result = embeddings.embed_query(input_text[0])
6     print(query_result)
7     doc_results = embeddings.embed_documents(input_text)
8     print(doc_results)

在上述代码中,第3行是实例化一个类对象,并且指定 Embedding 模型为 text-embedding-v4,它是属于Qwen3-Embedding 系列,也是目前千问最好的文本向量模型,支持维度有 2048、1536、1024(默认)、768、512、256、128、64这些。不过通过 DashScopeEmbeddings 只能使用默认的 1024 维度,这也就是我们之前所说的非官方 SDK 的限制。

第5、7行代码,则是分别用于对查询和文档进行向量化,不同的 Embedding 模型背后实现的方式并不一样,下文将进行介绍。

上述代运行结束以后的输出结果为:

[-0.0863448828458786, 0.0336710587143898, 0.030886666849255562, ...]
[[-0.0863448828458786, 0.0336710587143898, 0.030886666849255562, ...], 
[-0.08091967552900314, -0.012189434841275215, -0.0039899577386677265, ...]]

3.4.2 Qwen3 Embedding 原理#

Qwen3 Embedding 系列模型是专为文本表征、检索与排序任务而设计,它基于 Qwen3 基座模型所训练,充分继承了 Qwen3 在多语言文本理解能力方面的优势[1]。

Qwen3 Embedding 模型在编码时接收单段文本作为输入,取模型最后一层[EOS]标记对应的隐藏状态向量,作为输入文本的语义表示,整个过程如图3-5所示。

图 3-5. Qwen3 Embedding 模型原理图 [4]

同时,为了在搜索类任务中取得最佳效果,Qwen3 Embedding 模型还支持对不同的内容进行有针对性的向量化处理,也就是图1中输入所示。

例如在 RAG 文档入库阶段,我们可以直接将对应的文本转化为向量;而在用户提问检索阶段,我们则可以将指令与用户提问 Query 拼接到一起作为输入,然后让 Embedding 模型返回对应的向量,最后去向量库中检索。

这样模型在编码 Query 时不再只是“语义理解”,而是“为检索目的做语义表达”。也就是说,Query 向量是可以根据不同指令而改变的,不同的指令也让 Query 向量投影到了不同的子空间中。例如:用于检索、用于分类、用于相似度计算等等,虽然底层模型一样,但最后 Query 的表示会不同。

3.4.3 Dashscope 使用示例#

在通过阿里云百炼的 Dashscope 调用 Embedding 的时候,我们可以通过参数 text_type 来控制生成不同类型的 Embedding 向量,示例如下:

1 resp = dashscope.TextEmbedding.call(
2     model="text-embedding-v4",
3     input="机器学习的相关论文",
4     text_type="query",
5     instruct="Given a research paper query, retrieve relevant research paper")

同时,如果 instruct 参数为空,那么不管 text_type"query" 还是 "document",最后返回的结果都是一样的。尽管这一点官方文档没有明确说明,但是我们可以通过计算两个向量的余弦相似度自行验证:

 1 def test_dashscope_embedding():
 2     input_text = ["机器学习是什么?深度学习又是什么?"]
 3     from dashscope import TextEmbedding
 4     resp1 = TextEmbedding.call(model="text-embedding-v4", text_type="query",
 5                                dimensions=1024, input=input_text[0])
 6     v_query = np.array(resp1["output"]["embeddings"][0]["embedding"])
 7 
 8     resp2 = TextEmbedding.call(model="text-embedding-v4", text_type="document",
 9                                dimensions=1024, input=input_text)
10     v_document = np.array(resp2["output"]["embeddings"][0]["embedding"])
11     cos_sim = np.dot(v_query, v_document) / 
12     	      (np.linalg.norm(v_query) * np.linalg.norm(v_document))
13     print(cos_sim,np.linalg.norm(v_query),np.linalg.norm(v_document))
14 
15     resp1 = TextEmbedding.call(model="text-embedding-v4", text_type="query",
16                 dimensions=1024, input=input_text[0],
17                 instruct="Given a research paper query, retrieve relevant research paper")
18     v_query = np.array(resp1["output"]["embeddings"][0]["embedding"])
19     cos_sim = np.dot(v_query, v_document) / 
20     		  (np.linalg.norm(v_query) * np.linalg.norm(v_document))
21     print(cos_sim)
22     print(resp1["usage"])  # {'total_tokens': 26}

在上述代码中,我们首先比较了同一个输入,分别使用 text_type="query"text_type="document" 两种模式来向量化,并计算两者的余弦相似度,即第4~13行。然后,比较了加指令后生成的向量与text_type="document" 模式下的向量的相似度。

上述代码运行结束后的输出结果为:

1.0 1.0000000080061002 1.0000000080061002
0.8773372034726875
{'total_tokens': 26}

从输出结果可以看出, text_type="query"text_type="document" 两种模式下生成向量的模长均为1,余弦相似度也为1,这就意味着这两个向量是完全一样的。不过在使用中还是可以自行加上,没准哪天可能官方 API 就做了更新。

这里需要注意的是,按照官方文档说法提供的指令需要为英文,且使用此功能时,必须将 text_type 参数设置为 query [2]。

3.4.4 指令拼接#

这里,我们还可以根据千问在 github 上公布的示例代码 [3],来手动拼接指令和输入,示例代码如下:

 1 def embedding_token_construct():
 2     def get_detailed_instruct(task_description: str, query: str) -> str:
 3         return f'Instruct: {task_description}\nQuery:{query}'
 4 
 5     task = 'Given a research paper query, retrieve relevant research paper'
 6     queries = [get_detailed_instruct(task, '机器学习是什么?深度学习又是什么?')]
 7     print(f"拼接后的输入为: {queries}")
 8     tokenizer = AutoTokenizer.from_pretrained(data_info.QWEN3_EMBEDDING_06B_DIR, padding_side='left')
 9     batch_dict = tokenizer(queries, padding=True, truncation=True, max_length=8192, )
10     for i in range(len(batch_dict['input_ids'])):
11         for _, token_id in enumerate(batch_dict['input_ids'][i]):
12             token_str = tokenizer.decode([token_id], skip_special_tokens=False)
13             print(f"{token_id:6d} -> {repr(token_str)}")
14         print(f"Token 数量 = {len(batch_dict['input_ids'][i])}")

在上述代码中,第2行是将输入和任务指令拼接到一起。第8~9行则是进行 tokenize 操作。第10~14行是我们将 Token ID 转换成 Token,也可以顺便查看 tokenize 后的结果。

上述代码运行结束后将会看到类似如下结果:

# 拼接后的输入为: ['Instruct: Given a research paper query, retrieve relevant research paper\nQuery:机器学习是什么?深度学习又是什么?']

#   641 -> 'In'
#   1235 -> 'struct'
#     25 -> ':'
#  16246 -> ' Given'
#    264 -> ' a'
#   3412 -> ' research'
#   5567 -> ' paper'
#   3239 -> ' query'
#     11 -> ','
#  17179 -> ' retrieve'
#   9760 -> ' relevant'
#   3412 -> ' research'
#   5567 -> ' paper'
#    198 -> '\n'
#   2859 -> 'Query'
#     25 -> ':'
# 102182 -> '机器'
# 100134 -> '学习'
# 102021 -> '是什么'
#  11319 -> '?'
# 102217 -> '深度'
# 100134 -> '学习'
#  99518 -> '又'
# 102021 -> '是什么'
#  11319 -> '?'
# 151643 -> '<|endoftext|>'
# Token 数量 = 26

从输出结果我们可以清晰的看到每个 Token 到底是长什么样的。同时,值得一提的是在 Qwen3 Embedding 中,Embedding 时会在输入最后拼接上 '<|endoftext|>' 这个 Token,并且取它对应的向量作为整个句子的向量表达。

以上完整示例代码可参见 Code/Chapter03/C10_qwen_embedding.py 文件。

3.4.5 DashScopeEmbeddings 实现#

在经过上面内容介绍以后,相信大家应该已经对上面 DashScopeEmbeddings 模块中的 embeddings.embed_query()embeddings.embed_documents() 之间的区别有了一定的理解。没错,这两个方法就是分别针对请求和原始文档进行向量化的。所有适配 LangChain 框架的 Embedding 模型,例如 OpenAIEmbeddingsDashScopeEmbeddings,都有这两个方法,但是内部具体的实现却不同。

DashScopeEmbeddings 类中,embed_query()embed_documents() 的实现如下:

 1 def embed_documents(self, texts: List[str]) -> List[List[float]]:
 2     embeddings = embed_with_retry(
 3         self, input=texts, text_type="document", model=self.model)
 4     embedding_list = [item["embedding"] for item in embeddings]
 5     return embedding_list
 6 
 7 def embed_query(self, text: str) -> List[float]:
 8     embedding = embed_with_retry(
 9          self, input=text, text_type="query", model=self.model)[0]["embedding"]
10     return embedding

从这里可以看出,embed_query()embed_documents()的差异在于 text_type 参数的值不一样。不过根据我们在第2.2节中的介绍可以发现,目前为止 text_type="query"text_type="document" 对于同样的文本返回的向量都是一样。同时,由于 embed_query() 这个方法也无法直接传入 Instruct 这个参数,所以本质上两者是一样的。

当然,根据需要我们也可以修改这部分代码,使得在对 Query 向量化的时候可以使用指令模式。

这里顺便提一下在 OpenAIEmbeddings 类中,embed_query()embed_documents() 的实现如下:

1 def embed_query(self, text: str) -> List[float]:
2     return self.embed_documents([text])[0]
3 
4 def embed_documents( self, texts: List[str], 
5 		chunk_size: Optional[int] = 0) -> List[List[float]]:
6     engine = cast(str, self.deployment)
7     return self._get_len_safe_embeddings(
8     		texts, engine=engine, chunk_size=chunk_size)

从上述代码我们可以发现,OpenAI 的实现方式更加简单。

引用#

[1] https://qwen.ai/blog?id=qwen3-embedding

[2] https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=2842587

[3] https://github.com/QwenLM/Qwen3-Embedding

[4] Zhang Y, Li M, Long D, et al. Qwen3 embedding: Advancing text embedding and reranking through foundation models[J]. arXiv preprint arXiv:2506.05176, 2025.