
個人開発しているAI日記アプリ「Deep Journal(ディープジャーナル)」に、**「新しい長期記憶機能」**を実装しました。
以前から「長期的な傾向」を表示する機能はありましたが、常に最新の要約で上書き更新する仕組みだったため、「数ヶ月前の詳細なコンテキスト」が徐々に薄れていく課題がありました。
今回はこれを刷新し、**「過去の記憶を保持し続けられる」**アーキテクチャへと進化させました。
LLMのコンテキストウィンドウ(トークン制限)の壁を超えて、いかに長期間のコンテキストを保持し続けるか。
その実装アプローチとして採用した**「階層型要約システム (Hierarchical Summary System)」**について解説します。
👉 アプリ成果物: Deep Journal(ディープジャーナル)
※初期リリース時の技術構成については、前回の記事で紹介しています。
👉 自分だけのAIメンターと交換日記。Gemini API × Reactで「Deep Journal (ディープジャーナル)」を個人開発した話
課題:トークン制限とコスト
日記データは日々増え続けます。
「過去全ての日記」を毎回プロンプトに含めれば完璧ですが、トークン課金も処理時間も爆発し、すぐにコンテキスト長の上限に達します。
RAG(ベクトル検索)も検討しましたが、「全体的な傾向」や「ぼんやりとした文脈」を拾うには不向きでした。
解決策:人間の記憶モデルを模倣する
そこで、人間の脳が行っている(とされる)記憶の圧縮プロセスを参考にしました。
**「詳細は忘れるが、エピソードの概要と感情は覚えている」**という状態をシステム化します。
アーキテクチャ
Firestore上に以下の3層構造でデータを管理します。
-
Level 1: Raw Entries (生の日記)
- オリジナルの日記データ。
-
Level 2: Monthly Summary (月次要約)
- 月が変わるタイミングなどで、その月の全日記をGeminiに渡し、数百文字の要約(+感情スコア、キーワード)に圧縮して保存。
-
Level 3: Global Summary (個人史)
- 全ての
Monthly Summaryを入力として、さらに上位の要約を生成。「このユーザーはどういう人物か」「どのような変遷を辿ったか」というプロファイル。
- 全ての
推論時のプロンプト構成
ユーザーが日記を書いた時の分析プロンプトには、以下を注入します。
【System Prompt】
あなたはユーザーの長年のパートナーです...
【Context: Global Summary】
(ユーザーの長期的な傾向、成長の軌跡...)
【Context: Recent Entries】
(直近数日分の生の日記...)
【Target】
(今日の日記)
「中間の過去(数ヶ月前)」の詳細は省かれますが、Global Summaryによって「過去に何があったか」の大筋はAIが理解している状態を作れます。
実装のポイント (Firebase + Gemini)
実装には runTransaction などは使わず、非同期のバッチ処理的に実装しました。
特に工夫したのは「再構築(Reconstruction)」機能です。
既存ユーザーのために、過去の全データを月ごとに分割し、並列(あるいはシーケンシャル)に要約APIを叩いて記憶を一気に生成するバッチ処理をクライアントサイドで実装しました。
// 過去の日記データを月ごとにグループ化して処理
const monthlyGroups = groupEntriesByMonth(allEntries);
const monthlySummaries = [];
// 各月ごとにGemini APIを叩いて要約を生成
for (const monthKey of Object.keys(monthlyGroups).sort()) {
const monthEntries = monthlyGroups[monthKey];
// プロンプト生成(システム日付を現在に固定)
const prompt = `
あなたは心理分析官です。以下の${monthEntries.length}件の日記(${monthKey}分)を読み、
この月がユーザーにとってどのような月だったか要約してください。
【現在の日付】: ${new Date().toLocaleDateString('ja-JP')}
...
`;
// Gemini API呼び出し (自作のfetchWithRetryラッパーを使用)
const result = await fetchWithRetry(API_ENDPOINT, {
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
});
// Firestoreに保存
await setDoc(doc(db, `users/${userId}/monthlySummaries`, monthKey), {
summary: result.summary,
sentiment_score: result.sentiment_score,
keywords: result.keywords
});
monthlySummaries.push(result);
}
// 最後に全期間(Global)のサマリーを生成
const globalPrompt = `
以下の「月ごとの要約」を元に、この人物の長期的な人生の傾向を個人史として統合してください。
${monthlySummaries.map(m => m.summary).join('\n')}
`;
// ... (Global Summary生成と保存)
API制限(Rate Limit)を考慮し、クライアントサイドで for ... of ループと適度な sleep を挟みながらシーケンシャルに処理を実行させることで、大量の過去データを持つユーザーでも安定して記憶を再構築できるようにしています。
まとめ
この「階層型要約」により、トークン消費を一定(Global Summary分 + 直近分)に抑えつつ、ユーザーには「ずっと覚えている」という体験を提供できました。
チャットボットやキャラクターAIを作る際の参考になれば幸いです。
#Gemini #Firebase #React #個人開発 #AI #LLM #DeepJournal #ディープジャーナル