做生产环境的LLM推荐系统久了,你会撞上一个特别尴尬的现实:数据团队交付的日志字段整齐、覆盖率高、时效性也好,但LLM就是不领情。你当然可以把这些字段拼成特征向量继续走传统模型路线,可既然选择了生成式推荐,从日志到输入的这一步就成了整个效果的天花板。我见过太多团队在这儿栽跟头——日志清洗得干干净净,模板套得规规矩矩,线上指标就是不涨。问题往往不在于模型,而在于我们根本没有把日志"说"成LLM听得懂的人话。
这篇文章就围绕我在这类系统里的实操经验展开,讲的不是怎么清洗日志,也不是怎么调prompt,而是两者中间经常被忽略的一层:Optimal Verbalization,也就是怎么把原始行为日志转化成对LLM推荐任务最友好的自然语言描述。适合正在做或准备做LLM推荐、又在纠结线上效果不达预期的工程师参考。
1. 问题的起点:日志进不了Prompt,再好的LLM也白搭
1.1 一份典型的生产日志,LLM视角下是什么样
推荐系统的日志格式,经历过的人都知道,大概是这么一坨东西:
2025-11-01 20:13:02, user_1024, click, item_8848, cat_id=electronics, scene=search, dwell=45 2025-11-01 20:15:37, user_1024, click, item_8851, cat_id=electronics, scene=home, dwell=12 2025-11-01 20:18:10, user_1024, click, item_7211, cat_id=accessories, scene=recommend, dwell=88 2025-11-01 20:22:49, user_1024, addcart, item_8851, cat_id=electronics, scene=detail, dwell=null对我们人来说,这串东西大致能看出来:这个用户先搜了电子产品,点开一个待了45秒,又在家页推了一屏里点了个相关的,待了12秒就跳走了,最后在详情页把第二个商品加进了购物车。但对LLM来说,这里没有"人话",只有一堆符号、ID和数字。
别小看这个差异。LLM的输入分布是自然语言,它对"item_8848"这种字符串不会有任何语义联想,对"cat_id=electronics"这种键值对也只能被动接收。你把原始日志用逗号拼成一行塞进去,模型确实不会报错,但它需要额外消耗大量能力去猜测这些字段的含义,留给真正推理"该推荐什么"的注意力就被稀释了。我自己的经验是,在相同预算下,把所有原始字段拼进prompt的方案,比翻译成自然语言段落后的方案,在候选命中上差出明显几个点,而这还只是第一步。
1.2 为什么"翻译成自然语言"比"拼接键值对"有效
有人会问:LLM不是号称能理解结构化数据吗,为什么不直接把JSON喂进去?
实测下来的结论是:能理解,但理解和"善于利用"是两回事。你可以把JSON看成一份字典,LLM要像查词典一样逐条检索;而自然语言描述是一个有前后逻辑的叙事,信息之间的关联是直接摆出来的。推荐任务最需要的是"这个用户现在处于什么意图状态",这个意图藏在行为顺序里——
- 用户先搜索电子产品,再点击某个商品38秒,然后加购;
- 用户点过电子产品,也点过配件,但两次点完都是5秒内跳出。
这两段话的信息密度几乎相同,但前者表达出清晰的购买意向,后者体现出"随便翻翻、没看到想要的"。用键值对拼出来的日志,这种状态差异要被模型自己慢慢"悟"出来;用语言描述则可以前置地把关系讲清楚。所谓verbalization,本质上就是在做这个事:把日志里隐含的行为关系显式化。
这里值得澄清一个误区:这不是简单的"日志格式化",不是造几个中文句子把字段读一遍就行。你翻译出来的东西需要服务推荐这个目标,让模型在结尾处能顺理成章地产出候选商品、回答"该推荐什么"。我习惯把这一步理解成:站在模型视角做特征工程——不是给人看的报表,而是给LLM看的上下文剧本。
2. "最优表达"不是玄学:先定义信息保真度、指令对齐度和上下文性价比
一开始我也以为,日志翻译成自然语言,读起来通顺、信息不漏就完事了。做了一阵子之后发现,"最优"这个词必须落到三个可量化的维度上,否则回头线上效果不对时,你根本不知道是数据清洗的问题、模板的问题还是模型的问题。
2.1 信息保真度:别把推荐真正依赖的信息弄丢
推荐系统靠什么做判断?主要靠三类信号:用户是谁、用户最近做了什么、用户在不同行为上的反馈强度。
"用户是谁"对应长期画像,通常来自历史统计;"最近做了什么"对应行为序列;"反馈强度"对应时间和动作类型——停留45秒比停留5秒更能说明兴趣,加购比点击更有分量。
信息保真度指的是你生成的描述性上下文,在多大程度上保留了这三类信号。我踩过的典型问题是:为了压缩长度,把用户一整天的行为按类目聚合,输出"该用户今天主要浏览了数码类商品",看起来非常干净,实际丢掉了具体的商品层级信息和行为顺序。结果线上推荐结果变得很"泛",都是大众化爆款,完全没有抓住用户当时想看的那一类小众商品。
所以我的建议是:在写模板前,先列一份"信息保留清单"。哪些字段必须出现在文本里(商品的关键属性、行为动作、时间相对顺序),哪些字段可以丢弃(设备型号、内部渠道号、无意义的搜索ID)。这个清单最好由推荐算法同学和数据同学一起过,别让NLP背景的人自己拍脑袋。
2.2 指令对齐度:让文本最终导向"推荐"这个动作
第二个维度比较隐蔽,我称之为指令对齐度。同样一段用户行为描述,不同的组织方式会把LLM引向不同的思考方向。
举个我实测过的对比:
版本A(中性叙述):
用户最近点击了3件电子产品,停留时间较长,后将其中1件加入购物车。版本B(带意图推断):
用户对电子产品表现出较强兴趣,尤其偏好便携类设备,当前处于加购阶段,存在较高购买可能。版本B在离线评估里效果更好,原因不是它"信息更多",而是它把日志里的信号翻译成了推荐任务关心的中间变量(兴趣强度、当前阶段、可能的偏好品类)。LLM拿到版本B之后,更容易直接在后续生成中给出有针对性的候选商品。
但这里有个度的问题。版本B如果过度推断,就变成"脑补",一旦用户的真实意图并不强时,模型会朝着错误方向自信地推荐。我的做法是:模板里先原样描述行为(做了什么、先后顺序、停留时长),再单独起一行输出"可参考的推断"(不超过一句),并且用"可能""较"这类限定词。这样既给了模型推理的抓手,又留了纠错空间。
2.3 上下文性价比:token预算下什么都想装,出门就超限
第三个维度最现实。生产环境给prompt的输入预算通常是固定的,比如1.5K到2K token。你可以在这个预算里塞用户历史、行为日志、候选商品信息、商品池说明、指令,但塞任何东西都意味着别的东西要减少。
上下文性价比就是一个"花多少token换多少推荐提升"的概念。我在做的时候会先把各信息块的预算固定下来,比如用户画像占300 token、行为日志描述占600 token、候选商品说明占500 token、指令占200 token,然后针对行为日志描述这600 token单独调优。
这里给出一个粗略的调优方向:
| 表达方案 | token占用 | 信息完整度 | 离线推荐效果 | 备注 |
|---|---|---|---|---|
| 原始键值对拼接 | 约1.2K | 高但模型难利用 | 基线 | 浪费预算,效果差 |
| 结构化摘要 | 约700 | 中高 | 高于基线约3个点 | 仍偏"信息堆叠" |
| 叙事化描述+推断 | 约600 | 中 | 高于基线约6个点 | 印象最深,但需防脑补 |
这个表不是通用结论,不同域差异很大。我要强调的是:每次都该用同一批验证样本对比候选方案,别靠感觉选模板。把表达当作一个可调参的组件来对待,收益远比你想象的大。
3. 我实际跑的加工管线:从原始事件到可用的描述性上下文
这部分我尽量给到可以直接落地的方案。日志到语言,我拆分成了四个阶段:事件规约、会话切分、序列压缩、模板化表达。每个阶段都有对应的产出物和检查点。
3.1 事件规约与会话切分:先给日志"断句"
日志是流水,但推荐需要"场景"。用户昨晚搜了一轮数码产品没买,今天早上点开App看了一条新闻,这两件事不该混进同一个context。
我用的规则很简单:以30分钟无操作作为会话断点,同时把发生时间跨超过4小时的不同场景(搜索、首页信息流、详情页)拆开,优先保最近的那个会话。这个规则不复杂,但很有效——切分后,行为叙事变得有头有尾,LLM不用自己去猜哪些行为是关联的。
事件规约这步,我会把原始字段过滤成五类:动作类型(曝光、点击、加购、下单)、目标商品(需要映射成可读名称,后面说)、场景来源(搜索/推荐/广告)、相对时间(当前会话的第几分钟)、反馈强度(停留时长分为"快速跳出""短浏览""详细查看"三档)。日志里那些user_id、item_id的纯ID字段,到这个阶段就该被映射成可读实体。
3.2 长序列压缩:最近的事讲详细,久远的事讲大概
一个重度用户一天能产生上百条点击,你不可能全塞进去。这里我用的是"分层压缩"策略:
- 最近10条行为:逐条叙事,保留顺序和动作细节;
- 更早到50条:按类目聚合成摘要,例如"此前还浏览过键盘、鼠标类配件商品,未产生加购";
- 超过50条:只保留画像级别的一句话,例如"历史偏好3C数码类,客单价偏高"。
这个策略的出发点很简单:推荐对近期行为的敏感度远高于远期行为。把最近的点击翻译得越具体,越能抓住用户当下的意图;把久远的历史压缩成概要,既保留长期偏好,又不浪费token。
我还做了一个时间衰减的细节:如果最近一次点击发生在一个星期以前,那"最近10条"也未必重要,此时让画像部分占主导。总之,压缩不是一刀切,它服务的仍然是"这段描述能不能支撑当前的推荐决策"。
3.3 模板化表达:一套经过验证的上下文结构
压缩完的序列,要套模板生成最终文本。我用的模板结构如下,分享出来供参考:
【用户近期行为】 在最近的一次浏览会话中,用户先在搜索场景下查看多款电子产品, 又回到首页推荐流,其中对"XXX降噪耳机"停留较久(详细查看), 最终在详情页将"XXX降噪耳机"加入购物车。 期间还对"XXX运动手表"快速浏览后离开。 【可参考推断】 用户当前对音频类数码产品表现出较强偏好,处于高意向阶段, 可优先推荐同类商品或配件。 【推荐目标】 请根据以上信息,从候选商品中为用户推荐3个最合适的商品。 要求:优先匹配用户当前偏好,兼顾价格区间与历史消费水平。这个结构有三个值得注意的细节:
第一,"可参考推断"和"推荐目标"是分开的。推断承担"翻译"职责,推荐目标承担"执行"职责,不要揉在一起,否则模型会混淆哪些是事实、哪些是要求。
第二,商品名称必须是可读的。线上日志里的item_id在进入模板前要映射成"商品名+核心属性",这个映射表由商品中台提供。如果没有这个映射,LLM就只能瞎猜。
第三,模板里的每一句话都要有日志依据。我要求算法同学事后追溯每句描述对应的原始记录,一句话对应不上记录,这句话就不应该出现。这条硬规则帮我挡掉了不少幻觉类表达。
3.4 全链路参数示例
给一个具体配置,方便参考调参:
| 参数项 | 配置值 | 说明 |
|---|---|---|
| 会话断点 | 30分钟无操作 | 超过则开启新会话 |
| 会话合并上限 | 4小时 | 超过仅保留最近会话 |
| 近期逐条行为数 | 10条 | 超出部分进入聚合层 |
| 聚合层行为数 | 50条 | 按类目聚合,按动作加权 |
| 画像来源 | 近30天统计 | 类目偏好Top5、价格带 |
| 描述总预算 | 600 token | 超出时先压缩聚合层 |
| 输出商品数 | 3个 | 配合曝光位需求 |
这套参数不是最优解,但它是一个稳定的起点。有了这套起点,你后续做A/B才知道变量是什么。
4. 生产环境的三个硬约束:token、延迟、稳定性
很多人做LLM推荐的demo很兴奋,因为offline指标好看,一上生产就慌。问题大多不出在模型推理本身,而出在你对"生产环境"四个字缺乏敬畏。下面三个约束,是我在实际接入中撞得最疼的。
4.1 token预算的分配:跟商品描述和用户画像抢空间
生产推荐里,prompt里不只是行为日志,还有候选商品信息。候选商品往往有几十个,每个商品的标题、卖点、价格加在一起就能吃掉上千token。行为日志描述和商品信息是直接竞争关系。
我的处理办法是动态分配:当候选商品数量多时(比如超过20个),行为日志描述不再逐条展开,退化为"类目序列+最近3条行为细节";候选商品少时(小于10个),再把日志描述拉满。这个动态策略比固定预算要实用得多,因为它能在不牺牲关键信息的前提下,最大化整体信息的边际收益。
具体实现上,我写了一个简单的预算控制器:先计算候选商品所需token,剩余预算按7:3分给行为描述和画像,行为描述内部再按"最近3条 > 聚合层 > 画像层"的优先级裁剪。不要小看这个控制器,它把线上超限报错的概率从日常发生降到了几乎为零。
4.2 表达文本长度对推理延迟的影响
LLM生成推荐结果,延迟很大程度取决于输入长度——输入越长,prefill阶段计算量越大。我们的线上系统实测下来,输入从800 token增加到1600 token,端到端延迟大约增加40%到60%,这个增长在首页推荐场景里是致命的,接口会直接超时预算。
我做过一组对照:同样的样本,用600 token的表达和1200 token的表达,输出质量差别不超过5个点,但延迟差异明显。所以解决延迟的第一个思路不是优化模型,而是把表达控制在必要的范围内。第二个思路是做分层:对头部用户用完整叙事表达,对长尾用户用简化模板。头部用户数量少、带来的价值高,值得多花计算;长尾用户量大,简化模板能让系统整体负载降下来。
4.3 模板微调导致的线上漂移:别让文本变动变成隐藏的超参
日志表达改一个词,线上推荐结果可能就飘了。这个现象我遇到过好几次:把"停留较久"改成了"浏览较多",离线评估看起来没什么差别,线上曝光分布却出现明显偏移。原因在于LLM对措辞非常敏感,看似同义的改写,在长上下文里会微妙地改变注意力权重。
处理这个问题的办法是:把表达模板的改动当成一次模型改动来管理。每一条模板修改都要走灰度,至少观察2到3天的核心指标,而不是直接全量。同时,尽量在一个版本周期内只改动一处表达,否则出问题时你根本分不清是哪个改动引起的。我们的做法是给模板加版本号,线上数据回流时带上该版本号,后续分析样本都能追溯。
5. 线上实测踩过的坑:看起来没问题,实际很致命的细节
5.1 同义改写带来的"信息泄漏式"提升
有一次我们在优化表达时,把用户点击过的商品在描述中用更具体的标题重新描述了一遍,离线评估指标大幅提升。我们一度以为找到了最优表达,后来才意识到问题:描述里出现的商品和候选商品的重合度过高——模型只是把用户点过的商品重复推荐出来而已,根本没有泛化能力。
这个坑的经典特征:表达层越贴近日志原文,hit率越好看,但推荐多样性和新商品曝光率会同步下降。后来我们规定,行为描述中不得直接出现候选列表中的具体商品名,出现时必须做脱敏处理(比如用"类似款"替代),强制模型基于偏好推断去推荐,而不是复制粘贴。这条规则很痛苦,但它逼着系统真正学会了"理解偏好"而不是"记忆点击"。
5.2 过度聚合丢长尾偏好,曝光多样性下降
前面提到过,把行为聚合到类目级别会让推荐变得很"泛"。线上这个现象会持续恶化:模型不断推荐热门类目中的热门商品,长尾商品的曝光越来越少,最终用户的个性化体验被削弱。
我们的解法是在聚合层保留一个"例外清单":如果用户在聚合区间内点击过某个小众类目,并且停留时长超过阈值,这条行为单独展开描述。这个细节让长尾曝光的比例恢复了不少。我认为这可以给做类似系统的同事一个参考:追求压缩表达时,不要牺牲掉信息熵。你想要的表达,是一个信息密度高的表达,而不是一个平滑到没脾气的表达。
5.3 会话边界处理不当,用户画像被搅浑
还有一次,我们没有切分会话,把用户一周前和当天早上的行为放在一个context里,描述里出现了截然相反的偏好:一会儿进洗碗机,一会儿又要买电竞椅。模型在这种矛盾上下文面前,推荐结果来回横跳,稳定性和转化率都很差。
切分会话之后,问题基本消失了。说到底是表达里的"时间一致性"不够——LLM需要一个逻辑自洽的"此刻的用户",而不是一个拼凑起来的矛盾体。现在每一条行为描述都会带上相对时间标记,比如"5分钟前""昨天",让模型能够区分信号的新旧程度。
5.4 监控项:翻译质量不能只盯点击率
最后给一套我沉淀下来的监控项,供参考:
- 表达文本token长度分布:中位数、P95,防止模板改动后长度突然失控;
- 行为覆盖率:描述中有多少行为来自真实日志,防止幻觉和脱敏过度;
- 描述与候选商品重合度:防止信息泄漏式的伪提升;
- 长尾曝光占比:衡量推荐多样性,间接验证表达是否过度聚合;
- 按模板版本的线上指标分组:快速定位哪次表达改动影响了哪些指标。
这套监控不需要很复杂,把这五个指标落到一个看板上就够用。单独盯点击率,很容易在中间某层被带偏而不自知。
最后再分享一点个人体会:在LLM推荐系统里,日志到语言的这一步,本质上是一个"站在模型角度做特征工程"的活。它既不像提示工程那么灵光一现,也不像传统特征工程那样有现成工具链支撑,但它对线上效果的杠杆极大。我在做完这套verbalization管线后最大的感触是:模型的能力是固定的,它能不能发挥出来,很大程度取决于我们给它看的"用户故事"讲得够不够准。少一点拍脑袋的模板,多几次对照实验和数据回溯,这个环节回馈给你的提升,可能比换一个更大的模型更实在。