大多数构建 RAG 的团队应该直接使用现成的嵌入模型,然后继续前进。OpenAI 的 text-embedding-3-large 和 Cohere 的 embed-v4 价格低廉(大约每百万 token 0.12 到 0.13 美元),文档齐全,对大多数语料库已经足够。但有一个具体且可衡量的点,这时候情况会改变:如果你在自有领域查询上的 recall@10 使用通用模型时低于约 80%(也就是说,在 10 次测试查询中,有少于 8 次的正确文档能进入前 10 名),并且你能够收集几千个真实的查询‑文档对,那么自行微调嵌入模型通常能在准确率和成本上都超过 API。
那些在 此类微调中发表结果 的团队表示,一个经过微调的小型开放模型可以在指标上超过 OpenAI 或 Cohere 的成绩 10 分或更多。而且训练的成本还不到一顿午餐的钱。
通用嵌入模型是在广泛的网络文本上训练的。它们很擅长认识到 “car” 和 “automobile” 在向量空间中很接近。但在你的支持系统里,它们很难意识到 “ORA-01555” 和 “snapshot too old” 实际上是同一个问题,或者在你的代码库中,reconcile_ledger 和 “nightly balance job” 指的是同一件事。
这是一个词汇问题,微调可以直接解决。对比式微调会从你的数据中取出(查询,正确文档)对,将它们的向量拉近,同时把无关文档推远。模型整体并不会变得更聪明。它在一件事上会变得更好:把用户提问的表达方式映射到文档回答的表达方式。这就是检索嵌入的全部工作,这也是为什么即使是小模型也能带来显著提升。
下面的具体例子——也是后面工作示例所用的场景——是一个工单票据 RAG 系统。用户提交的工单往往语言混乱、缩写很多(比如 “db conn pool exhausted after upgrade”)。而知识库则使用正式的产品语言编写(例如 “Connection limits in Managed Databases”)。通用模型在这两段文字之间几乎找不到重叠。但在六个月的已解决工单上进行微调的模型——每张工单都与解决它的文章配对——会把它们视为近邻。
设置。 这是您在自己的系统中应该运行的比较,基于上面的支持票场景进行演示:10,000 个知识库块,1,000 个保留的测试查询(在训练过程中被隔离且从未展示给模型,仅用于验证其答案)带有标注的正确文档,以及 50,000 个训练配对(每个配对是一个真实的支持问题与解决它的文章匹配),这些配对来源于已解决的票据。关于这些数字来源的简要说明。
召回率和 MRR 行(如上所述:正确答案出现的频率以及它排名靠前的程度)是其他团队在进行此类微调后报告的典型结果。维度、存储和成本行只是事实:它们直接来源于模型规格和各提供商的标价,因此这些与您的数据无关。如果您想要针对自己的系统得到一个数值,则需要自行运行测试,本文末尾的框架会指导您完成此过程。
text-embedding-3-large (API) |
bge-base-en-v1.5 (现成的) |
bge-base-en-v1.5 (微调后) |
|
|---|---|---|---|
| 召回率@10 (领域查询) | 0.71 | 0.66 | 0.83 |
| MRR@10 (领域查询) | 0.58 | 0.52 | 0.71 |
| 召回率@10 (通用查询) | 0.89 | 0.84 | 0.82 |
| 向量维度 | 3,072 | 768 | 768 |
| 10M 块的存储 (pgvector) | ~123 GB | ~31 GB | ~31 GB |
| 嵌入成本,10M 块 | ~$650 (API)* | GPU 时间 | GPU 时间 |
| 查询延迟(嵌入 + 搜索,p50) | ~180 ms | ~45 ms | ~45 ms |
*假设每块 500 个标记,标准费率为 $0.13/M。如果您可以等待最多 24 小时,Batch API 可将此费用减半至约 $325;我们在 RAG 管道成本分解 中使用了相同的假设,以便两篇文章保持可比性。
在此表格中,有三点值得诚实地说明。
首先,在领域查询上,微调模型以显著优势获胜,召回率大约高出 12 个百分点@10,远超更大的 API 模型。这表明词汇差距在缩小,这与类似语料库上发表的微调模型的报告一致。其次,在通用英文查询上可能会稍有下降。微调是在广度和深度之间的权衡。如果你的查询主要是通用问题,微调模型可能达不到要求,这时应停止。第三,延迟和存储的提升与微调无关。它们来源于自行运行一个小模型:使用 768 维而不是 3072 维,意味着 pgvector 存储和索引大小仅为原来的四分之一,查询时无需往返 API 的网络开销。即使不对 bge-base 进行微调,自行托管也能获得这些好处。
训练过程本身规模较小。bge-base-en-v1.5 拥有 1.09 亿参数。在单个 H100 GPU Droplet 上对 5 万对数据进行微调大约需要 40 分钟,按小时计费,因此运行成本约为 3.40 美元。它也能跑在更便宜的卡上,比如 RTX 4000 Ada,此时同样的运行成本低于 1 美元。此项目的主要成本从来不是 GPU,而是收集高质量的查询‑文档对。
此部分旨在展示训练过程规模小且可复现,而非教程。完整的演练步骤应放在文档中;这里是上述比较的参考实现。
训练数据是从已解决的工单中提取的正样对组成的 JSONL 文件:
{"query": "db conn pool exhausted after upgrade", "positive": "Connection limits in Managed Databases: each plan tier has a maximum connection count..."}
{"query": "how do i rotate the ca cert", "positive": "Rotating certificates: Managed Databases uses a per-cluster CA that can be regenerated..."}
下面的代码大约有 30 行,使用了 sentence-transformers 库。它采用了一种称为 MultipleNegativesRankingLoss 的训练方法,其工作原理如下:在每个训练批次中,针对每个查询,其对应的正确文档即为正确答案,而同一批次中的其他所有文档会被自动视为错误答案。这就是全部的技巧,也是为什么你只需要收集正确的配对。错误的示例会随着批次中其它内容自动出现,无需额外获取。
from datasets import load_dataset
from sentence_transformers import (
SentenceTransformer,
SentenceTransformerTrainer,
SentenceTransformerTrainingArguments,
)
from sentence_transformers.losses import MultipleNegativesRankingLoss
model = SentenceTransformer("BAAI/bge-base-en-v1.5")
dataset = load_dataset("json", data_files="pairs.jsonl", split="train")
dataset = dataset.rename_columns({"query": "anchor", "positive": "positive"})
args = SentenceTransformerTrainingArguments(
output_dir="bge-base-support-ft",
num_train_epochs=2,
per_device_train_batch_size=64, # larger batches = more in-batch negatives
learning_rate=2e-5,
warmup_ratio=0.1,
bf16=True,
)
trainer = SentenceTransformerTrainer(
model=model,
args=args,
train_dataset=dataset,
loss=MultipleNegativesRankingLoss(model),
)
trainer.train()
model.save_pretrained("bge-base-support-ft/final")
评估使用同一库内置的 InformationRetrievalEvaluator,它直接针对保留的查询集报告 recall@k 和 MRR。存储和搜索在带有 HNSW 索引的托管 Postgres 上的 pgvector 上运行;微调模型不会改变你存储或查询它的方式,因此该设置正是 pgvector 文档所描述的。
模型选择在 Postgres 中仅在大小方面体现。在 768 维度下,1000 万个块大约需要 31 GB 的向量加索引。在 3,072 维度下,相同的语料库大约需要 123 GB,这会在你存储任何应用数据之前就迫使你升级到更大的数据库方案。这种差异每月都会重复出现,而训练成本则是一次性的。
如果你在决定将此部署到哪里,诚实的对比如下。
Together AI 和 Fireworks 都提供托管的微调服务,但它们支持的模型列表仅限于生成式模型:用于聊天、视觉和推理的 Llama、Mistral 和 Qwen 系列。就目前而言,两者均未将嵌入模型列为可微调的;嵌入仅作为推理端点提供。这一点很重要,因为本文的重点是训练嵌入模型,而不是提供服务。
Baseten 的训练文档 描述了两条路径:Loops 用于监督微调,Training Jobs 用于运行你自己的训练代码;两者都会生成可直接部署到其推理栈的检查点。在服务方面,Baseten 嵌入推理 是一个专门用于嵌入、重排和分类模型的引擎,Baseten 报告其吞吐量超过次佳解决方案的 2 倍,延迟降低约 10%。如果你希望从训练作业到服务端点拥有一条托管流水线,这也是一个可供评估的公平选项,尽管需要支付相较于原始算力的平台溢价。
Modal 是带你自己的训练作业方式的最接近的比较,他们已经发布了完全相同的工作流程:微调一个开放的嵌入模型以击败专有 API。区别在于计算的形态。Modal 提供按秒计费的 GPU 的无服务器函数,这对于突发的、并行的实验扫描非常合适。
GPU Droplet 只是一个普通的虚拟机:你通过 SSH 登录,运行上面的脚本,按小时付费,而你的向量存放在已经保存应用数据的同一个 Postgres 中。如果你已经在运行定期重新训练任务和生产数据库,那么 VM 加上托管 Postgres 的方案更简单。若需要进行数百次并行的超参数搜索,Modal 的方案更合适。两种方案都是可行的;根据你重新训练的频率来选择。
值得在这里直接提到 DigitalOcean,因为它正是上述数字背后的基础设施。GPU Droplets 是原始的计算资源,而不是托管的微调产品:你会得到一个已经安装好 GPU 驱动的虚拟机,并在其上运行自己的训练脚本,这正是本文训练过程所做的。这里没有类似 Baseten 的 Loops 或 Together 的微调端点那样的 “提交微调作业” API(用于聊天模型)。你得到的而是对该虚拟机周围整个栈的完全控制:你想要的任何训练脚本、带有 pgvector 的托管 Postgres 用于存储得到的向量,以及可以在同一账户内共存的训练运行和生产数据库,无需经过独立的平台。
OpenRouter 不属于此比较范围。它只是一个位于推理 API 之上的路由层,本身不进行任何训练,因此它只能为你提供他人的嵌入模型,而无法帮助你构建自己的模型。
在以下三种情况下,你可以毫无愧疚地跳过微调。
查询量低。训练成本很低,但运营成本不低:有人需要负责配对管道、评估集和重新训练计划。如果你的系统每天只服务几百次查询,且检索基本可用,那么这种所有权成本永远得不到回报。
词汇变化快。如果你的领域术语每月都在变化(新产品名称、新错误码),你的微调模型总是略显过时,重新训练的频率就会成为反复征收的税费。在这里,通用模型加上良好的关键词或混合搜索往往更持久。
没有标注的配对,也没有获取它们的途径。模型的好坏仅取决于你用于训练的查询‑文档配对。如果你无法从日志、工单或点击数据中挖掘这些配对,而只能从文档自身生成合成配对,那么相比上面的表格,提升幅度会小很多。合成配对只能让模型学到你文档的词汇,而不能学到用户的词汇,而这两者之间的差距正是你本来想要弥补的。
决定前先进行测量。构建一个包含 200 到 500 条带有正确文档标签的真实查询的测试集,运行您当前的嵌入模型,并计算 recall@10。然后:
如果以下三个条件全部满足,则进行微调:领域查询的 recall@10 低于约 80%,您可以组织至少几千个真实的查询‑文档对,且团队中有人可以负责一个再训练周期(对于大多数领域来说,每季度一次就足够了)。
如果满足以下任意条件,则继续使用 API:您的查询主要是通用英语,您的词汇变化速度快于您重新训练的速度,或者您没有真实配对的来源。
如果您实际需要的是更低的延迟或更小的向量,则在不进行微调的情况下自行托管。在您自己的 GPU 上使用股票开放模型即可同时获得这两者,并且在您拥有配对后可以以后再添加微调。
这并不是一个难以做出的决定。构建评估集,花一个下午进行测量,您就会知道自己处于哪一方。这比猜错便宜得多——无论是被锁定在您不需要支付的 API 账单上,还是构建您不需要的训练基础设施。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。