生活記録アプリにAI(Gemini API)によるライフガイド機能を実装した話 〜プロンプトエンジニアリング中心〜
紹介するアプリケーション
https://sbm-app.com/
AIのライフガイド分析機能は現在、スマホ(Android)のみの実装になっています。
→細かいところがまとまり次第Play申請予定になっています。
はじめに
このアプリは開発中のものなので、追記・修正・削除を行うことがありえます。
過去に実装した同アプリのAPIとWEBUIのNLPを用いた
カテゴリ分析記事:https://qiita.com/kaminuma/items/d4b079e10756e72bcda8
の追加の機能となります。
日々の活動や気分を記録するライフログアプリを開発していましたが、「記録して終わり」ではもったいない!ユーザーの記録データを活用して、個別の生活改善アドバイスを提供できないかと考えました。
そこで、Gemini APIを活用したAIライフガイド機能を実装しました。
この記事では、実際の実装コードを基に、どのようにユーザーの生活データを分析し、パーソナライズされたアドバイスを生成しているかを紹介します!
対象読者
- プロンプトエンジニアリングに興味がある方
- Gemini APIの実装例を探している方
- ライフログアプリのAI機能拡張に関心がある方
- パーソナライズ機能の設計に興味がある方
アプリの概要
既存機能
- 活動記録: 日々の活動をカテゴリ別に時間付きで記録
- 気分記録: その日の気分を5段階で記録
- カレンダー表示: 週表示/月表示での記録確認
新機能:AIライフガイド
- 個別分析: ユーザーの記録データを基にした行動パターン分析
- パーソナライズアドバイス: Geminiによる生活改善提案
- 設定可能な分析軸: 気分重視、活動重視、ウェルネス重視、バランス重視
- 口調カスタマイズ: 親しみやすい、専門的、励まし重視、カジュアル
システム構成
Android App (Kotlin) → 自作API (Spring Boot) → Gemini API
- フロントエンド: Android(Kotlin + Jetpack Compose)
- バックエンド: Spring Boot API
- AI: Google Gemini API
- データベース: MySQL 8.0
実装の核心:AIService クラス
1. データ取得とプロンプト生成
AIServiceクラスは、ユーザーのライフログデータを取得し、Gemini APIに送信するプロンプトを生成する中核的な役割を担っています。
@Service
public class AIService {
@Value("${gemini.api.key}")
private String geminiApiKey;
@Value("${gemini.api.url}")
private String geminiApiUrl;
public AIAnalysisResponseDto generateAnalysis(AIAnalysisRequestDto request, String userId) {
// 1. データ取得
List<ActivityGetEntity> activities = activityService.getActivitiesByUserAndDateRange(
userId, request.getStartDate(), request.getEndDate());
List<MoodRecordEntity> moodRecords = moodRecordService.getMoodRecordsByUserAndDateRange(
userId, request.getStartDate(), request.getEndDate());
// 2. データ存在チェック
if (activities.isEmpty() && moodRecords.isEmpty()) {
// エラーレスポンス返却
}
// 3. プロンプト生成
String prompt = generatePrompt(request, activities, moodRecords);
// 4. Gemini API呼び出し
return callGeminiAPI(prompt);
}
}
処理の流れ:
- データ取得フェーズ: ユーザーが指定した期間(通常は過去1週間)の活動記録と気分記録をデータベースから取得
- バリデーション: データが存在しない場合は、分析不可能としてエラーメッセージを返却
- プロンプト生成: 取得したデータと、ユーザーが選択した分析設定(分析軸、口調、詳細度)を組み合わせて、AIへの指示文を構築
- API呼び出し: 生成したプロンプトをGemini APIに送信し、分析結果を取得
2. プロンプトエンジニアリングの実装
プロンプトエンジニアリングは、AIから質の高い回答を得るための最も重要な要素です。ここでは、AIが理解しやすい形で情報を構造化し、明確な指示を与えています。
プロンプトの基本構造
プロンプトは大きく6つのセクションで構成されています:
- ペルソナ設定: AIの役割を明確化
- 分析方針: どの観点を重視するか
- 応答スタイル: どのような口調で答えるか
- 詳細レベル: どの程度詳しく分析するか
- データセクション: 実際の活動・気分データ
- 出力形式指定: JSON形式での構造化された回答を要求
private String generatePrompt(AIAnalysisRequestDto request,
List<ActivityGetEntity> activities,
List<MoodRecordEntity> moodRecords) {
StringBuilder prompt = new StringBuilder();
// A. ペルソナ設定
prompt.append("あなたは親しみやすい生活コーチです。以下のデータを分析してユーザーにインサイトを提供してください:\n\n");
// B. 分析方針の設定
switch (request.getAnalysisFocus()) {
case "MOOD_FOCUSED":
prompt.append("【分析方針】感情とメンタル面に重点を置いて分析してください。\n");
break;
case "ACTIVITY_FOCUSED":
prompt.append("【分析方針】活動パターンと時間配分に重点を置いて分析してください。\n");
break;
case "BALANCED":
prompt.append("【分析方針】気分と活動の両方をバランス良く分析してください。\n");
break;
case "WELLNESS_FOCUSED":
prompt.append("【分析方針】健康とウェルビーイングの観点で分析してください。\n");
break;
}
// C. 応答スタイルの設定
switch (request.getResponseStyle()) {
case "FRIENDLY":
prompt.append("【口調】親しみやすくフレンドリーな口調で回答してください。\n");
break;
case "PROFESSIONAL":
prompt.append("【口調】客観的で専門的な口調で回答してください。\n");
break;
case "ENCOURAGING":
prompt.append("【口調】ポジティブで励ましを含む口調で回答してください。\n");
break;
case "CASUAL":
prompt.append("【口調】親しい友人のような気軽な口調で回答してください。\n");
break;
}
// D. 詳細レベルの設定
switch (request.getDetailLevel()) {
case "CONCISE":
prompt.append("【詳細度】要点を絞った簡潔な分析にしてください。\n");
break;
case "STANDARD":
prompt.append("【詳細度】適度な詳細度で分析してください。\n");
break;
case "DETAILED":
prompt.append("【詳細度】包括的で詳しい分析にしてください。\n");
break;
}
// E. データ部分の追加
prompt.append(formatActivityData(activities));
prompt.append(formatMoodData(moodRecords));
// F. 出力形式の指定
prompt.append("以下のJSON形式で回答してください:\n");
prompt.append("{\n");
prompt.append(" \"overall_summary\": \"全体的な分析サマリー(100-300文字)\",\n");
prompt.append(" \"mood_insights\": \"感情・気分に関する洞察(100-200文字)\",\n");
prompt.append(" \"activity_insights\": \"活動パターンに関する洞察(100-200文字)\",\n");
prompt.append(" \"recommendations\": \"具体的な改善提案・アドバイス(100-250文字)\"\n");
prompt.append("}\n");
return prompt.toString();
}
3. データフォーマット処理
活動データの構造化
ユーザーの活動記録は、そのままではAIが理解しにくい形式になっています。そこで、以下のような処理を行って、AIが分析しやすい形に変換しています。
データ構造化の工夫ポイント:
- 時系列順に整理して、1日の流れを把握しやすくする
- カテゴリ別に集計して、活動パターンを可視化
- 具体的な内容(contents)も含めて、文脈を提供
private String formatActivityData(List<ActivityGetEntity> activities) {
if (activities.isEmpty()) {
return "### 活動記録\n記録された活動はありません。\n";
}
StringBuilder formatted = new StringBuilder();
formatted.append("### 活動記録詳細\n");
// 個別の活動を時系列で表示
for (ActivityGetEntity activity : activities) {
formatted.append(String.format("%s %s-%s: %s [%s] - %s\n",
activity.getDate().toString(),
activity.getStart() != null ? activity.getStart().toString() : "",
activity.getEnd() != null ? activity.getEnd().toString() : "",
activity.getTitle(),
activity.getCategory(),
activity.getContents() != null ? activity.getContents() : ""));
}
// カテゴリ別集計の追加
Map<String, Long> categoryStats = activities.stream()
.collect(Collectors.groupingBy(
ActivityGetEntity::getCategory,
Collectors.counting()
));
formatted.append("\n### カテゴリ別集計\n");
categoryStats.forEach((category, count) ->
formatted.append(String.format("%s: %d件\n", category, count))
);
return formatted.toString();
}
気分データの可視化
気分データは単なる数値(1〜5)だけでは味気ないため、以下の工夫を加えています:
可視化の工夫:
- 絵文字の活用: 数値を視覚的に理解しやすくする(😞😔😐😊😄)
- 統計情報の追加: 平均値、最高値、最低値を計算し、全体傾向を把握
- メモの活用: ユーザーが記録したメモを含めることで、気分変化の理由を提供
これにより、AIは「なぜその日の気分がそうだったのか」という文脈を理解し、より適切なアドバイスを生成できます。
private String formatMoodData(List<MoodRecordEntity> moodRecords) {
if (moodRecords.isEmpty()) {
return "### 感情記録\n記録された感情データはありません。\n";
}
StringBuilder formatted = new StringBuilder();
formatted.append("### 感情記録\n");
// 各日の気分を絵文字付きで表示
for (MoodRecordEntity mood : moodRecords) {
String emoji = getMoodEmoji(mood.getMood());
formatted.append(String.format("%s: %s (%d/5) - %s\n",
mood.getDate().toString(),
emoji,
mood.getMood(),
mood.getNote() != null ? mood.getNote() : ""));
}
// 統計情報の計算と追加
double avgMood = moodRecords.stream()
.mapToInt(MoodRecordEntity::getMood)
.average()
.orElse(0.0);
int maxMood = moodRecords.stream()
.mapToInt(MoodRecordEntity::getMood)
.max()
.orElse(0);
int minMood = moodRecords.stream()
.mapToInt(MoodRecordEntity::getMood)
.min()
.orElse(0);
formatted.append(String.format("\n### 感情統計\n"));
formatted.append(String.format("平均ムード: %.1f/5\n", avgMood));
formatted.append(String.format("最高: %d 最低: %d\n", maxMood, minMood));
return formatted.toString();
}
// 気分レベルを絵文字に変換
private String getMoodEmoji(int moodLevel) {
switch (moodLevel) {
case 1: return "😞";
case 2: return "😔";
case 3: return "😐";
case 4: return "😊";
case 5: return "😄";
default: return "😐";
}
}
4. Gemini API呼び出し実装
プロンプトが完成したら、いよいよGemini APIを呼び出します。ここでは、Spring BootのRestTemplateを使用して、HTTPリクエストを送信しています。
API呼び出しの重要ポイント:
- セキュアな認証: APIキーをヘッダーに含めて送信(URLには含めない)
- 適切なパラメータ設定: temperature(創造性)とmaxOutputTokens(回答長)を調整
- エラーハンドリング: API呼び出し失敗時の適切な処理
private AIAnalysisResponseDto callGeminiAPI(String prompt) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("x-goog-api-key", geminiApiKey);
// Gemini API仕様に準拠したリクエストボディ
Map<String, Object> requestBody = Map.of(
"contents", List.of(Map.of("parts", List.of(Map.of("text", prompt)))),
"generationConfig", Map.of(
"temperature", 0.7, // 創造性のバランス
"maxOutputTokens", 1000 // 回答の長さ制限
)
);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
try {
ResponseEntity<Map> response = restTemplate.exchange(
geminiApiUrl,
HttpMethod.POST,
entity,
Map.class
);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
return parseGeminiResponse(response.getBody());
} else {
throw new RuntimeException("Gemini APIから異常なレスポンスが返されました");
}
} catch (Exception e) {
logger.error("Gemini API呼び出しエラー", e);
throw new RuntimeException("Gemini API呼び出しエラー: " + e.getMessage());
}
}
5. レスポンス解析とJSON抽出
Gemini APIからの回答は、複雑なネスト構造になっています。また、AIの回答には説明文が含まれることもあるため、JSON部分だけを正確に抽出する必要があります。
レスポンス処理の課題と解決策:
-
課題1: Gemini APIの複雑なレスポンス構造
- 解決: candidates → content → parts という階層構造を正しく辿る
-
課題2: JSON以外のテキストが含まれる可能性
- 解決: extractJsonFromText()メソッドで確実にJSON部分を抽出
-
課題3: 不正なJSON形式への対応
- 解決: ObjectMapperによる構文検証を実施
private AIAnalysisResponseDto parseGeminiResponse(Map<String, Object> responseBody) {
try {
// Gemini APIレスポンス構造からテキストを抽出
List<Map<String, Object>> candidates = (List<Map<String, Object>>) responseBody.get("candidates");
Map<String, Object> content = (Map<String, Object>) candidates.get(0).get("content");
List<Map<String, Object>> parts = (List<Map<String, Object>>) content.get("parts");
String text = (String) parts.get(0).get("text");
// JSON部分を抽出
String jsonText = extractJsonFromText(text);
// JSONをパースしてレスポンス構築
TypeReference<Map<String, String>> typeRef = new TypeReference<Map<String, String>>() {};
Map<String, String> insights = objectMapper.readValue(jsonText, typeRef);
AIAnalysisResponseDto response = new AIAnalysisResponseDto();
response.setSuccess(true);
AIAnalysisResponseDto.AIInsightData data = new AIAnalysisResponseDto.AIInsightData();
data.setOverallSummary(insights.get("overall_summary"));
data.setMoodInsights(insights.get("mood_insights"));
data.setActivityInsights(insights.get("activity_insights"));
data.setRecommendations(insights.get("recommendations"));
response.setData(data);
return response;
} catch (Exception e) {
logger.error("Gemini APIレスポンス解析エラー", e);
// エラーレスポンス返却
}
}
6. 堅牢なJSON抽出ロジック
AIの回答からJSON部分を抽出するのは、意外と難しい処理です。AIは時々、JSONの前後に説明文を付けたり、マークダウン形式で回答したりするためです。
JSON抽出の戦略:
-
マークダウン形式への対応:
```json ```
で囲まれたJSONを検出 -
ブレースカウンティング:
{}
の対応を正確にカウントして、JSONの開始と終了を特定 - 構文検証: 抽出したテキストが有効なJSONかどうかを確認
この3段階のアプローチにより、様々な形式のAI回答から確実にJSONを抽出できます。
private String extractJsonFromText(String text) {
// ```json マーカーがある場合の処理
if (text.contains("```json")) {
int jsonStart = text.indexOf("```json") + 7;
int jsonEnd = text.indexOf("```", jsonStart);
if (jsonEnd > jsonStart) {
return text.substring(jsonStart, jsonEnd).trim();
}
}
// { } に囲まれた部分を抽出(ブレースカウンティング)
int braceCount = 0;
int jsonStart = -1;
int jsonEnd = -1;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '{') {
if (braceCount == 0) {
jsonStart = i;
}
braceCount++;
} else if (c == '}') {
braceCount--;
if (braceCount == 0 && jsonStart != -1) {
jsonEnd = i + 1;
break;
}
}
}
if (jsonStart != -1 && jsonEnd > jsonStart) {
String jsonText = text.substring(jsonStart, jsonEnd);
// JSON構文の検証
try {
objectMapper.readTree(jsonText);
return jsonText;
} catch (Exception e) {
logger.warn("抽出されたJSONの構文が無効です: {}", e.getMessage());
}
}
throw new RuntimeException("有効なJSON形式の回答が見つかりませんでした");
}
実際のプロンプト例
ここまでの処理を経て、最終的に以下のようなプロンプトが生成されます。このプロンプトには、ユーザーの1週間分のライフログと、どのように分析してほしいかの指示が含まれています:
あなたは親しみやすい生活コーチです。以下のデータを分析してユーザーにインサイトを提供してください:
【分析方針】気分と活動の両方をバランス良く分析してください。
【口調】親しみやすくフレンドリーな口調で回答してください。
【詳細度】適度な詳細度で分析してください。
### 活動記録詳細
2024-01-15 07:00-08:00: ジョギング [運動] - 公園を30分ジョギング
2024-01-15 09:00-12:00: 仕事 [作業] - プロジェクト資料作成
2024-01-16 19:00-21:00: 友人とディナー [趣味] - イタリアンレストランで食事
### カテゴリ別集計
運動: 5件
作業: 8件
趣味: 3件
### 感情記録
2024-01-15: 😊 (4/5) - 朝の運動で気分爽快
2024-01-16: 😐 (3/5) -
2024-01-17: 😄 (5/5) - 友人とのディナーが楽しかった
### 感情統計
平均ムード: 4.0/5
最高: 5 最低: 3
以下のJSON形式で回答してください:
{
"overall_summary": "全体的な分析サマリー(100-300文字)",
"mood_insights": "感情・気分に関する洞察(100-200文字)",
"activity_insights": "活動パターンに関する洞察(100-200文字)",
"recommendations": "具体的な改善提案・アドバイス(100-250文字)"
}
実際のAI応答例
このような構造化されたプロンプトを送信すると、Gemini APIは以下のような分析結果を返してきます。単なる一般論ではなく、ユーザーの実際のデータに基づいた具体的なアドバイスになっている点が特徴です:
{
"overall_summary": "運動習慣が確立されており、特に朝のジョギングが気分向上に大きく貢献しています。友人との交流も感情面でプラスの効果を示しており、全体的にバランスの取れた生活パターンです。",
"mood_insights": "気分スコアは運動後や社交的な活動後に高くなる傾向があります。特に朝の運動は1日の気分を安定させる効果があり、友人との交流は最高の気分状態を生み出しています。",
"activity_insights": "週5回の運動習慣は素晴らしく、仕事とのバランスも適切です。作業時間が多めですが、運動と趣味で適度にリフレッシュできています。継続的な運動パターンが確立されています。",
"recommendations": "現在の運動習慣を維持し、友人との交流頻度を少し増やすことで更なる気分向上が期待できます。作業日にも軽いストレッチを取り入れると良いでしょう。"
}
image: AIライフガイド機能のアプリ画面(分析結果表示の例)
プロンプトエンジニアリングのポイント
ここまでの実装から学んだ、効果的なプロンプトエンジニアリングのポイントをまとめます。
1. 段階的な情報提示
AIへの指示は、人間に説明するように段階的に行うことが重要です:
なぜ段階的にするのか:
- AIが文脈を正しく理解できる
- 複雑な指示でも混乱しない
- 一貫した品質の回答が得られる
実装では以下の順序で構築:
- ペルソナ設定 → 分析方針 → 口調 → 詳細度 → データ → 出力指示
2. 設定パラメータによる柔軟性
ユーザーごとに求める分析は異なります。そこで、以下のパラメータでカスタマイズ可能にしました:
分析軸(何を重視するか):
- MOOD_FOCUSED: 気分の変化と傾向を深く分析
- ACTIVITY_FOCUSED: 時間の使い方と活動パターンに注目
- BALANCED: 気分と活動の両方を均等に分析
- WELLNESS_FOCUSED: 健康的な生活習慣の観点から分析
口調(どう伝えるか):
- FRIENDLY: 親しみやすい友人のような口調
- PROFESSIONAL: データに基づいた客観的な口調
- ENCOURAGING: ポジティブで励ましを含む口調
- CASUAL: リラックスした気軽な口調
詳細レベル(どこまで詳しく):
- CONCISE: 要点のみ(忙しい人向け)
- STANDARD: 適度な詳細度(一般的)
- DETAILED: 深い洞察と具体例(じっくり読みたい人向け)
これらの組み合わせで、48通りのパーソナライズが可能です。
3. データの可視化
生の数値データをそのまま送るのではなく、AIが理解しやすい形に加工しています:
可視化の工夫:
- 絵文字の活用: 気分レベル(1〜5)を😞😔😐😊😄で表現
- 統計情報の自動計算: 平均、最高、最低を算出して全体傾向を提示
- カテゴリ別集計: 「運動: 5件、作業: 8件」のように活動パターンを可視化
これにより、AIは数値の羅列ではなく、意味のある情報として理解できます。
4. 構造化された出力
AIからの回答を確実にアプリで処理できるよう、厳密な出力形式を指定しています:
構造化の利点:
- JSON形式: アプリ側で確実にパースできる
- 文字数制限: 各項目に文字数制限を設けて、冗長な回答を防ぐ
- 型安全なパース: TypeReferenceを使用して、型安全に処理
これにより、「AIの回答が長すぎる」「必要な情報が含まれていない」といった問題を防げます。
5. エラーハンドリング
AI機能は外部APIに依存するため、様々なエラーケースを想定した実装が必要です:
想定されるエラーと対策:
- データ不足: 「記録がありません」というメッセージを返す
- JSON抽出失敗: 3段階の抽出ロジックでカバー
- API呼び出し失敗: タイムアウトやネットワークエラーへの対応
- 不正なAPIキー: 起動時にキーの妥当性をチェック
これらの対策により、ユーザーには常に適切なフィードバックが提供されます。
セキュリティと設定管理
APIキーの安全な管理
// 設定ファイルからの読み込み
@Value("${gemini.api.key}")
private String geminiApiKey;
// APIキーの検証
private boolean isValidGeminiApiKey() {
if (geminiApiKey == null || geminiApiKey.trim().isEmpty()) {
return false;
}
// ダミーキーの除外
if (geminiApiKey.contains("dummy") ||
geminiApiKey.contains("placeholder") ||
geminiApiKey.length() < 20) {
return false;
}
return true;
}
環境別設定
# application-local.yml
gemini:
api:
key: dummy_gemini_key
url: "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent"
# application-prod.yml
gemini:
api:
key: ${GEMINI_API_KEY} # 環境変数から取得
url: "https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent"
技術的な学び
実装を通じて得られた、Gemini APIとプロンプトエンジニアリングに関する知見をまとめます。
Gemini APIの特徴
実際に使ってみて分かったGemini APIの強みと特性:
強み:
- 自然な日本語生成: 日本語のプロンプトに正確に従い、自然な日本語で応答
- JSON構造の理解: 複雑な出力形式を指定しても、高い精度で構造化された回答を生成
- コンテキスト保持: 1000トークン以上の長いプロンプトでも、文脈を失わずに分析
- パラメータ調整: temperatureを0.7に設定することで、創造性と一貫性のバランスが良好
注意点:
- 回答にマークダウンや説明文が混じることがある → JSON抽出ロジックで対応
- トークン制限があるため、大量データの場合は要約が必要
効果的だった実装
技術的に成功した実装パターン:
1. Spring Bootとの親和性
- RestTemplateによるシンプルなHTTP通信
-
@Value
アノテーションによる設定管理 - 環境別設定ファイルでの切り替え
2. 堅牢なJSON処理
- 3段階のJSON抽出戦略(マークダウン → ブレースカウンティング → 構文検証)
- ObjectMapperとTypeReferenceによる型安全な変換
- エラー時のフォールバック処理
3. データの前処理
- Stream APIを使った効率的な統計計算
- 絵文字による直感的な可視化
- カテゴリ別集計による傾向把握
改善の余地
今後の機能拡張で実装したい項目:
1. パフォーマンス最適化
- キャッシュ機能: 同一期間・同一設定での再分析を避ける
- 非同期処理: API呼び出しを非同期化してレスポンス改善
- バッチ処理: 複数ユーザーの分析を効率的に処理
2. 分析機能の拡張
- 比較分析: 前週・前月との比較で成長を可視化
- トレンド分析: 長期的な変化パターンの検出
- 予測機能: 過去のパターンから未来の傾向を予測
3. ユーザビリティ向上
- リアルタイムフィードバック: 記録直後に簡易分析を提供
- カスタムレポート: ユーザーが分析項目をカスタマイズ
- 共有機能: 分析結果をSNSや友人と共有
まとめ
実際のコード実装を基に、Gemini APIを活用したAIライフガイド機能の開発過程を紹介しました。
実装の成功要因:
- 段階的なプロンプト構築で一貫した品質を実現
- パラメータ化された設定で柔軟なパーソナライゼーション
- 堅牢なJSON処理でアプリ側の安定性を確保
- 適切なエラーハンドリングで信頼性を向上
ユーザー価値:
- 記録データから具体的な「気づき」を獲得
- 個別最適化されたアドバイスの提供
- 継続的な自己改善サポート
技術的価値:
- Spring BootとGemini APIの実践的な統合例
- プロンプトエンジニアリングの具体的な実装方法
- JSON応答の安全な処理方法
この実装により、単なる記録アプリが「AIライフコーチ」として進化し、ユーザーの生活の質向上に貢献できるアプリケーションになりました。
参考情報
- Gemini API: https://ai.google.dev/docs
この記事が、AI機能の実装やプロンプトエンジニアリングに興味がある方の参考になれば幸いです。実際のコードを基にした具体的な実装例として、ぜひご活用ください。