欢迎光临
我们一直在努力

HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(番外篇):【AI 推荐】场景优先的智能推荐引擎——从“偏好不可靠“到“食材即真理“

HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(番外篇):【AI 推荐】场景优先的智能推荐引擎——从"偏好不可靠"到"食材即真理"

摘要:上一篇我们为《灵犀厨房》接入了 AI 食材识别,用户拍照即可识别冰箱里的蔬菜肉类。但推荐结果却让人哭笑不得:明明识别出"西兰花、白菜",推荐列表却全是"青椒肉丝、脆皮五花肉、酱骨架"——用户偏好标签"高蛋白"完全绑架了推荐逻辑!本篇,我们将重构推荐引擎的核心算法,建立"场景优先,偏好辅助"的评分体系,通过食材分类表、类型一致性检查、偏好冲突检测三大机制,让推荐结果真正贴合用户的实际烹饪场景。


一、引言:当 AI 识别遇上用户偏好

经过前 19 篇的积累,《灵犀厨房》已经掌握了语音播报、声控操作、通知提醒、元服务直达、桌面卡片等核心能力。但在 AI 推荐这个"招牌功能"上,却存在一个致命缺陷:

场景用户操作识别结果推荐结果问题
冰箱有蔬菜 拍照识别 西兰花、白菜 青椒肉丝、脆皮五花肉 ❌ 偏好"高蛋白"绑架推荐
冰箱有肉类 拍照识别 猪肉、鸡肉 蒜蓉西兰花、醋溜白菜 ❌ 偏好"快手菜"误导推荐
用户改偏好 切换到"素食" 西兰花、白菜 蒜蓉西兰花、香菇青菜 ✅ 但用户可能只是试试

问题根源:旧版推荐引擎的评分权重中,偏好标签权重(30分)高于食材匹配权重(20分),导致用户随意修改的偏好标签完全主导了推荐结果,而真正反映用户意图的食材识别结果反而成了"配角"。


二、核心原理:场景优先的评分体系

2.1 权重重构:食材为王

// —- 评分权重常量(新版)—-
const SCORE_INGREDIENT_BASE = 50; // 食材匹配基础权重(最高)
const SCORE_INGREDIENT_FULL = 30; // 全匹配奖励
const SCORE_PREF_TAG = 20; // 偏好标签基础权重(降级)
const SCORE_PREF_CONFLICT = 25; // 偏好冲突惩罚
const SCORE_SEASON = 15; // 季节权重
const SCORE_TYPE_MISMATCH = 30; // 类型不一致惩罚
const RECENT_PENALTY = 20; // 历史惩罚

权重对比:

维度旧版权重新版权重变化
食材匹配 20 50 ⬆️ +150%
全匹配奖励 30 🆕 新增
偏好标签 30 20 ⬇️ -33%
类型不一致 -30 🆕 新增
偏好冲突 -25 🆕 新增

2.2 食材分类表

建立 7 大类食材分类表,用于识别食材类型和检测类型一致性:

const FOOD_CATEGORIES = new Map<string, string[]>([
['vegetables', ['西兰花', '白菜', '菠菜', '生菜', '番茄', '黄瓜', '茄子', '土豆', '胡萝卜', '洋葱', '蒜', '姜', '青椒', '豆芽', '蘑菇', '香菇', '青菜', '芹菜', '韭菜', '豆角']],
['meat', ['猪肉', '牛肉', '鸡肉', '羊肉', '鸭肉', '排骨', '里脊', '五花肉', '肘子', '骨架', '肉末', '肉丝', '肉片', '鸡块', '鸡翅', '鸡腿']],
['seafood', ['鱼', '虾', '蟹', '龙虾', '鱿鱼', '扇贝', '带鱼', '鲈鱼', '鲫鱼', '鲤鱼']],
['eggs', ['鸡蛋', '鸭蛋', '鹌鹑蛋', '蛋']],
['dairy', ['牛奶', '奶酪', '黄油', '奶油']],
['staples', ['米饭', '面条', '面包', '饺子', '包子', '馒头', '饼', '粥', '粉']],
['fruits', ['苹果', '香蕉', '橙子', '柠檬', '草莓', '葡萄', '西瓜', '梨', '桃', '菠萝']]
]);

2.3 偏好冲突检测

定义偏好标签与食材类型的冲突关系:

const PREFERENCE_CONFLICTS = new Map<string, string[]>([
['高蛋白', ['vegetables', 'fruits']], // 高蛋白不适合纯蔬菜/水果场景
['低脂', ['meat']], // 低脂不适合纯肉类场景
['素食', ['meat', 'seafood']], // 素食不适合肉类/海鲜场景
['清淡', ['meat']], // 清淡不适合重口味肉类场景
]);


三、分层架构:推荐引擎在 HSP 中的位置

按照《灵犀厨房》四层架构,推荐引擎位于共享库(HSP)的 Business 层:

#mermaid-svg-XMa3qqaK33Y1EzGH{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XMa3qqaK33Y1EzGH .error-icon{fill:#552222;}#mermaid-svg-XMa3qqaK33Y1EzGH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XMa3qqaK33Y1EzGH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XMa3qqaK33Y1EzGH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XMa3qqaK33Y1EzGH .marker.cross{stroke:#333333;}#mermaid-svg-XMa3qqaK33Y1EzGH svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XMa3qqaK33Y1EzGH p{margin:0;}#mermaid-svg-XMa3qqaK33Y1EzGH .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-XMa3qqaK33Y1EzGH .cluster-label text{fill:#333;}#mermaid-svg-XMa3qqaK33Y1EzGH .cluster-label span{color:#333;}#mermaid-svg-XMa3qqaK33Y1EzGH .cluster-label span p{background-color:transparent;}#mermaid-svg-XMa3qqaK33Y1EzGH .label text,#mermaid-svg-XMa3qqaK33Y1EzGH span{fill:#333;color:#333;}#mermaid-svg-XMa3qqaK33Y1EzGH .node rect,#mermaid-svg-XMa3qqaK33Y1EzGH .node circle,#mermaid-svg-XMa3qqaK33Y1EzGH .node ellipse,#mermaid-svg-XMa3qqaK33Y1EzGH .node polygon,#mermaid-svg-XMa3qqaK33Y1EzGH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XMa3qqaK33Y1EzGH .rough-node .label text,#mermaid-svg-XMa3qqaK33Y1EzGH .node .label text,#mermaid-svg-XMa3qqaK33Y1EzGH .image-shape .label,#mermaid-svg-XMa3qqaK33Y1EzGH .icon-shape .label{text-anchor:middle;}#mermaid-svg-XMa3qqaK33Y1EzGH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XMa3qqaK33Y1EzGH .rough-node .label,#mermaid-svg-XMa3qqaK33Y1EzGH .node .label,#mermaid-svg-XMa3qqaK33Y1EzGH .image-shape .label,#mermaid-svg-XMa3qqaK33Y1EzGH .icon-shape .label{text-align:center;}#mermaid-svg-XMa3qqaK33Y1EzGH .node.clickable{cursor:pointer;}#mermaid-svg-XMa3qqaK33Y1EzGH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XMa3qqaK33Y1EzGH .arrowheadPath{fill:#333333;}#mermaid-svg-XMa3qqaK33Y1EzGH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XMa3qqaK33Y1EzGH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XMa3qqaK33Y1EzGH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XMa3qqaK33Y1EzGH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XMa3qqaK33Y1EzGH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XMa3qqaK33Y1EzGH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XMa3qqaK33Y1EzGH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XMa3qqaK33Y1EzGH .cluster text{fill:#333;}#mermaid-svg-XMa3qqaK33Y1EzGH .cluster span{color:#333;}#mermaid-svg-XMa3qqaK33Y1EzGH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XMa3qqaK33Y1EzGH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XMa3qqaK33Y1EzGH rect.text{fill:none;stroke-width:0;}#mermaid-svg-XMa3qqaK33Y1EzGH .icon-shape,#mermaid-svg-XMa3qqaK33Y1EzGH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XMa3qqaK33Y1EzGH .icon-shape p,#mermaid-svg-XMa3qqaK33Y1EzGH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XMa3qqaK33Y1EzGH .icon-shape .label rect,#mermaid-svg-XMa3qqaK33Y1EzGH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XMa3qqaK33Y1EzGH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XMa3qqaK33Y1EzGH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XMa3qqaK33Y1EzGH :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

📊 数据源

📦 共享库(Shared HSP)

📱 主应用(Entry HAP)

🏛️ Foundation 层

🧠 Business 层

import from 'shared'

import

读取

读取

查询

HomeViewModel.etsUI 状态管理

HomeTabContent.ets首页 UI

RecommendEngine.ets核心推荐算法场景优先评分

Recipe.ets菜谱模型

UserPreference.ets用户偏好模型

RelationalStore菜谱数据库

图一解读:推荐引擎位于共享库的 Business 层,主应用通过 import { recommendEngine } from 'shared' 导入。这种架构确保主应用和元服务使用同一套推荐逻辑,避免代码重复和不一致问题。


四、关键实现步骤

Step 1:食材类型识别

/**
* 识别食材类型集合
* @returns 返回食材类型集合(如 {'vegetables', 'meat'})
*/

private identifyIngredientTypes(ingredients: string[]): Set<string> {
const types = new Set<string>();
for (const ing of ingredients) {
FOOD_CATEGORIES.forEach((list: string[], type: string) => {
if (list.some((item: string) => ing.includes(item) || item.includes(ing))) {
types.add(type);
}
});
}
return types;
}

Step 2:菜谱主要类型识别

/**
* 识别菜谱的主要食材类型
* @returns 返回主要类型(如 'meat'、'vegetables')
*/

private getRecipeMainType(recipe: Recipe): string | null {
const typeScores = new Map<string, number>();

for (const ing of recipe.ingredients) {
FOOD_CATEGORIES.forEach((list: string[], type: string) => {
if (list.some((item: string) => ing.includes(item) || item.includes(ing))) {
const currentScore = typeScores.get(type) ?? 0;
typeScores.set(type, currentScore + 1);
}
});
}

// 返回得分最高的类型
let maxScore = 0;
let mainType: string | null = null;
typeScores.forEach((score: number, type: string) => {
if (score > maxScore) {
maxScore = score;
mainType = type;
}
});
return mainType;
}

Step 3:偏好冲突检测

/**
* 检查偏好标签与食材类型是否冲突
*/

private isPreferenceConflict(tag: string, ingredientTypes: Set<string>): boolean {
const conflicts = PREFERENCE_CONFLICTS.get(tag);
if (!conflicts) return false;

// 如果识别的食材类型全部在冲突列表中,则判定为冲突
for (const type of ingredientTypes) {
if (!conflicts.includes(type)) {
return false; // 有不冲突的类型
}
}
return ingredientTypes.size > 0; // 只有当有识别食材时才判定冲突
}

Step 4:核心评分逻辑

private calcScore(recipe: Recipe, pref: UserPreference, ingredients: string[]): number {
let score = 0;
const debugParts: string[] = [];

// 1. 食材匹配评分(场景核心,权重最高)
if (ingredients && ingredients.length > 0) {
const matchCount = ingredients.filter((ing: string) =>
recipe.ingredients.some((ri: string) => ri.includes(ing) || ing.includes(ri))
).length;

if (matchCount > 0) {
const ingredientScore = matchCount * SCORE_INGREDIENT_BASE;
score += ingredientScore;
debugParts.push(`食材匹配+${ingredientScore}(${matchCount}/${ingredients.length})`);

// 全匹配奖励
if (matchCount === ingredients.length) {
score += SCORE_INGREDIENT_FULL;
debugParts.push(`全匹配奖励+${SCORE_INGREDIENT_FULL}`);
}
}
}

// 2. 食材类型一致性检查
if (ingredients && ingredients.length > 0) {
const ingredientTypes = this.identifyIngredientTypes(ingredients);
const recipeMainType = this.getRecipeMainType(recipe);

// 如果识别的是蔬菜/水果,但菜谱主要是肉类/海鲜,惩罚
if ((ingredientTypes.has('vegetables') || ingredientTypes.has('fruits')) &&
(recipeMainType === 'meat' || recipeMainType === 'seafood')) {
score += SCORE_TYPE_MISMATCH;
debugParts.push(`类型不一致${SCORE_TYPE_MISMATCH}(蔬菜→肉类)`);
}

// 如果识别的是肉类,但菜谱主要是蔬菜,轻微惩罚
if (ingredientTypes.has('meat') && recipeMainType === 'vegetables') {
score += Math.floor(SCORE_TYPE_MISMATCH / 2);
debugParts.push(`类型不一致${Math.floor(SCORE_TYPE_MISMATCH / 2)}(肉类→蔬菜)`);
}
}

// 3. 偏好标签评分(动态权重)
const ingredientTypes = ingredients && ingredients.length > 0
? this.identifyIngredientTypes(ingredients)
: new Set<string>();

for (const tag of pref.favoriteTags) {
if (recipe.tags?.includes(tag)) {
// 检查偏好标签与食材类型是否冲突
if (this.isPreferenceConflict(tag, ingredientTypes)) {
score += SCORE_PREF_CONFLICT;
debugParts.push(`偏好冲突${SCORE_PREF_CONFLICT}(${tag})`);
} else {
score += SCORE_PREF_TAG;
debugParts.push(`偏好匹配+${SCORE_PREF_TAG}(${tag})`);
}
}
}

// 4. 季节匹配评分
const season = this.getSeason();
if (recipe.seasonTags?.includes(season)) {
score += SCORE_SEASON;
debugParts.push(`季节匹配+${SCORE_SEASON}(${season})`);
}

// 5. 历史惩罚
if (this.recentIds.has(recipe.id)) {
score += RECENT_PENALTY;
debugParts.push(`历史惩罚${RECENT_PENALTY}`);
}

return score;
}


五、评分示例:场景对比

场景 1:识别蔬菜(西兰花、白菜)

菜谱食材匹配类型检查偏好匹配总分排名
蒜蓉西兰花 +50(1/2) +20(快手菜) 70 🥇
醋溜白菜 +50(1/2) +20(快手菜) 70 🥈
香菇青菜 +20(快手菜) 20 🥉
青椒肉丝 -30(蔬菜→肉类) +20(快手菜)-25(高蛋白冲突) -35
脆皮五花肉 -30(蔬菜→肉类) +20(高蛋白) -10

场景 2:识别肉类(猪肉、鸡肉)

菜谱食材匹配类型检查偏好匹配总分排名
红烧肉 +100(2/2)+30(全匹配) +20(高蛋白) 150 🥇
青椒肉丝 +50(1/2) +20(快手菜)+20(高蛋白) 90 🥈
口水鸡 +50(1/2) +20(高蛋白) 70 🥉
蒜蓉西兰花 -15(肉类→蔬菜) -15

六、代码交付清单

文件修改职责
shared/src/main/ets/business/RecommendEngine.ets 重构 场景优先评分、食材分类、类型检查、冲突检测
shared/Index.ets 保持 导出 RecommendEngine 和 recommendEngine
entry/src/main/ets/viewmodel/HomeViewModel.ets 修改 从 shared 导入推荐引擎
entry/src/main/ets/business/RecommendEngine.ets 删除 移除主应用中的重复实现

七、架构优化:统一推荐引擎

7.1 问题发现

原架构中存在推荐引擎重复实现:

entry/src/main/ets/business/RecommendEngine.ets ← 主应用版本
shared/src/main/ets/business/RecommendEngine.ets ← 共享库版本

主应用导入的是自己的版本,导致共享库中的优化无法生效。

7.2 修复方案

// 修复前:主应用导入自己的推荐引擎
import { recommendEngine } from '../business/RecommendEngine';

// 修复后:主应用导入共享库的推荐引擎
import { recommendEngine } from 'shared';


八、设计决策

决策选择理由
食材匹配权重 50 分(最高) 食材是用户意图的最直接表达,应作为推荐的核心依据
偏好标签权重 20 分(降级) 用户偏好可能随意修改,不应主导推荐结果
类型不一致惩罚 -30 分 识别蔬菜却推荐肉类,严重违背用户意图
偏好冲突惩罚 -25 分 "高蛋白"偏好与蔬菜场景冲突时,降低偏好权重
推荐引擎位置 Shared HSP 主应用和元服务共用一套逻辑,避免重复和不一致

九、总结与下篇预告

本篇我们重构了《灵犀厨房》的推荐引擎,建立了"场景优先,偏好辅助"的评分体系。通过食材分类表、类型一致性检查、偏好冲突检测三大机制,让推荐结果真正贴合用户的实际烹饪场景。

核心成就:

  • 食材匹配权重提升至 50 分,成为推荐的核心依据
  • 类型一致性检查,避免"识别蔬菜却推荐肉类"的尴尬
  • 偏好冲突检测,当偏好与场景冲突时自动降权
  • 统一到 Shared HSP,主应用和元服务共用一套逻辑

📚 本系列持续更新中:敬请关注。

🔗 专栏入口:[《HarmonyOS6.1全场景实战》合集] 📦 获取基线版本源码包:包括第1-15篇所有代码 + 架构文档 + Flask 后端 **如果你发现本文还有任何不严谨之处,欢迎随时指出,我们一起共建最优质的 HarmonyOS 6.1 学习内容!如果觉得有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬! 纯血鸿蒙,用心造厨。我们下一篇见!

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(番外篇):【AI 推荐】场景优先的智能推荐引擎——从“偏好不可靠“到“食材即真理“
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址