mem0でLLMチャットの記憶を管理してみる

mem0でLLMチャットの記憶を管理してみる

2026.05.26

はじめに

最近では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 memoryuser_id)を中心に扱います。

環境

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 Matsumurais an employee of Classmethod ... as of May 26, 2026 のように意味のある事実だけが英語で要約・構造化されている点です。[参照中の記憶] に出ているのが、その実体です。

これらの記憶はローカルのベクトルDBに永続化されます。
なお今回は長期保持向けの user memoryuser_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 を判断するため、あとから矛盾する発言をすると、同じ記憶が UPDATEDELETE として履歴に積まれていきます。DELETE は物理削除ではなく is_deleted=1 を立てる論理削除なので、履歴は消えずに追える形で残ります。

まとめ

  • mem0 を使うと、数十行のコードでLLMアプリに永続的な長期記憶を組み込める
  • 記憶は add() するだけで、LLMが会話から事実を抽出・整理して保存してくれる(更新や削除も自動で判断)
  • 記憶の実体はローカルのベクトルDB(Qdrant)+履歴DB(SQLite)に永続化される
  • user_id / run_id で長期記憶とセッション記憶を使い分けられる

今回触れなかった機能として、関係性まで保持する Graph Memory、マネージド版の Mem0 PlatformLangGraph / LlamaIndex 連携 などもあります。本格的なエージェント開発で記憶の扱いに悩んでいる方は、選択肢の一つに入れてみてはいかがでしょうか。

参考


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事