AI 推荐系统——LLM 和传统推荐算法怎么配合
AI 推荐系统——LLM 和传统推荐算法怎么配合
适读人群:做推荐系统的工程师,以及想在推荐场景用 LLM 的人 | 阅读时长:约16分钟 | 核心价值:LLM 不是银弹,但在推荐系统里有几个真正有价值的切入点
我在一个内容推荐项目里待了快一年,这个项目的产品是一个面向职场人的文章阅读 APP,DAU 大概30万。在这之前,推荐系统是传统的协同过滤+内容特征的混合方案,效果还行,但有两个痛点一直没解决:冷启动太烂,内容多样性不够。
加入 LLM 之后,这两个问题有了明显改善。但我要先讲清楚一件事:推荐系统不是把传统算法全换成 LLM,而是找到 LLM 真正有优势的位置,和传统算法配合。
谁来做实时个性化,谁来做冷启动,谁来保证多样性——分工清楚了,整个系统才跑得起来。
为什么 LLM 不能直接替换传统推荐算法
先说清楚 LLM 的局限,再说它能做什么。
速度问题。 推荐系统的在线部分对延迟极其敏感。用户打开 APP,要在 200ms 内返回推荐结果,否则体验就崩了。LLM 的推理延迟在 1-5 秒,没法用在在线召回阶段。
个性化深度问题。 协同过滤的核心优势是它从千万用户的行为数据里隐式学习"相似用户"模式,这种从大规模交互数据里学到的个性化,LLM 在没有专门微调的情况下做不到。
扩展性问题。 每天几十亿次的推荐请求,每次都调 LLM API,成本不可接受。
但 LLM 有几个传统算法真的搞不定的地方:
语义理解。 传统推荐算法看的是 item ID 和用户行为,理解不了内容的语义。两篇关于"职场晋升"的文章,一篇讲沟通技巧,一篇讲项目管理,传统算法把它们都标为"职场类",LLM 知道它们的区别,能做更细粒度的匹配。
冷启动推理。 新用户没有历史行为,传统算法无从下手。但新用户通常会填写一些基本信息(职业、行业、兴趣),LLM 可以基于这些文本信息推理他可能感兴趣的内容。
多样性注入。 推荐算法的优化目标是点击率,会导致推荐结果越来越窄(信息茧房)。LLM 可以理解"你最近看了太多 A 类内容,试试 B 类"这种逻辑,做主动的多样性干预。
系统架构:分工明确
我们最终的架构是这样的:
用户请求
|
v
在线推荐服务(< 200ms 预算)
|
+---> 召回层(传统算法,快速从百万级降到千级)
| |
| +---> 协同过滤召回(UserCF / ItemCF)
| +---> 内容相似召回(向量检索)
| +---> 热度召回(保底)
|
+---> 排序层(机器学习,从千级降到百级)
| |
| +---> LightGBM 粗排
| +---> 神经网络精排
|
+---> LLM 干预层(异步,在线等待结果)
| |
| +---> 新用户冷启动处理(无历史行为时激活)
| +---> 多样性重排(检测到单一化时激活)
|
v
最终结果(约20条)
离线服务(异步,不影响在线延迟)
|
+---> 用户兴趣画像更新(每小时)
| |
| +---> LLM 分析阅读行为,生成语义标签
|
+---> 内容 embedding 生成(新内容发布时)
|
+---> LLM 生成内容的语义向量LLM 的调用都在不影响在线延迟的位置:要么是预计算(提前生成好),要么是在流量低谷期异步执行。
冷启动:LLM 的主战场
新用户的冷启动是 LLM 真正发挥作用的地方。
我们的冷启动流程是:新用户注册时填写一个简单的偏好问卷(职业、行业、感兴趣的话题),然后用 LLM 把这些文本信息转化成推荐系统能用的特征。
import anthropic
import json
from typing import List, Dict
def generate_cold_start_profile(user_registration_info: dict) -> dict:
"""
根据用户注册信息,生成初始兴趣画像
用于冷启动推荐
"""
client = anthropic.Anthropic()
prompt = f"""根据以下新用户注册信息,生成一个初始兴趣画像,用于内容推荐系统的冷启动。
用户注册信息:
- 职业:{user_registration_info.get('job_title', '未填写')}
- 行业:{user_registration_info.get('industry', '未填写')}
- 工作年限:{user_registration_info.get('years_of_exp', '未填写')}
- 感兴趣的话题(用户自填):{user_registration_info.get('interests', '未填写')}
- 当前面临的挑战:{user_registration_info.get('challenges', '未填写')}
我们平台的内容分类:
- 职业技能(编程、设计、数据分析、项目管理等)
- 职场发展(晋升、跳槽、薪资谈判等)
- 行业洞察(科技、金融、互联网、制造等)
- 管理领导力
- 个人成长(时间管理、学习方法等)
- 副业创业
请生成推荐权重分配(各类权重之和为100)和初始标签列表,以 JSON 格式返回:
{{
"category_weights": {{
"职业技能": 权重,
"职场发展": 权重,
"行业洞察": 权重,
"管理领导力": 权重,
"个人成长": 权重,
"副业创业": 权重
}},
"interest_tags": ["标签1", "标签2", ...], // 10-15个精准标签
"inferred_level": "初级/中级/高级", // 推断的内容难度偏好
"reasoning": "简短说明推断依据"
}}"""
response = client.messages.create(
model="claude-haiku-20240307", # 用便宜的模型,这个任务不需要最强模型
max_tokens=400,
messages=[{"role": "user", "content": prompt}]
)
json_match = __import__('re').search(r'\{.*\}', response.content[0].text, __import__('re').DOTALL)
return json.loads(json_match.group())
def cold_start_recommend(
user_profile: dict,
item_pool: List[Dict], # 候选内容池
top_k: int = 20
) -> List[str]:
"""
基于 LLM 生成的用户画像,从候选池中选取推荐内容
用于新用户的前几次推荐
"""
client = anthropic.Anthropic()
# 把候选内容简化成描述
items_desc = "\n".join([
f"ID:{item['id']} | 标题:{item['title']} | 分类:{item['category']} | 标签:{','.join(item['tags'])}"
for item in item_pool[:100] # 最多放100条候选
])
profile_text = f"""用户画像:
- 兴趣权重:{json.dumps(user_profile['category_weights'], ensure_ascii=False)}
- 兴趣标签:{', '.join(user_profile['interest_tags'])}
- 内容难度偏好:{user_profile['inferred_level']}"""
prompt = f"""{profile_text}
候选内容列表:
{items_desc}
请从候选内容中选出最适合这个用户的 {top_k} 篇文章,注意:
1. 按相关性排序,最相关的排最前面
2. 保证多样性,不要全是同一类型
3. 只返回内容 ID 列表,JSON 数组格式:["id1", "id2", ...]"""
response = client.messages.create(
model="claude-haiku-20240307",
max_tokens=200,
messages=[{"role": "user", "content": prompt}]
)
json_match = __import__('re').search(r'\[.*\]', response.content[0].text, __import__('re').DOTALL)
return json.loads(json_match.group())多样性注入:防止信息茧房
这是另一个 LLM 发挥作用的场景。我们发现一个现象:用户最近三天全在看 Python 编程相关内容,推荐系统就一直给他推 Python 内容,用户留存开始下降(可能是内容看腻了,也可能是需求满足了)。
传统做法是人为设置多样性规则,比如"同一类别的内容不超过30%"。这个方法太粗暴,LLM 可以做更细粒度的判断。
def diversity_rerank(
ranked_items: List[Dict],
user_recent_history: List[Dict], # 用户近期阅读历史
diversity_ratio: float = 0.3 # 希望多样性内容占比
) -> List[Dict]:
"""
基于用户近期行为,对推荐列表进行多样性重排
"""
client = anthropic.Anthropic()
# 分析用户近期阅读模式
history_text = "\n".join([
f"- {item['title']}({item['category']})"
for item in user_recent_history[-20:] # 最近20篇
])
candidates_text = "\n".join([
f"ID:{item['id']} | {item['title']} | {item['category']}"
for item in ranked_items[:50]
])
prompt = f"""用户最近阅读的内容:
{history_text}
待排序的候选内容:
{candidates_text}
任务:对候选内容进行重排,在保持个性化相关性的同时,适度引入多样性(约占 {int(diversity_ratio*100)}%)。
多样性内容应该是:与用户历史有关联但视角不同、或用户可能感兴趣但尚未涉猎的领域。
请返回重排后的 ID 列表(JSON数组),只返回前20个:["id1", "id2", ...]
并简短说明重排逻辑(1-2句话)。
格式:
{{
"reranked_ids": [...],
"rationale": "重排说明"
}}"""
response = client.messages.create(
model="claude-haiku-20240307",
max_tokens=300,
messages=[{"role": "user", "content": prompt}]
)
import re
json_match = re.search(r'\{.*\}', response.content[0].text, re.DOTALL)
result = json.loads(json_match.group())
# 根据返回的 ID 列表重组内容
id_to_item = {item['id']: item for item in ranked_items}
reranked = [id_to_item[id] for id in result['reranked_ids'] if id in id_to_item]
return reranked离线用户画像更新:持续优化个性化
这是 LLM 在推荐系统里最不显眼但最有持续价值的角色——离线分析用户行为,更新语义级别的兴趣画像。
传统推荐系统的用户画像是 item ID 的统计分布,LLM 可以做语义级别的分析:
def update_semantic_user_profile(
user_id: str,
recent_reading_history: List[Dict],
current_profile: dict
) -> dict:
"""
每小时运行一次,基于用户最新阅读行为,更新语义兴趣画像
这是离线任务,不影响在线推荐延迟
"""
client = anthropic.Anthropic()
history_text = "\n".join([
f"- 《{item['title']}》({item['category']})"
f"完读率:{item['read_completion']}%,"
f"互动:{'收藏' if item.get('bookmarked') else ''}{'点赞' if item.get('liked') else ''}"
for item in recent_reading_history[-30:]
])
current_interests = json.dumps(current_profile.get('interest_tags', []), ensure_ascii=False)
prompt = f"""用户最近的阅读行为(最近30篇):
{history_text}
用户当前兴趣标签:{current_interests}
请分析用户的阅读行为,更新兴趣画像:
1. 完读率高(>70%)且有互动的内容,说明是强兴趣
2. 完读率低的内容,说明不感兴趣或内容质量差
3. 识别是否有新兴趣点出现
4. 识别是否有旧兴趣在衰减
返回更新后的兴趣标签列表(JSON数组),10-20个标签:
{{"interest_tags": [...], "emerging_interests": [...], "declining_interests": [...]}}"""
response = client.messages.create(
model="claude-haiku-20240307",
max_tokens=250,
messages=[{"role": "user", "content": prompt}]
)
import re
json_match = re.search(r'\{.*\}', response.content[0].text, re.DOTALL)
updates = json.loads(json_match.group())
# 合并更新到用户画像
updated_profile = current_profile.copy()
updated_profile['interest_tags'] = updates['interest_tags']
updated_profile['emerging_interests'] = updates.get('emerging_interests', [])
updated_profile['last_updated'] = __import__('datetime').datetime.now().isoformat()
return updated_profile效果数据
上了 LLM 辅助的推荐方案之后,我们的数据变化:
- 新用户7日留存:从 23% 提升到 31%(主要是冷启动改善)
- 人均阅读深度(篇数/天):从 4.2 提升到 5.1
- 内容多样性指数(用信息熵衡量):提升 18%
- 推荐延迟:P99 保持在 180ms 以内(因为 LLM 调用全在离线或低优先级路径上)
成本上:LLM 调用每天大概是 $80-120,对比推荐效果提升带来的广告收入提升,ROI 很可观。
但我要说实话:冷启动改善的效果是真实的,多样性干预的效果有争议——有些用户就是不想被"强制多样化",他们就想看同类内容。多样性干预对新用户帮助大,对老用户要谨慎,需要根据用户的显式反馈来决定是否干预。
推荐系统没有银弹,LLM 也不是。但在对的地方用,确实能解决传统算法解决不了的问题。
