1. 多模态检索为什么需要 Embedding + Reranker 两阶段
图文混合知识库的检索需求,和纯文本搜索完全不是一回事。你手里可能有一堆产品截图、PDF 扫描件、带表格的演示文稿、短视频封面,用户却用一句自然语言来查——“找一下去年那份带折线图的季度营收报告”。传统文本检索在这里直接失效,因为关键词根本对不上图像里的视觉语义。
Qwen3-VL-Embedding 与 Qwen3-VL-Reranker 这套组合,解决的正是这个断层。Embedding 模型是双塔(bi-encoder)结构,把文本、图像、文档图像、视频统一映射到一个共享向量空间,靠余弦相似度做粗筛召回;Reranker 是交叉编码器(cross-encoder),把 query 和候选文档拼在一起做深度交叉注意力,输出精细的相关性分数。前者负责“从百万级语料里快速捞出 100 条”,后者负责“把这 100 条按真实相关度重新排一遍”。
这套两阶段链路的核心价值在于计算与精度的折中。Embedding 可以预计算、建索引、走 ANN 近似检索,吞吐极高;Reranker 必须在线对每个 query-doc 对做前向推理,成本高但精度也高。工业界常见的做法就是 embedding 召回 top-100,reranker 精排后取 top-10 返回给上层应用。
Qwen3-VL-Embedding 还支持 MRL(Matryoshka Representation Learning),同一个模型可以截断出不同维度的向量,比如 512 维用于大规模索引、2048 维用于小规模高精度场景,不用重新训练。QAT(量化感知训练)则让 int8 量化后的向量几乎不掉点,存储成本直接砍到四分之一。这些特性对实际部署非常友好。
适合谁看:正在搭多模态 RAG、图文知识库、跨模态搜索的工程师;手里有 TaoToken 统一 Key、想快速验证 Qwen3-VL 系列检索效果的开发者;以及需要一套可复制基线、后续再调优的团队。
2. 用 TaoToken 统一 Key 接入 Qwen3-VL 系列模型
TaoToken 的定位是统一模型接入层,你不需要分别去申请 Embedding 和 Reranker 的独立 Key,一个 Key 就能调通整条链路。这对快速搭基线特别省事——省掉了多服务注册、多套鉴权、多份额度管理的麻烦。
接入前你需要准备三样东西:
第一,一个 TaoToken 账号,登录后在控制台生成 API Key。地址是 https://taotoken.net/api-keys ,生成后复制保存,后面所有请求都用它。
第二,确认你要调的模型 ID。Qwen3-VL-Embedding 和 Qwen3-VL-Reranker 分别有 2B 和 8B 两个规模。基线验证建议先用 2B 跑通流程,确认链路没问题后再换 8B 看精度提升。模型 ID 的命名规则在接入文档里有完整列表,地址是 https://taotoken.net/doc 。
第三,一个能发 HTTP 请求的环境。Python 用 requests 或 openai SDK 都行,curl 也可以。本文示例用 Python,因为后面要做向量计算和排序,Python 生态最顺手。
Base URL 统一用 https://taotoken.net/api ,注意这个地址不带任何查询参数。所有请求走标准的 OpenAI 兼容格式,Embedding 走 /v1/embeddings,Reranker 走 /v1/rerank(具体路径以接入文档为准)。
一个容易踩的坑:Embedding 和 Reranker 的输入格式不一样。Embedding 接受单条文本或图像,返回一个向量;Reranker 接受一个 query 加一组候选文档,返回每个候选的分数。别把两者的请求体搞混了。
另外,Qwen3-VL-Embedding 支持 instruction-aware,也就是说你可以在输入里加任务指令来引导嵌入方向。比如检索任务加“Represent this query for retrieving relevant documents”,分类任务加不同指令。这个特性在基线阶段可以先不用,但调优时很有价值。
如果你后续要做长期编码或 Agent 场景,可以考虑 Coding Plan,地址是 https://taotoken.net/coding-plan 。不过本文聚焦的是检索链路,用按量计费的 API Key 就够了。
3. 可复制的 Embedding 与 Reranker 调用配置
这一节给出完整的可复制配置。先建一个配置文件,把 Base URL、Key、模型 ID 集中管理,避免散落在代码各处。
{ "base_url": "https://taotoken.net/api", "api_key": "sk-your-taotoken-key-here", "embedding_model": "qwen3-vl-embedding-2b", "reranker_model": "qwen3-vl-reranker-2b", "embedding_dim": 1024, "top_k_recall": 100, "top_k_rerank": 10 }把这个存成config.json,后面代码统一读取。注意embedding_dim要和模型实际输出维度对齐,Qwen3-VL-Embedding 支持 MRL 截断,2B 模型默认输出维度以接入文档为准,这里写 1024 是示例值。
接下来是 Embedding 调用代码:
import json import requests import numpy as np with open("config.json") as f: cfg = json.load(f) def get_embedding(text=None, image_url=None): headers = { "Authorization": f"Bearer {cfg['api_key']}", "Content-Type": "application/json" } content = [] if text: content.append({"type": "text", "text": text}) if image_url: content.append({"type": "image_url", "image_url": {"url": image_url}}) payload = { "model": cfg["embedding_model"], "input": [{"role": "user", "content": content}], "encoding_format": "float" } resp = requests.post( f"{cfg['base_url']}/v1/embeddings", headers=headers, json=payload, timeout=30 ) resp.raise_for_status() return resp.json()["data"][0]["embedding"]这段代码的关键点:input是一个消息列表,content 里可以混排 text 和 image_url,这就是多模态嵌入的入口。返回的 embedding 是一个 float 列表,直接转 numpy 数组就能算余弦相似度。
Reranker 调用代码:
def rerank(query, documents): headers = { "Authorization": f"Bearer {cfg['api_key']}", "Content-Type": "application/json" } payload = { "model": cfg["reranker_model"], "query": query, "documents": documents, "top_n": cfg["top_k_rerank"] } resp = requests.post( f"{cfg['base_url']}/v1/rerank", headers=headers, json=payload, timeout=60 ) resp.raise_for_status() return resp.json()["results"]Reranker 的documents参数接受字符串列表,每个字符串可以是一段文本描述,也可以是多模态内容的文本化表示。返回的results里每条包含index和relevance_score,按分数降序排列。
如果你用 Cline 或 Claude Code 这类工具做开发辅助,可以在 MCP 配置里加上 TaoToken 的接入信息。以 Cline 的 MCP 配置为例,在 settings 里加:
{ "mcpServers": { "taotoken-retrieval": { "command": "python", "args": ["retrieval_server.py"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-your-key", "EMBEDDING_MODEL": "qwen3-vl-embedding-2b", "RERANKER_MODEL": "qwen3-vl-reranker-2b" } } } }这里三件套齐全:Base URL、Key、Model ID 都在 env 里。注意不要把生产库直连到 MCP,检索服务应该走独立的只读索引。
4. 验证请求:从向量召回到候选重排的完整动作
配置写好了,现在跑一次完整验证。目标是:给一个自然语言 query,从一组图文混合候选里召回 top-K,再精排,最后人工校验结果是否合理。
先准备候选集。模拟一个图文知识库,每条包含一段文本描述和一个图像 URL:
candidates = [ {"id": "doc_001", "text": "2025年Q3季度营收折线图,显示环比增长12%", "image": "https://example.com/chart_q3.png"}, {"id": "doc_002", "text": "产品发布会现场照片,展示新款智能音箱", "image": "https://example.com/launch.png"}, {"id": "doc_003", "text": "用户增长柱状图,月度活跃用户突破500万", "image": "https://example.com/user_growth.png"}, {"id": "doc_004", "text": "技术架构图,展示微服务拆分方案", "image": "https://example.com/arch.png"}, {"id": "doc_005", "text": "季度财报摘要,净利润同比增长8%", "image": "https://example.com/finance.png"}, ]第一步,对所有候选做 Embedding,存成向量矩阵:
import numpy as np def cosine_sim(a, b): a, b = np.array(a), np.array(b) return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) candidate_vectors = [] for c in candidates: vec = get_embedding(text=c["text"], image_url=c["image"]) candidate_vectors.append(vec) candidate_vectors = np.array(candidate_vectors)第二步,对 query 做 Embedding,算相似度,取 top-K:
query = "找一下带折线图的季度营收报告" query_vec = get_embedding(text=query) sims = [cosine_sim(query_vec, cv) for cv in candidate_vectors] ranked_indices = np.argsort(sims)[::-1] top_k = ranked_indices[:cfg["top_k_recall"]] print("Embedding 召回排序:") for idx in top_k: print(f" {candidates[idx]['id']} score={sims[idx]:.4f} {candidates[idx]['text']}")预期输出里,doc_001(Q3 营收折线图)应该排在最前面,doc_005(财报摘要)次之,doc_002(发布会照片)和doc_004(架构图)应该靠后。
第三步,把 top-K 候选的文本描述送给 Reranker 精排:
rerank_docs = [candidates[idx]["text"] for idx in top_k] rerank_results = rerank(query, rerank_docs) print("\nReranker 精排结果:") for r in rerank_results: orig_idx = top_k[r["index"]] print(f" {candidates[orig_idx]['id']} score={r['relevance_score']:.4f} {candidates[orig_idx]['text']}")第四步,校验。对比两次排序的差异:如果 Reranker 把doc_001稳定排在第一位,说明链路正常;如果 Reranker 把某个 Embedding 阶段排名靠后的文档提到了前面,说明交叉注意力捕捉到了更细粒度的相关性信号,这正是两阶段的价值。
实测下来,Embedding 阶段doc_001和doc_005的分数可能很接近(都在 0.85 左右),但 Reranker 会把doc_001明显拉开,因为“折线图”这个视觉关键词在交叉编码器里权重更高。这就是精排的意义。
如果你想用模型对话快速验证单条请求的返回结构,可以走 https://taotoken.net/models ,直接发一条测试请求看 JSON 长什么样,比写代码快。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
这一节列几个实际会撞上的报错,以及对应的排查路径。
401 Unauthorized:最常见。先检查Authorizationheader 里的 Key 有没有多余空格,再确认 Key 是不是从 https://taotoken.net/api-keys 生成的、有没有过期。如果 Key 没问题,检查 Base URL 是不是写成了带路径的地址——Base URL 只到https://taotoken.net/api,后面的/v1/embeddings是代码里拼的,别混在一起。
local proxy failed:这个报错通常出现在你本地配了 HTTP 代理、但代理没启动或端口不对的时候。检查环境变量HTTP_PROXY和HTTPS_PROXY,如果不需要代理就清掉。另外确认 requests 的timeout设了没有,网络不通时没设超时会一直挂。
reading choices 报错:典型的是KeyError: 'choices'或IndexError: list index out of range。这说明返回的 JSON 结构和你预期的不一样。先打印完整resp.json()看实际返回。常见原因是模型 ID 写错了,服务端返回了错误信息而不是正常结果。确认embedding_model和reranker_model的值和接入文档里列的一致。
OAuth 相关报错:如果你在用 Claude Code 或类似工具,可能会遇到 OAuth token 过期的问题。这类工具如果配置了 TaoToken 作为后端,需要在 settings 里把认证方式改成 API Key 而不是 OAuth。以 Claude Code 为例,检查~/.claude/settings.json里的apiKey字段,确保填的是 TaoToken 的 Key,而不是空的或旧的 OAuth token。
Reranker 返回空结果:检查documents列表是不是空的,或者top_n设得比文档数还大。另外确认 query 字段没有传成列表。
Embedding 维度对不上:如果你在 config 里写了embedding_dim: 1024,但实际返回的是 2048 维,余弦相似度计算会报形状错误。解决办法是打印一次len(vec)确认实际维度,然后改 config。
一个通用排查技巧:所有请求都加resp.raise_for_status(),让 HTTP 错误直接抛异常,而不是等到解析 JSON 时才报奇怪的错。再配合打印resp.status_code和resp.text,定位速度会快很多。
6. 把这条链路接进你的检索系统
跑通基线之后,下一步是把它接进实际系统。几个实用建议。
索引层用 int8 量化 + 中等维度(比如 512 或 1024)。Qwen3-VL-Embedding 的 QAT 特性让 int8 量化几乎不掉点,存储直接省 75%。MRL 让你可以在同一个模型上截断维度,大规模索引用低维、小规模精调用高维,不用维护两套模型。
召回阶段 top-K 设 100 左右比较合适。太小会漏掉相关文档,太大 reranker 的在线推理成本会飙升。100 条候选走 cross-encoder 精排,延迟通常在可接受范围内。
Reranker 的输入可以只传文本描述,也可以把图像 OCR 后的文本拼进去。如果你的候选文档本身有高质量的结构化描述,直接用描述文本效果最好;如果只有原始图像,建议先做一轮轻量 OCR 或 caption 生成,再送给 Reranker。
指令感知(instruction-aware)在调优阶段很有用。比如你的场景是“找相似产品图”,可以在 Embedding 输入里加“Represent this image for finding visually similar products”,让嵌入空间更贴合你的任务分布。基线阶段可以先不加,等发现召回不准时再针对性加指令。
最后,整条链路的 Key 管理用 TaoToken 统一管,Embedding 和 Reranker 共用一个 Key,省掉了多服务鉴权的复杂度。接入文档在 https://taotoken.net/doc ,模型列表和参数说明都在里面。需要生成新 Key 或查看用量,走 https://taotoken.net/api-keys 。如果后续要做长期编码或 Agent 集成,Coding Plan 在 https://taotoken.net/coding-plan 。