Amazon Bedrock Flows で冷蔵庫の写真からクリスマスらしい料理レシピを生成するアプリを作ってみた

Amazon Bedrock Flows で冷蔵庫の写真からクリスマスらしい料理レシピを生成するアプリを作ってみた

Bedrock Flows のおかげで、生成 AI ワークフローを利用したアプリがお手軽に作れちゃいました
2025.12.10

この記事はアノテーション株式会社 AWS Technical Support Advent Calendar 2025 | Advent Calendar 2025 - Qiita 10 日目の記事です。

はじめに

こんにちは、アノテーションの かねこ です。

最近は生成 AI やエージェントが巷で人気ですね。弊社でも業務内への取り入れ方を模索しており、Q Developer や Kiro 、Bedrock の活用方法を検討したり、 PoC を始めて効果検証してみたりしています。

アドベントカレンダーに記事を寄せるにあたり、せっかくなら生成 AI をテーマにしたいなと思ったこと、アドベントカレンダーから連想して何かクリスマスらしさを取り入れたいと思ったこと、最近自炊のメニューを考えるのが手間なので楽したいなと思ったこと、これらの怠惰希望を悪魔合体させた結果、 「冷蔵庫の写真からクリスマスらしい料理レシピを生成するアプリを作ろう」 という発想に至りました。

また、生成 AI アプリの作成に際しては、 Amazon Bedrock Flows というノーコードでワークフローを作成できる機能があり、キャッチアップしてみたいなという思いがあったので、これの利用を前提に作成しました。これにより、Step Functions や Lambda を利用せずとも、生成 AI の生成結果を組み合わせたアプリを簡単に作成できるようになります。

https://dev.classmethod.jp/articles/bedrock-flows-ga/

レシピ(構成図)

アプリは以下のような構成で作成しました。

スクリーンショット 2025-12-08 17.58.18

  • フロントエンド
    • 時間を掛けたくなかったので Streamlit を利用しました
    • Kiro でこのあたりの設計・実装はさくっとやってもらいました
  • バックエンド
    • 当初、Bedrock Flows のみで画像読み取りとレシピ生成を実行しようとしていました
    • しかし、Flows での画像の取り扱いが思うようにいかず、最終的に画像から食材を抽出する部分は S3 + Lambda 経由で別の Bedrock を単品で呼び出して、フローの外に出すことにしました (詳しくは後述)

フローは Bedrock Flows のマネジメントコンソール上で見るとこんな感じの構成になっています。

スクリーンショット 2025-12-08 18.11.43

  • フローの開始ノード (Flow input)
  • S3 にアップされた画像を Bedrock に渡して、食材を抽出して文字列として返す Lambda 関数を呼ぶノード (ExtractIngredients)
  • 食材からレシピを考えるノード (GenerateRecipe)
    • 当初のプロンプトでは、写真にない食材をメインにしたレシピを生成しがちでした
    • 買い物が面倒なので、できるだけ有り物の食材でレシピを考えるよう、プロンプトを少し修正しました
  • レシピから写真にない食材を探すノード (CheckMissingIngredients)
    • ご丁寧に、写真に写っていない調味料まで買い出しリストに加えてくれていたので、基本的な調味料は省くよう、プロンプトを少し修正しました
  • 結果をまとめるノード (CombineResults)
    • ここはあんまり頑張る部分ではないので Claude Haiku 4.5 モデルにしてみました (他はリッチに Sonnet 4.5)
  • フローの出力ノード (FlowOutput)

CDK のソースは以下のような感じで構成しました。(全部は長いので、Flow の設定箇所だけ抜粋)

const flow = new bedrock.CfnFlow(this, 'ChristmasRecipeFlow', {
      name: 'christmas-recipe-generator',
      description: '冷蔵庫の食材からクリスマスレシピを生成するフロー',
      executionRoleArn: flowRole.roleArn,
      definition: {
        nodes: [
          {
            name: 'FlowInputNode',
            type: 'Input',
            outputs: [
              {
                name: 'document',
                type: 'String',
              },
            ],
            configuration: {
              input: {},
            },
          },
          {
            name: 'CompressImage',
            type: 'LambdaFunction',
            inputs: [
              {
                name: 's3_uri',
                type: 'String',
                expression: '$.data',
              },
            ],
            outputs: [
              {
                name: 'functionResponse',
                type: 'String',
              },
            ],
            configuration: {
              lambdaFunction: {
                lambdaArn: imageCompressor.functionArn,
              },
            },
          },
          {
            name: 'GenerateRecipe',
            type: 'Prompt',
            inputs: [
              {
                name: 'ingredients',
                type: 'String',
                expression: '$.data',
              },
            ],
            outputs: [
              {
                name: 'modelCompletion',
                type: 'String',
              },
            ],
            configuration: {
              prompt: {
                sourceConfiguration: {
                  inline: {
                    modelId: `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/jp.anthropic.claude-sonnet-4-5-20250929-v1:0`,
                    templateType: 'TEXT',
                    templateConfiguration: {
                      text: {
                        text: '以下の食材を使って、クリスマスにふさわしい料理のレシピを1つ作成してください。\n\n利用可能な食材:\n{{ingredients}}\n\n重要な制約:\n- できるだけ手元の食材だけで作れるレシピにしてください\n- 追加で購入が必要な食材は最小限(2〜3品目程度)に抑えてください\n- 基本的な調味料(塩、こしょう、醤油、砂糖、油など)は常備されている前提で構いません\n\nレシピには以下を含めてください:\n- 料理名\n- 材料(分量付き)\n- 作り方(手順番号付き)\n- 調理時間\n- クリスマスらしい演出のポイント\n\n日本語で出力してください。',
                        inputVariables: [
                          {
                            name: 'ingredients',
                          },
                        ],
                      },
                    },
                    inferenceConfiguration: {
                      text: {
                        temperature: 1,
                        maxTokens: 2000,
                      },
                    },
                  },
                },
              },
            },
          },
          {
            name: 'CheckMissingIngredients',
            type: 'Prompt',
            inputs: [
              {
                name: 'recipe',
                type: 'String',
                expression: '$.data',
              },
              {
                name: 'ingredients',
                type: 'String',
                expression: '$.data',
              },
            ],
            outputs: [
              {
                name: 'modelCompletion',
                type: 'String',
              },
            ],
            configuration: {
              prompt: {
                sourceConfiguration: {
                  inline: {
                    modelId: `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/global.anthropic.claude-haiku-4-5-20251001-v1:0`,
                    templateType: 'TEXT',
                    templateConfiguration: {
                      text: {
                        text: '以下のレシピに必要な材料と、現在利用可能な食材を比較し、不足している材料をリストアップしてください。\n\n重要: 基本的な調味料(塩、こしょう、醤油、砂糖、油、酢、みりんなど)は常備されている前提なので、不足材料に含めないでください。\n\nレシピ:\n{{recipe}}\n\n利用可能な食材:\n{{ingredients}}\n\n出力形式(JSON):\n{\n  "missing_ingredients": ["材料1", "材料2"]\n}\n\n不足材料がない場合:\n{\n  "missing_ingredients": []\n}',
                        inputVariables: [
                          {
                            name: 'recipe',
                          },
                          {
                            name: 'ingredients',
                          },
                        ],
                      },
                    },
                    inferenceConfiguration: {
                      text: {
                        temperature: 0.3,
                        maxTokens: 500,
                      },
                    },
                  },
                },
              },
            },
          },
          {
            name: 'CombineResults',
            type: 'Prompt',
            inputs: [
              {
                name: 'recipe',
                type: 'String',
                expression: '$.data',
              },
              {
                name: 'missing',
                type: 'String',
                expression: '$.data',
              },
            ],
            outputs: [
              {
                name: 'modelCompletion',
                type: 'String',
              },
            ],
            configuration: {
              prompt: {
                sourceConfiguration: {
                  inline: {
                    modelId: `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/global.anthropic.claude-haiku-4-5-20251001-v1:0`,
                    templateType: 'TEXT',
                    templateConfiguration: {
                      text: {
                        text: '以下の情報を整形して出力してください。\n\n## レシピ\n{{recipe}}\n\n## 不足材料\n{{missing}}\n\n上記を見やすく整形して、日本語で出力してください。',
                        inputVariables: [
                          {
                            name: 'recipe',
                          },
                          {
                            name: 'missing',
                          },
                        ],
                      },
                    },
                    inferenceConfiguration: {
                      text: {
                        temperature: 0.3,
                        maxTokens: 2000,
                      },
                    },
                  },
                },
              },
            },
          },
          {
            name: 'FlowOutput',
            type: 'Output',
            inputs: [
              {
                name: 'document',
                type: 'String',
                expression: '$.data',
              },
            ],
            configuration: {
              output: {},
            },
          },
        ],
        connections: [
          {
            name: 'FlowInputToCompressImage',
            type: 'Data',
            source: 'FlowInputNode',
            target: 'CompressImage',
            configuration: {
              data: {
                sourceOutput: 'document',
                targetInput: 's3_uri',
              },
            },
          },
          {
            name: 'CompressImageToGenerateRecipe',
            type: 'Data',
            source: 'CompressImage',
            target: 'GenerateRecipe',
            configuration: {
              data: {
                sourceOutput: 'functionResponse',
                targetInput: 'ingredients',
              },
            },
          },
          {
            name: 'GenerateRecipeToCheckMissing',
            type: 'Data',
            source: 'GenerateRecipe',
            target: 'CheckMissingIngredients',
            configuration: {
              data: {
                sourceOutput: 'modelCompletion',
                targetInput: 'recipe',
              },
            },
          },
          {
            name: 'CompressImageToCheckMissing',
            type: 'Data',
            source: 'CompressImage',
            target: 'CheckMissingIngredients',
            configuration: {
              data: {
                sourceOutput: 'functionResponse',
                targetInput: 'ingredients',
              },
            },
          },
          {
            name: 'GenerateRecipeToCombine',
            type: 'Data',
            source: 'GenerateRecipe',
            target: 'CombineResults',
            configuration: {
              data: {
                sourceOutput: 'modelCompletion',
                targetInput: 'recipe',
              },
            },
          },
          {
            name: 'CheckMissingToCombine',
            type: 'Data',
            source: 'CheckMissingIngredients',
            target: 'CombineResults',
            configuration: {
              data: {
                sourceOutput: 'modelCompletion',
                targetInput: 'missing',
              },
            },
          },
          {
            name: 'CombineToOutput',
            type: 'Data',
            source: 'CombineResults',
            target: 'FlowOutput',
            configuration: {
              data: {
                sourceOutput: 'modelCompletion',
                targetInput: 'document',
              },
            },
          },
        ],
      },
    });

やってみた

Streamlit で Bedrock Flows を呼び出すためのアプリを起動します。

スクリーンショット 2025-12-08 18.20.47_2

ほとんど手を動かすことなく、ここまでの UI を作成できてしまうのは驚きですね。

実際の我が家の冷蔵庫から、野菜室の写真を撮影したので、このアプリにアップロードします。
(あまりに雑多すぎるので画像はぼかしました⋯年末にきれいにします⋯)

しばらくすると、プレビュー画像の右側に生成されたレシピが表示されます。

スクリーンショット 2025-12-08 18.23.26_2

レシピが無事に表示されました!ちゃんとしたレシピになっているのか見ていきましょう。

スクリーンショット 2025-12-08 18.37.33

写真に映っていたトマト、にんじん、ほうれんそう、玉ねぎがきちんと検知できています!
ただ、写真には映っていない豆腐、マッシュルーム、きゅうりも含まれてしまっていますね⋯。おそらく、白菜、じゃがいも、青ねぎの葉っぱを誤って認識したのかなと思います。

スクリーンショット 2025-12-08 18.37.43

生成されたレシピは、ちゃんと抽出された食材をベースにして作られていそうですね。
この内容を 30 分で作れる自信はないです。

スクリーンショット 2025-12-08 18.38.04

「クリスマスらしさ」を出すように依頼したので、レシピのクリスマスらしさの説明を足してくれています。確かに色合い的にはクリスマスっぽい気がします。

また、抽出された食材とレシピの差分から、買い出しが必要な食材についてもちゃんと考えてくれています。

苦戦したところ

当初、画像を Bedrock Flows に渡す際、Base 64 エンコードした画像を InvokeFlowinputs[].content に詰めて渡して、さらにその中のノードで処理させることを考えていました。

しかしながら、ペイロードのサイズがあまりに長すぎるからか、 dependencyFailedException が発生してしまい、画像を直接渡す方法は断念しました。

An error occurred (dependencyFailedException) when calling the InvokeFlow operation

ならばと S3 に画像をアップロードし、 Lambda で圧縮し、 Bedrock Flows の Lambda 関数ノードで圧縮した画像を出力することで、次のプロンプトノードに生画像を渡す方法も考えました。

しかしながらこれもだめで、Lambda 側の画像のサイズやレスポンスフォーマット等をいくら調整しても、プロンプトノードが返す生成が画像を全く認識していないような内容になりました。(画像が読み込めません、というエラーは返ってこなかったので、何かは認識しているようでしたが⋯)

そのため、Bedrock Flows で画像を処理するようなフローを組む場合には、その処理を Lambda 関数にオフロードし、その Lambda 関数から Bedrock を別途呼び出して画像を処理させるような形に、ひと手間を加える必要があるかもしれません。

さいごに

さて、若干苦戦するところはあったものの、概ね数時間で生成 AI アプリを作成することができました。
Step Functions を使わずとも直接 Bedrock の生成結果を組み合わせることができるので、生成 AI ワークフローを作成するには非常に便利な機能だなと思いました。

出来上がったフローやレシピには改善の余地が多分にありますが、まずはすぐ動くものを作ってみて、叩いて、改善していくための基盤がツールとして整っているので、何か業務に活用するヒントがあるかもっと触ってみようと思います。

(レシピは作れたら作ります⋯たぶん)

また、Bedrock Flows は CloudFormation や CDK にも対応しています。グラフィカルな UI でさくっとフローを作るのも良いですが、コードの方が Kiro 等のコード生成ツールとも相性が良いので、使いやすい方を選択いただければ良いのかなと思います。

最後に、弊社では生成 AI を活用した業務効率の改善に取り組んでいます。Bedrock や Kiro をはじめとした生成 AI ツールをどのように運用に組み込み、お客様により早く価値をデリバリーできるかを日々検討し、可能なものから施策として取り入れて運用に乗せてしています。

記事を読んで少しでも生成 AI や弊社に興味をお持ちいただけた方は、以下のリンクより弊社ホームページや採用ページをご覧いただければ幸いです。

それでは、最後までお読みいただきありがとうございました。

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイトをご覧ください。

この記事をシェアする

FacebookHatena blogX

関連記事