搜索框里那半秒,花在哪了
顾客按下搜索之后
顾客在小程序里输入「有没有低脂酸奶」,或者店员在收银机上查「这个条码是什么」——从按下搜索到出结果,中间那半秒到两秒,是怎么花掉的?
如果后端接的是一个逐字生成的模型,时间大致分三段:把输入读进去(这段可以并行,比较快)、把每个字一个一个写出来(这段是串行的,慢)、以及中间的调度与网络往返。
真正贵的是第二段。而它慢的原因,往往跟模型多大关系不大,跟一个问题关系很大:同样的一段开头,被算了多少遍。
被重复算的那段开头
生成式模型每写一个字,都要把前面的全部内容重新过一遍注意力。这里的「前面」包括系统提示、任务说明、输出格式要求、商品库的说明文字——这些内容在一次搜索里几百上千个字,每生成一个字就要重算一次。
问题是,这些内容对每个请求几乎都一样。「你是门店导购助手,请根据商品库回答,输出格式为……」这段提示,今天被算了几万遍。
前缀复用做的就是一件事:把这段每次都要重算的公共开头算一次,存下它的中间状态,后面的请求直接从相同位置接着算,只处理各自不同的那部分输入。
省下来的不是模型能力,是重复劳动。
门店场景反而不容易命中
按理说,门店系统的模板最固定,应该最容易命中。实际做起来,有两个坑。
第一个坑:前缀里塞了会变的东西。 比如为了个性化,把门店名、当日促销、实时库存拼进系统提示。这样一来,每家店、每个时段的前缀都不一样,缓存全部失效,命中率接近零。
正确做法是把前缀切成两段:稳定的部分放前面(角色、格式、规则),会变的部分挪到用户输入里。这对缓存命中率的影响,比换一个更快的模型大得多。
第二个坑:连锁的差异化需求。 总部希望所有门店共用一套提示以提升命中率,区域希望加上本地活动。这里没有标准答案,但可以算账:如果为了加一句促销语,把命中率从高位打到低位,那这句促销语的实际成本是每次搜索多出来的那段计算。
命中率这个数怎么看
前缀复用划不划算,看一个数就够了:真实请求里,有多少比例能匹配到足够长的公共前缀。
算法不复杂,拿一周的查询日志跑一遍:把每次请求的输入按长度排序,统计前 N 个字完全相同的请求占比。门店搜索的特点是输入短、重复高——「多少钱」「有没有」「怎么退」这类开头会反复出现,命中率通常不低;但如果是自由输入的长句问答,前缀就散了。
还要看两个前提:
- 缓存开多大。 缓存和模型抢同一块显存。为了命中率把缓存开大,并发就掉;开小,命中率就掉。这两个数要一起测,不能分开看。
- 并发下会不会被挤掉。 单请求测试时命中率很好看,十路并发一起来,缓存被冲掉,后面的请求又退回全量计算。这种退化在演示里看不到。
验收时看什么
看 P95,不看平均。 平均首字延迟只掉了一点,可能是因为大部分请求命中了、少数请求慢得离谱。门店里被投诉的永远是那少数几次。
用真实日志,不用造的数据。 拿一批典型查询自己编,命中率一定虚高,因为你会不自觉地写得规整。真实日志里有错别字、有口语、有半截话。
测冷启动。 缓存是空的,第一波请求全部要全量计算。开店那一刻正好是这种状态。
测并发切换。 十路同时来,看命中率掉多少、延迟抖多少。
什么情况下不值得做
请求前缀本来就散(每个顾客一句完全不同的自由提问)、输出只有几个字(省下的计算还不够缓存的查找开销)、或者设备显存本来就紧张——这三种情况,加一层前缀缓存只会让系统更难维护。
它适合的是那种「模板高度固定、调用量很大、单次输出不短」的场景:商品详情生成、导购话术、固定的问答对、批量文案。判断标准很朴素:同一个开头,你一天要算多少遍。
一句话
搜索框里那半秒,很大一部分不是花在「想」上,是花在「把同一段话重新读一遍」上。前缀复用不改变模型,只改变你让它重复劳动的次数——而它到底省不省,取决于你的请求够不够像。