ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大语言模型增强推荐系统:语义召回与排序融合实践

大语言模型增强推荐系统:语义召回与排序融合实践

做推荐系统这几年,我越来越觉得内容社区的推荐链路里缺一种东西:对语义的理解。今年我们有幸和某高校的研究团队深度合作,把大语言模型真正拉进了在线推荐主链路——不是离线算特征那种试水,而是让LLM参与线上笔记的召回和排序。这是我们平台第一次把LLM放进笔记推荐的核心流程,整个过程踩了不少坑,也拿到了实打实的收益。今天把这次的方案设计、实现细节和排障经验完整梳理一遍,给打算做类似改造的同行一些参考。

先交代一下背景。我们平台的推荐链路和大多数内容社区一样,早期靠的是传统协同过滤加多路召回,配合排序模型做个性化排序。这套体系在前期够用,但随着笔记类型越来越丰富,问题开始冒头:语义复杂的笔记、长尾内容、新用户的冷启动,传统方法处理起来越来越吃力。这次和高校团队合作,目标很明确——不是推翻原有系统,而是把LLM作为新能力叠加进来,解决旧链路解决不了的问题。

1. 项目背景:笔记推荐为什么需要大模型

1.1 传统推荐链路的三个死穴

做推荐的人都知道,传统协同过滤的本质是“物以类聚、人以群分”。它依赖行为共现来捕捉相似性,但对内容本身的理解非常浅。举个例子,一篇标题是“周末带娃徒步装备清单”的笔记,从分词角度看,它和“户外装备推荐”关键词重叠很多,但用户的真实阅读动机是“新手家长想带孩子安全走完一条初级路线”,这和玩户外的深度爱好者完全是两类人。传统召回能捞出“户外装备”的文章,却捞不出“新手家长遛娃”这个语义场景。

第二个死穴是兴趣表达滞后。传统用户画像主要是统计行为标签,比如“看过健身内容”“收藏过美食笔记”,但用户当下这一刻的兴趣往往是场景化的、组合式的。比如一个用户最近刚当上爸爸,他这一周的兴趣可能是“婴儿辅食”加“家庭收纳”加“周末遛娃”,这种实时组合兴趣,靠历史统计标签很难精准捕捉。

第三个死穴是冷启动。新笔记没有互动行为,在传统的协同过滤体系里基本处于“无人问津”状态。平台不是没有做过补救,比如给新笔记加一个流量保护时间窗,但这只是延长了“被看到”的时间,并没有解决“推给谁更合适”的问题。新用户的冷启动也一样,只有一两次点击行为时,很难推断他到底想要什么。

1.2 为什么这次选LLM而不是继续堆NLP特征

其实在过去两年里,我们也在尝试用传统NLP手段做内容理解,比如BERT句向量、关键词抽取、主题模型。但推到线上总有几个问题过不去:传统句向量对“推理型语义”捕捉有限,它能把句子映射到向量空间,但很难理解“这篇笔记解决了什么问题、适合谁看”;而且传统NLP特征的迭代周期太长,换一个任务就要重新训练一轮模型,很难跟上业务需求变化的速度。

LLM不一样的地方在于两点。第一是通用语义理解能力强,不需要为每个内容垂类单独训练模型,一个底座模型就能同时理解健身、育儿、旅行、职场这些差异很大的领域;第二是能做轻量推理,它可以从一篇笔记里推断出目标读者是谁、解决了什么痛点、适合什么场景下阅读——这些是传统文本向量化拿不到的信息。坦白说,LLM的推理能力用在推荐上,有点“杀鸡用牛刀”的意思,但它带来的增量是实打实的。

1.3 项目边界的确定

立项之初,团队里有过一些激进的想法,比如让LLM直接生成完整的推荐流。但这个想法很快被否了,原因有三个:一是生成式推荐的可控性和可解释性太差,一旦结果不合理,排查成本极高;二是线上的延迟预算不允许每次请求都让LLM生成一遍;三是推荐系统讲究归因,A/B测试出了正向结果,你得能说清楚是哪个环节起了作用。

所以我们把项目边界定得很收敛:不改变原有推荐框架,把LLM作为增强模块叠加进现有链路。具体来说,就是做两件事:一是在召回阶段新增一个语义召回通道,二是在排序阶段把LLM产出的语义特征作为辅助信号接进去。这个边界让整个项目的风险可控,也让实验归因变得清晰。

2. 整体技术方案设计:LLM在推荐链路里的位置

2.1 方案选型时的核心权衡

做方案选型时,我们认真对比了三种路线:LLM做召回、LLM做排序、LLM做生成式推荐。

LLM做召回,核心思路是把笔记用向量表示成一个索引库,线上根据用户的语义向量去检索。这种方法实现起来直接,对现有链路侵入小,而且可以用向量检索库做近似近邻搜索,延迟可控。

LLM直接做排序,就是让模型对一批候选笔记逐条打分或两两比较。这种方法效果通常更好,但延迟是个大问题,尤其是在排序阶段动辄几百上千条候选的情况下,让LLM打分一遍根本跑不动。就算只对前几十条做精排,延迟也会明显超过推荐接口的预算。

生成式推荐,就是直接让LLM根据用户画像生成推荐结果,比如返回一批笔记ID或关键词。听起来很酷,但当时阶段的技术成熟度还不够,生成结果的稳定性、幻觉问题、与现有推荐结果的衔接方式都存在很大不确定性。

最终我们选择了“双通道召回 + 排序特征融合”的组合方案。逻辑很简单:让LLM以离线为主、在线为辅的方式介入。离线把笔记语义向量化建索引,在线把用户行为序列压缩成语义画像去检索;排序阶段再把语义特征作为交叉特征接入现有模型。每个环节的延迟和成本都可控,LLM承担的“推理”工作被切到离线或准实时,在线只做向量点积和特征拼接。

2.2 总体架构:三个关键模块

整个系统分三个模块。第一个是笔记语义向量库,负责把全量的笔记内容转换成向量。第二个是用户兴趣语义画像模块,负责把用户近30天的行为序列压缩成一串语义标签或一段语义向量。第三个是融合排序模块,负责把语义召回的候选和传统召回的候选合并,并把LLM产出的语义特征喂给排序模型。

这三个模块的职责边界非常清晰:笔记向量库是静态资产,一天构建一次,增量笔记实时补充;用户画像模块是准实时更新的,每几小时根据新行为刷新一次;融合排序是在线高时效模块,需要处理实时请求。

这里有个关键的架构决策:用户语义画像没有走“全量行为重新推理”的路线,而是增量更新。我们预计算好用户长期兴趣向量和短期兴趣向量,线上结合起来使用。这样既能保证画像的新鲜度,又不需要在每次推荐请求时都跑一遍LLM推理。

2.3 为什么没有让LLM直接端到端生成推荐流

这个决定被团队反复讨论过。到项目后期,我们也做过一个实验版本,让LLM根据用户画像直接生成“关键词集合”,然后拿关键词去检索笔记。效果有一定惊喜,但问题是稳定性不够:同一个用户在不同时间点请求,生成的关键词差异很大,导致推荐流结果不一致;而且一旦生成方向偏了,整个推荐流就跑题,连兜底能力都没有。

相比之下,双通道召回加融合排序的方案,本质上是在“老系统”的基础上做加法。老系统的多路召回仍然在运行,LLM语义召回只是额外提供一批候选。哪条路径出了问题,可以立刻切掉那一路,系统整体不会崩。这种渐进式的落地方式,是这次项目能平稳上线的重要原因。

3. 核心实现细节与工程化落地

3.1 笔记语义向量化:从原始文本到可检索向量

笔记向量化的目标是让每篇笔记都能被语义检索到。我们处理的文本内容包括标题、正文、标签和部分评论摘要,同时把图片OCR后的文字也拼了进去。文本长度方面,正文动不动上千字,直接送进Embedding模型既不经济也没必要,我们采用“截断+分段+池化”的策略:标题和正文前512个字符作为主干内容,再抽取正文中与标题语义相符的两个片段做补充,最后用加权池化合并成一条向量。

Embedding模型方面,我们没有直接选用通用底座模型,而是在通用模型基础上用站内笔记数据做了进一步的领域适配。这一步很关键:通用模型能理解“户外装备”是什么意思,但未必理解“溯溪鞋”在我们平台语境下更接近“亲子户外”而不是“登山装备”。领域适配用了大约20万条精品笔记,通过对比学习方式微调,收敛后同义笔记的向量相似度有明显提升。

向量维度用的是1024维,索引用Faiss的HNSW结构,参数上M设为64、efConstruction设为160、efSearch设为128。这个参数组合是在召回质量与检索速度之间反复调出来的。索引库规模在千万级,单个查询的平均耗时控制在10毫秒以内,内存占用也可接受。

提示:HNSW的索引参数不要照搬默认值。M越大,图连接越稠密,召回越准但内存和构建时间也越高;efSearch越大,检索越准但单次查询变慢。建议按自己的数据规模做网格搜索,重点关注P99延迟和召回率曲线。

3.2 用户行为序列的语义化压缩

在线用户画像的构建,我们设计成了两步。第一步是把用户近30天的行为记录下来,按笔记所属的语义簇做聚合,形成“带有上下文的行为序列”;第二步是把这条序列交给LLM,让它压缩成一组结构化的兴趣标签。

给LLM的输入不是原始行为列表,而是先经过一次预处理。我们为每个笔记生成过语义标签,用户点击、点赞、收藏的笔记都会带上标签,然后按时间加权汇总。输入给LLM的是一串类似这样的内容:“近7天高频标签:婴儿辅食、家庭收纳;近30天长线兴趣:职场效率、个人成长;近期搜索词:新手妈妈辅食、早教游戏”。LLM的任务是输出一组不超过15个的兴趣标签,并且每个标签限定在4到8个字。

手工设计了输出模板,用了约束解码的技术,让LLM输出固定格式的JSON,避免自由文本带来的解析问题。这一步极其重要,如果让LLM自由发挥,结果里会混入很多口语化表达,下游检索根本没法用。

# 用户兴趣画像生成的核心调用逻辑(简化版) def build_user_semantic_profile(user_id): behavior_summary = aggregate_user_tags(user_id, days=30) prompt = build_profile_prompt(behavior_summary) result = llm_generate(prompt, format="json", temperature=0.1, max_tokens=256) tags = parse_json_tags(result) return validate_and_filter(tags)

这一步踩过一个大坑:给LLM的行为序列如果太长,生成结果的稳定性会急剧下降,而且耗时变长。后来我们把时间窗口从90天缩到30天,并且按标签做了一次预聚合,输入长度控制在一千个字符以内,生成耗时基本稳定在1到2秒。这个耗时放在在线链路上还是太长,所以我们改成了异步准实时更新,每4小时全量刷新一次,用户有突发热点时再触发一次增量刷新。

3.3 在线推理链路:延迟预算与降级策略

线上召回阶段原本有4路传统召回,现在加了第5路“LLM语义召回”。每路召回默认取Top 200,合并后进入粗排。新增的语义召回这一路,逻辑是拿用户语义画像里的标签去向量库检索,取Top 200后与其它召回结果做去重合并。

合并逻辑里有一个细节容易忽略:当一个笔记同时被传统召回和语义召回命中时,我们在排序阶段会标记一个“是否语义召回”的特征维度。这样排序模型可以自行学习,语义召回带来的候选是否应该被优先展示,而不是人工拍板决定合并时谁优先。这个做法对线上效果提升帮了很大忙。

延迟预算方面,向量检索本身的耗时在10毫秒以内,完全可以接受。真正的风险在于,如果某个环节“回退”到重新调用LLM(比如用户画像过期又没刷新),延迟会飙到秒级。所以我们专门加了熔断机制:在线召回链路发现用户画像超过48小时没更新,就会自动跳过语义召回通道,把空间让给传统召回,宁可少一路召回也不拖垮整体延迟。

提醒:LLM相关的在线服务,一定要设置严格的超时控制。我们一开始没设置,结果某个时刻LLM服务过载,推荐接口的P99延迟从120毫秒涨到900多毫秒,线上反馈立刻就出来了。后来加了超时300毫秒自动降级的策略,再没出过类似问题。

3.4 排序层:语义特征如何与现有CTR模型融合

排序模型最初的设计没有给语义特征留位置,直接拼一个embedding进去,效果提升很有限。后来我们改了融合方式:把用户向量和笔记向量同时输入给排序模型,用双线性交互层做特征交叉。具体来说,就是把用户语义向量和笔记语义向量做外积,再经过一个线性层降维,得到一组“交互特征”。实践证明这个改造比直接拼接有效得多,说明“用户与笔记之间的语义匹配度”本身就是一个强特征。

排序模型的整体结构没有大的改动,只是在特征工程部分增加了语义交互特征。这种轻量级的改造方式,让排序模型的训练不需要从头再来,线上效果验证也顺利。我们对比过直接拼向量和交互特征两种方式,离线AUC提升从0.2个百分点提升到0.6个百分点,差距还是明显的。

排序层还有另外一个细节:LLM语义画像生成的有部分标签是带权重的,权重通过行为频次和时效性计算。这些权重我们也一并接入了排序模型,让模型不仅能知道用户“喜欢什么”,还能知道“最近多喜欢、长期多喜欢”。这个信息对内容的实时刺激度判断很有帮助。

4. 落地过程与效果评估

4.1 分阶段灰度策略:离线验证、影子模式、线上小流量

整个落地过程分了四个阶段。第一个阶段是离线评估,核心看两件事:语义召回通道和传统召回通道的候选重叠率是多少、单独用语义召回能不能覆盖到传统召回覆盖不到的高质量笔记。这一阶段没有线上流量,纯粹是数据回放和分析,用时大约两周。

第二阶段是影子模式,也就是语义召回的候选结果只在日志里记录,不真正进入推荐流。通过影子模式我们拿到了“假如启用语义召回,排序模型会如何对待这些新候选”的模拟结果,同时确认了线上服务的稳定性。

第三阶段是小流量灰度,先放5%的流量,持续观察一周。这个阶段我们重点盯的是监测报表里是否有异常指标波动,尤其是负面指标。确认没有问题后,把灰度比例逐步提高到25%、50%,最后全量。

这里要特别强调一点:灰度放量的节奏不能只看正向指标,还要看负向指标。我们当时发现点击率提升了,但回搜率也有一点点上升,后来排查发现是部分语义召回结果虽然相关,但内容过于相似,导致用户点了两篇“差不多的文章”后觉得没新鲜感。这个问题后来通过在召回结果里引入多样性惩罚项解决的。

4.2 核心指标对照:A/B测试的结果

这次A/B测试持续了两周,实验组和对照组各覆盖了30%的日活流量。核心指标数据是这样的:

指标对照组实验组相对变化
点击率CTR5.12%5.28%+3.1%
人均阅读时长12分36秒13分05秒+3.8%
新笔记7日曝光占比9.8%13.4%+36.7%
长尾笔记(收藏数低于500)曝光占比21.3%26.1%+22.5%
推荐流人均浏览笔记数17.218.1+5.2%

点击率提升3.1个相对百分比,在推荐场景中已经是相当可观的变化了。但坦白说,我们最看重的不是点击率,而是新笔记曝光占比和长尾笔记曝光占比这两个指标的明显改善。这说明语义召回真正解决了一些结构性的问题,不只是把原有热门内容换了个位置。

4.3 两个意料之外的收益

第一个意外收益发生在冷启动上。新笔记被语义召回之后,往往会直接匹配到兴趣标签匹配的用户,哪怕这篇笔记没有点赞数据。过去新笔记需要靠保护期机制慢慢积累行为,现在语义召回通道相当于直接给了新笔记一个“语义冷启桥梁”,新笔记曝光效率提升明显。

第二个意外收益在垂类内容的差异化分发上。传统召回容易把健身内容集中在几个头部关键词上,语义召回能理解同样是“健身”的笔记,有的是减脂塑形、有的是增肌训练、有的是康复训练,匹配给不同兴趣阶段的人群。这种细分能力在没有LLM之前,我们完全没法在线实现。

4.4 成本账:GPU开销和资源评估

任何方案上线都要算成本。我们这次主要开销在两块:一是离线向量化,全量笔记跑一遍大约需要消耗几十张GPU卡数天的算力;好在这是离线任务,可以用低峰时段跑。二是在线用户画像刷新,虽然做了准实时更新,但每天仍然需要调用LLM百万次级别,这在生产环境的成本不是小数目。

为了控制成本,我们做了两件事。第一是把生成画像的底座模型做了一次知识蒸馏,用一个更小规模的模型替代了原始大模型,效果损失控制在可接受范围内。第二是做了画像缓存和增量复用,只有画像中新增了“显著行为变化”的用户才触发重新生成,大部分用户直接复用旧画像。这两个优化让在线推理成本降了大约70%。

5. 常见问题与排查技巧实录

5.1 线上延迟偶发飙高,怎么定位

我们遇到过一个很诡异的问题:整体延迟都正常,但每天固定有几个时段P99延迟会突然涨到400毫秒以上。一开始以为是LLM服务压力大,后来查了监控发现涨延迟的时段正好是用户画像批量刷新任务触发的时段,大量并发请求打到了向量检索服务的同一个分片上,导致单分片负载过高。

解决方式是给向量检索服务加了分片级的负载均衡和限流,同时把画像刷新任务改成错峰执行。这里想说的是,排查延迟问题的时候,不要只盯着计算服务本身,要全链路看一遍,尤其是数据刷新、索引重建这类容易被人忽视的辅助任务。

5.2 LLM生成画像的“幻觉”引发不相关召回

有一段时间,实验组里出现了一批莫名其妙的推荐,比如给一个只关注美妆的用户推荐了“职场晋升建议”。排查发现,是LLM在生成兴趣标签时“脑补”出了用户可能感兴趣的领域。用户行为序列里只要出现过两三次相关的点击,LLM就会推断得更远,生成一些用户并没有表现出来的标签。

解决这个问题,一方面是在生成提示词里加了严格约束,要求LLM只能基于给定的行为标签做归纳,不允许扩写;另一方面加了校验层,新生成的标签如果和用户历史行为标签集合的语义相似度低于阈值,就直接丢弃。这两个手段叠加后,不相关召回的占比降低到了可接受范围。

提示:LLM的生成结果一定要有校验环节,不要直接拿来当特征使用。哪怕约束格式再严格,内容的语义漂移风险依然存在。

5.3 离线与在线特征的一致性难题

排序模型在离线训练时用到的用户向量和笔记向量,和线上实时推理时用的向量偶尔会出现不一致。原因是离线训练使用的是历史版本的用户画像,而线上用到的是实时刷新后的画像。一开始没注意到这个问题,后来发现A/B实验中实验组的模型离线评估指标正常,但线上效果波动大。

排查后发现,问题出在线上的用户画像刷新频率高于离线训练样本的生成频率。我们最后统一了策略:离线训练样本使用“画像生成时间点后24小时”才开始计数的行为标签,线上推理也按照同样的逻辑取用画像版本。这样离线在线的样本分布基本对齐了,波动明显减少。

5.4 向量召回结果“扎堆”导致多样性下降

全量上线一周之后,我们注意到一个质量反馈:推荐流里同一类笔记的占比偏高,比如连续三篇都是健身内容,只是角度不同。原因是语义召回本质上会把同语义簇的笔记都拉进来,如果排序模型对这类相关性的加分较强,就会出现“一窝蜂”的情况。

解决思路是在召回阶段引入多样性约束:从向量库检索时,除了按相似度排序,还要按语义簇做配额限制。每条语义簇最多返回一定比例的笔记,避免单簇垄断。同时在排序阶段增加一个“最近曝光惩罚”特征,同一类内容的曝光频次越高,排序权重越低。改造后推荐流的多样性指标明显回升。

5.5 常见问题速查表

现象可能原因排查手段解决方案
P99延迟飙高画像批量刷新任务与在线检索争抢资源查看服务监控与分片耗时错峰刷新、分片限流
推荐内容与用户不相干LLM生成的兴趣标签出现语义漂移抽查生成标签与行为标签的相似度增加标签校验层、收紧生成约束
A/B效果与离线评估不一致离线在线用户画像版本不一致对比训练样本和线上日志的画像版本号统一样本口径
同类笔记扎堆向量检索缺少多样性约束分析召回集合的语义簇分布增加配额限制和曝光惩罚
新笔记曝光低向量库索引未及时更新检查增量索引构建任务缩短增量构建周期

6. 踩坑之后的经验与可复用建议

6.1 三个立项前必须想清楚的问题

如果你正在规划类似的LLM推荐改造,我建议先回答三个问题。第一个问题是你用什么标准衡量效果。点击率、阅读时长、新笔记曝光、长尾分发,这些指标各有侧重,最好在立项前就明确主指标和护栏指标,否则后期很容易被局部指标带偏。

第二个问题是你的成本预算在哪里。LLM在推荐里的在线调用成本远高于传统模型,如果只看效果不看成本,方案可能很难在大规模流量上持续跑下去。提前想清楚哪些环节必须在线调用LLM、哪些可以离线预计算,能省下大量预算。

第三个问题是你的回滚方案是什么。任何大型改造都要想清楚“出了问题怎么撤”。我们这次因为做了双通道召回和渐进式灰度,回滚操作非常简单——把语义召回这一路关掉就行。如果一开始就搞成端到端的LLM推荐,回滚的复杂度会成倍增加。

6.2 推荐落地LLM的通用经验

从这次项目里,我提炼出三条比较通用的经验。

第一,LLM在推荐系统里的任务边界要收敛。不要试图让LLM解决推荐全链路的所有问题,要选出那些旧方法确实解决不了、且LLM有明显优势的场景,比如多语义理解、意图推理、长尾内容匹配。边界收敛,效果和成本都可控。

第二,先用离线数据把方案验证扎实了再碰线上。我们花了两周时间做离线评估和模拟,虽然看起来“浪费时间”,但恰恰是这一步过滤掉了好几个有缺陷的方案,节省了后面更昂贵的线上试错成本。

第三,LLM产出的特征和结果一定要加校验层。LLM本质上是概率生成,没有绝对的稳定性。不管是画像标签、推荐理由还是打分结果,过一道规则校验或语义校验,能把很多潜在风险提前挡在门外。

6.3 后续可以怎么扩展

这次系统上线只是第一步,后续至少有三个方向可以继续深挖。第一个方向是多模态语义化,目前笔记向量化主要依赖文本,但图片和视频在笔记内容中的权重极高,接下来可以引入跨模态理解,把图片场景、画面信息也编码进去。第二个方向是生成式推荐的继续探索,有了这次搭建的语义画像和向量召回底座,下一步可以尝试让LLM生成更结构化的推荐策略,而不是简单替代召回。第三个方向是兴趣的长期记忆建模,目前画像用的是30天窗口,如果能结合更长期的行为记忆,对理解用户的稳定偏好会有帮助。

如果让我对准备做类似改造的同学说一句掏心窝的话,那就是:别把LLM当银弹,也别把它当玩具。它确实能解决传统推荐解决不了的问题,但它带来的工程复杂度、成本、不确定性都是真实存在的。最好的方式就是像这次一样,从小场景切入,和一支靠谱的研究团队紧密配合,用工程化的手段控制风险,把一个点做透。这样带来的收益,远比铺一个大摊子然后草草收尾要实在得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进