设计动机
人工智能领域的论文进入爆发式增长,数量越来越多,文献检索也面临着很大的挑战。我个人在使用 General/Coding Agent 进行文献检索(寻找相关工作或者领域梳理)时能体验到的局限性:
- 通用 agent 通过黑盒 web search api 进行开放网络检索,通常只能拿到 top-k,难以穷举,也难以回答「这个领域有多少篇论文」的问题;
- 通用 agent 通过 search api 进行的检索噪音大,返回的是整个互联网的相关性条目,需要花费额外的开销进行数据清洗和整理。
上述局限性导致 agent 在查找文献的时候浅尝辄止,或者开销大、速度慢。所以在上半年的时候我给自己 vibe coding 了一套 arXiv 元数据抓取 -> 向量化 -> 关键词或语义召回 -> LLM 精筛的管线,但成本仍然比较高,并且精筛的时候速度很慢,用起来不怎么顺手。
九月中旬的时候 Jev 横空出世,我意识到它非常适合用来做召回后的文献精筛和分类,于是就开始重构自己的文献基础设施,把它迁移到 Jev 上来。可以先通过一张 GIF 示意图感受精筛的速度:
本文其余部分将介绍检索系统的设计,以及在生产环境中运行它所需的一些优化。
检索结构
我曾做过一段时间的推荐算法,但没太接触过 Agentic Search 领域,还带着点相关的思维定势,所以最初的设想是通过一个漏斗式的结构完成召回、粗筛、精筛。后来发现论文检索的特点跟信息流推荐还很不一样:
- 需要扩增:论文存在引用关系,通过一篇早期文章的引用关系进行扩增检索是很常见的手段;
- 需要补充:不管是人还是 Agent,检索的过程都有疏漏的可能,所以在初次检索完成后还要进行复核,看看是否有不合适的条目被纳入,或者是否有早些关注到的标记点被遗漏。
这样的话漏斗式的检索显然就不够用了。再结合多路召回所需的合并去重、假阳性需要批量剔除,我发现集合是解决这个问题的良好数据结构,因此就将整套检索的底层架构用集合代数的接口重写了一遍。
检索结构设计为:Agent 操作的基本单元是论文集合(满足确定性、互异性、无序性的基本属性),支持并集、交集、差集等基本的集合运算;同时引入了一些实用的功能性对象或算子:
- 虚拟根:用一个虚拟对象表示检索起点,比如全库论文集合、arXiv 每日公告的论文集合。使用虚拟根是为了避免对大规模条目进行持久化,占用过多存储空间;
- 规则过滤:通过 arXiv 分类标签、时间窗口、资源种类、embedding 相似度、关键词检索等条件进行论文条目过滤;
- AI 精筛:利用 Jev 进行条件判定,条件是由 agent 根据用户意图生成的;
- AI 分类:利用 Jev 的多标签概率判定接口进行分类,一篇论文可以同时归属两个分类,所以这块的分类没有设计互斥性。
我把整个检索系统用节点图的形式进行可视化,前端使用 ReactFlow 构建。节点图的形式足够灵活,能够应对各种不同的生产需求。
典型的工作流如上所示:若干关键词与语义召回通道,一个 union 节点将它们合并、去重成一个大候选集,一个调用 Jev 的 AI 筛选节点,然后是一个按主题给论文分类的 AI 分类节点。
大多数时候,agent 遵循这个典型工作流。不过,对于更大、更复杂的请求,它可能会在审查过程中发现缺口,构建新的集合来填补这些缺口,并不断迭代,直到得到一个“最终”集合,再进入分类。最终形成的工作流可以复杂得像电路图一样。
通用 agent 和编程 agent 通常把结果记录在文本文件里,或记录在由脚本处理的 XML/JSON 中。把集合保存在带索引的数据库中,扩展性要好得多,效率也高得多。把这种大规模处理交给确定性代码,还能防止模型幻觉,也防止上下文被压缩时细节丢失。agent 只持有集合的句柄;所有成员级别的操作都在数据库内部完成。
另一个实际问题是召回深度。关键词匹配要么命中要么不命中,但语义召回是一种排序:第 300 名可能仍然相关,而第 3000 名可能已经跑题,很难提前选定 K。因此,在创建集合之前,agent 先运行一次开销很小的探测,只返回总数、少量样本和分布。对于语义召回,探测还包含一条排名-相似度曲线,agent 可以根据曲线的拐点决定在哪里截断。
在早期版本中,探测和集合创建是两次独立的计算。对于同一个 top-10000 的语义查询,探测花了 2 分钟,而创建集合要把所有内容重新计算一遍,又花了 37 分钟。现在探测的完整排名会被存储下来,创建集合只需从中取前 N 个,无需重新计算。
召回的性能
其实上述的思路非常自然,ComfyUI 等低代码节点图工作流已经深入人心,在很多人的脑海中应该都浮现过类似的文献检索思路。但是让这样的系统能够投入生产使用,最大的问题不是数据抽象,而是面临较大规模数据时的性能优化,以及如何平衡速度、成本、效果这几个维度。
Parallax 目前通过 OAI 接口抓取了 arXiv 下 cs.* 分类自 2020 年来的元数据更新,大概有 82 万篇,元数据及其索引约占 7.8GB。为了实现语义召回,系统接入 Perplexity 完成语义嵌入,每篇论文的摘要经 pplx-embed-v1-0.6b 嵌入成 1024 维向量。按 float32 算单条 4KB,82 万篇理论上约 3.3GB;如果压成 halfvec 半精度表示,单条 2KB 共计约 1.65GB。但这只是裸数据大小,真正落到 PostgreSQL + pgvector 里,为了同时满足精确和快速两个目标,向量实际存了多份,加上索引的总占用达到了约 17GB。
关键词召回
PostgreSQL 做模糊匹配的常规手段是 trigram 索引:把文本切成三个字符一组的片段建倒排,查询时先用片段找候选,再回表核对原文。但它在常见短语上表现很差。我们在约 69 万篇的库里查摘要中的 motion generation,执行计划确实走了 trigram 索引,却花了 34 秒,其中 32 秒都在等 IO。原因是 mot、ion、gen 这类片段太常见了,索引筛出了 2.5 万个候选,真正命中的只有 776 篇,两万多次回表都是白读。
我们的做法是额外加一个相邻词片段索引:对标题和摘要里每一对相邻的词,取前词末尾 3 个字符加后词开头 3 个字符(以及对应的 5 字符版本)拼成片段,比如 motion generation 会产生 ion:gen 和 otion:gener,用 GIN 数组索引存起来。两个词相邻出现的组合比单个三字符片段罕见得多,候选数就能大幅收窄;词足够长时优先用 5 字符版本,因为 ion:gen 这种组合本身也还是偏常见。
这个片段只作为必要条件使用,最后仍然用原来的 ILIKE 逐字段核对原文,所以结果集合与旧实现完全一致,返回的总数也是精确的。改完后同一个单短语查询约 30 毫秒;Agent 常用的七短语组合查询从 23 秒降到 0.09 秒,在同一快照下新旧结果的论文 ID 逐条一致。
语义召回
向量检索绕不开速度和精度的权衡。精确计算要把候选向量全扫一遍;HNSW 这类近似最近邻(ANN)索引快很多,但会漏,而且 K 越大漏得越多。我们最终保留了两条路径,按任务情况自动选择。
近似路径用的是分区 + 半精度 + 重排:
-
1024 维 float32 的 HNSW 索引有 4.4GB,缓存不足时一次查询要读几百 MB 的索引页,即使走了索引也要二十多秒。所以索引改用 halfvec,体积减半,只用于找候选;
-
候选多取几倍,再用原始 float32 向量重新算分排序,修正半精度带来的误差;
-
向量按年份分区,每个分区一张独立的 HNSW 图,用户限定时间窗口时无关年份直接跳过。
ANN 的关键参数是搜索宽度 ef_search 和候选池倍数。我们用 32 条线上真实的语义查询做了对照,以精确 top-K 作为真值:
| ef_search / pool multiplier | Recall @ K=300 | K=1000 | K=2000 | Latency |
| 1000 / 50 | 100% | timeout | timeout | 67–74 s |
| 400 / 3 (current default) | 99.7% | 98.0% | 96.5% | mostly 1.6–7 s |
| 200 / 2 | 99.0% | 95.8% | 94.8% | 1.4–6 s |
| 100 / 1 | 97.0% | 94.0% | 93.2% | 3–4 s |
漏掉的论文基本都卡在第 K 名附近,相似度和第 K 名只差千分之几,对检索结果的影响比召回率数字看起来要小。
宽时间范围、大 K 或者 ANN 超时的时候就切到精确路径。直接跑一条全库精确召回 SQL 时长不可控,发生中断又缺乏检查点,只能重新跑,因此我们把候选按批切开,每批单独算出 top-K,最后合并再取总 top-K。
这样做不会损失精度:如果一篇论文在全局排进了前 K,那全库比它分高的论文不到 K 篇,它所在的那一批里比它分高的自然也不到 K 篇,所以它一定在本批的 top-K 里,合并时不会漏掉。只要所有批次用同一套打分和同一个排序规则(分数相同按论文 ID 排),分批结果和一次性全排是完全相同的。
分批会带来一定的额外时间开销,但系统可靠性和资源可控性都获得了提升:每批完成就落库,进程重启后可以从断点继续;某一批超时就拆成两个小批重算;大任务在批与批之间让出资源,不会把其他用户的小查询堵住。我们在生产库上做过一次验收,全库 81 万篇候选、K=5000,分 407 批算完,用时约 5 分钟,和同一快照下一次性 float32 全量精确查询对照,5000 条结果的 ID、顺序和分数逐项一致。
精筛与分类
相比于最初使用的 DeepSeek-Flash 进行精筛,九月下旬切换到 Jev 后在生产服务器上实测,新管线对 36,564 篇论文进行精筛判定只花了 5 分钟左右,是 LLM 版本速度的 10+ 倍,而成本只有不到五分之一。
为什么 LLM 的成本会高、延迟会高?因为我们设计的管线会收集判定审计信息(概率或者原因),LLM 并非直接输出 true 或者 false,而是输出一个带判定结果和原因的 JSON。如果打开模型的 CoT 思考,模型的思考长度不可控,故而延迟不可控;如果关闭模型的 CoT 思考,又会导致批处理时时常出现 JSON 格式错误,要么整批作废重试,要么聚集错误结果开新批次。
Jev 的接口则是给定一段上下文(state)和一组问题,直接返回校准过的概率。我们的精筛和分类都使用 Jev 的 Noul 元语实现:精筛是一篇论文加上 agent 拟定的纳入/剔除标准,概率不低于 0.5 即纳入;分类是每个类别各问一个 Noul,彼此独立判断,概率不低于 0.8 记为命中,0.2 到 0.8 之间记为待确认,留给用户和 Agent 复核。输出本身就是结构化的,不存在格式错误的问题,每篇论文在每个标准上的概率也都会持久化到数据库里。
多标签分类的输入格式
多标签分类时一篇论文要回答十几个类别问题。最早的格式是每个问题都重复带一遍论文的标题和摘要,token 浪费很严重;改进的思路是把论文放进共享的 state,所有类别问题都引用它。再进一步,能不能把五篇论文一起放进 state,一次请求问完五篇的全部类别?我们用 120 篇真实论文、十二个类别做了对照:
| 格式 | 平均时间 | 输入 token 数 |
| 每个问题中重复论文 | 57.9 s | 617K |
| 共享状态,每次请求 1 篇论文 | 35.8 s | 210K |
| 共享状态,每次请求 5 篇论文 | 14.6 s | 114K |
单看速度和成本,五篇共享最好。但同样的分组重复跑两次只有 1 篇结果不同,而把这 120 篇打乱重新分组后,有 20 篇左右的标签发生了变化。比如一篇摘要里明确写了图神经网络的论文,单篇判断时「图学习」类别概率是 0.95,和另外四篇放在一起时降到了 0.78,跌破了命中线。
也就是说同组的其他论文会影响模型对这一篇的判断,这对分类来说是不能接受的。所以最终默认每次请求只放一篇论文,十二个类别在同一个请求里判断。这样比最初的格式输入 token 仍然少了约 66%,同时避免了跨论文的干扰。
批处理与并发
单个 Jev 请求只需几百毫秒,但要稳定跑完几万篇,大部分工作量其实在模型之外:
- 自适应装箱:精筛的批大小不是固定的,而是根据 Jev 的上下文窗口和当前批内元数据 token 长度动态计算。最初的实现是逐篇尝试、每次重新估算整批 token,批越大越慢,后来改成二分查找最大可装前缀,装箱开销明显下降,发出去的请求内容与原来完全一致;
- 限速:账号有每秒请求数和每秒 token 数的额度,我们用 GCRA 算法按理论发送时刻推进节拍,锁等待和线程调度不再占用额度。在限速成为瓶颈的场景下,实际速率从额度的 73% 提到了 98%;
- 公平性:所有任务共享同一个账号额度,排队时当前在途请求少的用户优先,避免小任务被大任务饿死。
剖析时还发现,一度限制吞吐的不是网络和模型,而是执行进程自己把 CPU 吃满了。每次 HTTP 请求背后有约 120 条 SQL,包括费用预留、发送前检查、结算和结果落库,这些是为了保证每笔调用可恢复、不重复扣费、出错可追溯。之后把配额相关的 SQL 从每请求约 36 条压到约 15 条,并把同一时段完成的结果合并成一次提交,吞吐才继续往上走。
按输入计费口径算,精筛一篇论文大约三百多个 token,一万篇的花费在一块钱左右。精筛变便宜之后,Agent 的用法也变了:以前会尽量把候选压小再提交精筛,现在可以先宽召回,把可能相关的都捞进来,再交给 Jev 逐篇判断。
实测效果
在一个「LLM 智能体记忆机制」主题的论文检索案例中,Agent 会先根据用户提供的线索对库中的论文进行低成本探针调研,搞清楚论文的大致分布情况,然后创建 6 路关键词 + 5 路语义召回,合并后得到三万多篇候选文献;Agent 会拟定提供给 Jev 进行判定的业务标准,什么样的论文可以被纳入(include),什么样的论文需要被剔除(exclude),然后提交后台进行并发批处理判定。
在另一项任务“多模态理解与统一多模态模型的八月工作综述”中,智能体通过三个关键词与语义召回渠道获得了466篇候选论文,筛选后保留了428篇切题论文。随后,智能体阅读了该集合中的标题,起草了一份类别列表,并调用了Jev分类节点。结果是10个类别,包括基准与数据集、具身AI、安全与隐私,以及统一理解与生成。
用户指出,基准类别仍有超过一百篇论文,可以进一步拆分,因此智能体探查了这些条目,并将基准子集划分为11个子类别。
在分类过程中,Jev会为每篇论文给出其满足每个类别标准的纳入概率。这些概率都存储在数据库中,供用户和智能体查看。
Why Parallax?
为什么要设计 Parallax?在 RSI 真正到来、AI 领域论文的价值完全消散之前,对于 Frontier Lab 以外的学者、学生或开发者来说,arXiv 上的论文仍然是第一手的领域信息来源。但现在论文的产出量越来越大,文献所涵盖的总体洞察越来越多,人们却越来越难以获得与论文增长相匹配的、对领域的洞察和理解。
我希望能找全文献,看看整个领域或者某个我所关注的细分方向,在空间或者时间维度上发生了什么。要做到这一点就只能对全量论文做快速的召回扫描,而无法仅仅通过 Google Search 或者 arXiv Search 的 top-K 结果来实现。「找全文献」这件事情的需求,从 GitHub 上琳琅满目的 awesome paper list 就可见一斑。
我一年半之前还在游戏行业工作的时候,曾看过用户研究部门的分享,介绍他们如何观察新品类的形成。Steam 的标签系统是用户共建的,通过对评价和标签做持续观测,能看到一种说法是如何从零碎的短语逐渐变成大部分玩家的共同描述,最后成为一种新的「品类」。比如「魂-like」或者「类银河战士恶魔城」,最初用户想表达的只是「这款游戏的难度和叙事像黑魂」或者「玩法像银河战士和恶魔城」。
arXiv 文献数量的暴涨,也让 AI 学术研究进入了高频、高动态的时期;而 Jev 的到来让我发现能够通过一种更加低成本的方式,来灵活地观察领域的变化和动向。
Parallax 已上线生产环境,欢迎访问 https://parallax.ink 注册试用。