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 层:
图一解读:推荐引擎位于共享库的 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)