1. 中文 RAG 检索命中率上不去,先别急着换大模型
做中文 RAG 的朋友大概率遇到过这种场景:知识库文档切得好好的,生成端也换成了能力更强的模型,可回答还是答非所问,甚至一本正经地编。你把 Prompt 改了十几版,把 temperature 调到 0,把 top_k 从 3 加到 10,效果依然飘忽。这时候真正该怀疑的,往往不是"嘴",而是"眼睛"——Embedding 模型。
Embedding 干的事,是把一段文本映射成高维空间里的一个坐标点,语义越接近的文本,坐标越接近。检索阶段就是拿问题的坐标去库里找最近的几个块。如果这个坐标系本身标歪了,后面重排再强、生成再聪明,也只是在错误的候选集里挑挑拣拣。中文场景尤其明显:很多国际明星模型以英文语料为主,中文成绩参差不齐;中文的分词、成语、一词多义、近义表达,需要足够的中文语料才能捕捉到位;再加上国内技术文档普遍中英夹杂,对双语对齐能力还有额外要求。
这篇就聚焦中文 RAG 场景下的 Embedding 选型,围绕 C-MTEB 榜单和检索命中率,对比主流中文向量模型在长文档切分、相似度阈值上的表现,给出可复制的接入配置和检索验证脚本,并演示如何通过 TaoToken 统一 Key 通道完成多模型切换与效果回归。适合正在搭 RAG、被召回率折磨、想系统做一次选型自测的开发者。全文会落到能直接跑的代码和配置上,不空谈榜单。
2. TaoToken 统一 Key 通道:多模型切换与效果回归的前置准备
选型这件事最烦的地方在于:候选模型可能来自不同厂商、不同部署方式,有的要本地跑,有的走 API。如果每个模型都单独配一套 Key、一套 SDK、一套环境变量,光是切换和回归测试就能把人耗死。我试过用统一通道把这件事收敛下来,思路是让所有 Embedding 请求都走同一个 Base URL 和同一把 Key,模型差异只体现在 model 字段上,这样切换模型就是改一个字符串,回归脚本不用动。
TaoToken 在这里扮演的就是这个统一入口。它的 API 地址是 https://taotoken.net/api,兼容 OpenAI 风格的接口协议,所以任何支持 OpenAI Embedding 接口的客户端都能直接对接。你需要先去控制台拿一把 Key,入口在 https://taotoken.net/api-keys ,拿到之后所有模型共用这一把,不用为每个模型单独申请。模型列表和可用 ID 可以在文档里查,地址是 https://taotoken.net/doc 。
这里要强调一个概念:统一 Key 通道的价值不在"省事"本身,而在于它让效果回归变得可行。选型的正确姿势是拿自己的数据测,而不是刷榜单。可你要测 5 个模型,如果每个模型都要改代码、改环境、重启服务,你大概率测两个就放弃了。统一通道下,你只需要维护一个候选模型 ID 列表,循环跑一遍,把 Hit@3 打出来对比,二十分钟就能出结论。
配置上,核心就三件套:Base URL、API Key、Model ID。Base URL 固定为 https://taotoken.net/api ,Key 从控制台获取,Model ID 按你要测的模型填。下面给一份可直接复制的配置,覆盖 Python 环境变量和代码内初始化两种写法。注意不要把 Key 硬编码进仓库,用环境变量或者 .env 文件管理。
对于需要长期做编码和 Agent 任务的团队,如果选型之后还要跑大量回归和批量建库,可以了解下 Coding Plan,入口在 https://taotoken.net/coding-plan ,它更适合持续性的调用场景。而单纯想先验证某个模型对话或向量效果,用模型对话页面快速试一下更轻,地址是 https://taotoken.net/models 。选型阶段建议先用模型对话确认接口通不通,再进到批量回归。
3. 可复制的接入配置:Base URL、Key 与 Model ID 三件套
这一节给能直接落地的配置片段。先明确路径和字段,避免你复制过去发现对不上。
环境变量方式,适合本地开发和 CI。新建一个 .env 文件放在项目根目录:
# .env TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的Key EMBED_MODEL=bge-m3然后在 Python 里读取。用 openai 官方 SDK 即可,因为接口是 OpenAI 兼容的:
import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( base_url=os.getenv("TAOTOKEN_BASE_URL"), api_key=os.getenv("TAOTOKEN_API_KEY"), ) def embed(texts, model=None): model = model or os.getenv("EMBED_MODEL") resp = client.embeddings.create( model=model, input=texts, ) return [d.embedding for d in resp.data]如果你更习惯用配置文件管理多模型候选,可以写一个 JSON,把要对比的模型 ID 列进去。这样回归脚本读这个文件循环即可:
{ "base_url": "https://taotoken.net/api", "models": [ "bge-m3", "bge-large-zh-v1.5", "Qwen3-Embedding-0.6B", "gte-Qwen2-7B-instruct" ], "top_k": 3, "chunk_size": 400, "chunk_overlap": 50 }注意这里的 model 字段值要以文档里实际可用的 ID 为准,不同通道对模型名的写法可能有差异,接入前先去 https://taotoken.net/doc 核对一遍。chunk_size 和 chunk_overlap 是给建库脚本用的,和 Embedding 模型联动——512 token 上限的模型别配太大的块,超出部分会被静默截断,你以为存进去了,模型压根没看见。
对于用 Claude Code 做开发的同学,如果要把这套接入固化到工具链里,Claude Code 的配置入口在 https://taotoken.net/claude-code ,里面会涉及 Base URL 和 Key 的填写方式,思路和上面一致。Cline、Codex 这类工具如果走 MCP 或 auth.json 配置,同样是把 Base URL 指向 https://taotoken.net/api ,Key 填控制台拿的那把,Model ID 按需选。三件套齐了,工具侧就通了。
配置阶段最容易踩的坑是把 base_url 写成带 /v1 或者不带 /v1 的版本。OpenAI SDK 会自动拼接路径,所以 base_url 一般填到域名加 /api 这一层,具体以文档说明为准。填错了典型表现是 404,而不是 401,这个区分后面排障会用到。
4. 检索验证脚本:用 Hit@3 在自己的数据上一锤定音
配置通了之后,别急着上生产,先做一次小规模自测。比刷三天榜单更有用的,是这个二十分钟的小评测:收集 20 到 50 个真实用户会问的问题,人工标注每个问题的正确答案落在哪个块或哪份文档,给每个候选模型各建一份索引,跑检索算 Hit@3——前 3 个检索结果里是否包含正确的块。
先写建库和检索的最小实现。为了聚焦 Embedding 效果,这里用内存里的余弦相似度,不引入向量数据库,避免其他变量干扰:
import numpy as np from config import client, embed # 复用上一节的 client 和 embed def cosine_topk(query_vec, doc_vecs, k=3): q = np.array(query_vec) d = np.array(doc_vecs) q = q / (np.linalg.norm(q) + 1e-10) d = d / (np.linalg.norm(d, axis=1, keepdims=True) + 1e-10) sims = d @ q idx = np.argsort(-sims)[:k] return idx.tolist(), sims[idx].tolist() def build_index(chunks, model): vecs = embed(chunks, model=model) return vecs def retrieve(query, chunks, doc_vecs, model, k=3): q_vec = embed([query], model=model)[0] idx, sims = cosine_topk(q_vec, doc_vecs, k=k) return [(i, chunks[i], sims[j]) for j, i in enumerate(idx)]然后是 Hit@3 的计算。核心逻辑十行就够:
def hit_at_k(questions, truths, chunks, doc_vecs, model, k=3): hit = 0 for q, truth_ids in zip(questions, truths): results = retrieve(q, chunks, doc_vecs, model, k=k) got_ids = [r[0] for r in results] if any(t in got_ids for t in truth_ids): hit += 1 return hit / len(questions)把候选模型循环跑一遍,输出对比表:
import json with open("candidates.json", "r", encoding="utf-8") as f: cfg = json.load(f) questions = [...] # 你的 20-50 个真实问题 truths = [[3], [17], ...] # 每个问题的正确块 ID chunks = [...] # 切好的文档块 for model in cfg["models"]: doc_vecs = build_index(chunks, model) score = hit_at_k(questions, truths, chunks, doc_vecs, model, k=cfg["top_k"]) print(f"{model:35s} Hit@3 = {score:.3f}")跑完你会得到类似这样的输出(数值仅为示例,以你实测为准):
bge-m3 Hit@3 = 0.880 bge-large-zh-v1.5 Hit@3 = 0.820 Qwen3-Embedding-0.6B Hit@3 = 0.900 gte-Qwen2-7B-instruct Hit@3 = 0.860哪个模型 Hit@3 高就用哪个。在你的数据上,这个数字比任何公开榜单都权威。这里还要顺带看相似度阈值:把每个问题的 top1 相似度打出来,如果正确块和错误块的相似度挤在一起(比如都在 0.7 附近),说明这个模型的区分度不够,光靠阈值卡不住,得靠重排。长文档场景下,重点看块被截断后命中率有没有掉——512 token 的模型配大块,Hit@3 通常会明显下滑,这就是"眼睛"没看全的直接证据。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth
接入和回归过程中,报错基本集中在几类。逐个对照排查,能省不少时间。
401 Unauthorized。最常见的原因是 Key 没读到或者读错了。检查 .env 是否被 load_dotenv 正确加载,环境变量名有没有拼错,Key 前后有没有多余空格或换行。还有一种情况是 Key 复制时带了引号,代码里又当字符串处理,导致实际发送的 Key 多了引号。排查方法是在初始化 client 后打印一下 api_key 的前几位和后几位,确认和 https://taotoken.net/api-keys 里显示的一致。如果 Key 本身没问题,检查 base_url 是否指向了正确的通道。
local proxy failed / connection error。这类报错通常是网络层的问题,表现为连接超时或拒绝。先确认 base_url 拼写正确,协议是 https,路径到 /api 这一层。如果本地有网络工具干扰,先关掉再试。注意不要在任何配置里写代理相关的字段,保持直连即可。用 curl 快速验证连通性:
curl -s -X POST https://taotoken.net/api/embeddings \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"bge-m3","input":["测试文本"]}'能返回 JSON 就说明通道没问题,问题在代码侧。
reading choices / 返回结构解析失败。这个报错一般出现在你复用了对话接口的解析逻辑去解析 Embedding 响应。Embedding 返回的是 data 数组,每个元素有 embedding 字段,没有 choices。如果你看到 "reading 'choices'" 之类的报错,说明代码里在按对话响应结构取值。改成resp.data[0].embedding即可。反过来,如果你在对话场景看到 "reading 'embedding'",那就是拿错了接口。
OAuth / 鉴权方式不匹配。有些工具默认走 OAuth 流程,而统一 Key 通道走的是 Bearer Token。如果你在 Claude Code 或类似工具里遇到 OAuth 相关报错,去配置里把鉴权方式改成 API Key,Base URL 填 https://taotoken.net/api ,Key 填控制台那把。Claude Code 的具体配置参考 https://taotoken.net/claude-code 。Cline 走 MCP 的话,检查 MCP 配置里的 env 字段是否把 Base URL、Key、Model ID 三件套都带上了,缺一个都会鉴权失败。
维度不匹配。换模型后如果报向量维度对不上,说明你还在用旧模型的索引。不同模型的向量空间互不相通,连维度都可能不一样。库里存的是 A 模型的坐标,查询用 B 模型算坐标,等于两个人拿着不同城市的地图对暗号。解决办法只有一个:删掉旧索引,用新模型全量重建。对应到代码就是清空 doc_vecs 重新 build_index。
查询前缀没加导致效果打折。有些模型要求给查询加特定前缀才能发挥全力,比如 BGE v1 时代著名的"为这个句子生成表示以用于检索相关文章:",v1.5 已弱化这一要求;Qwen3-Embedding 支持在查询侧附带任务指令。麻烦在于前缀用错了不报错,只是效果悄悄打折。换模型时务必去模型主页看一眼查询侧和文档侧分别怎么处理,把前缀逻辑写进 embed 函数的 query 分支里。
6. 选型落地:从候选到生产,把"眼睛"调准
把前面的流程串起来,一套可落地的选型路径是这样的。先用 C-MTEB 的 Retrieval 单项圈出候选范围,别只看总均分——总分是十八般武艺的平均值,而你只关心它找资料找得准不准。榜单只当候选名单,别当圣旨,这几年榜首常换人,而且公开榜单存在被应试训练的问题,榜上高分未必等于在你的数据上好用。
圈定候选后,用第 4 节的脚本在自己的数据上跑 Hit@3,同时观察相似度分布和长文档截断的影响。资源与速度也要纳入考量:建库时每个块都要算一遍向量,查询时每个问题都要实时算,文档量一大,向量模型的速度就是真金白银和用户体验。长度上限直接和 chunk_size 联动,想用大块就选长上下文模型。部署与合规方面,涉密数据必须本地跑,不在乎数据出门又不想碰运维,API 更省心。
场景速查可以这样记:学习练手或轻量应用,bge-small 或 bge-base-zh-v1.5 开箱即用;中文为主的生产起步,bge-large-zh-v1.5 或 Qwen3-Embedding-0.6B;中英混合、多语言、想配大块,bge-m3 或 Qwen3-Embedding;追求开源效果上限且有专业显卡,Qwen3-Embedding-8B、gte-Qwen 档,但务必自测;不想碰部署,走 API 方案,先过数据合规这一关。
最后提醒三个换模型时的坑:换 Embedding 模型必须全量重建索引,没有例外;查询指令和前缀要按说明书来,用错了不报错但效果打折;维度不是越大越好,维度翻倍索引体积和内存占用大致翻倍,检索耗时也上涨,新一代模型普遍支持套娃维度,可以按需截短,效果损失很小。
整套流程里,统一 Key 通道让多模型切换和回归变成改一个字符串的事,这是能坚持做完自测的前提。需要拿 Key 的走 https://taotoken.net/api-keys ,接口细节查 https://taotoken.net/doc ,想先快速验证模型效果的用 https://taotoken.net/models ,长期做编码和 Agent 回归的看 https://taotoken.net/coding-plan 。把"眼睛"调准了,生成端的能力才真正发挥得出来。