iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

Part 4|第 12/30 篇
今日要做的事: 把策略口號拆成 hard、soft、context IDs,存入 Firestore,並把 Preset 編譯成當日設定。
今天要解決的目的: 讓 DishFlow 的策略可以被保存、編譯與重跑;推薦與排名留到後面。

Day 9 的 Strategy 頁只有幾顆 chips,按下去還只是畫面狀態。

今天要補上資料層:按下「忙碌平日」後,系統到底得到哪些 ID?


今日任務卡

項目 內容
產出 Strategy 共用外殼、v1 ID 目錄、Preset、compileStrategies() 輸出契約
工具 Cloud Firestore;本地純函式或測試程式
不使用 Gemini、候選菜生成、Hard 過濾、Soft 排名
驗收 文件可讀回、ID 合法、Preset 不移除安全限制、compile 可重現

程式在 material/code/day11。這一篇只做到 store/compile,還沒有進入推薦與排名。


今天走的路

先把「高蛋白」「剩料優先」這類口號收成英文 ID,分成 hard、soft、context。接著寫 compileStrategies():忙碌平日可以少勾幾項,但 allergy 仍要合併進來。同一份輸入用 node test.js 跑兩次,JSON 要一樣。然後用同一個 Firebase 登入,把目錄和 preset_busy_weekday 寫進去。最後到 Firestore 對三處:strategies、strategy_presets、daily/2026-09-26 的 session。

https://ithelp.ithome.com.tw/upload/images/20260926/20121052OOOiOSHxsE.png

1. 問題:口號不能驗收

A 只有一句需求;B1 加上手邊想到的食材;B2 會貼完整當日 JSON。
若 C 仍然只存「想吃高蛋白、便宜、快速」,那麼四種方法最後只能靠模型文案判斷有沒有聽懂。

問題不在口號太短,而在它沒有穩定的機器語意:

「便宜」是超過就不合格,還是越便宜越好?
「少洗鍋」是偏好,還是多一個鍋就禁止?
「今天外食」是策略,還是資料流情境?

先把這三種責任拆開,Day 12、Day 13 才能分別處理門禁與排序。


2. 設計:同一外殼,三種執行責任

2.1 Strategy 共用外殼

每份文件都用英文 snake_case ID,UI 再顯示繁體中文:

{
  "id": "leftover_first",
  "type": "soft_preference",
  "enabled": true,
  "scope": "user_default",
  "weight": 0.8,
  "params": {
    "prefer_priority": "use_soon"
  },
  "ui": {
    "label_zh": "剩料優先",
    "group": "cooking"
  }
}

三種類型只回答三件事:

type 意義 今天 compile 到哪裡
hard_constraint 違反就不合格 hard_constraint_ids
soft_preference 合格方案之間調順序 soft_preferences
context_module 描述今天的環境 context_modules

weight 只對 soft 有效。
hard 與 context 即使保留相同外殼,也不能因為 weight 較低就被忽略。

2.2 v1 ID 目錄

今天先固定名稱,不加入運動或精細營養功能:

Hard
  allergy
  forbidden_foods
  diet_pattern
  equipment
  budget_cap
  time_cap

Soft
  leftover_first
  prefer_protein
  prefer_non_fried
  minimize_dishes
  meal_balance_recent

Context
  food_source
  eating_window_16_8

prefer_protein 只讀粗 tags,例如 protein_present,不計克數、不變成營養處方。
meal_balance_recent 只回應既有 today_meals tags,不在本篇增加新營養資料。

設備固定從 profile 讀:

{
  "equipment": [
    "induction_cooktop",
    "deep_wok",
    "tamagoyaki_pan",
    "microwave"
  ],
  "burners": 1,
  "max_pots_willing": 2
}

這裡有一個容易混淆的地方:burners=1 是設備限制,屬於 hard;max_pots_willing=2 是少洗鍋偏好,屬於 soft。

2.3 Preset 只組合,不覆蓋安全

「忙碌平日」不是第四種 Strategy:

{
  "id": "preset_busy_weekday",
  "label_zh": "忙碌平日",
  "applies": {
    "active_strategy_ids": [
      "equipment",
      "budget_cap",
      "time_cap",
      "leftover_first",
      "minimize_dishes"
    ],
    "session_overrides": {
      "food_source": "home"
    }
  }
}

這份內容已寫進 strategy_presets/preset_busy_weekday。清單裡沒有 allergy。編譯時仍會補上 allergy、forbidden_foods、diet_pattern。Preset 沒有刪掉它們的權限。


3. 實作/實跑:存定義,再編譯當日狀態

Firestore 路徑固定為:

users/{uid}/strategies/{strategyId}
users/{uid}/strategy_presets/{presetId}
users/{uid}/daily/{yyyy-mm-dd}.session.active_strategy_ids
users/{uid}/daily/{yyyy-mm-dd}.session.strategy_param_overrides

今天寫進 Firestore 的是英文 ID。Day 10 儲存的仍是中文 strategy_chips,尚未沿用今天這套 ID。從 Day 11 開始,中文只放在 ui.label_zh;即使標籤改名,compile 對到的仍是同一個 ID。

編譯順序定成純函式:

function compileStrategies(profile, catalog, preset, dailySession) {
  const safetyIds = ["allergy", "forbidden_foods", "diet_pattern", "equipment"];
  const activeIds = union(
    safetyIds,
    preset?.applies.active_strategy_ids ?? [],
    dailySession.active_strategy_ids ?? []
  );

  return partitionAndApplyOverrides(activeIds, catalog, dailySession);
}

這個純函式只做四件事:

  1. 合併不可被 Preset 移除的安全 IDs。
  2. 拒絕不存在或 type 不合法的 ID。
  3. 套用當日參數覆寫,但不改回 catalog。
  4. 依 hard、soft、context 分組輸出。

在 material/code/day11 執行 node test.js,六項測試都通過。同一份輸入印了兩次,兩份 JSON 相同。

https://ithelp.ithome.com.tw/upload/images/20260926/20121052uUR0enl6mP.png

https://ithelp.ithome.com.tw/upload/images/20260926/20121052BXYKzqPwIZ.png

實際輸出是:

{
  "hard_constraint_ids": [
    "allergy",
    "forbidden_foods",
    "diet_pattern",
    "equipment",
    "budget_cap",
    "time_cap"
  ],
  "soft_preferences": [
    {
      "id": "leftover_first",
      "weight": 0.8,
      "params": { "prefer_priority": "use_soon" }
    },
    {
      "id": "minimize_dishes",
      "weight": 0.6,
      "params": { "max_pots_willing": 2 }
    }
  ],
  "context_modules": [
    { "id": "food_source", "params": { "source": "home" } }
  ]
}

hard_constraint_ids 前四個是安全 ID。Preset 沒有勾 allergy,編譯仍把它留著。leftover_first 同時帶目錄裡的 prefer_priority。

接著用同一個 Firebase 專案開 http://localhost:3001,Google 登入後按「寫入 Firestore」。這頁只寫策略目錄、preset_busy_weekday,以及今天的 session。狀態列顯示已寫入 strategies、preset_busy_weekday、daily/2026-09-26;程式沒有更新 pantry。

https://ithelp.ithome.com.tw/upload/images/20260926/2012105207w7x1Uoup.png

Console 裡,strategies 的文件名就是這些 ID。打開 allergy 後,可以看到 type 是 hard_constraint,ui.label_zh 是「過敏」。

https://ithelp.ithome.com.tw/upload/images/20260926/20121052DbSt7B3Pot.jpg

equipment 的 params.burners 是 1,類型仍是 hard。這對上「爐口數是硬限制」。

https://ithelp.ithome.com.tw/upload/images/20260926/20121052QvTUQCUitG.jpg

preset_busy_weekday 的 active_strategy_ids 是 equipment、budget_cap、time_cap、leftover_first、minimize_dishes。裡面沒有 allergy。session_overrides.food_source 是 home,中文標籤是「忙碌平日」。

https://ithelp.ithome.com.tw/upload/images/20260926/20121052Uqrlp9Rv2g.jpg

daily/2026-09-26 的 session.active_strategy_ids 是 leftover_first、minimize_dishes、food_source。strategy_param_overrides 裡,food_source.source 是 home,minimize_dishes.max_pots_willing 是 2。preset_id 與文件上的 strategy_preset 都是 preset_busy_weekday。這三個 ID 是當天勾選的項目;allergy 不寫在這份清單裡,它是編譯時從安全 ID 補上去的。

https://ithelp.ithome.com.tw/upload/images/20260926/201210521OIVpbbRej.jpg

3.1 回扣 Eat-Cost Balance

拆成 Strategy ID 之後,Eat-Cost Balance 各自有了資料入口:

Eat      → today_meals + food_source context
Cost     → budget_cap + time_cap + leftover_first
Balance  → meal_balance_recent + prefer_non_fried + prefer_protein

這不是一個總分公式。
Eat 提供已吃事實,Cost 定義做不做得到,Balance 只在安全集合內調方向。


4. 驗證:今天只驗資料與 compiler

這次測試到 compile 為止:

測試 預期
重複 ID 拒絕載入
未知 type 拒絕 compile
Preset 未列 allergy compiler 仍從 safety IDs 合併
當日修改 soft weight 只改輸出,不改 catalog
中文 label 改名 ID 與 compile 結果不變
同一輸入跑兩次 深度相等

本地測試與 Firestore Console 檢查結果:

firestore_write_and_console_readback = PASS
  users/{uid}/strategies/{strategyId}
  users/{uid}/strategy_presets/preset_busy_weekday
  daily/2026-09-26 的 session.active_strategy_ids 已讀回
invalid_catalog_rejected = PASS
  重複 ID:duplicate id: allergy
  未知 type:unknown type: slogan
preset_keeps_safety = PASS
  preset 未列 allergy,hard_constraint_ids 仍含四個安全 ID
compile_deterministic = PASS
  同一輸入兩次 JSON 深度相等

推薦是否安全、哪道菜排第一、hard_ok 和 Gemini 是否遵守策略,都還沒有測。這些要等 Rule Engine 與候選菜接上後才有答案。


5. 今日結論

今天的結論是:策略要能被保存、組合與重現,才不會只停在畫面上的一句話。

  • hard、soft、context 共用外殼,但執行責任不能混。
  • Preset 只改當日組合,不能清掉 profile 的安全限制。
  • 今天止於 store/compile;沒有推薦引擎結果,也不提前宣布 C 勝過 A、B1、B2。

下一篇: 只做過敏與設備的雙重門禁,驗蝦、花枝排、客人過敏與共用鍋具。


附錄 A:v1 Strategy 設定

hard_constraint:
  allergy:
    source: profile.hard_constraints.allergens + daily.session.guest_allergens
  forbidden_foods:
    source: profile.hard_constraints.forbidden_ingredients
  diet_pattern:
    source: profile.hard_constraints.diet_tags
  equipment:
    equipment:
      - induction_cooktop
      - deep_wok
      - tamagoyaki_pan
      - microwave
    burners: 1
  budget_cap:
    params: max_twd
  time_cap:
    params: max_minutes

soft_preference:
  leftover_first:
    params: prefer_priority=use_soon
  prefer_protein:
    evidence_tag: protein_present
  prefer_non_fried:
    evidence_tag: non_fried
  minimize_dishes:
    params: max_pots_willing=2
  meal_balance_recent:
    params: window_days in [3, 5, 7]

context_module:
  food_source:
    params: source in [home, convenience_store, supermarket, eat_out]
  eating_window_16_8:
    params: window_start, window_end, user_declared=true

附錄 B:compile fixture

{
  "profile_safety_ids": ["allergy", "forbidden_foods", "diet_pattern", "equipment"],
  "preset_id": "preset_busy_weekday",
  "daily_session": {
    "active_strategy_ids": ["leftover_first", "minimize_dishes", "food_source"],
    "strategy_param_overrides": {
      "food_source": { "source": "home" },
      "minimize_dishes": { "max_pots_willing": 2 }
    }
  },
  "assertions": [
    "safety_ids_cannot_be_removed",
    "unknown_ids_are_rejected",
    "catalog_is_not_mutated",
    "same_input_same_output"
  ]
}

上一篇
[Day 10] 先讓資料閉環,再讓推薦變聰明:Stitch MCP 接到 Antigravity
系列文
DishFlow AI Agent:用 Google AI 打造 Eat-Cost Balance 的下一餐決策系統 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言