HarmonyOS 推荐排序实战:中式美食首页怎么判断收藏、浏览和食材命中谁该靠前

HarmonyOS 推荐排序实战:中式美食首页怎么判断收藏、浏览和食材命中谁该靠前
推荐排序最怕的不是不够智能而是排出来以后没人说得清为什么。中式美食首页推荐如果一会儿按收藏排一会儿按最近浏览排一会儿又被购物清单食材拉走用户看到的就不是懂我而是怎么又乱了。所以我这次先不急着上复杂模型而是用本地样本把收藏、浏览、食材命中和分类偏好拆开先确认每一道菜靠前都有理由。图 1 - 中式美食首页推荐排序验证同一批候选菜在不同用户信号下会有不同排序关键是每个排序结果都要能解释。栏目内容应用名称中式美食工程环境HarmonyOS、ArkTS、ArkUI、Stage 模型本文主题首页推荐排序、用户行为信号、本地样本验证、排序解释核心文件HomeRecommendViewModel.ets、DishRepository.ets、FavoriteRepository.ets、ViewedRepository.ets、ShoppingListRepository.ets验收入口打开首页推荐流、切换用户样本、检查前五名排序原因读完能带走什么怎么把推荐排序从拍脑袋权重改成可解释、可回放、可调整的工程逻辑本章导读这次只解决一个问题中式美食首页推荐到底该怎么排才不会把用户意图排乱。收藏过的菜要不要永远排在最前面。最近浏览和购物清单食材冲突时谁更可信。推荐结果为什么要带explain不能只返回分数。本地样本怎么跑才能发现看起来合理但实际很怪的排序。验收时怎么判断推荐排序是真的能用而不是只在代码里能跑。当前验证环境推荐排序属于业务规则不是纯 UI所以我会把环境和数据边界先说清楚。这里讨论的是本地优先的推荐策略数据来自应用内行为不依赖服务端画像。项目说明技术栈HarmonyOS、ArkTS、ArkUI页面入口首页推荐流候选数据DishRepository提供的菜谱集合用户行为收藏、最近浏览、购物清单食材、分类点击验证方式固定样本、输出排序快照、人工检查前五名我现在不会把它包装成算法模型。项目这个阶段更需要的是可解释的规则出错能排查权重能调整后面用户数据变多再升级也不晚。一、首页推荐先解决别乱排首页推荐不是广告位也不是随机列表。用户打开中式美食首页通常有几种很具体的状态。用户状态可能的信号首页应该怎么回应昨天收藏了几道家常菜收藏列表家常菜可以靠前但不能一直霸屏最近看了鸡胸肉、减脂菜浏览历史高蛋白、低油菜谱可以更靠前购物清单里有番茄和鸡蛋食材命中能用现有食材做的菜值得提前刚点过早餐分类分类点击早餐方向可以加一点权重这里最容易偷懒的写法是给每个菜写一个固定score然后按分数倒序。这种写法上线很快但很快会遇到问题用户明明最近在看汤首页还一直推收藏过的炒菜或者用户购物清单里有番茄推荐却没把番茄相关菜提前。所以我把推荐分成两层候选菜从仓储来用户信号从行为仓储来排序服务只负责把两边合起来。exportinterfaceRecipeCandidate{recipeId:stringtitle:stringcategory:stringingredients:string[]tags:string[]}exportinterfaceRecommendSignal{favoriteRecipeIds:string[]recentlyViewedIds:string[]shoppingIngredients:string[]clickedCategories:string[]}这两个结构很普通但边界要清楚。候选菜描述有什么可推荐用户信号描述这个人最近可能想要什么。不要把信号塞进菜谱对象里否则后面调试会很麻烦。二、收藏是强信号但不能永远霸榜收藏表示用户明确喜欢它当然应该加分。但收藏也有副作用如果分数太高首页会长期被旧收藏占住新菜没有机会出现。functionscoreFavorite(recipe:RecipeCandidate,signal:RecommendSignal):number{returnsignal.favoriteRecipeIds.includes(recipe.recipeId)?36:0}我没有给收藏 100 分是因为收藏只能说明喜欢过不能说明现在就想吃。用户收藏红烧肉不代表每天都想吃红烧肉。首页推荐要照顾长期兴趣也要照顾当前场景。信号权重判断为什么收藏高但不封顶喜欢过可信最近浏览中等最近关注但可能只是随手看食材命中中高直接影响今天能不能做分类点击辅助场景偏好但不一定强这也是我不把收藏写成硬置顶的原因。硬置顶看起来尊重用户偏好实际会让推荐位越来越死。三、食材命中更接近今天能不能做美食应用和普通内容推荐不一样。用户不是只看内容他可能真的要下厨房。购物清单里的食材、最近搜索过的食材往往比单纯浏览更接近当下需求。functionscoreIngredient(recipe:RecipeCandidate,signal:RecommendSignal):number{consthitCountrecipe.ingredients.filter((item)signal.shoppingIngredients.includes(item)).lengthreturnMath.min(hitCount*14,42)}这里做了上限。否则一道菜命中很多食材就会把其他信号完全压掉。推荐排序最怕一路信号说了算因为用户行为本来就是混合的。命中情况加分解释命中 0 个食材0和当前购物清单无关命中 1 个食材14有一点相关命中 2 个食材28很可能能做命中 3 个以上42 封顶避免食材数量压过全部信号我会特别看番茄炒蛋这类菜。它可能收藏过也可能命中购物清单。如果它一直排第一要看是因为收藏还是因为食材命中而不是只看最终分数。四、最近浏览要轻一点但不能没有最近浏览的信号比较微妙。用户看过一道菜可能是真的感兴趣也可能只是误点。它比收藏弱但比完全无行为强。functionscoreViewed(recipe:RecipeCandidate,signal:RecommendSignal):number{constindexsignal.recentlyViewedIds.indexOf(recipe.recipeId)if(index0)return0returnMath.max(12-index*2,4)}这里按顺序做了衰减越近看过加分越高。这样能避免很久以前看过的菜一直影响首页。浏览位置加分最近第 1 个12最近第 2 个10最近第 3 个8更靠后最低 4这个规则很粗但够用。它至少能表达一个判断最近看过有参考价值但不能和收藏、食材命中抢主导权。五、排序结果必须带解释只返回score的推荐结果调试起来很痛苦。页面上看到一道菜排第一你不知道它是因为收藏、食材命中还是分类点击。后面用户反馈不准时也没法快速定位。exportinterfaceScoredRecipe{recipe:RecipeCandidate score:numberexplain:string[]}exportfunctionscoreRecipe(recipe:RecipeCandidate,signal:RecommendSignal):ScoredRecipe{constexplain:string[][]letscore0constfavoritescoreFavorite(recipe,signal)if(favorite0)explain.push(收藏命中)scorefavoriteconstingredientscoreIngredient(recipe,signal)if(ingredient0)explain.push(购物清单食材命中)scoreingredientconstviewedscoreViewed(recipe,signal)if(viewed0)explain.push(最近浏览)scoreviewedif(signal.clickedCategories.includes(recipe.category)){explain.push(分类偏好)score10}return{recipe,score,explain}}explain不是给用户看的最终文案而是给开发调试和后续产品文案准备的。先让工程上能解释后面才有机会把它变成“因为你最近关注鸡胸肉”这种用户可见提示。六、本地样本要看前五名不只看第一名推荐排序很容易被第一名骗过去。第一名看着对不代表整个列表体验就对。中式美食首页是流式浏览前五名甚至前十名都很重要。constsampleRecipes:RecipeCandidate[][{recipeId:tomato-egg,title:番茄炒蛋,category:家常菜,ingredients:[番茄,鸡蛋],tags:[快手]},{recipeId:egg-rice,title:鸡蛋炒饭,category:主食,ingredients:[米饭,鸡蛋],tags:[快手]},{recipeId:lotus-soup,title:莲藕排骨汤,category:汤羹,ingredients:[莲藕,排骨],tags:[滋补]},{recipeId:spicy-chicken,title:辣子鸡,category:下饭菜,ingredients:[鸡肉,辣椒],tags:[重口味]}]constsampleSignal:RecommendSignal{favoriteRecipeIds:[tomato-egg],recentlyViewedIds:[egg-rice,lotus-soup],shoppingIngredients:[鸡蛋,番茄],clickedCategories:[家常菜]}这个样本不是为了模拟所有用户而是为了验证一个最常见场景收藏、浏览和食材同时命中时排序有没有明显不合理。exportfunctionrankRecipes(recipes:RecipeCandidate[],signal:RecommendSignal):ScoredRecipe[]{returnrecipes.map((recipe)scoreRecipe(recipe,signal)).sort((a,b)b.score-a.score)}我会把结果打印成快照而不是只看页面。菜谱分数来源判断番茄炒蛋收藏、食材、分类排前面合理鸡蛋炒饭食材、最近浏览可以靠前但不应压过收藏强命中莲藕排骨汤最近浏览有相关性但不是当前最强辣子鸡无明显命中不应该排前七、为什么不直接做复杂推荐模型不是不能做而是现在还没到那个阶段。中式美食目前更需要把基础行为数据和页面反馈打稳。如果一上来接复杂模型问题会变成黑盒用户为什么看到这道菜开发者自己也说不清。方案优点当前问题固定热门排序简单稳定不看用户行为纯收藏优先尊重兴趣容易旧菜霸榜本地规则排序可解释、可回放需要持续调权重复杂模型上限高当前数据量和解释链路不足所以我现在选本地规则排序。它不炫但对当前项目更稳能上线、能解释、能迭代。工程验收记录推荐排序的验收不能只看有没有列表。我会按样本去验。验收项操作方式通过标准收藏命中样本里收藏番茄炒蛋番茄炒蛋进入前两名食材命中购物清单加入番茄、鸡蛋含这些食材的菜整体靠前最近浏览最近看过鸡蛋炒饭鸡蛋炒饭有加分但不硬置顶分类偏好点击家常菜分类家常菜有轻量加分解释输出查看explain前五名每一道都能解释来源多样性前五名检查分类不连续出现过多同类菜这张表才是这次推荐排序能不能继续往前做的依据。只要解释链路还不清楚就不要急着加更多规则。常见问题复盘1. 为什么不让收藏直接置顶收藏是强信号但不是当前意图。用户收藏过很多菜如果全部置顶首页会失去新鲜感也会挡住当前食材和最近浏览信号。2. 为什么推荐结果要有explain因为推荐出错时要排查。没有explain只能猜分数有了explain就能知道是收藏权重太高还是食材命中太强。3. 为什么不用服务端推荐当前中式美食还是本地行为为主先用本地规则更直接。后面数据量上来再把本地快照变成服务端样本也不迟。4. 为什么要看前五名首页推荐不是只展示第一道菜。前五名决定用户第一屏的感觉如果第一名对、后面四个乱体验仍然不好。本章小结中式美食首页推荐当前先用本地规则不急着做黑盒模型。收藏、浏览、食材和分类要拆开打分每一路信号都要能解释。推荐排序验收要看前五名快照不能只看第一名是否顺眼。