iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

Part 2|第 3/30 篇
今日要做的事: 定稿 LifeFlow Context v1(profile、pantry、daily、粗單位 enum),並在 AI Studio 實跑一次「冰箱文字 → JSON」。
今天要解決的目的: 沒有同一組欄位,就沒有 Day 1 說的 completeness。B1 跟 C 的差距,也從今天開始變成存得下來的東西。

Day 1 結尾說了:在問 Gemini 之前,先把「半盒豆腐」跟「上次剩的三片香菇」收成 JSON。

今天就做這件事。還沒有 Vision、還沒有 Firebase 專案,只有欄位、單位,跟一條 normalize 規則。


今日任務卡

項目 內容
產出 Context schema v1、單位詞彙表、Firestore 路徑草圖、文字→JSON 一輪實驗
工具 編輯器;Google AI Studio 直接輸入(Gemini 3.1 Pro Preview 試 parser)
不使用 正式 Firebase 專案、Auth、Security Rules;路徑先畫在紙上
人工素材 Firestore 樹狀圖;AI Studio 設定、schema、System instructions、實跑結果四張截圖
對應最終指標 completenessparsed_okno_hallucination,也是 B1→B2 差異的資料底座

這步該直接輸入還是上雲? 直接輸入就夠。

Schema 是契約,不是雲端服務。今天要驗證的是「模型能不能把生活話收進 enum」,不是「我有沒有成功開出一筆 Google Cloud 帳單」。能在 AI Studio 跑通的 parser,Part 3 再搬進 Firestore。


1. 問題:今天記得的寫法,明天就變了

Day 0 打開冰箱看到半盒豆腐。Day 1 拍備料照時,我寫的是「一盒」「一撮」「一包」。

如果每天把這種句子原封不動貼給模型,第二天欄位就開始飄:

同一樣東西 昨天寫 今天模型可能回
豆腐 半盒 150g
香菇 上次剩的三片 一些香菇
雞胸 一個拳頭 200 公克去皮雞胸

B1 只跑單日還撐得住,因為你記得自己剛打了什麼。C 要連記 7 天,還要算 Day 1 那條公式:

day_score = 0.3×庫存 + 0.3×餐點tags + 0.2×習慣 + 0.1×身體 + 0.1×tip_card
completeness = 平均 × (有記天數 ÷ N)

欄位沒鎖死的話,庫存那格分數表面上拿到了,真的要扣庫存時全錯。

沒有 schema 就沒有完整性分數;沒有完整性分數,A/B1/B2/C 那張表最後只是感覺。

還有一件事順便講清楚。B2 之所以叫「認真貼」,是因為它貼得出完整的庫存 JSON。那份 JSON 從哪來?就是今天定的這組欄位。C 比 B2 多的是歷史、規則、跟煮完回寫,不是多一套魔法單位。


2. 設計

2.1 四層狀態,不要全部塞成一份

長期的、當天的、這一次推薦的,混在同一個 Prompt 裡,隔天一定對不齊。v1 先切四層:

profile          過敏、廚具、預設預算、策略預設。很少改
pantry_items     庫存(粗單位)。每煮一餐就會動
daily/{date}     今日餐、session、tip_card。一天一份
requests/{id}    組裝後的 snapshot。評測用,事後可重跑

對應到之後的 Firestore,長這樣:

users/{uid}
  ├── profile/main
  ├── pantry_items/{itemId}
  ├── body_metrics/{yyyy-mm-dd}
  ├── daily/{yyyy-mm-dd}
  ├── requests/{requestId}
  └── cook_runs/{runId}          ← Day 1 講的 Outflow 回寫落點

https://ithelp.ithome.com.tw/upload/images/20260917/20121052BzsfN2pzyI.jpg
圖:全部掛在 users/{uid} 底下。今天只定形狀,Part 3 才真的開專案。

cook_runs 今天可以先空著,但路徑要留。Day 1 說 A、B1、B2 都沒有 Outflow;C 要扣庫存、把剩料變成明天的候選,寫回的地方就是這裡。

2.2 單位用 enum,不做公克精算

Day 1 已經講過理由:電子秤買回來第三天就會被推到角落。v1 的目標是「人記得住、程式比對得了、模型讀得懂」,不是營養標示等級的公克。

允許的 unit

unit 什麼時候用 例子
each 論顆、個、條 蛋 2、香蕉 1、雞腿 1
piece / slice 切開後的一塊或一片 香菇 3 slice、吐司 2 piece
box / pack / bag 包裝單位 豆腐 0.5 box、火鍋料 1 pack
bunch 一把青菜 青江菜 1 bunch
handful 一把、一撮 蔥花 1 handful
fist 一個拳頭大的體積 切丁雞胸 1 fist
palm 一個掌心厚薄的肉片 雞胸片 1 palm
cup / tbsp / tsp 家用量杯與調味 米 0.5 cup、醬油 1 tbsp
ml 液體優先 牛奶 200 ml
portion 實在分不清 剩菜 1 portion

同一食材在庫存裡鎖定一種主單位。今天用 box 記豆腐,明天不要改成 piece、後天又寫 g。那樣表面上 completeness 還在加分,實際扣庫存會錯。

每筆數量固定這個形狀:

{
  "name": "豆腐",
  "qty": 0.5,
  "unit": "box",
  "qty_text": "半盒",
  "approx": true
}
欄位 用途
qty + unit 機器比對、扣庫存
qty_text 給人看,保留語音或手打的原文
approx: true 永遠假設是約量,不要假裝精準

2.3 設備與過敏必須是欄位,不能靠印象

Day 1 把廚房寫死了:單口電磁爐、深炒鍋、玉子燒鍋、微波爐,沒有烤箱、沒有氣炸鍋、沒有蒸籠、沒有第二個爐口。

這種東西不能每次憑印象打在 Prompt 裡,理由很簡單:忘記打的那一天,模型就會發明一台蒸籠出來救時間。之後 time_okhard_ok 都要讀這段,所以它得是固定欄位(完整 JSON 在 3.1)。

過敏原同理,而且要分細。Day 1 那道蝦過敏題的重點就在這裡:crustaceanmolluskshellfish 要分開存。混成一個「海鮮」標籤,那尾透抽會跟著蝦一起被封殺,你反而多買一份食材。

2.4 為什麼是 Gemini,為什麼不上向量庫

選型 Day 1 的 3.5 講過了,今天補「跟 schema 有什麼關係」。

選 Gemini: 後面立刻要做 Vision(便當、冰箱)、Structured Output(鎖 JSON)、Function Calling(Agent 自己去拿庫存)。同一條產品線接得上去,今天不必比價十家模型。選型是為了 Vision、JSON、Tools 這三件事,不是為了品牌好看。

不上向量庫: 核心問題不是「語意最像哪道食譜」,而是「這半盒豆腐、100 塊、20 分鐘、單口電磁爐、對蝦過敏,哪些方案根本不合格」。那是約束過濾,用結構化欄位加規則做才準。向量近似會讓「有沒有踩到過敏」變成機率問題,最後連分數都算不清。


3. 實作

順序照 2.1 的四層走:先寫很少改的 profile,再寫每天會動的庫存與當日紀錄,最後把它丟進 AI Studio 實跑。

3.1 profile:把 Persona A 收成一筆

2.3 說的設備與過敏,就住在這裡:

{
  "display_name": "自煮者",
  "goals": {
    "primary": "reduce_decision_fatigue",
    "secondary": ["less_food_waste", "budget_control"],
    "notes": "平日不想洗很多鍋"
  },
  "hard_constraints": {
    "allergens": [],
    "forbidden_ingredients": [],
    "diet_tags": []
  },
  "kitchen": {
    "equipment": [
      "induction_cooktop",
      "deep_wok",
      "tamagoyaki_pan",
      "microwave"
    ],
    "burners": 1,
    "max_pots_willing": 2,
    "servings_default": "1-2"
  },
  "defaults": {
    "typical_budget_twd": 100,
    "typical_time_minutes": 20,
    "preferred_food_sources": ["home", "convenience_store", "supermarket", "eat_out"]
  }
}

burners: 1 這一格看起來很小,但它是 Day 1 那句「一邊燉咖哩一邊煎歐姆蛋做不到」的程式版。

Golden 題「朋友對蝦過敏」到來時,我不會去改這份人生檔案,而是在當次 session 疊一層 guest_allergens: ["crustacean"]。朋友走了就拿掉,我自己的 profile 不該被今天的客人污染。

3.2 庫存:把「上次剩的三片香菇」寫進去

這就是 Day 1 預告過、卻永遠進不了 B1 句子的那些東西:

[
  {
    "name": "豆腐",
    "name_normalized": "tofu",
    "qty": 0.5,
    "unit": "box",
    "qty_text": "半盒",
    "approx": true,
    "location": "fridge",
    "expire_on": "2026-09-18",
    "priority": "use_soon",
    "source": "manual"
  },
  {
    "name": "雞蛋",
    "name_normalized": "egg",
    "qty": 2,
    "unit": "each",
    "qty_text": "兩顆",
    "approx": true,
    "location": "fridge",
    "priority": "normal",
    "source": "manual"
  },
  {
    "name": "香菇",
    "name_normalized": "shiitake",
    "qty": 3,
    "unit": "slice",
    "qty_text": "上次煮麵剩的三片",
    "approx": true,
    "location": "fridge",
    "expire_on": "2026-09-18",
    "priority": "use_soon",
    "source": "manual",
    "note": "leftover_from_udon"
  },
  {
    "name": "青江菜",
    "name_normalized": "bok_choy",
    "qty": 1,
    "unit": "bunch",
    "qty_text": "一把",
    "approx": true,
    "location": "fridge",
    "priority": "normal",
    "source": "manual"
  }
]

priority: use_soonexpire_on 是後面 leftover_hit 的判斷依據。少了這兩格,「有快過期的東西要先吃掉」就變成一句口號。

3.3 當日紀錄:daily

對齊 Day 1 的 day_score,有記才有分:

{
  "date": "2026-09-17",
  "today_meals": [
    {
      "slot": "lunch",
      "label": "排骨便當",
      "tags": ["fried", "vegetable_low", "protein_present"]
    }
  ],
  "activity_context": {
    "steps": 8000,
    "activity_level": "moderate",
    "source": "manual"
  },
  "session": {
    "budget_twd": 100,
    "available_time_minutes": 20,
    "food_source": "home",
    "active_strategy_ids": ["leftover_first", "budget_cap"]
  },
  "tip_card": "香菇三片明天到期,優先用掉"
}

today_meals 只記粗標籤就好。中午吃了排骨便當,記 friedvegetable_low 就夠讓晚餐往互補方向走,不必算出那塊排骨幾大卡。

身體數據放 body_metrics/{date},同一天最多一筆,進模型只給 snapshot 加 trend,並帶上 usage_policy: context_only_no_medical_advice。這是 Day 1 那條硬邊界的欄位版:體脂是 Context,不是「今晚只能吃水煮雞」的公式。

3.4 在 AI Studio 實跑一次 parser

流程固定成這樣:

使用者、照片、語音
    → 模型抽出草稿 JSON(可以較自由)
    → normalize_unit() 對到詞彙表
    → schema validate
    →(之後)寫入 Firestore

https://ithelp.ithome.com.tw/upload/images/20260917/20121052Y0Cu3nmkC3.jpg

圖:模型負責草稿,程式負責把單位鎖進 enum。Firebase 存得進去,不代表之後扣得動。

今天不只貼一句 Prompt 就算完。我把 Google AI Studio 右側每個設定都打開講一遍,因為鐵人賽有不少讀者是第一次進來,「按鈕在哪、為什麼這樣設」比結論重要。

為什麼故意挑 Pro:我想讓 Temperature 出現

我先試的是 gemini-3.5-flash-lite。夠快、也做得動這題,但右側面板常常只剩 Thinking levelTemperature 整個不見。換成 Pro 系模型之後它才回來。

所以今天的做法我直說:

目的 選哪個
教設定、讓讀者看得到 Temperature Gemini 3.1 Pro Preview
之後自己大量重跑、想省額度 換回 Flash 或 Flash Lite 都沒問題

不是「Pro 比較會收冰箱 JSON」。這題靠的是 Structured outputs 的 schema,不是模型智商。挑 Pro 只是為了把設定面板講完整。

另外提一下:我有 Google AI Pro 訂閱,它主要提高 AI Studio 網頁端 的額度與可選模型,跟「要不要綁 Cloud 付費 API key」是兩件事。今天全程在 Playground 手動跑,跳出 Link a paid API key 直接關掉就好。

Step 1 — 開 Playground,先認識右邊那排設定

aistudio.google.com,左側選 Playground。今天的重點都在右邊 Run settings:模型、System instructions、Temperature、Thinking level,還有一排 Tools 開關。

https://ithelp.ithome.com.tw/upload/images/20260917/201210521FBINkJ8vy.png

圖:今天的設定面板。Model 是 Gemini 3.1 Pro Preview、Temperature 拉到 0、Thinking level 選 Medium、Structured outputs 打開,Search 與 Code 與 Function 全關。下方輸入框那顆藍色標籤代表 schema 已經生效。

Step 2 — 每個設定在幹嘛(不用背官方定義)

把上面那張圖的右側拆開講。記住一句就好:這題要穩,不要創意。

Model:Gemini 3.1 Pro Preview

選哪顆大腦。Flash 像速食店窗口,Pro 像願意多看一眼菜單的店員。今天挑 Pro 主要是面板比較完整,順便讓示範好看一點。之後正式 pipeline 換回 Flash 也行,schema 對的話兩邊都該過。

System instructions:系統指令

這格不是「再寫一次 Prompt」,比較像給模型掛一張工作證:你是誰、什麼不准做,整場對話都生效。使用者每次輸入就專心講冰箱裡有什麼,不必每次重念一遍「不准公克」。

我把它存成一組叫「格式化輸出食物文字」的指令:

你是庫存正規化器。只根據使用者給的文字抽出食材,不要發明冰箱裡沒寫的東西。
禁止輸出公克、g、gram。
unit 只能用白名單。approx 永遠為 true。
不要解釋、不要 markdown。

https://ithelp.ithome.com.tw/upload/images/20260917/201210524ce00UtNNM.png

圖:工作證掛好。注意最底下那行 Instructions are saved in local storage,換瀏覽器或清快取就可能不見,重要指令建議另外存一份進專案裡。

為什麼要特別寫「不要發明」?因為模型很愛幫忙。你寫「兩顆蛋」,它有時候會順手加一筆「建議的鹽」進庫存。庫存系統最怕這種好心生出來的幽靈食材。

Temperature:設 0

這個設定最常被講成「創意開關」,我比較喜歡想成「今天要不要擲骰子」。

數值 體感
接近 0 同一題跑幾次,答案長得差不多。適合抽欄位、填表、產出要進資料庫的東西
拉高到 0.7~1 開始發揮、換說法、偶爾加戲。適合寫文案,不適合扣庫存

庫存 JSON 要的是可重現,不要今天寫 box、明天心情好改成 portion。所以直接設成 0

不過講清楚:Temperature 0 不等於正確率 100%。 它只是比較不亂發揮。該漏的香菇還是可能漏,所以後面才需要 schema 跟人工三條驗收。

Thinking level:選 Medium

有的模型會先想一下再答,介面上會出現一塊可以展開的 Thoughts。這題本質是填表,不是證明費馬最後定理。

檔位 什麼時候用
Minimal 或 Low 大量重跑、純格式題、想省額度
Medium 規則有點多,又想看它有沒有讀懂「剩的三片」。我今天用這個
High 留給真的要推理的題。庫存 parser 開這麼高只是燒額度

截圖裡模型確實回了一塊 Thoughts,代表它有先想過一輪。正式產品如果在意反應速度跟成本,記得回來調低。

Structured outputs:打開

這是今天真正的主角。以前我們只能在 Prompt 裡寫「請輸出 JSON」,模型有時還是會先寒暄兩句,再給你半份 markdown。Structured outputs 是把 OpenAPI/JSON Schema 交給 Studio,輸出形狀由合約保證,不是靠拜託。

其他 Tools 為什麼全關

開關 今天 原因
Grounding with Google Search 我冰箱裡有什麼,不該去搜尋引擎找答案
Code execution 不需要它幫我算「0.5 盒是多少」
Function calling 那是 Part 4 讓 Agent 自己去讀 Firestore 的事,今天別提前開
URL context、Maps 跟庫存無關就別開,少一個變因

一句偷懶口訣:用不到的能力,預設當噪音關掉。

Step 3 — 把粗單位 enum 寫進 Structured outputs

點 Structured outputs 旁邊的 Edit,切到 Code Editor,把預設的 response: string 刪掉,改貼我們的合約:

{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "qty": { "type": "number" },
          "unit": {
            "type": "string",
            "enum": [
              "each", "piece", "slice", "box", "pack", "bag",
              "bunch", "handful", "fist", "palm",
              "cup", "tbsp", "tsp", "ml", "portion"
            ]
          },
          "qty_text": { "type": "string" },
          "approx": { "type": "boolean" },
          "expire_hint": {
            "type": "string",
            "description": "若文字有提到到期或快壞就填,否則空字串"
          },
          "note": {
            "type": "string",
            "description": "剩料來源等補充,沒有就空字串"
          }
        },
        "required": ["name", "qty", "unit", "qty_text", "approx"]
      }
    }
  },
  "required": ["items"]
}

https://ithelp.ithome.com.tw/upload/images/20260917/20121052o9rkIRJAag.png

圖:重點在 unitenum。模型再想輸出公克,也會被這份名單擋在門外,比在 Prompt 裡寫一百次「拜託不要公克」有效。

Save。這一步等於把 Day 1 講的粗單位,第一次變成模型很難違反的輸出合約。

再補一句常被誇大的事:Structured outputs 保證形狀合法,不保證內容正確。 它能逼出一個叫 qty 的 number,但不能保證「上次剩的三片香菇」真的被抽出來。漏東西還是要靠 Prompt 跟人工驗收。

Step 4 — 貼任務,然後 Run

格式已經被 schema 鎖住,Prompt 可以短。重點是把生活量詞對到 enum:

把下面冰箱描述收成 items 陣列。
蔥花「一撮」用 handful;雞腿「一包」用 pack;香菇「三片」用 slice。

文字:
冰箱有半盒豆腐(明天到期)、兩顆蛋、一把青江菜、
上次煮烏龍麵剩的三片香菇、蔥花一撮、雞腿肉一包。

Step 5 — 實跑結果:三條驗收都過

https://ithelp.ithome.com.tw/upload/images/20260917/20121052A4tx8ECnnT.png
圖:Temporary chat,約 1,438 tokens。右邊設定仍是 Temperature 0、Structured outputs 打開。中間是我貼的 Prompt,下面是模型吐出來的 JSON。

輸出精簡如下,完整六筆在截圖裡:

{
  "items": [
    {
      "name": "豆腐",
      "qty": 0.5,
      "unit": "box",
      "qty_text": "半盒",
      "approx": true,
      "expire_hint": "明天到期",
      "note": ""
    },
    {
      "name": "雞蛋",
      "qty": 2,
      "unit": "each",
      "qty_text": "兩顆",
      "approx": true,
      "expire_hint": "",
      "note": ""
    },
    {
      "name": "青江菜",
      "qty": 1,
      "unit": "bunch",
      "qty_text": "一把",
      "approx": true,
      "expire_hint": "",
      "note": ""
    },
    {
      "name": "香菇",
      "qty": 3,
      "unit": "slice",
      "qty_text": "三片",
      "approx": true,
      "expire_hint": "",
      "note": "上次煮烏龍麵剩的"
    },
    {
      "name": "蔥花",
      "qty": 1,
      "unit": "handful",
      "qty_text": "一撮",
      "approx": true,
      "expire_hint": "",
      "note": ""
    },
    {
      "name": "雞腿肉",
      "qty": 1,
      "unit": "pack",
      "qty_text": "一包",
      "approx": true,
      "expire_hint": "",
      "note": ""
    }
  ]
}

對一下今天的及格線:

驗收 結果
沒有出現 ggram、公克
每個 unit 都在白名單 boxeachbunchslicehandfulpack
三片香菇留下來,note 還寫了來源

第三條是我最在意的。B1 站在冰箱前只會打出「冰箱好像還有豆腐、蛋、青菜」,上面這份多出來的效期、香菇、qty_text,就是 Day 1 那張成績單開始拉開的地方。

失敗怎麼辦?改 Prompt、收緊 schema、或加一層程式直接拒收。不要為了讓它過關就跑去開 Firebase。工具哲學跟 Day 0、Day 1 同一條:這步用直接輸入驗證就夠。

跑通之後如果在意額度,把 Model 切回 Flash Lite 再跑同一題。schema 正確的話兩邊都該過,差的只是面板上有沒有 Temperature,不是「會不會變成 Agent」。


4. 驗證與翻車

3.4 只證明了順風的一題。真正要壓的是下面這幾條,我會在接下來幾天連續記錄時故意撞:

壓測 預期
要求模型用「克」輸出 parser 拒絕,或轉成粗單位並強制 approx=true
同一食材兩天 unit 不同 表面上 completeness 有分,扣庫存會錯。鎖單位比多記欄位重要
把 90 天體重一次塞進 Prompt 違反「工具不增加困擾」,C 只要摘要
「一些青菜」「適量香菇」 逼成 handfulportion,不准留自由字串當 unit
模型省略效期與邊角料 這輪算失敗。那正是 B1 會丟掉、C 必須留住的東西

翻車預告:第一週你會很想把每樣東西改成公克,因為看起來比較科學。別改。公克一上場,連續記錄的放棄速度會比 Part 3 相機壞掉還快。Day 1 已經預告過第 3 天就會想放棄,單位訂太精只會讓那天提早到來。


5. 今日結論

  1. Schema 是 C 能被評分的前提。 completeness 的每一格,都對應今天定下的欄位。
  2. 粗單位加 enum,比精準克數更能連記 7 天。 qty_text 給人看,unit 給程式扣。
  3. 能在 AI Studio 驗證的 parser,先不要上雲。 路徑畫好就行,正式專案是 Part 3 的事。

下一篇: 第一張台灣便當丟進 AI Studio,看 Vision 會不會裝懂。欄位已經備好,接下來測模型看不看得見餐盤上的東西。


上一篇
[Day 1] 先畫邊界:這 30 天要打哪一格,哪些事 AI 不准決定
系列文
DishFlow AI Agent:用 Google AI 打造 Eat-Cost Balance 的下一餐決策系統3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言