1. はじめに
ClaudeやChatGPTなどのAIと会話していると、「さっき名前を伝えたのに、また聞かれた」「趣味を話したら次の会話でも覚えてくれていた」という経験があると思います。
AIは会話のたびに記憶がリセットされる仕組みになっています。それでも「覚えている」ように見えるのは、アプリケーション側で過去の情報を上手く管理しているからです。
本記事では、この「記憶の仕組み」の中でも長期記憶に焦点を当て、実際にPythonで3パターンの実装を作って比較します。
2. 記憶とは
AIは記憶できない
AIは会話のAPIを呼び出すたびに、前の会話の内容を完全に忘れます。毎回ゼロからスタートするのが基本的な動作です。
「記憶があるように見える」のは、アプリケーション側が過去の会話履歴をプロンプトに詰めて毎回送っているからです。AIが自ら記憶しているわけではなく、会話のたびに「ここまでの流れはこうでした」と説明書きを送り込んでいるイメージです。
短期記憶と長期記憶
この「記憶の仕組み」は大きく2種類に分けられます。
短期記憶(Short-term Memory)
現在進行中の会話の中でやりとりした内容を保持する仕組みです。セッションを跨いだ記憶はできません。
長期記憶(Long-term Memory)
コンテキストウィンドウの外に情報を保存・取得する仕組みです。セッションを跨いだ記憶が可能です。
長期記憶の2つの難しさ
長期記憶を実装するにあたって、2つの問いを解決する必要があります。
- 何を保存するか:すべての会話をそのまま保存するとノイズが増えます。重要な情報だけを選んで保存する仕組みが必要です。
- どうやって取り出すか:保存した情報を後から正しく引き出せるかどうかが、AIの返答品質を左右します。
この2つの問いへのアプローチの違いが、実装の幅を生み出します。
3. 実装方法
今回は長期記憶の実装に絞り、「何を保存するか」「どう検索するか」の2軸を変えた3つのパターンを作ります。同じシナリオを3パターンで実行し、挙動の違いを比較します。
なお、実装コードは以下で公開しています。
https://github.com/keisuke-fbr/agent_memory_demo
使用技術
- Python 3.12
- Google Gemini API(
gemini-2.5-flash-lite/gemini-embedding-001) - SQLite(Pattern 1・2) / ChromaDB(Pattern 3)
Pattern 1:Naive Memory
すべての会話をそのまま保存し、返答するたびに直近のN件だけをAIに渡します。
ユーザーのプロンプトと一緒に過去のやり取りを渡すだけなので実装が比較的容易です。
アーキテクチャ
flowchart TD A[ユーザー発話] --> B[直近N件を取得] C[(データベース)] -->|読み込み| B B --> D[プロンプト生成] D --> E[LLM] E --> F[全会話を保存] F -->|書き込み| C style A fill:#f5f5f5,stroke:#666666 style B fill:#dae8fc,stroke:#6c8ebf style C fill:#f5f5f5,stroke:#666666 style D fill:#dae8fc,stroke:#6c8ebf style E fill:#ffe6cc,stroke:#d79b00 style F fill:#d5e8d4,stroke:#82b366
ポイント:シンプルな代わりに、N件の枠から溢れた情報は永遠に参照されなくなります。
Pattern 2:Keyword Memory
まずAIが「この情報は将来も役立つか?」を判断し、重要な情報だけを保存します。取り出すときは3文字ずつのキーワード一致(3-gram)で検索します。
すべての会話を保存せず、やり取りをそのままコンテキストに含めないので、データ効率は良いものの、キーワード検索から漏れると情報がLLMに渡りません。
アーキテクチャ
flowchart TD
A[ユーザー発話] --> B[3-gram検索]
C[(データベース)] -->|読み込み| B
B --> D[プロンプト生成]
D --> E[LLM]
E --> F{保存すべきか}
F -->|はい| C[(データベース)]
F -->|いいえ| H[破棄]
style A fill:#f5f5f5,stroke:#666666
style B fill:#dae8fc,stroke:#6c8ebf
style C fill:#f5f5f5,stroke:#666666
style D fill:#dae8fc,stroke:#6c8ebf
style E fill:#ffe6cc,stroke:#d79b00
style F fill:#fff2cc,stroke:#d6b656
style H fill:#f8cecc,stroke:#b85450
限界:「ラーメン」という記憶は「食べ物」というキーワードでは引き出せません。文字列が一致しないと取り出せないのがキーワード検索の根本的な制約です。
Pattern 3:Vector Memory
AIがユーザーの発話のみを対象に選別して保存し、意味の近さ(ベクトル類似度)で検索します。
検索された結果をコンテキストに含めます。キーワード検索を行わないため、キーワード漏れがなくなるほか、すべての会話をコンテキストに含める必要もありません。
アーキテクチャ
flowchart TD
A[ユーザー発話] --> B[ベクトル検索]
C[(データベース)] -->|読み込み| B
B --> D[プロンプト生成]
D --> E[LLM]
E --> F{保存すべきか}
F -->|はい| C[(データベース)]
F -->|いいえ| H[破棄]
style A fill:#f5f5f5,stroke:#666666
style B fill:#dae8fc,stroke:#6c8ebf
style C fill:#f5f5f5,stroke:#666666
style D fill:#dae8fc,stroke:#6c8ebf
style E fill:#ffe6cc,stroke:#d79b00
style F fill:#fff2cc,stroke:#d6b656
style H fill:#f8cecc,stroke:#b85450
強み:「食べ物」と「ラーメン巡り」のように、文字は違っても意味が近ければ引き出せます。
3パターン比較
| Pattern 1 | Pattern 2 | Pattern 3 | |
|---|---|---|---|
| 保存の判断 | 全件保存 | AIが選別 | AIが選別 |
| 検索方法 | 直近N件 | 3-gram部分一致 | ベクトル類似度 |
| 同義語・言い換えへの対応 | × | × | ○ |
| 実装難易度 | 低 | 中 | 高 |
4. 実験方法
シナリオ設計
以下の8ターンの会話を3パターンすべてで実行します。
| ターン | 発話内容 | 役割 |
|---|---|---|
| 1 | 私の名前は健二です。よろしくお願いします | 名前の情報 |
| 2 | 趣味はラーメン巡りです | 趣味の情報 |
| 3 | 東京の渋谷に住んでいます | 住所の情報 |
| 4 | 最近仕事が忙しくて少し疲れています | 状況の情報 |
| 5 | 先週末は映画を見てきました | 一時的な話題 |
| 6 | 今日はいい天気ですね | 一時的な話題 |
| 7 | ところで、私の名前覚えていますか? | テスト① |
| 8 | 私がよく食べるものを教えてもらえますか? | テスト② |
テストの狙い
テスト①(名前の想起)
ターン4〜6に一時的な話題を挟むことで、Pattern 1の「直近6件」ウィンドウからターン1(名前)が押し出されるよう設計しています。
テスト②(意味検索)
「食べ物」という言葉が会話中に一度も登場しないにもかかわらず、「ラーメン巡り」という記憶にたどり着けるかを確認します。文字列の一致に頼るPattern 2と、意味の近さで探すPattern 3の差が最も鮮明に現れる設問です。
5. 結果
ここでは、各会話ごとにユーザーの質問・LLMが参照した内容・LLMの回答などをまとめます。
Pattern 1:Naive Memory
ターン1
- ユーザーの質問
- 私の名前は健二です。よろしくお願いいたします。
- LLMの参照内容
- LLMの回答
- 健二さん。はじめまして。
ターン2
- ユーザーの質問
- 趣味はラーメン巡りです。
- LLMの参照内容
- 会話1
- user:私の名前は健二です。よろしくお願いいたします。
- agent:健二さん。はじめまして。
- 会話1
- LLMの回答
- ラーメン巡り、いいですね!
ターン3
- ユーザーの質問
- 東京の渋谷に住んでいます
- LLMの参照内容
- 会話1
- user:私の名前は健二です。よろしくお願いします
- agent:健二さん、はじめまして!こちらこそ、どうぞよろしくお願いします。😊
- 会話2
- user:趣味はラーメン巡りです
- agent:ラーメン巡り、いいですね!
- 会話1
- LLMの回答
- 渋谷にお住まいなんですね!おしゃれな街でいいですね!😊 渋谷でよく行くラーメン屋さんとかありますか?
ターン4
- ユーザーの質問
- 最近仕事が忙しくて少し疲れています
- LLMの参照内容
- 会話1
- user:私の名前は健二です。よろしくお願いします
- agent:健二さん、はじめまして!こちらこそ、どうぞよろしくお願いします。😊
- 会話2
- user:趣味はラーメン巡りです
- agent:ラーメン巡り、いいですね!🍜✨ どんなラーメンがお好きですか?
- 会話3
- user:東京の渋谷に住んでいます
- agent:渋谷にお住まいなんですね!おしゃれな街でいいですね!😊渋谷でよく行くラーメン屋さんとかありますか?
- 会話1
- LLMの回答
- お仕事お忙しいのですね。お疲れ様です。
ターン5
- ユーザーの質問
- 先週末は映画を見てきました
- LLMの参照内容
- 会話2
- user:趣味はラーメン巡りです
- agent:ラーメン巡り、いいですね!🍜✨ どんなラーメンがお好きですか?渋谷でよく行くラーメン屋さんとかありますか?
- 会話3
- user:東京の渋谷に住んでいます
- agent:渋谷にお住まいなんですね!おしゃれな街でいいですね!😊
- 会話4
- user:最近仕事が忙しくて少し疲れています
- agent:お仕事お忙しいのですね。お疲れ様です。
- 会話2
- LLMの回答
- 映画、いいですね!🎬 どんな映画をご覧になりましたか?
ターン6
- ユーザーの質問
- 今日はいい天気ですね
- LLMの参照内容
- 会話3
- user:東京の渋谷に住んでいます
- agent:渋谷にお住まいなんですね!おしゃれな街でいいですね!😊
- 会話4
- user:最近仕事が忙しくて少し疲れています
- agent:お仕事お忙しいのですね。お疲れ様です。
- 会話5
- user:先週末は映画を見てきました
- agent:映画、いいですね!🎬 どんな映画をご覧になりましたか?
- 会話3
- LLMの回答
- 本当ですね!☀️ 晴れていると気分も上がりますね!
ターン7(テスト①:名前の想起)
- ユーザーの質問
- ところで、私の名前覚えていますか?
- LLMの参照内容
- 会話4
- user:最近仕事が忙しくて少し疲れています
- agent:お仕事お忙しいのですね。お疲れ様です。
- 会話5
- user:先週末は映画を見てきました
- agent:映画、いいですね!🎬 どんな映画をご覧になりましたか?
- 会話6
- user:今日はいい天気ですね
- agent:本当ですね!☀️ 晴れていると気分も上がりますね!
- 会話4
- LLMの回答
- はい、覚えていますよ!😊 あなたは「〇〇さん」ですね。
名前を把握できていないのに○○として無理やり回答している。
ターン8(テスト②:食べ物の想起)
- ユーザーの質問
- 私がよく食べるものを教えてもらえますか?
- LLMの参照内容(食べ物・趣味の情報なし)
- 会話5
- user:先週末は映画を見てきました
- agent:映画、いいですね!🎬 どんな映画をご覧になりましたか?
- 会話6
- user:今日はいい天気ですね
- agent:本当ですね!☀️晴れていると気分も上がりますね!
- 会話7
- user:ところで、私の名前覚えていますか?
- agent:はい、覚えていますよ!😊あなたは「〇〇さん」ですね。
- 会話5
- LLMの回答
- 以前の会話履歴を拝見したのですが、〇〇さんが「よく食べるもの」についてお話しされた記録が見当たりませんでした。
ラーメンの会話がコンテキストから押し出されており、LLMが参照できず回答できない。
テスト結果
| ターン | 発話 | 検索ヒット |
|---|---|---|
| 7 | 名前覚えていますか? | 0件 ← 押し出されてしまい参照できない |
| 8 | よく食べるものは? | 0件 ← 押し出されてしまい参照できない |
Pattern 2:Keyword Memory
ターン1
- ユーザーの質問
- 私の名前は健二です。よろしくお願いいたします。
- LLMの参照内容
- LLMの回答
- 健二さん。はじめまして。
- 保存される内容
- 健二
ターン2
- ユーザーの質問
- 趣味はラーメン巡りです。
- LLMの参照内容
- LLMの回答
- ラーメン巡り、いいですね!例えば、こってり系、あっさり系、それとも地域ごとの特色があるものがお好みですか?
- 保存される内容
- “ラーメン巡りは趣味である”
ターン3
- ユーザーの質問
- 東京の渋谷に住んでいます。
- LLMの参照内容
- LLMの回答
- 渋谷にお住まいなのですね!
- 保存される内容
- “渋谷にお住まい”
ターン4
- ユーザーの質問
- 最近仕事が忙しくて少し疲れています
- LLMの参照内容
- LLMの回答
- お仕事お疲れ様です。忙しいとどうしても疲れてしまいますよね。何かリラックスできることや、気分転換になるようなことはありますか?
- 保存される内容
- ”最近仕事が忙しい”
ターン5
- ユーザーの質問
- 先週末は映画を見てきました
- LLMの参照内容
- LLMの回答
- お!いいですね!どんな映画をご覧になりましたか?😊
- 保存される内容
- “先週末映画を見た”
ターン6
- ユーザーの質問
- 今日はいい天気ですね
- LLMの参照内容
- LLMの回答
- はい、本当に気持ちの良いお天気ですね!☀️ こんな日は、外に出て散歩したり、公園でゆっくり過ごしたりするのも楽しそうです。
- 保存される内容
ターン7(テスト①:名前の想起)
- ユーザーの質問
- ところで、私の名前覚えていますか?
- LLMの参照内容
- LLMの回答
- はい、お名前を覚えていますよ。
- 保存される内容
- 保存なし
「健二」はデータベースに保存されているものの、キーワード検索(名前)に引っかからないため、
適切な回答が行えていない。
ターン8(テスト②:食べ物の想起)
- ユーザーの質問
- 私がよく食べるものを教えてもらえますか?
- LLMの参照内容
- LLMの回答
- 申し訳ありませんが、私はあなたのことをまだよく知りません。あなたがよく食べるものを知るためには、いくつか質問させてください。
- 保存される内容
「ラーメン」はデータベースに保存されているものの、キーワード検索(食べ物)に引っかからないため、
適切な回答が行えていない。
テスト結果
| ターン | 発話 | 検索ヒット |
|---|---|---|
| 7 | 名前覚えていますか? | 0件 ← 「名前」が「健二」にマッチしない |
| 8 | よく食べるものは? | 0件 ← 「食べ物」が「ラーメン」にマッチしない |
Pattern 3:Vector Memory(ここでは類似度が高い上位3件を参照する)
ターン1
- ユーザーの質問
- 私の名前は健二です。よろしくお願いいたします。
- LLMの参照内容
- LLMの回答
- 健二さん。はじめまして。
- 保存される内容
- “名前:健二”
ターン2
- ユーザーの質問
- 趣味はラーメン巡りです。
- LLMの参照内容
- “名前:健二”(類似度:62.8%)
- LLMの回答
- 健二さん、ラーメン巡りがご趣味なんですね!どんなラーメンがお好きですか?😊
- 保存される内容
- “趣味はラーメン巡り”
ターン3
- ユーザーの質問
- 東京の渋谷に住んでいます
- LLMの参照内容
- 「趣味はラーメン巡り」(類似度:62.1%)
- 「名前:健二」(類似度:60.6%)
- LLMの回答
- 健二さん、東京の渋谷にお住まいなのですね!ラーメン巡りがお好きとのことですが、渋谷近辺でのおすすめのお店はありますか?
- 保存される内容
- 「東京の渋谷に住んでいます」
ターン4
- ユーザーの質問
- 最近仕事が忙しくて少し疲れています
- LLMの参照内容
- 「東京の渋谷に住んでいます」(類似度:61.9%)
- 「趣味はラーメン巡り」(類似度:61.9%)
- 「名前:健二」(類似度:61.1%)
- LLMの回答
- 健二さん、お仕事お忙しいんですね。お疲れ様です。お疲れとのことですが、何か気分転換になるようなことはされていますか?例えば、趣味のラーメン巡りはいかがですか?
- 保存される内容
- 「仕事が忙しい」
ターン5
- ユーザーの質問
- 先週末は映画を見てきました
- LLMの参照内容
- 「趣味はラーメン巡り」(類似度:63.8%)
- 「東京の渋谷に住んでいます」(類似度:62.5%)
- 「仕事が忙しい」(類似度:60.4%)
- LLMの回答
- それはいいですね!どんな映画をご覧になりましたか?
- 保存される内容
- 保存なし
ターン6
- ユーザーの質問
- 今日はいい天気ですね
- LLMの参照内容
- 「名前:健二」(類似度:62.3%)
- 「趣味はラーメン巡り」(類似度:59.6%)
- 「東京の渋谷に住んでいます」(類似度:59.4%)
- LLMの回答
- 健二さん、こんにちは!本当にいい天気ですね。こんな日は、ラーメン巡りにもぴったりかもしれませんね。渋谷でも美味しいラーメン屋さんはたくさんありそうですか?
- 保存される内容
- 保存なし
ターン7(テスト①:名前の想起)
- ユーザーの質問
- ところで、私の名前覚えていますか?
- LLMの参照内容
- 「名前:健二」(類似度:64.3%)
- 「東京の渋谷に住んでいます」(類似度:61.7%)
- 「趣味はラーメン巡り」(類似度:59.1%)
- LLMの回答
- はい、健二さんですね。東京の渋谷にお住まいで、ラーメン巡りがご趣味なんですよね。
- 保存される内容
- 保存なし
ターン8(テスト②:食べ物の想起)
- ユーザーの質問
- 私がよく食べるものを教えてもらえますか?
- LLMの参照内容
- 「趣味はラーメン巡り」(類似度:66.4%)
- 「名前:健二」(類似度:59.6%)
- 「東京の渋谷に住んでいます」(類似度:59.0%)
- LLMの回答
- 健二さん、お住まいが渋谷で、趣味がラーメン巡りとのことですので、もしかしたらラーメンをよく召し上がりますか?
- 保存される内容
- 保存なし
テスト結果
| ターン | 発話 | 検索ヒット(上位) |
|---|---|---|
| 7 | 名前覚えていますか? | 1位:"名前:健二" 64.3% |
| 8 | よく食べるものは? | 1位:"趣味はラーメン巡り" 66.4% |
テスト①:「私の名前覚えていますか」というクエリが「名前:健二」に64.3%の類似度でヒット。さらに渋谷・ラーメンの記憶も引き出して一度にまとめて回答を実施。
テスト②:ベクトル検索で食べ物との類似度が高い「趣味はラーメン巡り」が66.4%でヒットし、正しい情報を引き出し回答を実施。
6. まとめ
今回は長期記憶に関連する3パターンを実装し、実験しました。実験の結果、ベクトル検索が最も適切に情報を抽出できていました。また、テストケース以外でも参照した内容に基づいてユーザーと対話できていることが確認できます。
実際にエージェントを実装する場面では、例えば「大阪に引っ越した」という情報の更新や、同じことを繰り返し保存してしまう重複など、まだ対応できていない課題もあります。
さらなる実装に関しても今後取り上げていきたいと思います。

