☰
HarmonyOS 推荐排序快照实战:中式美食如何用 score 和 explain 回放首页推荐问题
2026/10/12 4:06:33 网站建设 项目流程

推荐排序一旦上线,最怕用户说“不准”,开发者却只能凭感觉猜。中式美食首页推荐会同时参考收藏、浏览、购物清单食材和分类点击,如果不留快照,后面根本说不清某一道菜为什么排在前面。我的处理方式很直接:每次排序都能导出一份score + explain快照,先让问题能回放,再谈怎么调权重。

图 1 - 中式美食推荐排序快照:重点不是展示一个分数,而是把输入、候选和解释串起来,方便排查。

栏目内容
应用名称中式美食
工程环境HarmonyOS、ArkTS、ArkUI、Stage 模型
本文主题推荐排序快照、score/explain、问题回放、权重调试
核心文件HomeRecommendViewModel.ets、RecommendRanker.ets、DishRepository.ets、FavoriteRepository.ets
验收入口构造固定用户信号,输出前五名快照,检查排序原因
读完能带走什么推荐问题怎么从“感觉不准”变成可复现、可讨论、可修正

本章导读

这次只讲推荐排序的排错方式,不重复讲完整首页推荐流。

  • 为什么推荐排序要保存输入信号。
  • 为什么score不能单独看,必须带explain。
  • 快照里要记录哪些字段,才能回放问题。
  • 权重调整以后,怎么对比前后结果。
  • 验收时怎么判断快照真的能帮开发排错。

当前验证环境

本文讨论的是中式美食本地推荐规则,不依赖服务端推荐系统。排序输入来自本地仓储,输出给首页推荐流使用。

项目说明
技术栈HarmonyOS、ArkTS、ArkUI
主要入口首页推荐流
信号来源收藏、最近浏览、购物清单食材、分类点击
调试方式固定样本 + 快照输出
验收重点同一份输入能不能稳定复现同一份排序

一、推荐问题不能靠感觉排查

如果首页推荐里番茄炒蛋排第一,开发者至少要回答三个问题。

问题没有快照时有快照时
为什么排第一只能猜看命中了哪些信号
是哪路权重太高很难判断看每路分数
调权后有没有变好靠肉眼比较对比前后快照
用户反馈能不能复现很难用同一份输入重跑

所以我不把推荐排序当成一个只返回数组的函数。它必须能把排序过程讲出来。

二、快照先记录输入信号

输入信号是推荐排序的起点。没有输入,就没法解释输出。

exportinterfaceRecommendSignalSnapshot{favoriteRecipeIds:string[]recentlyViewedIds:string[]shoppingIngredients:string[]clickedCategories:string[]generatedAt:number}

这里我会保留generatedAt,不是为了做复杂统计,而是为了排查“这个推荐结果是什么时候算出来的”。如果用户刚刚收藏了菜,但快照时间在收藏之前,那排序没变化就不是算法问题。

三、候选菜也要入快照

只保存用户信号不够。候选池如果漏菜,排序再正确也没用。

exportinterfaceCandidateSnapshot{recipeId:stringtitle:stringcategory:stringingredients:string[]}exportinterfaceRecommendDebugSnapshot{signal:RecommendSignalSnapshot candidates:CandidateSnapshot[]result:RankedSnapshotItem[]}

候选池快照能帮我排除一类问题:不是排序没把某道菜排上来,而是这道菜根本没有进入候选集合。

异常现象优先检查
明明有番茄却没推荐番茄菜候选池有没有番茄相关菜
收藏菜没有出现收藏 id 是否能在候选池找到
同类菜太多候选池是不是本身就偏一类
推荐结果为空仓储查询是否返回空集合

四、score要拆开,不要只留总分

总分只能告诉我谁高谁低,不能告诉我为什么高。

exportinterfaceScorePart{source:'favorite'|'viewed'|'ingredient'|'category'score:numberreason:string}exportinterfaceRankedSnapshotItem{recipeId:stringtitle:stringtotalScore:numberparts:ScorePart[]}

这样排查时就能看到每一道菜的分数组成。

菜谱favoriteviewedingredientcategorytotal
番茄炒蛋360281074
鸡蛋炒饭01214026
莲藕排骨汤0100010

这张表比一个总分有用得多。比如番茄炒蛋排第一,原因很清楚:收藏、食材、分类同时命中;鸡蛋炒饭靠前,是因为最近浏览和鸡蛋命中。

五、生成快照时不要改排序逻辑

快照只是记录,不应该影响排序结果。排序函数先算结果,再把结果转换成快照。

exportfunctionbuildRecommendSnapshot(signal:RecommendSignalSnapshot,candidates:RecipeCandidate[],ranked:ScoredRecipe[]):RecommendDebugSnapshot{return{signal,candidates:candidates.map((item)=>({recipeId:item.recipeId,title:item.title,category:item.category,ingredients:item.ingredients})),result:ranked.map((item)=>({recipeId:item.recipe.recipeId,title:item.recipe.title,totalScore:item.score,parts:item.parts}))}}

这里不要为了调试去改原排序逻辑。调试代码一旦改变业务结果,后面看到的就不是同一个问题了。

六、调权重要对比前后快照

调权重不能靠“感觉更顺眼”。我会把同一份输入跑两遍:一遍旧权重,一遍新权重。

exportinterfaceSnapshotDiffItem{recipeId:stringbeforeRank:numberafterRank:numberchangedReason:string}exportfunctiondiffSnapshot(before:RankedSnapshotItem[],after:RankedSnapshotItem[]):SnapshotDiffItem[]{returnafter.map((item,index)=>{constoldIndex=before.findIndex((old)=>old.recipeId===item.recipeId)return{recipeId:item.recipeId,beforeRank:oldIndex<0?-1:oldIndex+1,afterRank:index+1,changedReason:item.parts.map((part)=>part.reason).join('、')}})}

我重点看两类变化。

变化判断
强命中菜上升通常合理
无命中菜上升要警惕
同类菜连续霸榜需要加多样性约束
收藏菜全部压前收藏权重可能太高

工程验收记录

推荐快照的验收不看页面好不好看,只看能不能排查问题。

验收项操作方式通过标准
输入可复现固定收藏、浏览、食材样本多次运行输出一致
候选可检查打印候选池能确认目标菜是否进入候选
分数可解释查看parts每个总分都有来源
调权可对比调整食材权重后重跑排名变化能解释
异常可定位构造空候选池能看出是仓储问题,不误判为排序问题

这套验收的目的很朴素:推荐出了问题,先知道问题在哪里,而不是先猜怎么改。

常见问题复盘

1. 为什么不只保存总分?

总分无法排错。看到 74 分,只知道它高;看到收藏 36、食材 28、分类 10,才知道它为什么高。

2. 为什么候选池也要保存?

因为很多推荐问题不是排序问题,而是候选池问题。菜没进候选,再好的排序也排不出来。

3. 快照会不会影响性能?

线上不需要每次都持久保存完整快照。开发阶段可以输出,必要时只保留前几名和关键字段。

4. 为什么先做本地快照,不先接服务端日志?

当前中式美食的推荐规则还在本地验证阶段,本地快照更快、更直接。等规则稳定后,再考虑把关键字段上报或接服务端分析。

本章小结

  • 推荐排序不能只看结果数组,必须能回放输入、候选、分数和解释。
  • score要拆成多个来源,排查时才知道哪路信号影响了排序。
  • 调权重要对比前后快照,不要靠肉眼觉得顺眼。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询