9 月 15 日,TypeSafe AI 发布了 Jev。这款模型只需要一段材料和定义好的问题,就能返回 Yes/No、选项或分数,并给出对应概率。不到一周,它在 Hacker News 的发布帖就拿到 1900 多分、500 多条评论,并带火了包括mini-jev、von、Kev、Laya 在内的一众分类模型。最重要的是, $0.042 / MTok的输入,免费的输出,以及 70 到 500 毫秒的端到端延迟,Jev把让模型做判断这件事的成本压到了极限,而这个快速判断+低成本的特性,也让我想到了一个团队里2023年的早期项目——GPTCache 。GPTCache 是我们在2023 年推出的大模型语义缓存项目,可以帮用户在历史对话中找到相似问题的结果,直接复用之前生成的答案,从而减少重复的大模型调用。在后续的测试结果中,多数情况下,它也的确是有用的。1,000 次查询里有 876 次命中了缓存。但其中也出现了 39 次本不该命中的情况。由于问题一直没有被很好的解决,团队最后不得不在GPTCache 的 README 里后来加了一句“cache hit 可能出现 false positive”。01
增加一环判断,可以拯救GPTCache吗
2023 年年初,团队里几乎所有人都开始用 GPT。用得多了以后,很快就发现两个问题:一是贵,二是很多问题其实会被反复问到。比如“怎么重置密码”和“忘记密码怎么办”,以及各种常见 FAQ。因为表达方式不同,普通缓存会把它们当成不同的 key,但它们需要的答案往往完全一样。而我们的主业是向量数据库,本来就很擅长找语义相近的内容。既然如此,能不能把历史里问过的相似问题找出来,直接复用之前生成的答案,少调用一次大模型?这个产品3月立项,4 月就登上了Hacker News,标题是 "Slash Your LLM API Costs by 10x",还很快被 LangChain 集成,到今天已经有 8200 多个 GitHub Star。但随着使用场景越来越复杂,语义相似并不等于答案可以直接复用的问题,也变得越来越明显。比如:用户要求“只给代码,不要解释”,旧答案偏偏带了两段说明。聊天时不算大事,但交给下游程序就可能解析失败。用户要十个以 Gaming 结尾的句子,旧答案每句都以小写 gaming 结尾,意思一样,对用户来说就是要求没满足。“哪些降压药适用于英国”和“有哪些降压药”两者在向量空间里挨得很近,但地域限定却会改变药名和指南。还有一种更粗暴的情况是:两个问题一字不差,但缓存答案写到“以下是具体步骤:”就断了。问题再相似,这个答案也不能复用。为了解决这个问题,我们曾经尝试给 GPTCache 加过 Reranker,对检索结果重新打分。问题是,再优秀的 Reranker 模型,也只能解决提问是否相似的问题。旧答案有没有写完整、格式对不对、地区和版本是否匹配、信息有没有过期,它都不知道。另外,我们也想过在返回缓存之前,再调用一次大模型做检查。效果当然会更好,但成本逻辑又出了问题:本来就是为了少调用一次大模型,结果每次命中缓存之前,还要额外调用一个大模型检查,省下来的钱很快又花了回去。我们可以保持原有流程不变,只在最终返回答案之前,多加一层判断:把当前请求和缓存答案一起交给 Jev,看这份答案现在到底还能不能用。任务:用户现在要做的事和缓存答案解决的是不是同一件事。用户要最优解,缓存里只有一个可行解,就不能直接用。约束:地区、版本、数量这些条件有没有变化,contains 和 starts with 这样的区别也放在这里。输出:答案有没有写完整,语言、格式、大小写、“只给代码”这些要求有没有满足。时效:答案会不会随着时间失效。翻译通常没有这个问题,实时价格有。上下文:当前请求依赖的信息够不够。用户说“帮我改上面的代码”,当前对话里却没有代码,就不能从别的请求里找一段过来凑。最后对五项分别给分,取最低分判断是否放行。因为缓存答案要直接交给用户,只要有一个关键条件没有满足,就不能复用。(注意:这五项只判断“缓存里的答案现在能不能复用”,不会重新验证答案里的事实、计算或者代码本身是不是正确。)02
实验过程与分析
实验数据:来自 vCache 的 LmArena 和 SearchQueries,共 1,536 组“当前请求、缓存请求、缓存答案”。其中 384 组用于选择阈值,1,152 组用于最终测试。(实验过程中缓存固定,不新增,也不淘汰。)实验对象:Jev、Laya、GPTCache 默认的 Reranker,共三组(Laya 是一个基于 ModernBERT-large 的 421M 参数模型,也是 Jev 发布后比较完整的开源实现之一。)Jev 和 Laya 使用相同的判断框架。两个模型都会读取当前请求、缓存请求、缓存答案和时间信息,然后分别检查任务、约束、输出、时效和上下文五项,最后取五项中的最低分,决定这份缓存答案是否可以直接复用。Reranker 的实验方法不同。它不读取缓存答案,只输入当前请求和缓存请求,用 cross-encoder 计算两个问题的相关程度,再根据分数决定是否命中缓存。也就是说,Reranker 判断的是“两个问题是不是足够像”,Jev 和 Laya 判断的是“这份旧答案能不能直接回答现在的问题”。第一轮使用固定阈值,直接比较三种方法最终放对多少、放错多少。Jev、Laya 和 Reranker 的阈值分别设为 0.70、0.60 和 0.80。以下是是实验结果(4 条标签不确定的样本没有算进精确率和召回率。)Reranker 正确复用了 401 条,但是同时错放了 477 条。它的召回率最高,但错误也最多,原因在于它只知道两个问题很像,不知道旧答案本身有没有问题。Laya 能看到答案以后,错误复用降到了 361 条,但精确率仍然只有 46.8%。Jev 正确复用了 364 条,只错放了 1 条,同时拒绝了 141 条原本可以复用的答案。对缓存来说,我愿意接受这个方向的取舍:漏一次,是多花钱;错一次,意味着用户拿到了不适用的答案。不过第一轮实验结论还不能说明 Jev 的概率真的可靠,因为阈值本身可以调高,一个模型只要足够保守,自然也能减少错误。所以我又做了第二轮实验。第二轮,我只用前面的 384 组数据选择阈值,并要求复用精确率至少达到 99%。阈值定下来以后,直接跑剩余的 1,152 组测试数据,中间不再调整。我想看看,用历史数据定下来的阈值,到了新数据上还能不能用。毕竟线上系统不可能每来一批新请求,就重新人工找一个最佳阈值。假设今天根据历史数据确定 0.8 以上可以放行,明天换一批请求以后,0.8 最好还是代表差不多的可信程度。Jev 用 384 组校准数据选出的阈值是 0.80。这个阈值直接拿到剩余的测试数据上,正确复用 227 条,错误 0 条。Laya 在校准数据上只放行了 3 条,3 条都对。相同阈值到了测试数据以后,放行了 10 条正确答案,同时错放了 6 条。Reranker 找不到一个既能达到 99% 精确率、又能实际放行样本的阈值,只能全部拒绝。综合两轮结果不难发现:各自设定的阈值下,Jev 可以把缓存误命中压得很低。第二轮进一步说明,用一批数据定下 0.8,换到没有参与调参的数据以后,这个 0.8 仍然可以作为放行标准。最开始写时效检查时,我把“缺少明确的时间信息”也当成风险。这个规则放到实时价格、新闻或者库存上没问题,因为不知道答案是什么时候产生的,确实很难判断它现在还准不准。但它会误伤另外一类请求。比如有一道题问:“如果能把任意物品变成黄金,选什么最值钱?”这是一个假设题。回答它并不依赖今天是哪一天,也不需要知道最近发生了什么。可按照最初的规则,因为请求和答案里没有明确的时间信息,它仍然很容易在“时效”这一项拿低分。这里的问题其实出在规则本身:没有时间信息,不等于这个问题需要时间信息。于是我把时效检查改成两步。先判断这个答案是否依赖会随时间变化的外部事实。只有确实依赖价格、库存、新闻、政策这些会变化的信息,才继续检查答案够不够新。如果问题本身和时间无关,时效这一项直接通过。规则改完以后,这道假设题得到 0.92,可以复用。整个过程没有增加训练数据,也没有重新训练模型。业务规则改了,模型的判断也跟着变了。同理,其他场景中缓存里的判断标准经常会变。用户要最优解,就不能拿普通可行解交差;contains 和 starts with 要分开;某些查询需要最新数据,某些查询几个月前的答案照样可以用。传统分类器遇到新的判断标准,往往要补数据、重新训练或者微调。但如果模型真的能执行用文字写出来的规则,很多改动其实可以先改规则,再拿反例验证。而这也是此次实验里 ,Jev 和其他判断模型差距比较大的地方。我一开始给 Laya 使用完整版规则,在 48 条分层样本里,没有一条达到 0.70。一个翻译请求配上候选答案 Bonjour.,也会被时效检查拒绝,虽然规则里已经明确写了翻译通常不依赖实时信息。后来把规则大幅缩短,Laya 才跑出了前面的主实验结果。即便如此,在校准集上挑出来的阈值到了测试集,也没有保持住原来的精确率。除了准确率之外,我还记录了一下几个测试中的判断用时:Jev 远端 API 的延迟中位数是 0.79 秒,比官方给出的 70 到 500 毫秒慢一些,网络和排队时间都算在里面。Laya 在本机 M4 Pro 上是 0.27 秒。Reranker 用 CPU 批量运行,由于测量方法不同,所以不与前两者做比较了。也就是说,一次完整的5项判断,比重新生成一次答案快一个数量级以上,价格也要便宜两个数量级。03
生成越贵,判断就越有性价比
回想2023 年做 GPTCache 时,我们只是想少调一次大模型。现在,这件事比三年前更有价值:推理模型一次回答可能会生成几千甚至上万个 token。Agent 做一个任务,也经常要连续调用很多次模型。生成越来越贵,能让它少生成一次判断的性价比也就越高。相应的,在落地场景上,除了做语义缓存,大模型在生成中也可以引入 Jev 这样的判断模型。比如一个推理模型已经跑了三千个 token,可以先判断一下:现在的信息是不是已经够了?如果已经够了,就停;如果明显跑偏了,也没必要继续生成。以前这么做不划算。因为判断“要不要继续”,还得再调用一个成本不低的模型。现在如果一次判断只需要几百毫秒,成本又很低,early termination 就值得一试。另外在运维中的自动扩缩容也可以考虑引 Jev 之类的判断模型。今天很多系统主要看 CPU、QPS、队列长度,但实际负载不全写在这些数字里。比如这批请求主要是很重的 hybrid search,还是简单点查;最近一批错误是节点出了问题,还是客户端参数写错;客户说下周有活动,要不要提前准备容量……这些问题不需要模型生成一篇分析报告。很多时候,只需要读一下日志、请求或者工单,然后给出一个明确判断。当然,权限、资源上限、安全规则等硬约束,还要继续写在代码里。但需要读懂内容才能判断的部分,不妨交给一个更便宜的模型。作者介绍
栾小凡
Zilliz CTO
LF Al & Data 基金会技术咨询委员会成员