Embedding 模型怎么选:别看榜单,测你自己的文档(第70篇-E56)
系列「企业级 AI Agent 实现拆解」E56 篇,Part 13 RAG 篇第五章。上一篇 把三个切分器的算法拆完了。这篇进到向量化这一步:模型怎么选。
先坦白一件事。
这篇按计划该摆一张「OpenAI vs 国产 vs 本地,效果/成本/速度实测对比」的大表。我不打算那么写,两个原因:
**第一,价格和榜单每个月都在变。**写死在文章里,三个月后就是过期信息,还会有人拿它当依据做决策。
**第二个更要紧:跑分跟你的语料没关系。**我用一份员工手册测出来的第一名,换到你的医疗病历、法律合同、代码注释上,排名可能整个翻过来。别人的分数,参考价值比你以为的低得多。
所以这篇给四样不会过期的东西:一套能跑的评测集(换模型改一行)、一套生产里的同类实现(DeepFlux 的 rageval,附真实跑分)、一张源码级差异表(Config 层面的事实)、一个成本算式(自己代入当期单价)。
先解释两个词,后面一直会用:
- 跑分:跟手机跑分一个意思。拿一套固定题目测模型,得出数字用来排名。你在各家宣传页看到的「MTEB 中文榜第一」就是这个。
- 评测集:一套题。每道题写清「问什么」和「哪片文档才是正确答案」,再配一段几十行的程序把题跑一遍、自动算分。英文里题库叫 dataset、跑分程序叫 harness,本文统称评测集。
这篇的核心主张就一句:别抄榜单的跑分,自己搭个评测集,用你自己的文档出分。
读完这篇你会知道
- 三个指标 Recall@1 / Recall@3 / MRR,各自会在什么时候骗你
- 一套 130 行的评测集,
Evaluate(ctx, name, emb, ...)换模型只改一个参数- 实测:两个模型 Recall@1 差 17 个点,但 Recall@3 反过来——单看一个指标会选错
- 逐题看排名,能识别出「这题换模型也救不了」
- 生产版评测集长什么样:DeepFlux
rageval的四组真实对照,其中两条反直觉- 一个看起来完美的 0% 负样本率,实际是「根本没捞上来」
- 「换 embedding 模型 = 整库重建」这条规则怎么写进 DDL 强制执行
- 七家 embedder adapter 的源码级差异:批量策略三种流派、
Model字段的真实语义- 把「贵不贵」算清楚的公式,以及维度本身就是存储成本
- 什么时候该停止折腾模型,去优化别的环节
一、三个指标,各自的盲区
先把话说清楚,不然看数据会看糊。
给定一个问题和一批候选片,检索会把所有片按相似度排个序。三个指标都是在问「正确的那片排第几」:
| 指标 | 定义 | 什么时候看它 |
|---|---|---|
| Recall@1 | 正确片排第 1 的比例 | 只取 TopK=1,或者要「直接给答案」 |
| Recall@3 | 正确片排进前 3 的比例 | RAG 通常 TopK=3~5,这个最贴近真实 |
| MRR | 排名倒数的平均值(第 1 名得 1 分,第 2 名 0.5 分,第 10 名 0.1 分) | 想区分「差一点」和「差很远」 |
Recall 只看有没有进榜,MRR 还看进榜的位置。三个一起看才完整。
为什么强调这个?看实测数据。
二、实测:单看一个指标会选错模型
我拿 12 片语料、6 道题,跑了两个本地确定性模型——不是云端大模型,是我自己写的两个几十行的土办法,专门用来证明这套评测集能区分模型好坏:
charBag:把每个字哈希到固定维度上计数。约等于「字面重合度」bigram:把相邻两个字作为一组去哈希。比单字多一点顺序信息
语料 12 片,评测集 6 题
模型 维度 调用次 文本条 Recall@1 Recall@3 MRR
charBag 单字袋(≈字面匹配) 4096 8 18 0.50 0.83 0.68
bigram 二元组袋 4096 8 18 0.67 0.67 0.74
看这两行:
- 按 Recall@1 选 →
bigram赢(0.67 vs 0.50) - 按 Recall@3 选 →
charBag赢(0.83 vs 0.67) - 按 MRR 选 →
bigram赢(0.74 vs 0.68)
同一份数据,换个指标换个冠军。
这不是我编的巧合,是两种模型的能力形状不同:charBag 更容易把正确答案带进前三但不容易排到第一,bigram 更容易一击命中但也更容易彻底错过。
选指标之前,先确定你的 TopK 是几。
TopK=3 就以 Recall@3 为主,TopK=1 就以 Recall@1 为主。
拿错指标选模型,是在优化一个你根本不会用到的场景。
三、逐题看,比看总分有用
总分告诉你「哪个好」,逐题告诉你「为什么」和「接下来该干什么」。
【charBag 单字袋(≈字面匹配)】
年假能休几天 正确片排第 1 位,Top1=annual ← 标题词直接命中
生病了要交什么材料 正确片排第 1 位,Top1=sick ← 问法和原文用词不同
报销的截止时间 正确片排第 2 位,Top1=sick ← 同义改写
辞职要提前多久说 正确片排第 1 位,Top1=resign ← 「辞职」vs 原文「离职」
三万块的费用谁签字 正确片排第 10 位,Top1=reimburse ← 需要推理金额区间
公司发不发五险一金 正确片排第 2 位,Top1=device ← 口语化提问
【bigram 二元组袋】
年假能休几天 正确片排第 1 位,Top1=annual ← 标题词直接命中
生病了要交什么材料 正确片排第 4 位,Top1=probation ← 问法和原文用词不同
报销的截止时间 正确片排第 1 位,Top1=reimburse ← 同义改写
辞职要提前多久说 正确片排第 1 位,Top1=resign ← 「辞职」vs 原文「离职」
三万块的费用谁签字 正确片排第 5 位,Top1=reimburse ← 需要推理金额区间
公司发不发五险一金 正确片排第 1 位,Top1=insurance ← 口语化提问
三类信息一眼看得出来:
① 有的题谁都会做。「年假能休几天」「辞职要提前多久说」两个模型都排第 1。这类题不区分模型,加进评测集只是浪费——但也别删,它们是回归测试的底线。
② 有的题模型间互有胜负。「生病了要交什么材料」charBag 第 1、bigram 第 4;「报销的截止时间」正好反过来。这些才是真正在做区分的题。
**③ 有一题两个都跪:「三万块的费用谁签字」。**第 10 位和第 5 位。
第三类最值钱。这道题的正确答案是「五千元至两万元须经分管副总审批」——要答对,得先理解「三万块」落在哪个金额区间。
**这不是换模型能解决的问题。**向量检索匹配的是语义相似度,不做数值比较。你把模型从土办法换成最贵的云端模型,它照样不知道 30000 > 20000。
逐题明细里出现「所有模型都排到很后面」的题,那是信号,不是噪声。
它在说:这个需求需要的不是更好的 embedding,是关键词混合检索、查询改写、或者干脆用结构化查询。
charBag 那题 Top1 命中的是 reimburse(报销时限),因为「费用」两个字在那片里——纯字面干扰。这也解释了为什么单靠字面匹配的检索会翻车。
四、评测集的代码
核心就一个函数,接口入参:
func Evaluate(ctx context.Context, name string, emb embedding.Embedder,
chunks []Chunk, cases []Case, batch int) (*Result, error)
**第三个参数是 embedding.Embedder 接口。**换模型就是换这一个实参,评测逻辑一行不动——这是第 67 篇《Document 组件源码》讲的接口设计在选型场景下的直接红利。
评测集的结构简单到没什么可说:
type Chunk struct {
ID string
Text string
}
type Case struct {
Query string
WantID string // 唯一正确的那片
Comment string // 这题在考什么,写清楚,将来回看有用
}
Comment 字段别省。三个月后你回来看数据,「这题为什么会错」全靠它。
顺手做成本统计:包一层
type countingEmbedder struct {
inner embedding.Embedder
calls int
texts int
}
func (c *countingEmbedder) EmbedStrings(ctx context.Context, texts []string,
opts ...embedding.Option) ([][]float64, error) {
c.calls++
c.texts += len(texts)
return c.inner.EmbedStrings(ctx, texts, opts...)
}
十行,统计出「调了几次 API、送了多少条文本」。上面那张表里 调用次=8 文本条=18 就是它数出来的(12 片按 10 一批 = 2 次,加 6 次查询 = 8 次;12 + 6 = 18 条)。
这个套路不止用于评测。生产上想给任何 Embedder 加计数、限流、重试、降级,都是这么包一层——因为 Embedder 就一个方法,装饰起来毫无负担。第 71 篇《Embedder 缓存层》会讲官方的缓存层用的也是同一个套路。
打分部分
ranked := make([]scored, len(chunks))
for i, ch := range chunks {
ranked[i] = scored{ch.ID, cosine(qv[0], vectors[i])}
}
sort.SliceStable(ranked, func(i, j int) bool { return ranked[i].score > ranked[j].score })
rank := 0
for i, s := range ranked {
if s.id == c.WantID {
rank = i + 1
break
}
}
switch {
case rank == 1:
hit1++; hit3++; mrrSum += 1
case rank > 0 && rank <= 3:
hit3++; mrrSum += 1 / float64(rank)
case rank > 0:
mrrSum += 1 / float64(rank)
}
用 sort.SliceStable 而不是 sort.Slice:分数打平时保持原始顺序,这样同一份数据每次跑出来的名次一致,不会因为排序不稳定让数据抖动。评测集最重要的品质是可重复。
换成真模型
emb, err := dashscope.NewEmbedder(ctx, &dashscope.EmbeddingConfig{
APIKey: os.Getenv("DASHSCOPE_API_KEY"),
Model: "text-embedding-v3",
})
r, err := Evaluate(ctx, "dashscope-v3", emb, chunks, cases, 10)
改这两行,其余不动。想同时比三家,就往 models 数组里多塞几个。
五、生产里的评测集长什么样
上面那套 130 行是玩具,够讲清楚原理,不够用来做真决策。
DeepFlux 里有一套真的,server/tools/rageval/,1900 行非测试代码 + 1280 行测试。做的事和玩具版完全一样——喂语料和题目,出分数——但多出来的部分全是被生产逼出来的。
| 玩具版(本文第四节) | rageval(生产) |
|
|---|---|---|
| 指标 | Recall@1/@3、MRR | Recall@20、MRR@10、nDCG@10、negative_rate |
| 正确答案 | 一个 WantID |
分级相关度 0-3 + 负样本清单 |
| 题目分类 | 无 | query_class 分桶(semantic / exact_id / table / ambiguous / multi-hop) |
| 语料 | 硬编码在 Go 里 | YAML 数据集,带 sha256 |
| 结果 | 打印到终端 | JSON 归档,报告里每个数字可追溯 |
三个差别值得单独说。
① 多了一个指标:negative_rate(不该出现的东西出现了没有)
玩具版只问「正确答案排第几」。生产还得问反面:有没有把不该给的东西捞上来。
评测集里每道题可以挂一张负样本清单:
// NegRef · 毒药/负样本:命中即扣分。
type NegRef struct {
DocID string `yaml:"doc_id"`
Reason string `yaml:"reason"`
}
源码注释管它叫「毒药」,很贴切。比如问「年假能休几天」,把《劳务派遣人员管理办法》里的年假条款捞上来——它确实相关,但适用对象不是提问的人,答上去就是错的。这种错比「没检索到」危险得多,因为它会生成一个看起来有理有据的错误答案。
negative_rate 就是统计这个:**毒药进了 top-3 的题占多大比例。**这个指标只有越低越好,跟其他三个方向相反。
② 正确答案是分级的,不是唯一的
type RelevantRef struct {
DocID string `yaml:"doc_id"`
Relevance int `yaml:"relevance"` // 0-3(>=2 记入 MRR;>=1 记入 Recall)
SpanStart int `yaml:"span_start"`
SpanEnd int `yaml:"span_end"`
}
注意那行注释里的两条不同门槛:
- Recall 宽松:
relevance >= 1,沾边就算捞到了 - MRR 严格:
relevance >= 2,只有真正相关的才配算排名
为什么要分开?因为真实文档里「有点关系」和「就是答案」是两回事。一份文档提到了年假但只是引用了政策编号,它 relevance=1——检索到不算错,但它排第一就是失败。用同一条线卡两个指标,就分不出这个区别。
数据集加载时还有一道硬校验:
// Validate · 确定性标注完整性:相关/负样本 doc_id 必须在 corpus 中,
// 否则标注悬空,指标无意义。
标注里写了一个语料中不存在的 doc_id,直接报错退出,不是警告。因为悬空标注会让分数变得没有意义却依然显示为一个正常数字——这比报错难发现得多。
③ 一次真实对照:reranker 到底买到了什么
同一份语料(zh-eng-v2-001,35 道题)、同一个 embedding 模型,只差一个重排序:
| 配置 | recall@20 | mrr@10 | ndcg@10 | negative_rate | p50 延迟 |
|---|---|---|---|---|---|
| qwen3.7-text-embedding,无 reranker | 1.000 | 0.871 | 0.955 | 11.4% | 262ms |
| qwen3.7-text-embedding,+ gte-rerank-v2 | 1.000 | 0.857 | 0.952 | 8.6% | 640ms |
| 达标线 | ≥0.80 | ≥0.70 | ≥0.70 | ≤0.10 |
**加了 reranker,三个指标里有两个变差了。**MRR 掉 1.4 个点,nDCG 掉 0.3 个点,延迟从 262ms 涨到 640ms。
但它把 negative_rate 从 11.4% 压到 8.6%——而这恰好是唯一一项没达标的指标(线是 ≤10%)。
所以这个决策是:花两倍延迟、掉一点排序质量,换那三道题不再把毒药送进 top-3。不配 reranker,这套配置就不达标。
回头看第一节那句话,这就是它在生产里的样子:
只看 MRR,你会拒绝这个 reranker。只看 negative_rate,你会以为它免费。
顺带一个观察:还有第三份 run 用的是另一个 embedder(bge-small-zh-v1.5,512 维)+ 同一个 reranker,四项指标跟 qwen 那组逐位相同,只有延迟不同(481ms vs 640ms)。合理的解释是:两个 embedder 的 top-20 候选集都已经把相关文档全捞进来了(recall@20 都是 1.000),后面的排序完全由 reranker 决定,上游换谁都一样。
如果这个解释成立,那这个数据集上更贵的 embedder 买不到精度,只影响延迟和成本。但这条是我的推断——归档 README 里没把这份列进有效 run 表,我没有进一步的证据。
④ 最值钱的一条:几个模型跑出一样的分数,那是 bug
rageval 的归档目录开头挂着一条作废声明,值得整段抄:
⚠️ 2026-07-29 之前的所有 run 作废
harness 构造
SearchHandler时没有注入NamespaceRepo……FlagVectorV2永远读不到 → 一律走SearchPlanned查旧列embedding vector(1536)。而非 1536 维的真 embedder 数据全写在embedding_v2,向量腿恒返回 0 行,整轮评测退化为纯词法腿。判定证据:三个架构/维度完全不同的模型跑出逐位相同的指标——bge-small-zh-v1.5(512d)、百炼 text-embedding-v4(1024d)、qwen3.7-text-embedding(1024d) 全部是
0.700 / 0.562 / 0.653。修复后vec=26(此前恒 0)。
把判定逻辑单独拎出来:
三个维度和架构都不同的模型,跑出小数点后全部相同的分数——这不可能是巧合。
唯一的解释是:它们的输出根本没参与计算。
这是本文最实用的一条经验,而且反直觉。评测集的正常状态是换模型分数就得动;分数纹丝不动看起来像「稳定」,实际是「你测的东西跟模型无关」。
同一个坑还有第二层。rageval 默认跑的是假模型:
--deterministic=false 是关键:默认 true 是 PR smoke 模式,用 hash embedder,
指标无业务含义。
默认模式用哈希充当 embedding,目的是让 CI 能快速跑通流程。它一样会输出一份格式完整、看起来很正常的分数报告——只是那些数字跟检索质量没有任何关系。忘记加 --deterministic=false,你拿到的就是一份精美的假数据。
所以评测集交付时要配一条自检:先确认换模型时分数会变,再相信它给出的任何排名。
⑤ 「换模型 = 整库重建」可以写进 DDL
第七节会讲这条规则。DeepFlux 把它做成了迁移里的一段断言——000127 要把 memories.embedding 从 1536 维改成 512 维:
SELECT count(embedding) INTO n FROM memories;
IF n > 0 THEN
RAISE EXCEPTION
'memories.embedding 有 % 行非空向量(1536 维)。512 维模型与之不兼容,'
'ALTER 会丢弃全部旧向量。请先决定重索引策略……', n;
END IF;
只在整列为 NULL 时才允许改类型,否则主动报错终止迁移。迁移注释写得很直白:
512 维模型与 1536 维旧向量语义空间不兼容,静默丢弃是最坏做法。
这就是「换 embedding 模型 = 整库重新向量化」从一句口头纪律变成一道不可绕过的闸。顺带说明为什么会有这次维度变更:原来对齐 OpenAI text-embedding-3-small 写死 1536 维,但那个 embedder 服务在生产从未部署,memories.embedding 全表是 NULL——**向量去重和语义召回一直是哑的,只是没人发现。**换成本地 ONNX 的 bge-small-zh-v1.5(512 维)后,维度必须跟着收窄。
(HNSW 索引绑定列类型,改维度前必须先 DROP INDEX 再重建——这个细节第 72 篇《pgvector 入门》会讲。)
六、七家 adapter 的源码级差异
这部分是从 eino-ext/components/embedding/ 各家源码里读出来的,不涉及效果和价格,所以不会过期。
| adapter | Model 字段填什么 |
维度可配 | BaseURL | 批量策略 |
|---|---|---|---|---|
openai |
模型名 | ✅ text-embedding-3 及以后 |
可改(也支持 Azure) | 原样转发 |
dashscope |
模型名(v1/v2/v3) | ✅ 仅 v3:1024/768/512 | 写死在常量里 | 原样转发 |
ark |
endpoint ID(ep-*) |
✅ | 可改 | 多模态时逐条循环 |
ollama |
本地模型名 | ❌ | 可改(默认本机) | 一把全送 |
gemini |
模型名 | — | — | — |
qianfan |
模型名 | — | — | — |
tencentcloud |
不用填(写死 hunyuan-embedding) |
❌ | — | 硬编码 200 一批 |
四个值得单说的点:
① ark 的 Model 填的不是模型名
// Model specifies the ID of endpoint on ark platform
// Required
Model string `json:"model"`
火山方舟这里要填的是你在平台上创建的推理接入点 ID(ep- 开头那串),不是 doubao-embedding 这种模型名。第一次用几乎必踩。
它的认证也比别家复杂:APIKey 或 AccessKey/SecretKey 二选一,APIKey 优先;用 AK/SK 配预置接入点时还要额外填 ProjectName。
② 批量策略有三种流派,只有一家帮你分批
帮你分批的(tencentcloud),源码里硬编码了上限,还贴了 SDK 文档链接:
// NOTE: len of req.InputList must less equal than 200, so we need to split texts into batches
// reference: https://pkg.go.dev/github.com/tencentcloud/...
batchSize := 200
for l := 0; l < len(texts); l += batchSize {
r := min(l+batchSize, len(texts))
req.InputList = common.StringPtrs(texts[l:r])
// ...
}
服务端分批的(ollama),客户端一把全送,本地服务自己排队:
req := &api.EmbedRequest{
Model: e.conf.Model,
Input: texts, // 全部塞进去
// ...
}
resp, err := e.cli.Embed(ctx, req)
啥也不管的(openai / dashscope),你传多少就往上游发多少。超了上游的条数限制就报错,分批是你自己的事。
这就是第 66 篇《最简 RAG》那段代码里为什么要自己写 const batch = 10 循环——不是我谨慎,是这层真的不管。
③ dashscope 的 BaseURL 改不了
const (
baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1"
dimensions = 1024
)
包级常量,EmbeddingConfig 里没有对应字段。要指向别的端点(代理、私有网关、兼容层),只能用 openai adapter 手动填 BaseURL——反正 dashscope 走的就是 OpenAI 兼容协议,内部也是转手给 libs/acl/openai。
顺带一句:dimensions = 1024 是默认值,NewEmbedder 里会在你没填时兜上:
if ecfg.Dimensions == nil {
dim := dimensions
ecfg.Dimensions = &dim
}
④ 调用时可以临时换模型
embedding.Options 里只有一个通用字段:
type Options struct {
Model *string
}
func WithModel(model string) Option { /* ... */ }
各家实现的姿势是统一的——拿构造时的配置当默认值,允许调用时覆盖:
options := embedding.GetCommonOptions(&embedding.Options{
Model: &e.conf.Model,
}, opts...)
所以做 A/B 测试不用建两个 Embedder,一个实例加 embedding.WithModel("...") 就能切。这也是第 67 篇那套 Option 范式的又一次复用。
七、成本怎么算
不给单价(会过期),给算式。
建库成本(一次性):
总 token 数 ≈ 总字数 ÷ 每 token 字数
建库费用 = 总 token 数 × 单价
中文粗算 1 个 token ≈ 1~1.5 个汉字,具体看模型的分词器。10 万字的文档大概 7~10 万 token 量级。
隐藏成本:重建次数。
这是最容易漏算的一项:
换 embedding 模型 = 整库重新向量化。
不同模型的向量空间不通用,混着存算出来的相似度是废的(第 66 篇强调过)。
所以选型阶段每试一个模型,就是一次全量建库。先用小样本评测集筛掉大部分候选,只对最后一两个跑全量——这就是这套评测集省钱的地方:12 片语料 8 次调用,比全量重建便宜好几个数量级。
查询成本(长期):
每日查询费用 = 日查询量 × 单条查询 token 数 × 单价
单条查询就是用户那句话,十几个 token,单价上通常微不足道。真正的长期成本在建库和重建。
维度也是成本,而且是存储成本。
1024 维 × float32(4 字节) = 4 KB / 片
10 万片 = 400 MB 纯向量
再加索引结构的开销。所以 dashscope v3 允许把维度降到 768 或 512 不是花活——512 维直接省一半存储,检索也快。代价是精度略降。
这件事该怎么定?**用评测集测。**把同一个模型的 1024 / 768 / 512 三个维度各跑一遍,看 Recall@3 掉多少。掉 1 个点就换来一半存储,通常划算。
八、什么时候该停止折腾模型
一个实操判断:
Recall@3 上到 0.9 以上,就别再换模型了。
剩下那 10% 里,多半是「三万块的费用谁签字」这一类——需要数值推理、多跳推理、精确匹配的题。这些换模型解决不了。
该去做的事,按性价比排:
- 回头看切片。第 68 篇《切片策略》那组数据:同一份文档、同一个模型,切法从 0/4 变到 4/4。切片的杠杆比模型大得多,而且不花钱
- 查询改写 + 重排序(第 74 篇《多查询 + 重排序》会讲)。用户问得含糊时,先让 LLM 把问题改写成几个更明确的说法再检索
- 混合检索。向量负责语义,关键词(BM25 / 全文索引)负责精确匹配型号、编号、金额。两条路的结果合并
- 加缓存(第 71 篇会讲)。同样的文本反复算向量是纯浪费,官方
embedding/cache就是干这个的
小结
- 别抄别人的跑分:榜单是通用语料,你的语料有行业术语和内部黑话,排名会变
- 单指标会选错:实测两个模型,Recall@1 一个赢、Recall@3 另一个赢、MRR 又反过来。先确定 TopK,再选指标
- 逐题明细比总分有用:所有模型都排到很后面的题,是「该换技术路线」的信号,不是「该换模型」
- 评测集的关键是接口入参:
Evaluate(ctx, name, emb embedding.Embedder, ...),换模型改一个实参 - 包一层就能统计成本:
Embedder只有一个方法,装饰它毫无负担 - 生产版要多一个反向指标:
negative_rate(不该召回的召回了多少)。生产实测 reranker 掉 1.4 点 MRR、涨 1 倍延迟,换来 neg 从 11.4% 压到 8.6%——而那是唯一没达标的项 - 正确答案要分级:Recall 认
relevance>=1,MRR 只认>=2。一条线卡两个指标就分不出「沾边」和「就是答案」 - 几个模型跑出逐位相同的分数 = 评测集坏了,不是模型稳定。DeepFlux 靠这个信号发现向量腿恒空,作废了修复前的全部 run
- 换模型 = 整库重建,这条可以写进 DDL:
000127在列非空时RAISE EXCEPTION,宁可迁移失败也不静默丢向量 - 只有 tencentcloud 帮你分批(硬编码 200),openai / dashscope 原样转发,超限自己接着
ark的Model填 endpoint ID,dashscope的 BaseURL 是常量改不了- 维度是存储成本:512 维省一半空间,掉多少精度用评测集测
- Recall@3 过 0.9 就转战别处:切片、查询改写、混合检索、缓存
下一篇(第 71 篇)拆 Embedder 接口和官方的 Redis 缓存层:Cacher / Generator 两个接口怎么分工,以及 HashGenerator 里一个让 key 变得比原文更长的实现细节。
代码状态说明:
第二~四节:评测集代码(约 130 行 harness + 60 行本地模型)在
eino v0.9.13下go vet通过并真机运行,分数和逐题排名都是真实输出,原样粘贴。但那两个模型是我自己写的本地土办法(字袋 / 二元组袋),不是任何云端模型,分数只用于证明评测集能区分模型,不代表任何商业模型的水平。各家云模型的效果和延迟我没有 API Key,没有实测。第五节:
rageval的代码、数据集 schema、四组跑分 JSON、迁移断言,全部来自 DeepFlux 仓库既有产物(server/tools/rageval/、docs/sales/evidence/runs/、000127迁移),不是我为这篇文章跑的,我只做了引用和解读。其中「两个 embedder 加 reranker 后指标逐位相同 = 上游被 reranker 抹平」一条是我的推断,已在正文标注——归档 README 未将那份 run 列入有效表。修复前的两份 run 已被官方作废,正文没有引用它们的数字,只引用了作废这件事本身。第六节:adapter 差异全部来自
eino-ext源码,可自行核对。
更多推荐


所有评论(0)