第1644篇:电商推荐系统与大模型结合——当传统推荐遇到LLM的新玩法
第1644篇:电商推荐系统与大模型结合——当传统推荐遇到LLM的新玩法
我在电商推荐这个坑里泡了好几年,从协同过滤做到深度学习,从双塔模型做到GraphSAGE,自认为懂得不少。但大模型出来之后,确实给了我很多新的思路,也让我意识到传统推荐系统有些问题其实一直没被很好解决。
今天不是要全盘否定传统推荐,而是聊聊大模型能在哪些地方真正补位,以及怎么把两者结合起来。
传统推荐系统的真实痛点
先说问题。协同过滤和各种深度学习推荐模型,工业界已经用得很成熟了,但有几个问题始终很头疼:
冷启动问题:新用户没有行为数据,新商品没有交互历史,推啥都像在瞎猜。
跨品类迁移:用户在A品类的偏好,很难迁移到B品类。你买过很多护肤品,系统对你的时尚品类推荐还是一团糟。
语义理解缺失:传统推荐系统不懂"红色连衣裙"和"复古风碎花裙"的关系,只能靠用户行为来隐式学习。用户主动搜索的意图,在推荐里经常被忽视。
解释性差:为什么推荐这个商品?模型输出就是个浮点数,说不清楚,用户和运营都不信任。
这四个问题,大模型正好能不同程度地解决。
整体架构:LLM作为推荐的增强层
我不建议把大模型做成推荐系统的主路径,原因很简单:延迟高、成本贵。一个电商首页要推20个坑位,如果每个都调用大模型,算算成本,老板要找你谈话。
合理的定位是:大模型作为增强层,在特定场景发挥优势,主力还是传统模型。
大模型介入三个环节:
- 离线阶段:生成用户的语义画像、商品的语义向量
- 在线召回:冷启动用户用LLM推断偏好
- 结果展示:生成个性化推荐理由
用大模型生成语义用户画像
传统用户画像是一堆标签:25-30岁、女性、爱好运动、购买力中等。这些标签是规则或分类模型打的,很割裂。
大模型可以做一件传统方法很难做的事:把用户的行为序列"读懂",生成有语义的画像描述。
@Service
public class UserProfileGenerationService {
@Autowired
private ChatClient chatClient;
@Autowired
private UserBehaviorRepository behaviorRepo;
/**
* 生成用户的语义画像(离线批处理,每天更新)
*/
@Scheduled(cron = "0 2 * * * ?") // 每天凌晨2点
public void batchGenerateProfiles() {
// 取最近7天有活跃行为的用户
List<Long> activeUserIds = behaviorRepo.findActiveUsers(
Duration.ofDays(7), 1000
);
for (Long userId : activeUserIds) {
try {
generateUserProfile(userId);
} catch (Exception e) {
log.error("用户画像生成失败:{}", userId, e);
}
}
}
public void generateUserProfile(Long userId) {
// 获取最近30天的行为数据
UserBehaviorSummary behavior = buildBehaviorSummary(userId);
String prompt = """
请根据以下用户购物行为数据,生成一段自然语言的用户画像描述。
【购买记录(近30天)】
%s
【浏览记录(近7天,取Top20)】
%s
【搜索历史(近30天)】
%s
【评价关键词】
%s
请生成:
1. 用户画像描述(100字以内,重点是消费偏好和风格倾向)
2. 偏好标签(5-10个,精简准确)
3. 潜在需求推断(用户可能还没买但需要的东西,2-3条)
4. 价格敏感度(低/中/高)
5. 适合推荐的内容风格(比如:实用型、品质型、潮流型)
以JSON格式返回。
""".formatted(
behavior.getPurchaseHistory(),
behavior.getBrowseHistory(),
behavior.getSearchHistory(),
behavior.getReviewKeywords()
);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
UserSemanticProfile profile = parseProfile(response);
// 生成画像的向量表示,存入向量库
float[] profileVector = embeddingModel.embed(profile.getDescription());
userProfileRepo.save(UserProfileRecord.builder()
.userId(userId)
.semanticDescription(profile.getDescription())
.preferenceLabels(profile.getPreferenceLabels())
.potentialNeeds(profile.getPotentialNeeds())
.priceSensitivity(profile.getPriceSensitivity())
.contentStyle(profile.getContentStyle())
.profileVector(profileVector)
.updatedAt(Instant.now())
.build());
}
}这个画像有什么用?可以用来做更细粒度的召回。比如某个用户的潜在需求里包含"家庭健康类产品",即使他最近没搜索过这类,也可以主动把相关商品加进候选集。
冷启动:大模型的真正价值所在
新用户冷启动是传统推荐的硬伤。通常的做法是推热门商品,但这个太糊弄人了,千人一面,没有任何个性化。
大模型可以利用新用户在注册或者引导流程中填写的少量信息,做出相对合理的偏好推断:
@Service
public class ColdStartRecommendationService {
@Autowired
private ChatClient chatClient;
/**
* 新用户首次推荐
* 输入:用户在引导页填写的基础信息
*/
public List<Long> getInitialRecommendations(
UserOnboardingInfo onboardingInfo,
int topN) {
// 生成用户偏好推断
String inferredPreferences = inferPreferences(onboardingInfo);
// 用推断出的偏好,从商品向量库中检索
float[] preferenceVector = embeddingModel.embed(inferredPreferences);
List<Long> candidateIds = productVectorStore.search(
preferenceVector, topN * 3 // 召回更多候选
);
// 用规则过滤(库存、价格区间等)
return filterAndRank(candidateIds, onboardingInfo, topN);
}
private String inferPreferences(UserOnboardingInfo info) {
String prompt = """
一个新用户在电商App注册时提供了以下信息,请推断他/她的购物偏好:
- 年龄段:%s
- 性别:%s
- 所在城市级别:%s
- 感兴趣的类目(用户自选):%s
- 引导页点击偏好(从A/B选项选择):%s
请生成一段150字以内的偏好描述,要像"描述这个人的购物风格"那样写,
不要用标签堆砌,要有语义内容。重点描述:喜欢什么风格/品类/价位区间。
""".formatted(
info.getAgeGroup(),
info.getGender(),
info.getCityTier(),
String.join("、", info.getSelectedCategories()),
formatOnboardingChoices(info.getOnboardingChoices())
);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}这个方案在我们的AB测试中,相比纯热门推荐,新用户7日留存率提升了11%。不是什么惊天大数字,但对于冷启动问题,这已经是很实质的改进了。
推荐理由生成:提升用户信任的关键
这是大模型最容易出效果的点,也是传统推荐系统普遍缺失的。
用户看到一个推荐,如果有一句精准的理由——"因为你之前买过XX护肤品牌,这款新品适合同类肤质"——点击率和转化率会显著提升。不是因为说服了用户,而是建立了信任感。
@Service
public class RecommendationReasonService {
@Autowired
private ChatClient chatClient;
/**
* 批量生成推荐理由(在线,但要控制时延)
*/
public Map<Long, String> batchGenerateReasons(
Long userId,
List<Product> recommendedProducts,
UserSemanticProfile userProfile) {
// 一次调用生成所有推荐理由,减少API调用次数
String productsStr = recommendedProducts.stream()
.map(p -> String.format("商品ID:%d,名称:%s,类目:%s,特点:%s",
p.getId(), p.getName(), p.getCategory(), p.getFeatures()))
.collect(Collectors.joining("\n"));
String prompt = """
请为以下推荐商品生成简短的推荐理由,基于用户画像定制。
【用户画像】
%s
【推荐商品列表】
%s
要求:
- 每条推荐理由15-25字
- 要结合用户偏好,有针对性,不要通用废话
- 语气自然,像朋友推荐,不要像广告
- 以JSON返回:{"商品ID": "推荐理由"}
示例好理由:「你常买运动类,这款跑鞋透气性好评很高」
示例差理由:「热销爆款,品质保证,值得购买」(太通用,不要这样)
""".formatted(
userProfile.getSemanticDescription(),
productsStr
);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
return parseReasonMap(response);
}
}有一个容易忽视的工程问题:生成推荐理由不能阻塞主推荐链路。我们的做法是:
- 主推荐链路不等推荐理由,先返回商品列表
- 前端展示商品后,异步请求推荐理由
- 推荐理由采用LRU缓存,相同用户+商品的组合缓存6小时
这样推荐理由的生成完全不影响主链路延迟。
大模型辅助的语义商品理解
传统推荐里,商品的表示依赖结构化属性(类目、品牌、价格区间)和用户行为学到的Embedding。但有些语义信息这两种方式都没法很好表达。
比如"日系小清新风格连衣裙"和"莫兰迪色系碎花衬衫",从类目上都是女装,行为上可能有交叉,但风格语义差异很大。
用大模型给商品生成语义描述,再做Embedding,可以更准确地捕获这种语义差异:
@Service
public class ProductSemanticEnrichmentService {
@Autowired
private ChatClient chatClient;
@Autowired
private EmbeddingModel embeddingModel;
/**
* 商品入库时生成语义丰富化信息(异步处理)
*/
@Async
public void enrichProduct(Long productId) {
Product product = productRepo.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
String prompt = """
请为以下商品生成语义丰富化描述,用于推荐系统的语义匹配:
商品名称:%s
商品类目:%s
商品属性:%s
商品描述:%s
请生成:
1. 风格标签(5个,如:日系、复古、极简、运动休闲等)
2. 适合场景(3个,如:通勤、约会、户外运动等)
3. 适合人群描述(50字以内,描述适合什么样的用户)
4. 与什么商品搭配好(品类层面,3个)
5. 语义摘要(100字以内,综合描述这个商品的卖点和适合人群)
以JSON格式返回。
""".formatted(
product.getName(),
product.getCategoryPath(),
formatAttributes(product.getAttributes()),
product.getDescription()
);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
ProductSemanticInfo semanticInfo = parseSemanticInfo(response);
// 生成语义向量
float[] semanticVector = embeddingModel.embed(semanticInfo.getSummary());
productSemanticRepo.save(ProductSemanticRecord.builder()
.productId(productId)
.styleLabels(semanticInfo.getStyleLabels())
.usageScenarios(semanticInfo.getUsageScenarios())
.targetAudience(semanticInfo.getTargetAudience())
.complementaryCategories(semanticInfo.getComplementaryCategories())
.summary(semanticInfo.getSummary())
.semanticVector(semanticVector)
.build());
}
}这个语义向量可以参与召回:用用户的偏好向量和商品的语义向量做相似度匹配。相比纯行为Embedding,对新上架商品和小众商品的召回效果明显更好。
搜索意图到推荐的衔接
用户在搜索页的行为,包含了极强的即时意图信号,但传统推荐系统对这个信号的利用很不充分。
我设计了一个"搜索意图增强推荐"的模块:
@Service
public class SearchIntentAwareRecommender {
@Autowired
private ChatClient chatClient;
/**
* 用户在搜索结果页,下方的推荐要结合搜索意图
*/
public List<Long> getSearchContextRecommendations(
Long userId,
String searchQuery,
List<Long> searchResultIds, // 搜索结果商品
int clickedResultIndex, // 用户点击了第几个结果
int topN) {
// 解析搜索意图
SearchIntent intent = parseSearchIntent(searchQuery);
// 根据意图扩展候选集
List<Long> candidates = expandCandidates(intent, searchResultIds, userId);
// 精排
return rankByContext(candidates, userId, intent, topN);
}
private SearchIntent parseSearchIntent(String query) {
String prompt = """
解析用户的搜索查询,提取购物意图信息:
搜索词:%s
请识别:
1. 核心需求(用户想买什么)
2. 风格偏好(如果有的话)
3. 功能需求(如果有的话)
4. 价位倾向(从搜索词判断,如"便宜"、"平价"、"高端"等)
5. 相关扩展需求(用户可能同时需要的配套商品)
以JSON格式返回。
""".formatted(query);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
return parseSearchIntentFromJson(response);
}
}这个模块让"搜索后推荐"的点击率提升了18%,因为推荐更贴近用户当前的购物场景了。
一些实战经验和避坑指南
1. 不要用大模型做实时精排
精排需要在100ms内处理几百个候选,大模型做不到。大模型的角色是离线预计算(生成向量、生成画像)和小规模在线增强(生成理由、处理冷启动)。
2. 向量要分类型存储
行为向量和语义向量要分开存,在召回时可以分别使用,不要混在一起。我们用两个独立的向量索引,按不同权重融合。
3. 推荐理由要防止低质量输出
大模型偶尔会生成"好评如潮,深受用户喜爱"这种废话理由。我加了个后处理检测,如果理由里没有出现任何用户画像关键词,就丢掉重新生成一次。
4. AB测试要控制变量
LLM推荐增强涉及多个组件,不要一起上线所有变化,要逐个测试效果,否则搞不清楚哪个在起作用。
5. 成本控制
批处理(用户画像更新、商品语义生成)用便宜的小模型,在线场景用贵的大模型。这样总成本能降低60%以上,效果差距不大。
大模型和传统推荐的结合,目前我觉得是个非常有价值的方向。但它不是银弹,传统推荐的工程基础必须打好,大模型才能真正发挥增强作用。
