试想一下,线上系统每天刷新几千万条用户行为日志,点击、收藏、停留时长、购买,全在里面堆着。传统做法是清洗后算特征,喂给排序模型。可到了大语言模型时代,这些结构化的、散落的数值字段反而成了障碍——模型读不懂“uid=1024, item=SKU778, type=click, ts=171000120”到底意味着什么。这个标题所讲的,核心就是一口把日志“翻译”成人话,再做语言学习式的最优表述,让大模型在推荐场景里真正用起来,还能扛住生产环境的压力和成本。我最近在一套面向新用户的内容推荐服务里完整跑过这个链路,从日志清洗、表述模板设计、微调到灰度上线都过了几轮,踩了不少坑,也沉淀了一些可复用的方法,下面逐一拆开讲。
1. 整体设计:为什么要把日志“转译”成语言再做推荐
先说一个很直接的问题:既然大模型这么强,直接把用户历史行为按“user_id + item_id”喂进去不行吗?我试过,效果很差。原因是纯ID序列对模型来说是一堆无意义的符号,它没法把“昨天点击过三次数码类文章”和“今天推荐手机评测”这两件事联系起来。要让语言模型发挥语义理解的优势,必须把行为日志映射成自然语言描述,让模型读到的是有逻辑有意图的事件流,而不是一串稠密且单调的键值对。
这个思路的核心可以用一句话概括:把日志当作原料,把语言当作中间表达,让模型在语言层完成推荐推理。听起来不复杂,但放到生产环境里就有很多细小的分岔需要做取舍。
1.1 从行为日志到模型输入:中间需要一个表述层
大多人第一次接触LLM推荐,容易直接跳到“把你的prompt写得花团锦簇”这一步,结果效果不稳定。真实链路里第一步不是prompt,而是日志的语义化。
我这边把整条推荐链路拆成四段:原始日志接入、日志清洗与场景切分、表述生成、模型推理与后处理。其中表述生成这部分是很多人会忽略但其实最值得投入的地方。它的本质是在原始日志之上抽象出一个“用户故事”:用户在什么时间,对什么内容,做了什么动作,动作背后大概是什么偏好,形成一个或几个句子。
这里有一个关键设计原则:表述的粒度要和推荐决策的粒度对齐。比如推荐目标是“下一件要看的文章”,那么一条日志里最关键的就不是历史订单,而是近期的阅读序列和点击密度。如果推荐目标是“购物车加购”,那表述重心就要放在商品类目偏好和价格带上。日志结构是通用的,但表述模板必须跟着业务目标走。
从实现上看,日志转语言的流程大致是:
- 日志接入后,先按会话session聚合,而不是按天硬切。
- 提取关键事件,保留行为顺序,去掉冗余字段。
- 字段数值映射成语义标签,比如停留时长从“120秒”转成“长时间阅读”。
- 填充到预定义的句子里,生成一条能直接喂给模型的事件描述。
这步做好,后面模型的稳定性会质变。我见过有人在这一步偷懒,直接用现成日志字段拼话术,结果线上推荐经常出现“用户都没看过的内容被当成强偏好”,原因就是表述层把偶然点击和真实兴趣混在一起了。
1.2 为什么选“语言化”而不是向量召回或序列模型
在方案选型的时候,同步也考虑了两条更常规的路:一条是用户行为序列直接接双塔模型做向量召回,另一条是用户日志聚合成特征后进排序模型。这两条路成熟稳定,资源开销也小。但它们在内容理解上有个上限:整个系统很难感知到“这篇技术文章跟用户刚看过的源码解析是同主题但不同难度层级”这层关系。
语言模型的优势在于它把文本里的语义关联直接当作知识来用。用户对五篇包含“后端”“数据库”关键词的文章产生过行为,不需要经过一层Embedding训练,模型天然就知道下一条推荐可以考虑缓存中间件相关的内容。这类能力天然适合内容型推荐,尤其适合新用户冷启动——后台没有足够的交互数据做协同过滤时,语言描述本身就能承担“特征”的角色。
当然,我不建议把所有场景都改成LLM推荐。成本在那里摆着,一套线上服务每天处理上千万次请求,纯语言化推理的开销远高于双塔模型。比较合理的架构是:双塔负责第一阶段的粗召回,LLM负责对候选集合做细排序和解释生成。日志语言化在这个架构里充当主链条上的“翻译官”,把小规模的候选集转成可推理的语义文本。
我最终选型时,确定了这样的部署方式:日志解析服务独立成一个;表述生成不单独占一个模型,而是靠模板加规则去生成,节省调用量;大模型只做候选打分排序。这样算下来,每天500万次推荐请求,纯增量的大模型推理量控制在50万次左右,成本基本可控。
2. 核心细节解析:最优表述的构建原则与实现方法
标题里的“Optimal Verbalization”翻成中文就是最优表述,这个词值得琢磨。并不是任何自然语言句子都能让模型给出好结果,表述的好坏直接决定推荐的准确性。我刚开始做实验的时候,用同样一份日志,只是调整了表述句子的顺序和用词,NDCG@10能从0.31跳到0.44,变化非常吓人。
2.1 日志清洗与事件语义化的几个关键操作
日志清洗是最不性感但最影响结果的一步。几个我反复踩的点列出来:
- 时区不统一。线上服务会跨多机房部署,用户日志如果用本地时间记录,聚合时可能出现时间倒挂。我在清洗层统一把时间全部转成UTC毫秒时间戳,并在会话划分前固定好时区。
- 过滤机器行为。有些刷量点击和异常高频请求会混进日志,如果直接进表述层,模型会把异常行为学成正常偏好。我是按IP维度和设备维度做频率统计,超过95分位的高频访问直接剔除。
- 事件去重。用户在同一个商品页短时间反复进入退出,会产生多条重复曝光。这里不是简单只留一条,而是记录首次进入时间和末次退出时间,同时计算停留时长。
事件语义化的核心是把连续数值变成离散语义标签。我问过一些开发者,大家会倾向于直接把数值放进句子,比如“用户停留120秒”。但实测效果是,“用户长时间阅读并仔细比较”这种带判断的表述,模型更容易形成偏好推断。因为模型学习的是语义标签之间的关联,而不是数字之间的相对大小。具体映射我用了一个简单阈值表:
| 行为类型 | 原始数值范围 | 语义标签 | 表述示例 |
|---|---|---|---|
| 阅读停留 | >= 90秒 | 长时间深读 | 认真阅读完 |
| 阅读停留 | 30~90秒 | 中等停留 | 浏览了一部分 |
| 点击频率 | 同源内容点击>=3次 | 强偏好 | 连续多次查看同主题内容 |
| 购买行为 | 加购未支付 | 高意向 | 加入购物车但尚未结算 |
| 分享转发 | 点击分享按钮 | 社交认可 | 愿意分享给他人 |
这套设计背后考虑是规避模型的数值盲区。大模型对绝对数值的判断一般弱于分类语义标签,与其让它猜“90秒”和“85秒”有什么区别,不如直接告诉它“看完了一篇长文”。语义标签粒度不用太细,4到6档就够了,太细模型反而会过拟合噪声。
2.2 句子结构怎么搭:行为顺序、关注焦点和负反馈
表述生成不是把多个句子随便拼在一起。我复盘出来的最优模板遵循三个原则:
第一,事件按时间顺序排,但同级重要性的行为可以并列。时间顺序给模型呈现一个逻辑链条,让它自己体会趋势变化。比如一个人上周阅读的全是入门教程,这周开始读进阶实战,这就是明显的兴趣升级信号。并列关系则适合表达强度,例如连续点击、多个同类目行为并行出现。
第二,越晚近的行为权重越高。人的兴趣会漂移,三个月前的购买记录对今天的推荐意义不大。我在表述层把行为按时间加权,最近7天的行为完整保留,7~30天的行为压缩成一句概括,30天往前的行为只保留长期稳定偏好。句子结构上采用“近期 + 中期 + 长期”的三段式,让模型自然感受到时间的影响。
第三,不要只表达正反馈,负反馈同样重要。用户点了“不感兴趣”按钮、快速划走、在文章界面只停留四秒,这些都是强烈的负信号。我尝试在表述里显式加上负样本句,比如“用户对短视频类内容快速划过3次,表现出较低的接受意愿”。这个操作让模型在排序时明显降低被打过负反馈标签的内容权重,整体线上点击率提升了3.2%。
这里我放一个实际在用的表述生成片段,用Python做了简化,方便说明请求处理时的逻辑:
def generate_verbalization(session_events, long_term_profile): recent = [] mid_term = [] for ev in session_events: if ev.days_ago <= 7: recent.append(f"在{ev.time_range},用户{ev.semantic_action}了内容《{ev.title}》({ev.category})") elif ev.days_ago <= 30: mid_term.append(f"过去一个月内,用户对{ev.category}内容保持较高关注") long_part = f"用户长期偏好集中在{long_term_profile}" return "\n".join(recent + mid_term + [long_part])在实际生产实现里,generate_verbalization的输入来自一个实时的会话特征缓存,输出会拼上一段候选商品描述,再一起发给模型打分。
2.3 离线评估:先定准指标,再谈效果优化
做LLM推荐的评估,不能只看最终点击率。我这边搭了一套离线评估框架,包含三个层面的指标:
- 排序质量指标:NDCG@10、MRR、Recall@20。
- 表述质量指标:表述与用户行为的一致性打分,由人工抽检和模型自评两部分组成。
- 稳定性指标:同一样本多次推理的打分方差、对不同表述模板的敏感度。
这里要特别说一下表述质量的评估。我抽了2000条线上日志,人工对照原始行为与生成表述,判断是否有信息补全、信息扭曲或信息缺失。一开始这个环节漏掉很多问题,后来检查才发现部分模板会生成“用户对XX类内容有高度兴趣”,但其实用户只是误点了两次。针对这类情况,我会在表述生成后增加一层置信度计算:行为数量太少时,在句子里加“在有限的交互中,用户表现出轻微的偏好倾向”这种弱化措辞。
排序质量上的对比数据我整理过一次,明显看得出语言化表述带来的增益在长尾内容上更突出。
| 指标 | 纯特征排序模型 | 特征排序 + LLM语言化重排 | 提升幅度 |
|---|---|---|---|
| NDCG@10 | 0.383 | 0.452 | +18.0% |
| MRR | 0.271 | 0.334 | +23.2% |
| 长尾内容曝光占比 | 12.6% | 18.4% | +46.0% |
长尾内容曝光涨这么多,我理解是因为语言模型天然能理解语义相似性,不需要这些内容跟用户历史行为有严格的ID共现,所以之前很难被传统协同过滤发现的冷门内容有了出头机会。
3. 实操过程:从原始日志到线上稳定服务
很多分享停在离线实验阶段,但我更关心的是完整落地。这一节把整个实施流程过一遍,涉及数据链、训练、部署三块,每块都有明确的操作步骤。
3.1 数据管道构建:日志接入、会话聚合、语义化缓存
第一环是日志接入,我这边采用的方式是消息队列实时接入加离线批处理补偿。原始日志落在Kafka对应主题中,一个消费者组负责实时解析,另外每天凌晨跑一个批任务回补当天数据质量检查。主要做两项:字段完整性校验和格式统一。
接下来是会话聚合,这是个吃资源的步骤。会话切分策略比较简单:相邻两条行为间隔超过30分钟就断开,切出来的会话作为表述生成的最小单元。会话内的事件按类型排序,核心事件优先排,比如购买 > 深度阅读 > 点击 > 曝光。
语义化这一步我放在了独立的服务里,输入是会话事件列表,输出是一段文本。为了提高复用率,我还加了缓存。
缓存设计得很粗暴:用用户ID和会话ID做键,语义化结果存Redis,有效期48小时。只有会话发生新事件时才更新。这个设计让模型的重复调用量降了40%左右,因为同一用户短时间内的多次推荐请求不再重复拼表述。
数据管道的完整流程如下:
- 日志接入,统一时区,过滤异常流量。
- 会话切分,按用户维度和时间窗口聚合。
- 提取关键行为,映射到语义标签。
- 按模板生成表述并缓存。
- 把表述拼接候选商品信息,组成模型输入。
3.2 提示模板调优与模型微调的平衡
这一步涉及标题里“Language Learning”的另一个侧面。当我拿到一批日志转语言生成的自然语言样本后,模型不能只靠零样本推理干活,需要用这些样本做有监督微调,让模型学会从表述中抽取偏好信号。
我使用的微调方式是LoRA,而不是全量微调。原因很现实:基座模型太庞大,全量微调的显存和训练时间根本无法接受,而且容易破坏模型原有的通用能力。LoRA只训练一小部分低秩参数,适配推荐任务足够用,还能随时切换回通用能力。
训练数据的构造是整个过程中最费精力的。每条训练样本由三部分组成:
- 用户日志转语言后的行为描述。
- 一个候选推荐项的描述。
- 标注标签:是否推荐、推荐理由、推荐强度。
标注标签不是全部靠人工,我用的策略是“人工标注 + LLM自动标注 + 规则校验”三者结合。先人工标注500条高质量种子样本,再用这批种子样本通过少量示例示范让模型自动标注一大批,最后由规则层过滤掉自相矛盾的标注。
微调参数我用的是:
learning_rate: 2e-4 batch_size: 16 max_seq_len: 2048 lora_rank: 16 lora_alpha: 32 epochs: 33个epoch是我调出来的平衡点,训练太少模型学不会,训练太多会出现灾难性遗忘,模型开始对推荐任务过拟合,反而丢掉了语言理解能力。训练完成后必须跑一遍通用能力回归测试,比如常识问答、摘要、指令遵循各抽几十条,分数掉得太明显要回退参数。
这里再加一层提示模板稳定性控制:微调后的模型虽然见过大量样本文本,但线上推广时提示里会有新出现的候选商品描述,所以要设计带均匀随机插入新内容的训练样本。具体做法是每次训练时,把候选商品描述里10%替换成随机从知识库抽取的内容,让模型对没见过的东西也保持稳定判断。
3.3 线上服务部署:推理性能与成本控制的实操
线上推理我用的是轻量级部署方案,因为推荐场景对延迟敏感,一般要求在200毫秒内返回。
关键优化点有三个。第一个是输入截断策略。日志转语言后的用户行为描述可能很长,加上候选商品描述后经常超过2000 token。我写了一个动态截断函数:保留行为描述的前半部分和最近3条事件,中间压缩成摘要。这个策略能控制95%的请求输入长度在1500 token以内,精度损失控制在5%以内。
第二个是批量推理时动态padding。一组请求里各输入长度差异很大,直接padding到最大长度可以开满GPU算力,但浪费严重。我采用的是按长度分桶,把长度接近的请求放到一个batch里,配合KV Cache复用,整体吞吐量大约提升了1.6倍。
第三个是静态内容预计算。候选商品描述、常用行为标签这些可复用的内容在服务启动时预先向量化或预先格式化,推理时直接查缓存。实测下来生成部分的耗时占比可以降到总耗时的30%以下。
部署架构上我拆成两个池子:一个池子跑在线请求,一个池子专门跑离线重放任务。离线重放任务不直接服务用户,而是用历史日志不停回放,用来发现模型的不稳定输出和候选集的覆盖盲区。这个设计帮我发现了好几次日志语义化字段变更引发的模型效果滑坡。
3.4 灰度发布与A/B测试:先别急着全量
LLM推荐的灰度发布和传统模型不同,它涉及的变更面更广:可能改了表述模板、换了模型权重、调了推理参数,每一层都会影响最终输出。我习惯做的是多层叠加灰度:
第一个层级先开小流量,占比5%。这个阶段只看基础设施指标,包括推理延迟、错误率、显存占用,不看业务指标。第二个层级扩到20%,观察点击率和用户反馈。第三个层级再扩到50%,这时候会看留存和长尾内容分发比例。最后冲全量之前,必须回看离线重放任务的指标是否与新流量表现一致。
A/B测试的注意点是,不能只设一个实验组和一个对照组。我通常设三组:全特征排序模型作基准组、特征排序 + LLM排序组、特征排序 + LLM排序 + 表述优化组。三个组两两对比,分别能分离出LLM本身的增益和表述优化的增益。
有一次灰度我印象很深:20%流量时整体点击率提升明显,但细分人群之后发现新用户点击率涨了8%,老用户反而跌了4%。排查后发现是表述模板里太多强调近期行为,老用户的长期偏好被挤掉了。紧急调整了长短期行为配比之后,老用户指标恢复正常。这类问题在离线评估里很难暴露,只能靠分层灰度去发现。
4. 常见问题与排查技巧实录
做这套系统的过程中,遇到的问题五花八门,很多不是模型原理层面的,而是工程细节。我把最有代表性的几个问题按现象、定位、解决列成了速查表。
4.1 高频问题速查表
| 问题现象 | 可能原因 | 定位手段 | 解决操作 |
|---|---|---|---|
| 推荐结果全是热门内容,长尾消失 | 表述中过度强调大众偏好 | 检查生成表述和候选描述,是否语义集中 | 加入类别均匀化采样,调整候选集分布 |
| 同一条日志生成不同表述 | 模板字段缺失导致随机填充 | 对比同session多次生成结果 | 在模板渲染时增加默认值,避免空字段 |
| 模型对候选内容评分特别高但点击率低 | 表述中缺少负反馈信号 | 人工抽查top-10推荐内容的日志序列 | 补充负样本句,明确“划走”“不感兴趣”等行为 |
| 调用成本超标 | 输入token太长 | 查看在线日志的token分布 | 启用动态截断策略,批量分桶 |
| 日志有延迟,模型推荐滞后 | 消息队列堆积 | 观察消费者lag指标 | 增加消费者并行度,并加一轮离线补偿 |
还有一个常见但容易被忽视的问题:用户隐私字段误入表述。日志里偶尔会带手机型号、地理位置甚至设备标识,如果不加过滤,模型可能学到一些既没有推荐价值又敏感的关联。我在语义化服务里加了字段白名单机制,只有白名单里的字段才能进入模板,其他一律丢弃。
4.2 避坑心得与一票否决项
有几个深坑花了很长时间才趟平:
坑一:全量日志进prompt,以为信息越多越好。早期设计时我试着把用户一年内的日志全塞进去,结果推荐模型开始学用户的历史惯性,给用户推的全是同类内容,严重影响探索。后来统一改成“近期 + 中期 + 长期”三段式,才在相关性和多样性之间找到平衡。
坑二:别让模型直接对数字做运算。有一版表述里写了“用户点击了17次”,模型在做打分时经常把具体出现过的数字当成强特征,下面的行为一多,反而把偏好淹没了。改成“大量点击”“多次点击”之后,效果稳定很多。语言模型擅长的是语义理解,不是数值比较,表述层要主动替模型把数值转成判断。
坑三:新内容的表述与训练样本分布严重不一致。上线期间引入了一批新栏目内容,候选描述里出现很多训练时没见过的品类词,模型给这类内容打分偏低。后面在微调训练样本里会定期注入新内容的描述,并对未知描述做后处理纠偏,比如调低置信度或加倍多样本投票。
所谓一票否决项,我指的是三类问题:有用户数据泄漏风险的行为一律不做;模型在相同输入下产生严重不稳定输出的版本不能上线;带有明显毒性或歧视倾向的生成表述必须拦截。这三类问题和推荐效果无关,属于安全底线。
4.3 后续方向:表述的自适应与跨场景复用
这套链路跑通之后,我认为最有价值的后续扩展方向是让表述自学习。
当前模板是人工设计的,虽然效果不错,但对新业务场景的适配速度还是太慢。比如从文章推荐扩展到短视频推荐,同样的行为语义标签需要重新设计。下一步的计划是维护一个“行为语义词典”,把日志字段和语义标签做成可配置的映射表,配置表变化时自动批量重算历史样本,让模型快速适应新场景。
另一个方向是跨场景复用。用户在一个业务域的日志转语言结果,能不能经过脱敏后复用到另一个推荐场景?比如阅读行为的表述能否辅助商品推荐。目前的实验表明,只要表述抽象到“长时间深度参与”“快速划过”“内容主题偏好”这个层级,跨场景迁移是有可能的。真正卡点反而是数据权限和隐私合规,技术本身已经能看到路子。
我自己的体会是,这套日志转语言、语言驱动推荐的方案,核心不只是把数据喂给大模型,而是构建了一个“用自然语言描述用户兴趣”的中间层。中间层一旦稳定,下游的模型替换、业务扩展都会变得非常灵活。以后模型能力更强,可能不需要这么复杂的表述工程,但今天要想在真实生产环境里用LLM做推荐,把日志转成高质量语言样本这件事,值得认真投入。