「飲み会でのささやかな抵抗」NomikAI - AIが教えてくれる、食べていい量の話
はじめに:飲み会の楽しさと、翌朝の絶望感
こんにちは、第4回 Agentic AI Hackathon with Google Cloudに参加しているFLINTERSのチーム「NomikAI」です。
「飲み会、楽しいはずなのに、心のどこかでずっとカロリーを気にしている自分がいる」
そんな経験はありませんか?
自分でメニューを選べるランチなら、サラダセットにしたり、ご飯を少なめにしたりとコントロールが可能です。しかし、 「飲み会」という場において、私たちの『食の主権』はしばしば失われます。
-
断りづらい空気感: 目の前に運ばれてくる揚げ物の盛り合わせ。幹事が良かれと思って選んだボリューム満点のコース料理。「残すのは申し訳ない」「周りに合わせて食べなきゃ」という心理的プレッシャー。
-
先の見えない恐怖: 「次は何が来る?」「まだメインがあるの?」。ゴールが見えないまま食べ進め、気づいた時にはもう手遅れ。翌朝、体重計の上で絶望するまでがセットです。
-
楽しめない葛藤: 「これを食べたら、明日の昼を抜かなきゃ…」。そんな計算を頭の片隅でしているせいで、せっかくの会話も、美味しいお酒も、心から楽しめなくなってしまいます。
「健康管理はしたい。でも、飲み会の場を白けさせたくもない。」
そんな世の大人たちが抱える『ささやかな、でも切実な悩み』を解決したい。 その想いから生まれたのが、飲み会専用のAI栄養士 NomikAI(ノミカイ) です。
スマホをサッと取り出して写真を撮る。それだけで、AIが「今この瞬間の正解」を教えてくれます。
NomikAIとは?
NomikAIは、食事画像からカロリーを推定し、あなたに最適な食べ方をアドバイスするAIアプリです。飲み会やコース料理など、自分で選べない食事シーンでも、スマートに健康管理ができるよう設計されています。
主な機能
1. 3段階の食べ方提案
目の前の料理を撮影すると、AIが食品を識別してカロリーを推定。さらに、残りのカロリーと品数を考慮して、次の3つの食べ方を提案します。
- 少なめ(30%): カロリーを抑えたいときに。控えめに
- 推奨(70%): バランス重視。次の料理も考慮した食べ方
- 多め(100%): しっかり食べたいときに。全量OK
タップ1つで選択でき、実際に食べた量が自動計算されます。「この唐揚げ、全部食べていいの?」という迷いがなくなります。
2. コース料理の品数管理
コース料理で特に便利なのが、残り品数の入力機能。「あと3品来る」とわかっていれば、AIは後半の料理も考慮したアドバイスをしてくれます。デザートまで楽しめる余裕を残しながら、バランスよく食べられるのがポイントです。
3. ユーザー登録不要のシンプル設計
面倒な会員登録は一切不要。アプリを開いて、目標カロリーを設定したら即スタート。ブラウザにデータを保存するため、ページをリロードしても情報が保持されます。

使い方フロー
- セッション開始: 目標カロリーと品数(オプション)を入力
- 料理を撮影: テーブルに運ばれた料理をスマホで撮影
- AIが分析: Gemini 2.5 Flashが食品名とカロリーを推定
- 食べ方を選択: 少なめ・推奨・多めから選んでタップ
- 次の料理へ: 残りカロリーが更新され、次の料理へ最適なアドバイス
シンプルな4ステップで、複雑な計算は不要。データはブラウザ内にのみ保存されるため、プライバシーも安心です。
UI/UXのポイント
NomikAIはモバイルファーストで設計されています。飲み会の席でサッと使えるよう、大きなボタン、わかりやすいアイコン、直感的な操作を追求しました。
デザインシステムには shadcn/ui を採用。Tailwind CSSとRadix UIをベースにした、モダンで洗練されたUIコンポーネントで構成されています。カロリー残量の視覚化、料理履歴のカルーセル表示など、見た目の良さと使いやすさを両立しました。
「飲み会で、もう料理選びに悩まない」。NomikAIは、そんな未来を実現するために生まれたアプリです。

システムアーキテクチャ
NomikAIは、Google Cloud上でフロントエンド(Next.js)とバックエンド(Node.js/Express)を完全分離した構成を採用しています。両者はCloud Runで動作し、IAM認証によって安全に連携します。
技術スタックの選定理由
Gemini 2.5 Flashを採用した理由は、画像認識とテキスト生成を同時に高速処理できる点にあります。食事画像から料理名とカロリーを推定し、残りカロリー数に基づいたアドバイスを1回のAPIコールで生成できるため、レスポンス時間を最小化できます。コース料理対応のような複雑なプロンプトでも安定した構造化出力が得られる点も決定的でした。
Google Genkitは型安全なAIフロー構築を可能にするフレームワークです。Zodスキーマを用いて入出力を厳密に定義することで、AIモデルの応答が期待する形式を満たすことを保証します。プロンプトとロジックを分離できるため、プロンプト改善のイテレーションが高速化されます。
Cloud Runの採用は、オートスケーリングと従量課金の両立が理由です。ハッカソンフェーズでは無料枠内で運用でき、ユーザー増加時には自動的にスケールします。第2世代実行環境(gen2)を指定することでコールドスタートを改善し、ユーザー体験を向上させています。
Terraformによるインフラのコード化は、環境の再現性と変更の追跡可能性を実現します。GitHub ActionsでTerraform Planを自動実行し、インフラ変更をレビュー可能にしています。
VPCを使わない設計判断
当初はVPC ConnectorでFirestoreとの通信を保護する案も検討しましたが、以下の理由で採用しませんでした。
コスト削減: VPC Connectorは月額約7ドルの固定費が発生します。ハッカソンフェーズでは他のサービスがほぼ無料枠で収まるため、この固定費は相対的に大きな負担です。
十分なセキュリティ: FirestoreとCloud StorageはIAMで適切にアクセス制御されており、VPCなしでも安全性は確保されています。Cloud Run間の通信はIAM認証で保護されるため、ネットワークレベルの分離は不要と判断しました。
IAM認証フロー
フロントエンドからバックエンドへのリクエストは、Google認証ライブラリでIDトークンを取得し、Authorization: Bearer <ID_TOKEN>ヘッダーに含めて送信されます。バックエンド側のCloud RunはIAM認証を必須とし、roles/run.invoker権限を持つフロントエンドのサービスアカウント以外からのアクセスを拒否します。この設定により、公開されているバックエンドのURLに直接アクセスしても認証エラーとなり、不正利用を防ぎます。
技術的工夫とハイライト
4-1. Google Genkitによる型安全なAIフロー
const AnalyzeMealAndSuggestRefinementInputSchema = z.object({
photoDataUri: z.string().describe('A photo of the meal...'),
remainingCalories: z.number().describe('The remaining calorie intake for the day.'),
remainingDishes: z.number().optional().describe('The number of remaining dishes in the course meal (optional).'),
});
const AnalyzeMealAndSuggestRefinementOutputSchema = z.object({
foodName: z.string().describe('The name of the main dish in the photo, in Japanese.'),
calorieEstimate: z.number().describe('The estimated calorie count of the meal.'),
suggestedRefinement: z.string().describe('A suggestion on how to adjust calorie intake based on the meal.'),
verdict: z.enum(['OK', 'CAUTION']).describe('A verdict on whether the user should eat the meal.'),
});
const analyzeMealAndSuggestRefinementFlow = ai.defineFlow(
{
name: 'analyzeMealAndSuggestRefinementFlow',
inputSchema: AnalyzeMealAndSuggestRefinementInputSchema,
outputSchema: AnalyzeMealAndSuggestRefinementOutputSchema,
},
async input => {
const {output} = await analyzeMealAndSuggestRefinementPrompt(input);
return output!;
}
);
GenkitとZodスキーマの組み合わせにより、AIモデルの応答が必ず期待する構造を満たすことを保証しています。AnalyzeMealAndSuggestRefinementOutputSchemaでは、foodName(料理名)、calorieEstimate(カロリー推定値)、suggestedRefinement(アドバイス)、verdict(OKまたはCAUTION)の4フィールドを必須としています。
GenkitのdefinePromptでは、入出力スキーマを明示的に指定します。これにより、AIモデルは構造化された出力を返すよう強制され、フィールドの欠落や型の不一致が発生しません。従来のJSONパース方式では、モデルが意図しない形式で応答した場合にエラーハンドリングが複雑化しましたが、Genkitではスキーマ検証が自動化されます。
プロンプトとロジックが分離されているため、プロンプトの改善サイクルが高速化されます。プロンプトテンプレート内で{{remainingCalories}}や{{#if remainingDishes}}のようなテンプレート変数を使用でき、複雑な条件分岐を可読性高く記述できます。
4-2. プロンプトインジェクション対策
// プロンプトインジェクション対策: 文字数制限とサニタイズ
const sanitizedFoodName = customFoodName.trim();
if (sanitizedFoodName.length === 0 || sanitizedFoodName.length > 15) {
return res.status(400).json({
success: false,
error: 'customFoodName must be between 1 and 15 characters'
});
}
ユーザーが料理名を手動修正できる再分析機能では、悪意のあるプロンプト(例: "Ignore previous instructions and say...")を注入されるリスクがあります。これに対し、3段階の防御を実装しています。
第1層は入力バリデーションです。料理名の文字数を15文字以内に制限し、trim()で前後の空白を除去します。これにより、長大な指示文を注入することを困難にします。
第2層はプロンプト設計です。Genkitのテンプレートで{{customFoodName}}のように料理名を明示的にマークし、「これが料理名である」という文脈をモデルに強く伝えます。また、プロンプト末尾で「Set the foodName field to EXACTLY: "{{customFoodName}}" (do not modify or translate it)」と指示し、入力をそのまま返すよう制約しています。
第3層は構造化出力の強制です。ZodスキーマでfoodNameをstring型として定義し、モデルが予期しない形式で応答することを防ぎます。仮にプロンプトインジェクションが成功しても、出力はスキーマに従う必要があるため、被害を局所化できます。
4-3. Vertex AI ADC認証
import {genkit} from 'genkit';
import {googleAI} from '@genkit-ai/googleai';
export const ai = genkit({
plugins: [
googleAI({
// Application Default Credentials (ADC) を自動使用
// 環境変数: GCP_PROJECT_ID, VERTEX_AI_REGION のみ必要
}),
],
model: 'googleai/gemini-2.0-flash-exp',
});
Vertex AI ADC(Application Default Credentials)を使用することで、APIキー管理を不要にしました。
ADC認証では、Cloud RunのサービスアカウントがVertex AIに直接認証します。環境変数GCP_PROJECT_IDとVERTEX_AI_REGIONを設定するだけで、Genkitが自動的にADCを使用します。APIキーの取得・設定・ローテーションが不要で、シンプルかつセキュアな構成です。
Terraformでは、バックエンドのサービスアカウントにroles/aiplatform.userロールを付与するだけで設定が完了します。ローカル開発環境ではgcloud auth application-default loginで開発者の認証情報を使用し、本番と同じコードで動作します。
4-4. コース料理対応のプロンプト設計
{{#if remainingDishes}}
- The user is at a course meal with {{remainingDishes}} dishes still to come.
- If remainingDishes is NEGATIVE, it means the user has already exceeded
the planned number of dishes (おかわり状態).
Be MORE cautious with recommendations.
- Consider how much of this dish they should eat, keeping in mind the remaining dishes.
{{/if}}
{{#if remainingDishes}}
- Consider the balance between remaining dishes ({{remainingDishes}})
and remaining calories ({{remainingCalories}}kcal) to provide pacing advice:
* If many dishes remain but calories are running low:
Suggest restraint now (e.g., "この後まだ{{remainingDishes}}品来ることを考えて、今は少し控えめにしましょう")
* If few dishes remain but calories are still plenty:
Encourage eating more (e.g., "残り{{remainingDishes}}品でカロリーはまだ{{remainingCalories}}kcal残っているので、前半我慢した分、食べちゃっても良いかもしれませんね")
* For balanced situations: Give straightforward pacing advice
{{/if}}
コース料理では「残り料理数」と「残りカロリー」のバランスを考慮したアドバイスが求められます。序盤で食べ過ぎると後半で調整が困難になり、逆に我慢しすぎると満足度が低下します。
プロンプトでは、{{#if remainingDishes}}の条件分岐で通常の食事とコース料理を区別します。コース料理モードでは、以下の3パターンのアドバイスをモデルに指示しています。
パターン1: 残り料理数が多いがカロリーが少ない → 「この後まだ{{remainingDishes}}品来ることを考えて、今は少し控えめにしましょう」のような自制を促すメッセージ。
パターン2: 残り料理数が少ないがカロリーが十分残っている → 「残り{{remainingDishes}}品でカロリーはまだ{{remainingCalories}}kcal残っているので、前半我慢した分、食べちゃっても良いかもしれませんね」のような積極的なメッセージ。
パターン3: バランスが取れている → 標準的なペース配分のアドバイス。
また、「おかわり」で料理数がマイナスになった場合(remainingDishes < 0)は、よりCAUTION寄りの判定をするよう指示しています。
ユーザーは「提案通り」「もっと食べる」「少しだけ」の3択ボタンで摂取量を選択します。プロンプトでは、70%レベルの具体的な量(「提案としては[X個/X杯]がおすすめです」)と、100%の場合のカロリー(「全部食べると約[カロリー]kcalになります」)の両方を含めるよう指示し、ユーザーが情報に基づいた選択をできるようにしています。
インフラとデプロイ
NomikAIのインフラは Google CloudとTerraformによるIaC(Infrastructure as Code) で構成されています。手動でポチポチ設定する煩わしさを排除し、再現性とメンテナンス性を重視した設計です。
Terraformの構成
主要なファイル構成は以下の通りです。
-
apis.tf: 必要なGoogle Cloud APIを有効化(Cloud Run、Artifact Registry、Vertex AI、IAM) -
artifact-registry.tf: Dockerイメージを保存するリポジトリ(nomikai-images)を作成 -
iam.tf: フロントエンドとバックエンド用のサービスアカウントを定義。バックエンドにはroles/aiplatform.userを付与してVertex AI(Gemini API)へのアクセスを許可 -
cloud-run.tf: フロントエンドとバックエンドのCloud Runサービスを定義。バックエンドはIAM認証必須で、フロントエンドのサービスアカウントにのみ呼び出し権限を付与(roles/run.invoker)。フロントエンドはallUsersに公開
ADC(Application Default Credentials)認証により、サービスアカウントに直接Vertex AIの権限を付与することで、APIキー管理を不要にしています。
デプロイフロー
NomikAIはGitHub Actionsを使ったCI/CDパイプラインで運用されています。
アプリケーションデプロイ(自動)
mainブランチへのpushで自動的にデプロイが実行されます:
- セキュリティスキャン: Trivyで脆弱性チェック
- Dockerイメージビルド: フロントエンド・バックエンドを個別にビルド
- Artifact Registry: イメージをpush(commit SHA + latest タグ)
-
Cloud Run:
gcloud run deployで即座に反映
インフラ管理(Terraform)
Terraformによるインフラ変更は手動承認フローで安全に管理されています。
コスト見積もり
Cloud Runは使った分だけ課金のため、小規模アプリには最適です。min_instances = 0に設定することで、アクセスがない時間帯は料金が発生しません。
- Cloud Run: 月額約$5〜$15(アクセス頻度によって変動)
- Artifact Registry: 月額約$0.10〜$1(ストレージ容量)
- Vertex AI(Gemini 2.5 Flash): 月額約$5〜$10(API呼び出し回数に依存)
合計で月額$10〜$25程度の想定です。ハッカソン用の小規模プロジェクトとしては十分なコスト効率です。
開発の裏話と学び
NomikAIの開発は技術検証と実用性のバランスを探る旅でした。ここでは特に印象的だった点と失敗談を振り返ります。
Genkitの使いやすさ
Firebase GenkitはAIフローの構築が驚くほど簡単でした。プロンプトのバージョン管理、モデル切り替え、入出力のバリデーションをコードベースで管理できる点が素晴らしく、従来のOpenAI APIの直接呼び出しに比べて保守性が格段に向上します。特にai.prompt()によるプロンプトの定義と、generateメソッドでのスキーマ指定(Zod)は、型安全性とLLMの柔軟性を両立する最高の体験でした。
Gemini 2.5 Flashのマルチモーダル性能
料理写真の解析において、Gemini 2.5 Flashは想像以上の精度を発揮しました。唐揚げ、サラダ、ラーメンなど多様なメニューを正確に識別し、カロリー推定も実用レベルです。コスト($0.075/1Mトークン)と性能のバランスが良く、個人開発には最適なモデルです。
Cloud RunのIAM認証のシンプルさ
当初はAPI Gatewayを検討していましたが、Cloud RunのIAM認証だけで十分でした。フロントエンドのサービスアカウントにバックエンドの呼び出し権限(roles/run.invoker)を付与するだけで、セキュアな内部通信が実現します。フロントエンドのNode.jsサーバーからGoogleAuthライブラリでID Tokenを取得し、バックエンドにリクエストを送る構成はシンプルかつ堅牢です。
品数管理のエッジケース
セッション機能(SESSION_FEATURE_DESIGN.md)の設計中、「品数がゼロになったらどうするか?」「予定より多く料理が来た場合は?」といったエッジケースに直面しました。UIでは品数を任意項目にし、ゼロになっても警告のみで追加を許可する柔軟な設計にしました。実際の飲み会では計画通りにいかないことが多いため、現実的な妥協点を見つけることができました。
考えたけれど今回は実装しなかったこと
ハッカソンの限られた期間内でコア体験(写真を撮ってアドバイスをもらう)を最優先したため、今回は見送ったアイデアたちがいくつかあります。これらを実装することで、NomikAIはより良いアプリに進化できると考えています。
-
飲み物のカロリーへの対応
- 現状は料理のみを対象としていますが、飲み会の主役である「お酒」のカロリーや糖質も無視できません。飲みすぎへの警告も出せたら良いなと感じています。
-
カロリー推定の精度アップ
- 写真1枚では、例えば「唐揚げの下にポテトが隠れている」といったケースを完全に見抜くのが難しい場合があります。Vertex AI のマルチモーダル機能をフル活用したり、分析専用のエージェントを構成したりすることで、カロリー推定の精度をさらに高めたいと考えています。
-
メニュー分析の速度アップ(UXの向上)
- Gemini 2.5 Flashを利用しているとはいえ、現在は解析完了までに10〜20秒ほどの待ち時間が発生しています。写真が選択・追加された瞬間に、ユーザーが分析ボタンを押すのを待たずにリクエストを投げておくなどの工夫や、バックエンドの処理ロジックをパフォーマンス重視でリファクタリングすることで、飲み会の会話を妨げないようなUXを追求したいです。
-
Google カレンダーとの連携
- Google Workspaceとの親和性を活かし、カレンダーの予定から「明日も飲み会がある」「明日の朝は早い」といった情報を取得。それに基づいて「明日に備えて今日は1段階厳しめにアドバイスしますね」といった、提案ができると長期的な健康管理ができそうです。
-
食事履歴の保存とパーソナライズ(ユーザー登録)
- 「昨日は何を食べたか」を振り返る機能です。現在はステートレスな設計ですが、Firebase Authによるユーザー認証を導入し、Cloud FirestoreやCloud Storage(GCS)に解析結果を蓄積することで、ユーザーの好みに合わせた長期的な健康アドバイスが可能になると考えています。
まとめ
NomikAIは「飲み会で健康管理なんて無理だよ」という諦めに対するささやかな抵抗です。完璧なカロリー管理はできなくても、少し気にかけるだけで罪悪感が減り、翌日の体調も変わるかもしれません。
ハッカソンを通じて、Firebase Genkit、Gemini 2.5 Flash、Cloud RunのIAM認証などを実戦投入できたことは大きな学びでした。特に「シンプルな設計ほど強い」という原則を再確認できたことは、今後の開発に活かせる財産です。
もし興味があれば、ぜひ試してみてください!
Discussion