Hugging Face 如何用 Inference Endpoints、Jobs 与 Buckets 驱动 Papers with Code 搜索
3 个月前,我们启动了 Papers with Code 的复兴计划(另见官宣推文)。它的目标是让开放的 AI 研究触手可及、易于消化:让人们能轻松找到与一篇论文相关的各类产物,纵览 AI 各领域的最先进(SOTA)成果,分享有趣的研究并在彼此的工作之上继续构建。换句话说,它的目标是助推那股终将孕育出下一个 Transformer 的研究浪潮。
当然,让 AI 研究触手可及需要一个强大的搜索引擎,让人和智能体都能通过网站或 pwc search CLI 命令快速找到相关联的工作,智能体还可以借助Skill 来使用它。
值得注意的是,检索学术论文与检索普通文本并不相同。一个好用的论文搜索引擎不仅要能精确匹配标题或 arXiv 编号,还应能理解"适用于代码生成的小语言模型"这类查询——即使这些词从未在一篇论文中同时出现。它需要识别出"最早的 BERT 论文"是一个导航式请求,能容忍不完整的标题或拼写错误,并且在模型服务冷启动或暂时不可用时依然快速响应。
在 Papers with Code 项目中,我们把它做成了一个混合检索系统。这也源于我们在 ML6 的既往经验——我们曾为客户开发基于 RAG 的系统。实践证明,混合检索通常优于纯关键词检索和纯向量检索,因为它兼取两者之长(更多信息可参见这篇博客)。关键词检索能找到精确的提法,向量检索则能找到更模糊、语义相近的表述。需要说明的是,重排序器(reranker,又称 cross-encoder)可以进一步改善结果,但也会带来额外的开销和延迟。
Papers with Code 基于 PostgreSQL 数据库,其全文检索能力提供了快速的词法基线。在稠密向量一侧,我们用 pgvector 增加语义召回,再用 倒数排序融合(RRF)算法把两者结合起来。稠密向量嵌入用到了三项 Hugging Face 服务:
- Hugging Face Jobs 为论文语料的嵌入计算提供可弹性突发的 GPU 算力。
- Hugging Face Storage Buckets 在数据库、实验和 Jobs 之间提供持久化的交接通道。
- Hugging Face Inference Endpoints 为实时查询和增量更新提供低延迟的嵌入服务。
如今,该系统维护着来自 arXiv 和 Daily Papers 的超过 11 万篇现有论文的嵌入。本文将讲解这套架构、其背后的设计决策,以及我们把它推向生产过程中的经验教训。
摘要
我们刻意把检索拆分为离线语料构建和在线检索服务两部分:
昂贵、面向吞吐量的工作交给 Jobs 运行。持久化产物存放在 Bucket 中。只有轻量的查询嵌入环节留在请求路径上,由一个受保护的 Inference Endpoint 承担,为在线检索提供动力。一旦该端点处于冷启动、繁忙或不健康状态,检索就立即回退到全文检索。这种分离让系统既强大又快速。
从严格的嵌入契约开始
嵌入管线的失效方式往往很隐蔽:模型版本(revision)变了、查询与文档的 prompt 混用了、向量被以不同方式截断,或者更新后的摘要与已存储的向量不再匹配。
我们的规避办法是把嵌入格式当作带版本的 API 来对待。每篇论文都被编码为:
normalized title + "\n\n" + normalized abstract
每次生成向量时,我们都会记录:
- 模型仓库及确切的 revision;
- 输出维度;
- 输入格式版本;
- 输入是查询还是文档;
- 归一化方法;
- 源标题与摘要的内容哈希。
我们的生产生成使用 Qwen/Qwen3-Embedding-0.6B,锁定到确切的 revision,输出 256 维 L2 归一化向量。我们是借助 MTEB 排行榜——比较嵌入模型的首选基准——选出这个模型的。值得注意的是,Qwen3 这类较新的嵌入模型还带来两个新特性:
- 可以指定动态嵌入维度,在质量与速度/存储成本之间做权衡。Qwen 模型称之为 "MRL",即 Matryoshka Representation Learning(套娃表示学习)的缩写,详情可见这里。我们选择了 256 维的嵌入以提升检索速度。
- 可以提供指令 prompt。Qwen 嵌入模型支持
documentprompt(我们用它嵌入论文)和queryprompt(在线检索用它嵌入用户查询)。
这份契约让一个嵌入从导出、经 GPU 推理、写入 PostgreSQL、直至进入在线检索的全程都得到遵循。
用 Jobs 把数据库快照变成向量语料
全语料嵌入是典型的批处理工作负载:它需要在一段较短时间内占用 GPU,受益于高吞吐,且不应在两次运行之间消耗资源。Hugging Face Jobs 非常契合这种形态:一个 Job 由一条命令、一个硬件规格以及可选的 Docker 镜像定义,还能运行依赖内联声明的 uv 脚本。
我们的语料构建首先从 PostgreSQL 可重复读快照中导出每篇论文的最新版本。导出器以流式方式处理数据行而非把整个目录载入内存,写出大小受控的 JSONL 分片,并生成包含行数和 SHA-256 校验和的清单文件。
我们把这次运行的不可变目录同步到一个私有 Storage Bucket,再把 Bucket 直接挂载到一个 l4x1 Job(NVIDIA L4 GPU,24GB 显存)中。从 worker 的视角看,它就是一个普通的文件系统:
hf jobs uv run \
--flavor l4x1 \
--timeout 6h \
--volume hf://buckets/OWNER/pwc-paper-embeddings:/bucket \
embed_papers_job.py \
--input /bucket/runs/RUN_ID/input \
--output /bucket/runs/RUN_ID/output \
--model Qwen/Qwen3-Embedding-0.6B \
--revision MODEL_REVISION \
--dimensions 256 \
--allow-matryoshka
worker 的处理流程:
- 校验输入清单和每个分片的校验和;
- 加载锁定的模型 revision;
- 按文本长度排序以减少 padding;
- 分批调用
encode_document(见模型卡说明); - GPU 显存不足时自动减小批大小;
- 把 Matryoshka 表示截断到 256 维并做归一化;
- 以原子方式写出 float16 Parquet 分片;并
- 记录吞吐量、包版本、硬件、显存峰值、行数和输出校验和。
每个完成的分片都有自己的标记文件,因此重启的 Job 可以跳过已验证的工作。这对大型语料很有用:重试应当是续跑,而不是覆盖已有的嵌入。
在 5,000 篇论文的试点中,Qwen Job 在 L4 GPU 上以 1024 维做到了每秒编码约 75 篇论文。同一轮计算还能以确定性的方式在 512 维和 256 维上落地,于是我们无需为额外推理付费就能比较存储与检索的权衡。
Buckets 是连接组织
Storage Buckets 是 Hub 上可变的、类 S3 的对象存储,专为 AI 工作负载优化。它们可以通过 hf://buckets/... 路径访问,并可在 Jobs 中读写挂载,无需另建存储集成。
对我们来说,Bucket 不只是存放向量的地方。它是三个生命周期各异的系统之间的边界:
- 生产数据库导出源记录;
- 临时性的 Jobs 消费这些记录并产出向量;
- 导入器在触碰检索索引之前先验证结果。
我们用不可变的运行前缀来组织产物:
runs/<run-id>/
├── input/
│ ├── manifest.json
│ └── papers-*.jsonl
└── output/
├── manifest.json
├── embeddings-*.parquet
└── embeddings-*.complete.json
Bucket 本身刻意设计为可变存储,因此不可变性是一条应用层规则:运行 ID 绝不被覆盖,每个产物都有清单和校验和覆盖。
这带来几项有用的性质:
- 可复现性:我们可以把一次数据库生成回溯到确切的语料快照、模型 revision 和一组产物。
- 安全的重试:Jobs 可以在同一运行前缀下从已完成分片处恢复。
- 低成本的实验:多个模型或维度可以复用同一份已验证的输入快照。
- 受控的上线:导入一个生成并不会激活它。我们先验证覆盖率并构建索引。
- 简单的回滚:在新一代被证明稳定之前,上一代及其产物保持可用。
只有当导入器复核了模式、校验和、维度、归一化、唯一的论文 ID 和当前内容哈希之后,我们才把向量加载进 PostgreSQL。随后为新一代单独构建 HNSW 索引,且只有当每一篇合资格的现有论文都被覆盖时,才原子性地将其标记为生效(HNSW 是一种支持快速向量检索的基于图的算法)。
Inference Endpoints 把语义检索放上请求路径
批量嵌入解决了检索的文档一侧。用户查询仍需在请求时用同一模型契约进行嵌入。
我们把锁定的模型部署为经过身份验证的 Inference Endpoint,后端是 Text Embeddings Inference(TEI)。该端点接收查询文本,使用模型的 query prompt 返回归一化的 256 维向量。值得一提的是,这里也可以用 vLLM 或 SGLang。
API 随后在生效的 pgvector 生成上执行余弦距离检索:
SELECT paper_id,
embedding <=> CAST(:query_vector AS halfvec(256)) AS distance
FROM paper_embeddings
WHERE generation_id = :active_generation
ORDER BY embedding <=> CAST(:query_vector AS halfvec(256))
LIMIT 50;
HNSW 索引保证了检索的速度。在 5,000 篇论文的试点中,256 维 Qwen 索引相对精确检索达到 0.9955 的 Recall@20,HNSW 查找延迟 p50 为 1.31 毫秒、p95 为 2.21 毫秒。其表和索引约占 1024 维版本存储量的 27%,而在该测试中 ANN 召回率基本持平。
该 Endpoint 配置为最多一个副本,空闲时可缩容到零。这是一个有用的成本杠杆——没有用量就不用付费。但这也意味着冷启动必须成为应用设计的一部分,而不是被视为异常事件,因为端点启动并开始承接流量需要一些时间。
因此我们的查询客户端有意设计了严格的行为:
- 生产环境 1 秒超时;
- 非阻塞的并发上限;
- 对响应的维度、有限性和范数做校验;
- 以查询和嵌入生成为键的短时缓存;
- 反复失败后触发熔断器;以及
- 日志中不落原始查询文本,只记录归一化指纹。
如果端点正在扩容、超时、返回了格式错误的向量,或没有可用并发,我们就立即跳过语义分支。用户仍然能收到词法检索结果,而不是苦等一个不可靠的依赖。
Inference Endpoints 的可靠性非常出色,还带有实用的仪表盘,可以快速查看关键分析数据。
混合检索强于任何单一分支
对每个查询,词法分支用加权的 PostgreSQL 全文检索最多取回 50 个候选,语义分支从 pgvector 取回最多 50 个候选。
我们用加权倒数排序融合(RRF)合并两者的排名:
score(d)=∑r ∈ {lexical, semantic}wrk+rankr(d) \text{score}(d) = \sum_{r \,\in\, \{\text{lexical},\, \text{semantic}\}} \frac{w_r}{k + \text{rank}_r(d)} score(d)=r∈{lexical,semantic}∑k+rankr(d)wr
RRF 简单而稳健,因为它融合的是排名,而非来自两套分数量纲不同系统的分数。基本思路是:如果一篇论文在词法分支和语义分支都排名靠前,它在混合检索中排名靠前的几率也更大。我们目前对两个分支使用相等权重,并取 (k=60)(k 是"排名常数",RRF 算法的超参数)。
稠密检索提升了概念型查询的召回率。全文检索对精确术语、标识符和罕见名称依然表现出色。在融合排序之上,我们还保留了确定性的身份匹配行为:
- 精确标题和 arXiv 编号始终置顶;
- 方法分类体系能识别"最早的 BERT 论文"这类导航式搜索;
- 不完整的标题和有限范围内的拼写错误使用保守的 trigram 候选;以及
- 含糊的模糊匹配宁可不给出结果,也不强行返回一个坏答案。
注意:混合检索并非总是最优解。建议先从关键词检索这个廉价快速的基线起步,只有当语义检索和/或混合检索确实能带来可观的检索质量提升时再把它们加上。还可以在关键词/语义/混合检索之后接一个重排序器来进一步改进,例如 Qwen3-Reranker 这样的模型。
一个 Endpoint,两条更新路径
大规模初始语料用 Jobs 嵌入,但 Papers with Code 在持续变化:新论文不断到来,摘要会被更正,新的 arXiv 版本会成为现行版本。
为寥寥几行变更启动一个 GPU Job 会带来不必要的启动和编排开销。因此,一个每小时运行的增量进程会选出缺失或内容有变的论文,把有界增量发送到同一个 TEI Endpoint——这次使用 document prompt。
每轮最多处理 500 篇论文,批大小为 16。写入嵌入之前,会先锁定源数据行并再次核对内容哈希。如果某篇论文在推理期间发生了变化,该向量即被丢弃,留待下一轮处理。
这形成了有益的分工:
- Jobs 负责全量重建、新模型世代和大规模回填。
- Inference Endpoints 负责交互式查询嵌入和小规模增量文档更新。
- Buckets 保存大规模构建的产物,让构建可续跑、可审计。
这条每小时运行的路径让生效索引紧贴实时目录,又不会把在线端点变成无节制的批处理器。
相关论文在线上几乎零成本
同样的文档向量还支撑着每个论文页面上的"相关论文"推荐。
由于源论文已有存储的向量,相关论文的获取在请求时完全无需调用模型——只是对当前生效世代的一次最近邻查询。如果某个向量暂时缺失,应用可以回退到之前的 arXiv 版本,或用现有的基于任务和引用的兜底逻辑填充结果。我们通过 Semantic Scholar API 获取引用数据,还构建了 s2-cli——一个查询其引用图谱的命令行工具。https://paperswithcode.co/chat 上的智能体就在使用它。
我们的心得
- 把吞吐型工作与延迟敏感型工作分开
语料嵌入和查询嵌入用的是同一个模型,但它们是两个不同的基础设施问题。Jobs 优化吞吐量和有界成本;Inference Endpoints 优化可用性和请求延迟。
- 让存储成为计算与生产之间的显式契约
Buckets 在计算与生产之间提供了显式的交接。带校验和的产物在数据进入生产索引之前筑起一道可审查的边界。
- 要固定的不只是模型名
revision、维度、prompt、归一化和输入格式化器都会影响检索效果。把它们一起存储,并在每个环节加以校验。
- 为冷启动而设计
当流量时断时续时,缩容到零很有价值,但前提是产品要有快速回退方案。混合检索天然给了我们这个回退:词法检索本身永远有用。
- 更小的向量可以是一种系统特性
Matryoshka 嵌入让我们把质量、内存、索引大小和延迟当作一个整体权衡来评估。在试点中,256 维在保持 ANN 召回率的同时,存储相比 1024 维大幅缩减。
- 激活应当是平淡无奇的
新一代数据在现行世代旁边导入,独立构建索引,经过完整且最新的覆盖检查后原子性激活。回滚只是一次配置变更,而不是一场紧急重算。
欢迎在 https://paperswithcode.co 试用搜索,或在 https://paperswithcode.co/chat 体验聊天界面,并随时向我们反馈!