系列「企业级 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 一批

四个值得单说的点:

arkModel 填的不是模型名

// Model specifies the ID of endpoint on ark platform
// Required
Model string `json:"model"`

火山方舟这里要填的是你在平台上创建的推理接入点 IDep- 开头那串),不是 doubao-embedding 这种模型名。第一次用几乎必踩。

它的认证也比别家复杂:APIKeyAccessKey/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% 里,多半是「三万块的费用谁签字」这一类——需要数值推理、多跳推理、精确匹配的题。这些换模型解决不了。

该去做的事,按性价比排:

  1. 回头看切片。第 68 篇《切片策略》那组数据:同一份文档、同一个模型,切法从 0/4 变到 4/4。切片的杠杆比模型大得多,而且不花钱
  2. 查询改写 + 重排序(第 74 篇《多查询 + 重排序》会讲)。用户问得含糊时,先让 LLM 把问题改写成几个更明确的说法再检索
  3. 混合检索。向量负责语义,关键词(BM25 / 全文索引)负责精确匹配型号、编号、金额。两条路的结果合并
  4. 加缓存(第 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
  • 换模型 = 整库重建,这条可以写进 DDL000127 在列非空时 RAISE EXCEPTION,宁可迁移失败也不静默丢向量
  • 只有 tencentcloud 帮你分批(硬编码 200),openai / dashscope 原样转发,超限自己接着
  • arkModel 填 endpoint IDdashscope 的 BaseURL 是常量改不了
  • 维度是存储成本:512 维省一半空间,掉多少精度用评测集测
  • Recall@3 过 0.9 就转战别处:切片、查询改写、混合检索、缓存

下一篇(第 71 篇)拆 Embedder 接口和官方的 Redis 缓存层:Cacher / Generator 两个接口怎么分工,以及 HashGenerator 里一个让 key 变得比原文更长的实现细节。


代码状态说明

第二~四节:评测集代码(约 130 行 harness + 60 行本地模型)在 eino v0.9.13go vet 通过并真机运行,分数和逐题排名都是真实输出,原样粘贴。但那两个模型是我自己写的本地土办法(字袋 / 二元组袋),不是任何云端模型,分数只用于证明评测集能区分模型,不代表任何商业模型的水平。各家云模型的效果和延迟我没有 API Key,没有实测。

第五节rageval 的代码、数据集 schema、四组跑分 JSON、迁移断言,全部来自 DeepFlux 仓库既有产物(server/tools/rageval/docs/sales/evidence/runs/000127 迁移),不是我为这篇文章跑的,我只做了引用和解读。其中「两个 embedder 加 reranker 后指标逐位相同 = 上游被 reranker 抹平」一条是我的推断,已在正文标注——归档 README 未将那份 run 列入有效表。修复前的两份 run 已被官方作废,正文没有引用它们的数字,只引用了作废这件事本身。

第六节:adapter 差异全部来自 eino-ext 源码,可自行核对。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐