こんにちは!新卒3期生として7月からアプリ開発チームに配属されました遠藤です。
この記事では、食材管理とChatGPTの連携による献立提案アプリを紹介します。
このアプリの概要やシステム構成、技術的課題の克服方法について、さらに初めてのFlutter開発やChatGPT連携を通じての成長体験もまとめています。ChatGPT連携に興味があるけど、具体的な使い方が分からないという人に向けて書いていますので、ぜひご覧ください!
何を作ったか
このアプリは、購入した食べ物のリストを元にChatGPTが献立を提案してくれるというアプリです。ユーザーは購入した食材の名前や個数、消費期限を入力し、登録したリストから献立を検索できます。ChatGPTとは、ユーザーの質問に対してAIが生成した文章を返してくれるという生成AIの一種です。ChatGPTとのAPI連携により、ユーザーが登録した食べ物を元に最適な献立を提案することができるため、無駄なく食べ物を消費することができます。
取り組んだ背景と目的
今回開発したアプリは私が本配属後初めて取り組んだものです。今後の開発でFlutterを使用していき慣れる必要があり、取り組みました。
また、ChatGPT関連の機能を使ってみたいなと思っていたため、このアプリ制作を通じてChatGPTとの連携をやってみようと思いました。
このアプリを作った目的は、今年4月から一人暮らしを始めて、お得な食べ物を購入しても使い切れずに結局は捨ててしまうということが何回かあり、何とかしたいと思っていたことと、食費を節約でき、食品ロスを防ぐことにつながるということは多くの人にとっても有用なのではないかと思い、作ることにしました。
FlutterとChatGPTの連携
以下が今回私が開発したアプリのシークエンス図です。
製品レベルのプロジェクトでは、アプリ内にAPIキーなどの認証情報を埋め込むことはせず、セキュアなサーバーを利用して通信を仲介するのが一般的ですが、今回は研修用のため、アプリ内に直接ChatGPTのapiキーを持たせて作りました。
class MyHttpRequest { static const _apiKey = ""; Future<List<Recipe>> getRecipes(List<Food> foods, int requestCount) async { //Future<List<Recipe>> String openApiKey = _apiKey; String foodList = ""; for (var food in foods) { foodList += " ${food.name} \n"; } String systemPrompt = """ # 命令 あなたは主婦の視点を持った料理研究家として、優先度が高い順に並んだ食べ物から作成されたレシピを3品教えてくれるようにプログラムされています。 しかし必ず指示された食べ物全てを使わないといけないという訳ではありません。 また、調味料などは自由に使って構いません。その際は献立で使う材料として追加して下さい。 ## Input Format: $foodList """; https://platform.openai.com/docs/guides/gpt/how-should-i-set-the-temperature-parameter var url = Uri.parse('https://api.openai.com/v1/chat/completions'); var body = jsonEncode({ 'model': 'gpt-3.5-turbo',// or 'model': 'gpt-4', 'messages': [ {'role': 'user', 'content': foodList}, {'role': 'system', 'content': systemPrompt} ], 'functions':[ { "name": "get_recipes", "description": "複数の食べ物のリストを基にレシピの提案を3品してください。食べ物リストが少なすぎる場合、素直に'わからない'と答えてください。", "parameters": { "type": "object", "properties": { "recipe1": { "type": "object", "properties":{ "menu": { "type": "string", "description": "提案する3つのレシピのうち1品目のレシピの名前。例:'豚肉の生姜焼き'", }, "ingredients": { "type": "array", "items": { "type": "object", "properties":{ "name":{ "type": "string", "description": "提案する3つのレシピのうち1品目のレシピで使う材料の名前。例1:豚ロース, 例2:玉ねぎ, 例3:薄力粉, 例4:すりおろし生姜, 例5:醤油, 例6:砂糖, 例7:料理酒, 例8:油" }, "quantity":{ "type": "string", "description": "提案する3つのレシピのうち1品目のレシピで使う材料の量。例1:100g, 例2: 1/4個, 例3:適量, 例4:小さじ1, 例5:大さじ1, 例6:小さじ1, 例7:大さじ1, 例8:小さじ1/2" }, }, "required": ["name","quantity"], }, "description": "提案する3つのレシピのうち1品目のレシピで使う材料の配列。", }, "process": { "type": "string", "description": "提案する3つのレシピのうち1品目のレシピの作り方。例:'1. 豚ロースに薄力粉をまぶす。\n2. ボウルにたれの材料(すりおろし生姜, 醤油, 砂糖, 料理酒)を入れて混ぜ合わせる。\n3. 中火で熱したフライパンにごま油をひき、1を入れて焼く。豚ロースに火が通り、三日月型に切った玉ねぎを入れる。\n4.豚肉の両面にこんがりと焼き色が付いたら2を入れる。\n5.中火で炒め合わせ、全体に味がなじんだら完成。'", }, }, }, "recipe2":{ //以下省略 }, "recipe3":{ //以下省略 } }, "required": ["recipe1","recipe2","recipe3"], } } ], "function_call": "auto", "temperature": 0.1, }); var response = await http.post( url, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $openApiKey', }, body: body, ); } }
難しかったところ、課題点とどう対処したか
まず、初めて使うChatGPTとの連携において、いくつか壁がありました。
一つ目は、ChatGPTからのレスポンスのときに、アプリで表示するためのフォーマットである「jsonフォーマット」で返すよう指定したにも関わらず高確率でテキストで返され、正しく情報を取得できないという問題が起きました。しかし、調べていくと、「Function Calling」という方法を見つけ、これを使うことで正しくレスポンスを取得することができました。これについて詳しく説明します。
Function Callingについて
Function Callingは、他のサービスの情報を元にChatGPT本来の自然言語で返すために使います。例えば、ChatGPTはリアルタイムの情報がないため今日の天気を聞いても答えることができません。しかし、今日の天気の情報を取得できるプラグインを用いて取得した情報を基にChatGPTに質問することで、自然言語でリアルタイムの天気の情報を送ってくれ、ユーザ側ではChatGPTからリアルタイムの天気の情報が送られることになります。
上記のコードは、Function Callingを用いてpostリクエストしたコードです。postリクエストを送るときにbodyという中身にchatGPTのバージョン情報や受け取りたい情報のフォーマットの指定、その他chatGPTの設定情報などを与えます。functionsというパラメータで「get_recipes」と名付けた関数を作成しています。function_callというパラメータをautoとすることで、実行される関数が自動で選択されます。今回はfunctionsで作成している関数は一つしかないので、autoにしても必然的に一つの関数に決まるのですが、複数関数を作成すると中身の情報を見て自動的に関数を選択し、欲しいフォーマットに沿った情報を返してくれるそうです。
今回のアプリでは、本来のFunction Callingの目的の使い方はせず、欲しいフォーマットで返してもらうという目的で使いました。 Function Callingの使い方に関しては、以下の公式ドキュメントを参考に作成しました。
Function Callingの使い方 platform.openai.com
次に、レスポンスのフォーマットが正しくなったのはいいものの、今度は一部の情報が空で返ってくるという課題に直面しました。この課題については、調べて試してもなかなか改善できなかったのと、そもそも概念的な理解が難しかったため、日々の業務でChatGPTの開発を行っている陳さんからアドバイスをいただきました。具体的な対処法としては、以下の3つの点を改善しました。
方法1:ChatGPTのバージョンをアップデートする
方法2:温度(temperature)プロパティの調整
方法3:functionsで指定するプロパティ「description」の説明を詳細に記述する
まず、ChatGPTのバージョンをgpt-3.5-turboからgpt-4にアップデートすることで、より高度なレスポンスが得られるようにしました。これを試した時点で、欲しい情報が入った状態で返ってきたため、一応課題は解決できましたが、gpt-3.5-turboに比べると一回のリクエストで約20倍コストがかかるようで、一回一回のリクエストの重みを感じながら使いました(笑)バージョンを落としてでも望ましい回答が得られる方がいいと思い、他の方法も試しました。
次は、温度(temperature)プロパティの調整をすると、回答の多様性をコントロールできるというものです。値が低ければ低いほどより確実な答えが返り、高いほどより創造的な回答が返ってきます。
実際に試してみました! 今回は使って欲しい食材を「卵、ゴーヤ、キウイ、ほうれん草」にして、chatGPTに献立を提案してもらいました。
バージョン3.5 値が低いとき(temperature = 0.1)
値が高いとき(temperature = 0.9)
値が低いときは味が想像できるレシピを提案してくれましたが、値が高くなると「キウイとゴーヤのサラダ」という何とも言えないレシピを提案してくれました(笑)
さらに、descriptionを充実させることで、より細かい情報を得ることができるようにしました。これは3つの中で単純でありながら効果が高い方法だと感じました。
さらにやってみたいこと
画像生成で、料理の画像を表示してみたいです。 世に出ている料理検索アプリは画像や動画が基本的にはあります。画像や動画があると、視覚的に分かりやすくなり、ユーザーの作るモチベーションに繋がると思うので、画像も表示できたらもっと使いやすくなるかなと思いました。
まとめ(所感)
Flutterでの開発は初めてだったので、APIから受け取ったデータや関数の定義の型などを意識しなければならないのが新鮮でした。また、今までの開発ではクラスやインスタンスを利用したデータの受け渡しや操作を行わず一つのファイルで変数を多用するという開発をしていたため、データの構造が見やすくなり、結果的に実装しやすくなるということが分かりました。 chatGPTは、基本的な知識しかなかったため、概念的な理解を理解するのがそもそも難しいなと感じました。今回動くものは作れましたが、完全に理解できたとは言えないので、これからも少しずつ勉強していきたいです。
参考資料
Tempertureについて
platform.openai.com
Function Callingについて
Kingsoftは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!