2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pydantic AIで作る「味の調整方法を教えてくれるカレーレシピシステム」

Posted at

はじめに

image.png

「カレーがなんだか物足りない」「市販のルーだけでは味に深みが出ない」――そんな悩み、ありませんか?

前回の記事では、Pydantic AIを使って曖昧なレシピを明確にする仕組みを紹介しましたが、実際に作ってみると「味の調整ができない」「どう直せばいいのかわからない」といった問題にぶつかります。

今回はその課題に対応するため、レシピに沿って作ったあとでも「味をどう整えるか」をアドバイスできる機能を追加しました。
カレーを題材に、曖昧な入力にも対応しつつ、調整の理由まで丁寧に教えてくれるシステムに仕上げています。

なぜ「味の調整技術」に注目したのか

image.png

前回記事からの発展

前回は「しんなり」などの曖昧な表現を明確にすることに注力しましたが、実際に調理してみると別の課題も浮かび上がってきました。
たとえば、レシピ通りに作っても味が薄かったり、辛すぎた場合の対処法がわからなかったりします。
また、スパイスの使い方が難しく、その分量の根拠も不明瞭でした。

実際の調理で起きる問題

「カレー粉を加えて香りを出す」
→ どのタイミングで?何秒炒める?焦がしたらどうする?

「塩コショウで味を調える」  
→ 何をどれくらい?薄かったら?辛すぎたら?

レシピの行間にある「経験則」や「工夫」も含めて、体系的に共有できるようにしたいと考えました。

味の調整技術の構造化の挑戦

前回からの進化ポイント

前回のシステムでは、以下のようなシンプルな構造でした:

class ClarifiedStep(BaseModel):
    original: str        # 元の曖昧な表現
    clarified: str       # 明確化された指示
    time: str           # 具体的な時間
    tip: str            # 失敗しないコツ

今回は、これを拡張して調理方法を造化しました:

class FinalRecipe(BaseModel):
    title: str
    ingredients: list[Ingredient] 
    steps: list[Step]
    
    # ここからが技術的なノウハウ
    spice_and_seasoning: list[str]       # スパイス技術と味調整
    liquid_balance_tips: list[str]       # 水分・油分バランス  
    finishing_and_presentation: list[str] # 仕上げの技術
    weather_considerations: list[str]     # 環境による調整
    adjustment_evidence: list[str]       # なぜその調整をしたか

前回の「明確化」から「調理方法の工夫」も含めてみた

システムプロンプトの工夫

system_prompt="""
あなたはカレー専門シェフです。

## 重要:技術的なノウハウを必ず含めること

【スパイス・調味料の扱いと味調整】
- カレー粉は油で30秒炒めて香りを引き出してから水を加える
- 市販ルーは火を止めてから加え、溶けたら再加熱で焦げ防止  
- 基本ブレンド例: クミン2:コリアンダー2:ターメリック1を弱火で30秒
- 辛すぎる時: 砂糖、はちみつ、牛乳で緩和
- 薄い時: 塩、醤油、コンソメで補強

【水分・油分バランスのコツ】  
- 煮込み時は具材が隠れる程度、蒸発したら段階的に追加
- 玉ねぎがしっとりコーティングされる程度が油分の目安
- とろみ調整: カレー粉使用時は小麦粉、ルー使用時は煮詰めで調整

【仕上げ・盛り付けのコツ】
- 最終味調整: 塩加減、酸味、甘さの微調整は火を止める直前に
- 香り仕上げ: ガラムマサラ、パクチー、レモン汁で最後の香り付け
"""

実際の出力例:技術指導の様子

入力

スタイル: インド風
メイン食材: 鶏肉  
味の好み: 辛め、ヘルシー
気温: 15°C、湿度: 40%

曖昧なレシピ:
玉ねぎを炒めてしんなりしたら肉を入れる。
カレー粉を加えて香りを出す。
水を加えて煮込み、塩コショウで味を調える。

出力例:調理方法

スパイス・調味料の扱いと味調整

  • カレー粉大さじ3は油で30秒炒めて香りを引き出してから水を加える(焦がすと苦味が出るため弱火で)
  • 基本ブレンド: クミン小さじ2、コリアンダー小さじ2、ターメリック小さじ1を弱火で30秒炒める
  • 辛め希望のためカイエンペッパー小さじ1/2を追加(辛すぎる場合はココナッツミルク50mlで緩和)

水分・油分バランスのコツ

  • 水400mlは具材が隠れる程度から開始、20分煮込み中に蒸発分として100ml追加
  • 油分は玉ねぎがしっとりコーティングされる程度(大さじ2が目安)
  • ヘルシー仕様のため油脂を20%削減、代わりにトマトの水分で旨味を補強

仕上げ・盛り付けのコツ

  • 香り仕上げ:火を止める直前にガラムマサラ小さじ1を加えて30秒混ぜる
  • 酸味バランス:レモン汁小さじ1でスパイスの重さを軽やか仕上げ
  • 温度管理:器を60度に温めておくと香りが立ちやすい

調整根拠・エビデンス

  • 「辛め」指定によりカイエンペッパー50%増量、最終的な辛さ調整方法も提示
  • 「ヘルシー」指定により油脂20%削減、トマトベースでコクを代替
  • 「しんなり」→「透明になるまで5分、中火で時々混ぜながら」に明確化

技術的な実装のポイント

1. 知識の体系化

# 従来:単純な材料リスト
ingredients = ["玉ねぎ", "", "カレー粉"]

# 今回:技術的なノウハウまで構造化  
class FinalRecipe(BaseModel):
    ingredients: list[Ingredient]
    # 調理技術のノウハウを体系化
    spice_and_seasoning: list[str]
    liquid_balance_tips: list[str] 
    finishing_and_presentation: list[str]
    adjustment_evidence: list[str]  # 根拠の明示

2. Pydantic AIによる知識の引き出し

agent = Agent(
    model,
    output_type=FinalRecipe,
    system_prompt="""
    調理技術の専門家として、以下を必ず提供すること:
    
    1. スパイスの扱い方(炒める時間、温度、順序)
    2. 水分・油分の科学的なバランス理論
    3. 仕上げの技術(香り付け、温度管理)
    4. なぜその調整をするのかの根拠
    """
)

プロンプトで「技術指導者」としての役割を明確に定義

3. 環境要因も考慮した動的調整

# 気温・湿度による科学的な調整
- 気温 > 30 & 湿度 > 60  塩分 10%汗で塩分が失われるため補正
- 気温 < 10  ガラムマサラ +30%体を温める効果強化
- 湿度 > 80  水分 20%湿気で水っぽく感じるため

なぜこのアプローチが有効なのか

1. 学びながら作れる

単にレシピ通りに作るだけでなく、「なぜこの工程が必要なのか」まで理解できるので、
他の料理にも応用が効き、失敗したときの対処法も見えてきます。
結果として、自分の好みにアレンジできる柔軟さも身につきます。

2. 失敗しにくく、同じ味を再現しやすい

「適量」「いい感じ」といった曖昧な表現を排除し、根拠のある具体的な指示を提示することで、
初心者でも迷わず作れ、毎回安定した味に仕上げやすくなります。
調整の考え方も体系的に理解できます。

3. 自分だけの最適なレシピに進化

気温や湿度、個人の味の好みなどに応じて、動的にレシピを調整します。
調整の理由も説明されるので納得感があり、作るたびに自然と料理スキルも向上します。

実装

自分はGoogle Colabで動作確認しました。

!pip install pydantic_ai

# APIキー設定
import os
from google.colab import userdata
os.environ["GEMINI_API_KEY"] = userdata.get('GEMINI_API_KEY')
# ── Imports ──────────────────────────────────────────────
import os, asyncio, nest_asyncio, gradio as gr
from pydantic import BaseModel, Field
from pydantic_ai import Agent
from pydantic_ai.models.gemini import GeminiModel

nest_asyncio.apply()           # Jupyter / Colab 用

# ── Pydantic Models ─────────────────────────────────────
class Ingredient(BaseModel):
    name: str
    amount: str

class Step(BaseModel):
    order: int
    text: str
    time: str
    tip: str

class Equipment(BaseModel):
    name: str
    purpose: str = Field(description="使用目的・理由")
    alternative: str = Field(description="代替器具")

class FinalRecipe(BaseModel):
    title: str
    ingredients: list[Ingredient]
    equipment: list[Equipment]           # 使用器具・道具
    steps: list[Step]
    spice_and_seasoning: list[str]       # スパイス・調味料の扱いと味調整
    liquid_balance_tips: list[str]       # 水分・油分バランスのコツ
    finishing_and_presentation: list[str] # 仕上げ・盛り付けのコツ
    weather_considerations: list[str]     # 気温・湿度への配慮事項
    adjustment_evidence: list[str]       # 調整根拠・エビデンス情報

# ── Agent ───────────────────────────────────────────────
model = GeminiModel(
    "gemini-2.0-flash",
    provider="google-gla"
)

agent = Agent(
    model,
    output_type=FinalRecipe,
    system_prompt="""
あなたはカレー専門シェフです。入力には必ず
- スタイル
- メイン食材
- 分量
- ユーザの味の好み (0〜3 語)
- 気温 (°C)
- 湿度 (%)
- 曖昧なレシピ本文
が含まれます。

## タスク
1. 【曖昧なレシピの明確化】まず入力された曖昧なレシピを分析し、以下を明確化する
   - "しんなり""透明になるまで5分"
   - "適量""小さじ1/2" "大さじ2"など具体的な分量
   - "色が変わったら""全体が茶色になったら"
   - "いい感じ""とろみがついて表面がフツフツしてきたら"
   - "煮込む""弱火で10分、蓋をして煮込む"
   - "味を調える""塩小さじ1/4、こしょう少々で調味"

2. 明確化した指示を工程ごとに分割し、加熱時間・火加減・コツを具体的に記載

3. 以下の調整規則を適用して調味料を補正し、ingredients と steps を更新する
   【気温・湿度による調整】
   - 気温 > 30 & 湿度 > 60 → 塩分 −10%、油脂 −15%、酸味 +10g(夏場のさっぱり調整)
   - 気温 < 10             → ガラムマサラ +30%、油脂 +10%(寒さ対策の温め効果)
   - 湿度 > 80             → 水分 −20%、とろみ成分 +10%(湿気対策)
   - 湿度 < 30             → 水分 +15%、保湿効果のある食材追加(乾燥対策)
   - 気温 15-25 & 湿度 40-60 → 標準レシピ(最適環境)

   【味の好みによる調整】
   - 'スパイシー' または '辛め'    → カイエンペッパー +50%
   - '甘め'                        → 砂糖 +30%
   - '薄味'                        → 塩分 −15%
   - '濃い味'                      → ガラムマサラ +20%、塩 +10%
   - 'さっぱり'                    → 酸味 +15%、油脂 −10%
   - 'こってり'                    → 油脂 +15%、うま味成分 +20%
   - 'ヘルシー'                    → 油脂 −20%、野菜 +30%
   - '時短重視'                    → 加熱時間短縮、電子レンジ併用
   (選択された好みをすべて加算適用)

3. 「スパイス・調味料の扱いと味調整」を統合して記載
   【カレーベースの作り方】
   - カレー粉は油で30秒炒めて香りを引き出してから水を加える
   - 市販ルーは火を止めてから加え、溶けたら再加熱で焦げ防止
   - インド風→カレー粉+個別スパイス、和風→ルー+隠し味で使い分け

   【スパイスブレンドと香り付け】
   - 基本ブレンド例: クミン2:コリアンダー2:ターメリック1を弱火で30秒
   - 仕上げスパイス: 火を止める直前にガラムマサラで香りアップ
   - 対立する味の要求がある場合の調整方法も記載

   【味のバランス調整】
   - 塩味・甘味・酸味・辛味の4要素でバランスを取る
   - 辛すぎる時: 砂糖、はちみつ、牛乳で緩和
   - 薄い時: 塩、醤油、コンソメで補強
   - 相反する要素を両立させる技術的工夫も説明

3-2. 「水分・油分バランスのコツ」を記載
   - 水分調整: 煮込み時は具材が隠れる程度、蒸発したら段階的に追加
   - 油分調整: 玉ねぎがしっとりコーティングされる程度が目安
   - とろみ調整: カレー粉使用時は小麦粉、ルー使用時は煮詰めで調整
   - 気温・湿度による調整: 湿度高→水分少なめ、乾燥→水分多めに

3-3. 「仕上げ・盛り付けのコツ」を記載
   - 最終味調整: 塩加減、酸味、甘さの微調整は火を止める直前に
   - 香り仕上げ: ガラムマサラ、パクチー、レモン汁で最後の香り付け
   - 温度管理: 器を温める、保温のコツ
   - 盛り付け: 色彩、食感のコントラストを意識した見た目の工夫

3-4. 「気温・湿度への配慮事項」を必ず記載
   - 調理中・保存中の環境への注意点
   - 季節や天候に応じた調理のコツ

## 重要: 調整根拠の必須記載
adjustment_evidence には必ず以下を含めること:
7-1. 曖昧表現の明確化例

7-2. 環境条件の分析結果

7-3. 味の好みによる調整と対立解決
   - 選択された各好み項目の分析
   - 相反する要素がある場合の解決方法
   - 例: "「さっぱり」「こってり」の対立 → メリハリ調理法採用:前半はトマトベースでさっぱり、後半にココナッツミルクでコクを追加"
   - 例: "「薄味」「濃い味」の対立 → バランス型採用:塩分は標準値、だしとスパイスで深みを演出"
   - 例: "「甘め」「辛め」「ヘルシー」→ 甘辛バランス型:はちみつで辛さをマイルドに、ココナッツミルクでヘルシーなコク"

7-4. スタイル・食材による基本設定

7-5. 使用器具の選定理由

8. 最終結果を以下 JSON 形式で返却
"""
)

# ── LLM 呼び出し(エラー修正版) ─────────────────────────────────────
async def personalize(style, ing, serv, prefs, temp, hum, vague) -> FinalRecipe:
    prefs = prefs[:3]                                # 最大 3 つ

    prompt = f"""
### レシピ条件
- スタイル: {style}
- メイン食材: {ing}
- 分量: {serv}
- 味の好み: {', '.join(prefs) or '特になし'}
- 気温: {temp}°C
- 湿度: {hum}%

### 曖昧なレシピ
{vague}
"""
    res = await agent.run(prompt)
    return res.data

# ── Markdown 整形 ────────────────────────────────────────
def recipe_to_md(r: FinalRecipe) -> str:
    md = [f"## 🍛 {r.title}", "### 材料"]
    md += [f"- {i.name}: {i.amount}" for i in r.ingredients]

    md += ["\n### 🔧 使用器具・道具"]
    for eq in r.equipment:
        md.append(f"- **{eq.name}**: {eq.purpose}")
        md.append(f"  🔄 代替: {eq.alternative}")

    md += ["\n### 📋 手順"]
    md.append("| 工程 | 作業内容 | 時間 | コツ・ポイント |")
    md.append("|------|----------|------|----------------|")
    for s in r.steps:
        md.append(f"| {s.order} | {s.text} | {s.time} | {s.tip} |")

    md += ["\n### 🌶️ スパイス・調味料の扱いと味調整"]
    for s in r.spice_and_seasoning:
        md.append(f"- {s}")

    md += ["\n### ⚖️ 水分・油分バランスのコツ"]
    for l in r.liquid_balance_tips:
        md.append(f"- {l}")

    md += ["\n### ✨ 仕上げ・盛り付けのコツ"]
    for f in r.finishing_and_presentation:
        md.append(f"- {f}")

    md += ["\n### 🌡️ 気温・湿度への配慮"]
    for w in r.weather_considerations:
        md.append(f"- {w}")

    md += ["\n### 📊 調整根拠・エビデンス"]
    for e in r.adjustment_evidence:
        md.append(f"- {e}")
    return "\n".join(md)

def create_smart_preference_interface():

    # グループの定義
    conflict_groups = [
        ["薄味", "濃い味"],
        ["さっぱり", "こってり"],
        ["甘め", "辛め"]
    ]

    # 独立選択肢
    independent_choices = ["ヘルシー", "時短重視"]

    return conflict_groups, independent_choices

with gr.Blocks(title="カレー明確化 & 味調整") as demo:
    gr.Markdown("## 🍛 曖昧レシピを「お好み & 天気」に合わせたレシピ生成")

    with gr.Row():
        style_dd = gr.Dropdown(["和風","インド風","タイ風"], value="インド風", label="スタイル")
        ing_dd   = gr.Dropdown(["鶏肉","牛肉","豚肉","野菜","シーフード"], value="鶏肉", label="メイン食材")
        serv_dd  = gr.Dropdown(["2人前","4人前","6人前"], value="4人前", label="分量")

    # 相反要素を分けたインターフェース
    gr.Markdown("### 🍴 味の好み選択(最大3つ)")
    gr.Markdown("**各ペアから1つまで** + 独立選択肢から選んでください")

    with gr.Row():
        with gr.Column():
            gr.Markdown("**塩味レベル**(どちらか1つ)")
            salt_radio = gr.Radio(["", "薄味", "濃い味"], label="", value="")

        with gr.Column():
            gr.Markdown("**コク・さっぱり感**(どちらか1つ)")
            rich_radio = gr.Radio(["", "さっぱり", "こってり"], label="", value="")

        with gr.Column():
            gr.Markdown("**甘辛バランス**(どちらか1つ)")
            sweet_radio = gr.Radio(["", "甘め", "辛め"], label="", value="")

    with gr.Row():
        gr.Markdown("**追加オプション**(複数選択可)")
        independent_cb = gr.CheckboxGroup(["ヘルシー", "時短重視"], label="")

    with gr.Row():
        temp_sl = gr.Slider(0, 40, value=25, step=1, label="気温 (°C)")
        hum_sl  = gr.Slider(0, 100, value=60, step=1, label="湿度 (%)")

    vague_tb = gr.Textbox(
        label="曖昧なレシピ本文",
        lines=5,
        placeholder="玉ねぎを炒めてしんなりしたら肉を…",
        value="""玉ねぎを薄切りにして炒める。しんなりしたら肉を入れる。
肉の色が変わったらカレー粉を入れて香りを出す。
水を加えて煮込み、ルーを入れて溶かす。
塩コショウで味を調えて完成。"""
    )

    # 選択結果の表示とバリデーション
    def update_selection_display(salt, rich, sweet, independent):
        selected = []
        if salt: selected.append(salt)
        if rich: selected.append(rich)
        if sweet: selected.append(sweet)
        selected.extend(independent)

        if len(selected) > 3:
            return f"⚠️ 選択数オーバー: {len(selected)}/3 ({', '.join(selected)}\n最大3つまでに減らしてください"
        elif len(selected) == 0:
            return "選択した好みなし"
        else:
            return f"✅ 選択: {', '.join(selected)} ({len(selected)}/3) → 適用予定"

    selection_display = gr.Markdown("選択した好みなし")

    # リアルタイム更新
    for component in [salt_radio, rich_radio, sweet_radio, independent_cb]:
        component.change(
            fn=update_selection_display,
            inputs=[salt_radio, rich_radio, sweet_radio, independent_cb],
            outputs=selection_display
        )

    gen_btn = gr.Button("🔥 レシピ生成", variant="primary")
    out_md  = gr.Markdown()

    def validate_and_run(style, ing, serv, salt, rich, sweet, independent, temp, hum, vague):
        # 選択肢をまとめる
        prefs = []
        if salt: prefs.append(salt)
        if rich: prefs.append(rich)
        if sweet: prefs.append(sweet)
        prefs.extend(independent)

        # バリデーション
        if len(prefs) > 3:
            return f"### ⚠️ エラー\n選択数が多すぎます: {len(prefs)}/3\n最大3つまでに減らしてください。"

        # レシピ生成を非同期で実行(エラー修正版)
        try:
            # nest_asyncio.apply() が効いているので、直接 asyncio.run が使える
            res = asyncio.run(personalize(style, ing, serv, prefs, temp, hum, vague))
            return recipe_to_md(res)
        except Exception as e:
            import traceback
            error_detail = traceback.format_exc()
            return f"### ❌ エラー\n```\n{str(e)}\n\n詳細:\n{error_detail}\n```"

    gen_btn.click(
        fn=validate_and_run,
        inputs=[style_dd, ing_dd, serv_dd, salt_radio, rich_radio, sweet_radio, independent_cb, temp_sl, hum_sl, vague_tb],
        outputs=out_md
    )

# ── Launch ────────────────────────────────────────────
if __name__ == "__main__":
    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY が未設定です")
    demo.launch(debug=True)  # デバッグモードを有効化

実際の画面と操作の様子

FireShot Capture 008 - カレー明確化 & 味調整 - [3b0e6412599b57ec95.gradio.live].png

まとめ

このシステムの本質は「レシピ生成」ではなく「個人に合わせた調理サポート」にあります。
これまでのレシピアプリは、材料と手順をただ表示するだけで、「醤油大さじ2」といった指示で終わっていました。

今回のシステムでは、
あなたの味の好みやその時の環境に合わせて調整を提案したり、
「味が薄い」と感じたときに取れる具体的な対処法を示したりします。
また、スパイスの使い方についてもサポート情報を提供し、
気温や湿度などによる味の変化にも対応できるようアドバイスします。

参考

謝辞

moritalous さんへ
前回の記事にコメント (味を整える も教えてください!!) いただき、ありがとうございました。
ご期待に添えたかはわかりませんが、まずは形にしてみましたので、よろしければご覧いただけますと幸いです。
また、いつも興味深い記事をありがとうございます。とても刺激になっており、学ばせていただいています。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?