
mem0でLLMチャットの記憶を管理してみる
はじめに
最近ではChatGPTやClaudeでもメモリー機能が展開されており、会話中に別のチャットで話した内容を参考に回答してくれます。(メモリ機能をオンにしていたことを忘れ、急にそのチャットでは会話してない内容もAI側から話されびっくりされたことがある人も多いのかも...?)
技術的には、会話履歴を毎回プロンプトに詰め込めば解決はしますが、トークンが肥大化するため、データ量によってはうまく機能しないことになります。
自作するAIチャットやエージェント機能もメモリこのようなAIに記憶を持たせるため、この記事では、AIエージェント向けのメモリレイヤーOSSである mem0 を Gemini API と組み合わせて動かしてみます。
mem0 とは
会話から「覚えておくべき事実」をLLMで自動抽出し、ベクトルDBに永続化、必要なときに意味検索で取り出してくれるライブラリです。OpenAI、Anthropic、Gemini など複数のLLMに対応しており、ベクトルストアもデフォルトでローカル(Qdrant)が起動するので、追加のインフラ準備は不要です。
記憶の作り方には特徴があります。会話をそのまま保存するのではなく、まず事実の候補をLLMで抜き出し(抽出フェーズ)、次に既存の記憶と意味検索で照合して、LLMが「新規追加・更新・矛盾の削除・据え置き」のいずれかを判断します(更新フェーズ)。このため記憶は会話の丸写しではなく、整理・更新された事実として溜まっていきます。
メモリの種類
mem0 は人間の記憶モデルになぞらえて、記憶を 短期記憶 と 長期記憶 に分けて扱います。
- 短期記憶:1回のやりとり内の一時情報。直近の会話履歴や、ツール出力などの作業状態(ワーキングメモリ)です。
- 長期記憶:セッションをまたいで残る知識。
add()するだけで mem0 が次の種類に自動で振り分けます。- Factual(事実):ユーザーの嗜好やアカウント情報など「何を知っているか」
- Episodic(エピソード):「いつ何があったか」という対話や出来事の記録
- Semantic(意味):概念どうしの関係
短期か長期かは、寿命ごとのレイヤーで指定します。下表の Conversation / Session が短期記憶、User / Org が長期記憶 にあたります。
| レイヤー | 寿命 | 指定方法 | 用途 |
|---|---|---|---|
| Conversation | 1ターン | (自動) | ツール呼び出し結果などの一時データ |
| Session | 数分〜数時間 | run_id |
オンボーディングフロー等、完了したらリセットしたいタスク |
| User | 永続 | user_id |
ユーザーの嗜好や設定など長期的に保持したい情報 |
| Org | 永続 | (共有) | 複数エージェントで共有するFAQやポリシー |
検索時は全レイヤーを横断してランキングされます。今回のは、永続的な記憶として最も使い勝手のよい user memory(user_id)を中心に扱います。
環境
- Python 3.10+
- Gemini API キー(Google AI Studio で取得)
pip install mem0ai google-genai
export GEMINI_API_KEY="your-api-key"
geminiで記憶付きチャットボットを作る
mem0 と Gemini を組み合わせて、セッションをまたいでも覚えているチャットボットを作ります。
ユーザーの発言に関連する記憶を search で引いてプロンプトに差し込み、生成したやりとりを add で記憶に書き戻します。
# chatbot.py
from google import genai
from mem0 import Memory
# gemini設定
config = {
"llm": {"provider": "gemini", "config": {"model": "gemini-3.5-flash"}},
"embedder": {"provider": "gemini", "config": {"model": "models/gemini-embedding-2", "embedding_dims": 3072}},
# gemini-embedding-2 は既定で3072次元であるため、embedder/store とも 3072 を明示して揃える
"vector_store": {"config": {"embedding_model_dims": 3072}},
}
client = genai.Client()
memory = Memory.from_config(config)
# 今回は簡略化のため、ユーザは test_user で固定にしています
def chat_with_memories(message: str, user_id: str = "test_user") -> str:
# 1. このユーザーの記憶から、発言に関連するものを検索
relevant = memory.search(query=message, filters={"user_id": user_id}, top_k=3)
memories_str = "\n".join(f"- {m['memory']}" for m in relevant["results"])
# いま参照している記憶を表示(裏で何を覚えているかが見える)
print(f"[参照中の記憶]\n{memories_str or ' (まだ覚えていることはありません)'}")
# 2. 記憶をシステムプロンプトに差し込んで Gemini で応答を生成
system_prompt = (
"あなたは親切なAIアシスタントです。"
"ユーザーについて分かっていることを踏まえて答えてください。\n"
f"ユーザーの記憶:\n{memories_str}"
)
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=[system_prompt, message],
)
answer = response.text
# 3. 今回のやりとりを記憶に追加(mem0 が事実を抽出して保存)
memory.add(
[
{"role": "user", "content": message},
{"role": "assistant", "content": answer},
],
user_id=user_id,
)
return answer
def main():
print("AI とチャット('exit' で終了)")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("exit", "quit"):
print("終了します")
break
print(f"AI: {chat_with_memories(user_input)}\n")
if __name__ == "__main__":
main()
動作確認
起動して、まず名前を教えます。初回なので参照できる記憶はまだありません。
You: 私の名前は松村です。
[参照中の記憶]
(まだ覚えていることはありません)
AI: 松村さん、こんにちは!何かお手伝いできることはありますか?
続けて所属を伝えます。すると [参照中の記憶] に、直前のやりとりから抽出された「名前は松村」が表示されます。
You: 私はクラスメソッドの社員です。
[参照中の記憶]
- User's name is Matsumura (松村).
AI: クラスメソッドにお勤めの松村さんですね。いつもお疲れ様です!
最後に、自分が何者かを尋ねてみます。
You: わたしはだれですか。
[参照中の記憶]
- User's name is Matsumura (松村).
- User (Matsumura) is an employee of Classmethod (クラスメソッド) as of May 26, 2026.
AI: あなたは松村(まつむら)様です。クラスメソッド株式会社にお勤めの社員様ですね。
名前と所属の2つの記憶を参照して、正しく答えています。注目したいのは、会話文がそのまま保存されるのではなく、抽出フェーズを経ているため、User's name is Matsumura や is an employee of Classmethod ... as of May 26, 2026 のように意味のある事実だけが英語で要約・構造化されている点です。[参照中の記憶] に出ているのが、その実体です。
これらの記憶はローカルのベクトルDBに永続化されます。
なお今回は長期保持向けの user memory(user_id)だけを使いましたが、「タスクが終わったら忘れていい」短期的な情報は run_id を付ければセッション単位で分離できます。
記憶はどこに・どんな形式で保存される?
長期記憶の保存先としてデフォルトでは、保存先が2つに分かれます。
| パス | 役割 | 形式 |
|---|---|---|
/tmp/qdrant/collection/mem0/ |
埋め込みベクトルと記憶テキストの本体。search() が意味検索で引く先 |
Qdrant のローカルDB(実体は SQLite) |
~/.mem0/history.db |
記憶の変更履歴(追加・更新・削除のログ) | SQLite |
ベクトルストア側(Qdrant)
コレクション名は既定で mem0。記憶1件が1つの point としてベクトルと、記憶テキスト(payload の data)・user_id・作成日時などのメタデータで格納されます。search() はここを検索しています。
変更履歴側(history.db)
記憶が追加・更新・削除されるたびに、history テーブルへ1行ずつ記録されます。
| カラム | 内容 |
|---|---|
memory_id |
対象の記憶ID |
old_memory / new_memory |
変更前後のテキスト |
event |
ADD / UPDATE / DELETE のいずれか |
is_deleted |
論理削除フラグ(DELETE でも行自体は残る) |
created_at |
記録時刻 |
実際に中身を覗いてみます。
$ sqlite3 ~/.mem0/history.db "SELECT event, new_memory FROM history;"
ADD|User's name is Matsumura (松村).
ADD|User (Matsumura) is an employee of Classmethod (クラスメソッド) as of May 26, 2026.
add() は内部で ADD / UPDATE / DELETE を判断するため、あとから矛盾する発言をすると、同じ記憶が UPDATE や DELETE として履歴に積まれていきます。DELETE は物理削除ではなく is_deleted=1 を立てる論理削除なので、履歴は消えずに追える形で残ります。
まとめ
- mem0 を使うと、数十行のコードでLLMアプリに永続的な長期記憶を組み込める
- 記憶は
add()するだけで、LLMが会話から事実を抽出・整理して保存してくれる(更新や削除も自動で判断) - 記憶の実体はローカルのベクトルDB(Qdrant)+履歴DB(SQLite)に永続化される
user_id/run_idで長期記憶とセッション記憶を使い分けられる
今回触れなかった機能として、関係性まで保持する Graph Memory、マネージド版の Mem0 Platform、LangGraph / LlamaIndex 連携 などもあります。本格的なエージェント開発で記憶の扱いに悩んでいる方は、選択肢の一つに入れてみてはいかがでしょうか。








