メインコンテンツへスキップ

記憶でエージェントを改善するLLMOps 〜 mem0をVertex AI + pgvectorで検証する

著者
Tomoyuki Kurosawa

こんにちは。ガオ株式会社の黒澤です。この記事では、AIエージェントに記憶を持たせるメモリレイヤー mem0 を Google Cloud 上で動かし、その挙動を検証します。

結論を先に述べると、mem0 は会話から「ユーザーごとの事実」を LLM で抽出して保存し、後から関連する記憶をベクトル検索で取り出してプロンプトに渡します。再学習も検索基盤の作り込みも要らず、addsearch だけでユーザー単位の記憶が溜まっていきます。今回は LLM・Embedder ともに Google Cloud の Vertex AI(Gemini Enterprise Agent Platform、旧 Vertex AI)を API キーなしで呼び、保存先を pgvector(PostgreSQL)とする構成で、記憶の保存・想起から、記憶を使うエージェント、複数エージェント間での共有まで動かしました。

想定読者

  • AIエージェントを開発していて、会話の文脈の永続化を検討している方
  • エージェントの改善手段としてメモリレイヤーが使えるか知りたい方
  • Google Cloud でエージェント基盤を組んでいる方

この記事でわかること

  • メモリレイヤーが RAG とどう違うのか
  • mem0 の仕組み(抽出・保存・想起)と基本的な使い方
  • mem0 を Google Cloud 上で、LLM・Embedder を Vertex AI 経由(API キー不使用)で動かした検証結果

LLMOps:エージェント改善の手段としてのメモリレイヤー
#

AIエージェントを改善する手段には、いくつかの選択肢があります。

  • ファインチューニング:モデルの重みを更新する。データ整備と再学習が必要で重い
  • RAG:外部ドキュメントをベクトル検索で引き、プロンプトに足す
  • プロンプトの改善:指示の出し方を変える

これらに加えて、ユーザーごとの記憶を扱う「メモリレイヤー」という手もあります。整理のために、モデルの重みを変えるか・何を扱うかで並べます。

手段モデルの重み仕組み扱うもの
ファインチューニング更新する学習モデルの振る舞い
RAG変えないベクトル検索ドキュメント(知識)
メモリレイヤー変えないベクトル検索ユーザーの記憶
プロンプト変えない指示

モデルそのものを作り変えるのはファインチューニングだけです。RAG とメモリレイヤーは、モデルを変えずにベクトル検索で文脈を足す点が共通しています。要素技術は同じです。

違うのは「何を引くか」と「状態を持つか」です。RAG はドキュメント(知識)を引く読み取り専用の仕組みで、メモリレイヤーはユーザーごとの事実を会話から抽出して書き換えていく仕組みです。mem0 はこの違いを次のように説明しています。

RAG answers “What does this document say?” Memory answers “what does this user need?”

(出典:RAG vs. AI Memory(mem0公式)

mem0 はメモリレイヤーを RAG の一種とは位置づけていません。RAG は「一度インデックスしたら読み取るだけ(read-only)」、メモリレイヤーは「状態を持ち、読み書きする(stateful and read-write)」もので、別レイヤーとして整理しています。そのうえで、本番のエージェントは両方使うものだと述べています。

Most production agents need both: RAG for knowledge, and memory for personalization.

(出典:RAG vs. AI Memory(mem0公式)

つまりメモリレイヤーは、RAG と検索の仕組みを共有しつつ、知識ではなく「このユーザーは過去にこう言った」という状態を扱う層です。会話やセッションをまたいでユーザーを覚えていくため、再学習を回さなくても、使うほど応答がそのユーザー向けに寄っていきます。LLMOps の改善ループを軽く回す選択肢の一つです。

今回はこのメモリレイヤーの代表格である mem0 を、実際に動かして確認します。


この記事で扱うこと・扱わないこと
#

扱うこと

  • mem0 の OSS 版(mem0ai ライブラリ)を Google Cloud 構成で動かす(LLM・Embedder は Vertex AI 経由、保存先は pgvector:検証はローカル Docker、本番は Cloud SQL 想定)
  • 記憶の保存・想起の挙動(自社でローカル実機検証)
  • 記憶を使って回答を変える最小エージェント、および複数エージェント間での記憶共有

扱わないこと

  • mem0 のマネージド Platform(SaaS)版の詳細
  • Langfuse などオブザーバビリティとの連携(観測はメモリレイヤーとは別の関心事のため)
  • Cloud SQL の本番構築手順(本番移行は今後のテーマ)

検証環境は次のとおりです。

要素バージョン
mem0(mem0ai2.0.7
LLMGemini 3.5 Flash(Vertex AI 経由)
Embeddergemini-embedding-2(1536次元、Vertex AI 経由)
ベクトルDBpgvector(PostgreSQL 16)
Python3.12

mem0 とは
#

mem0 は、AIエージェントに記憶を持たせるためのオープンソースのメモリレイヤーです(Python パッケージ mem0ai、ライセンスは Apache 2.0)。マネージドの Platform もありますが、今回は OSS 版を使います。なお mem0 OSS には API サーバとして動かす形態もありますが、本記事は mem0aiライブラリとして直接呼ぶ構成です(mem0 のサーバをホストするわけではありません)。

中核の処理はシンプルです。会話を渡すと、mem0 が記憶すべき事実を抽出して保存し、後から関連する記憶を検索して返します。公式ドキュメントは保存処理(add)をこう説明しています。

Mem0 sends the messages through an LLM that pulls out key facts, decisions, or preferences to remember.

(出典:Memory operations - add

会話をそのまま保存するのではなく、LLM を使って「覚えるべき事実」を抽出してから保存します。何を記憶に残すかの判断を mem0 が引き受ける点が、手組みの仕組みとの違いです。

図にすると次の流れです。

graph TB
  A["会話: 私は猫を飼っている"] --> B["LLM
記憶すべき事実を抽出"] B --> C["Embedder
記憶をベクトル化"] C --> D[("ベクトルDB
pgvector")] E["後の質問: ペットいた?"] --> F["Embedder
質問をベクトル化"] F --> G["ベクトルDBで意味の近い記憶を検索"] D --> G G --> H["想起: 猫を飼っている"]

登場する部品は2つです。

  • LLM:会話から記憶すべき事実を抽出する
  • Embedder(埋め込みモデル):記憶や検索クエリをベクトル(意味を表す数値の列)に変換する

操作は主に次の3つのメソッドに集約されます。

メソッド役割
add()会話を渡して記憶を保存(LLM による抽出込み)
search()クエリに関連する記憶を検索
get_all()保存済みの記憶を全件取得

Google Cloud で mem0 を動かす
#

ここから実際に手を動かします。LLM と Embedder はどちらも Vertex AI で動かし、保存先に pgvector を使います(検証はローカルの Docker、本番は Cloud SQL を想定)。

ノート

mem0 は API キー方式(Gemini Developer API、GOOGLE_API_KEY)でも動きます。本番なら有料ティア(入力が学習に使われない)を使えばよく、Google も「特定のエンタープライズ制御が不要なら、ほとんどの開発者は Gemini Developer API を使うべき」としています(出典 )。データレジデンシー・CMEK・VPC Service Controls・IAM といったガバナンス制御が必要なら Vertex AI(Gemini Enterprise Agent Platform)を選びます。本記事は企業の Google Cloud 案件を想定し、API キー管理を避けて IAM/サービスアカウント(ADC)で認証できる Vertex AI 経由にしました。

mem0 で Gemini 系を Vertex AI で使うには、provider: "gemini"(内部で新しい google-genai SDK を使用)を選び、環境変数で接続先を指定します。

export GOOGLE_GENAI_USE_VERTEXAI=true
export GOOGLE_CLOUD_PROJECT=<YOUR_PROJECT_ID>
export GOOGLE_CLOUD_LOCATION=global   # 今回の gemini-3.5-flash は global で提供される

認証はサービスアカウントの鍵(GOOGLE_APPLICATION_CREDENTIALS)でも、ローカル開発なら gcloud auth application-default login で作る ADC でも動きます。いずれも API キーではなく Google Cloud の資格情報を使います。

構成
#

検証構成は次のとおりです。

graph LR
  A[検証スクリプト] --> B[mem0ai OSS]
  B -->|"LLM"| C[Gemini 3.5 Flash]
  B -->|"Embedder"| D[gemini-embedding-2]
  B -->|保存| E[("pgvector
PostgreSQL")]
部品採用
LLM(記憶抽出)Gemini 3.5 Flash(Vertex AI 経由)
Embedder(ベクトル化)gemini-embedding-2(Vertex AI 経由)
ベクトルDBpgvector(検証: ローカル Docker / 本番想定: Cloud SQL for PostgreSQL)

今回の検証では保存先に**ローカルの pgvector(Docker、PostgreSQL 16)**を使い、本番では **Cloud SQL for PostgreSQL(pgvector 拡張)**を想定しています。

事前準備
#

  • Vertex AI の API を有効化し、呼び出せるサービスアカウント(ローカル開発では ADC)を用意する
  • ベクトルDB:検証ではローカルで pgvector 入りの PostgreSQL を起動し(docker run ... pgvector/pgvector:pg16)、本番では Cloud SQL for PostgreSQL を作成して pgvector 拡張を有効化する(CREATE EXTENSION vector;

mem0 のインストール
#

mem0ai 本体は軽量に保つため、各プロバイダの SDK をオプション扱いにしています。今回は本体に加えて次の2つが必要です。

  • google-genai:Vertex AI 経由の Gemini を LLM・Embedder に使うため
  • psycopg[binary,pool]:pgvector に接続するドライバ
uv add mem0ai google-genai "psycopg[binary,pool]"
pip install mem0ai google-genai "psycopg[binary,pool]"

uv を使うと uv.lock に推移的依存まで含めて固定されるので、検証環境を再現しやすくなります。今回の主要バージョンは mem0ai 2.0.7 / google-genai 2.8.0 です。provider: "gemini" は新しい google-genai SDK を使うため、非推奨方向にある旧 Vertex AI SDK(vertexai.language_models)には依存しません。再現する際は uv.lock でのバージョン固定を前提にしてください。

設定
#

Memory.from_config() に、LLM・Embedder・ベクトルストアの設定を渡します。LLM・Embedder の両方を provider: "gemini" にして、前述の環境変数で Vertex AI に向けます。

from mem0 import Memory

config = {
    "llm": {
        "provider": "gemini",
        "config": {
            "model": "gemini-3.5-flash",
            "vertexai": True,
            "project": "<YOUR_PROJECT_ID>",
            "location": "global",
        },
    },
    "embedder": {
        "provider": "gemini",
        "config": {
            "model": "gemini-embedding-2",
            "embedding_dims": 1536,  # 既定3072。output_dimensionality で可変(後述)
        },
    },
    "vector_store": {
        "provider": "pgvector",
        "config": {
            "host": "<CLOUD_SQL_HOST>",  # ローカル検証では localhost
            "port": 5432,
            "user": "<DB_USER>",
            "password": "<DB_PASSWORD>",
            "dbname": "postgres",
            "collection_name": "mem0",
            "embedding_model_dims": 1536,  # Embedder の出力次元と一致させる(後述)
            "sslmode": "require",          # 本番は暗号化接続を前提に
        },
    },
}

m = Memory.from_config(config)

本番で Cloud SQL に接続する場合は平文接続を避け、sslmode: "require"(環境に応じて verify-full 等)で暗号化接続を前提にします。Cloud Run などから接続する場合は、Cloud SQL Auth Proxy や Private IP 接続の構成に合わせて hostsslmode を調整してください。

重要

mem0 はデフォルトで匿名の利用テレメトリを PostHog に送信します(環境変数 MEM0_TELEMETRY=False で有効・無効を切り替えられます)。企業で導入する際は、送信される内容と自社のデータガバナンス方針を確認したうえで、有効・無効を判断しておくとよいでしょう。

はまりポイント1:Embedder は環境変数で Vertex AI にする
#

LLM の gemini プロバイダは config に vertexai / project / location を渡せますが、Embedder の gemini プロバイダは config からは受け取らず、google-genai SDK が環境変数(GOOGLE_GENAI_USE_VERTEXAI / GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION)を見て Vertex AI 経路を判断します。これらの環境変数を設定していないと、Embedder 側が API キー方式にフォールバックしようとします。

はまりポイント2:埋め込みの次元数
#

vector_storeembedding_model_dims は、使う Embedder の出力次元と一致させる必要があります。gemini-embedding-2 の出力次元は可変で、既定は 3072 です(output_dimensionality で 128〜3072 の範囲を選べます。推奨値は 768 / 1536 / 3072)。

ただし既定の 3072 をそのまま使うと、mem0 の pgvector ストアが HNSW インデックスを作る際に column cannot have more than 2000 dimensions for hnsw index というエラーになります(pgvector の HNSW インデックスは 2000 次元までのため)。そこで本記事では 2000 以下の 1536embedding_dims に指定し、embedding_model_dims1536 に揃えています。両者の次元が揃っていないと、これとは別に次元不一致のエラーになります。


実際に記憶させてみる
#

設定ができたら、会話を記憶させて、後から想起できるか確認します。

記憶させる
#

messages = [
    {"role": "user", "content": "私は東京在住で、休日はだいたい猫と過ごしています。"},
    {"role": "assistant", "content": "東京にお住まいで、猫を飼われているんですね。覚えておきます。"},
]

m.add(messages, user_id="kurosawa")

戻り値はこうなりました。会話の文章ではなく、要点が事実として抽出されています。

{
  "results": [
    {
      "id": "e752410b-...",
      "memory": "ユーザーは東京に住んでおり、休日はだいたい飼っている猫と一緒に過ごしている。",
      "event": "ADD"
    }
  ]
}

1往復の会話から、「東京在住」「休日は猫と過ごす」という事実が1件の記憶にまとめて抽出されました。

ノート

抽出される記憶の言語はモデルによって変わります。今回の gemini-3.5-flash は追加指示なしで日本語ベースの記憶を生成しましたが、gemini-2.5-flash で同じ会話を試した際は英語(“User lives in Tokyo.” など)で抽出されました。言語をそろえたい場合は、設定の custom_instructions(抽出時の最優先指示)で「日本語で抽出すること」などと指定できます。

今回の infer=True(デフォルト)の検証では、同じ会話をもう一度 add() しても新規追加されませんでした(results が空)。既存の記憶と重複すると判断されたためです。ただし infer=False で生データをそのまま保存する設定では重複が入り得るため、重複排除を一般仕様として当てにしすぎないほうがよいです。

想起させる
#

別の会話のタイミングで、関連する記憶を検索します。

results = m.search("このユーザーの住んでいる場所は?", filters={"user_id": "kurosawa"})
print(results)
{
  "results": [
    {
      "memory": "ユーザーは東京に住んでおり、休日はだいたい飼っている猫と一緒に過ごしている。",
      "score": 0.737,
      "user_id": "kurosawa"
    }
  ]
}

「住んでいる場所は?」という質問で、該当する記憶が score 0.74 で引けています。「休日は何をしている?」で検索しても同じ記憶が score 0.64 で返りました。キーワードの一致ではなくベクトルの意味的な近さで引いているため、質問の言い回しが変わっても想起できます。

記憶の一覧を見る
#

all_memories = m.get_all(filters={"user_id": "kurosawa"})
print(all_memories)
{
  "results": [
    {
      "memory": "ユーザーは東京に住んでおり、休日はだいたい飼っている猫と一緒に過ごしている。",
      "user_id": "kurosawa",
      "created_at": "2026-06-26T..."
    }
  ]
}

記憶は user_id 単位で蓄積されます。別の user_idadd() すれば、その人専用の記憶として別管理されます。


記憶を使うエージェントにしてみる
#

ここまでは記憶の出し入れ(add / search)そのものを見てきました。次に、この記憶を使って回答するエージェントを最小構成で組みます。手順は3ステップです。

  1. 質問に関連する記憶を search で引く
  2. 引いた記憶をプロンプトに差し込む
  3. その文脈つきで LLM に答えさせる
from google import genai

# 環境変数で Vertex AI 経由(GOOGLE_GENAI_USE_VERTEXAI=true / GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION=global)
genai_client = genai.Client()

def ask(question, user_id):
    # 1. 関連する記憶を引く
    results = m.search(question, filters={"user_id": user_id}).get("results", [])
    memories = [r["memory"] for r in results if "memory" in r]
    # 2. プロンプトに差し込む(記憶が無ければ素の質問だけ渡す)
    if memories:
        context = "このユーザーについて分かっていること:\n" + "\n".join(f"- {x}" for x in memories)
        prompt = f"{context}\n\n質問: {question}\n日本語で2〜3文で答えてください。"
    else:
        prompt = f"質問: {question}\n日本語で2〜3文で答えてください。"
    # 3. 文脈つきで答えさせる
    return genai_client.models.generate_content(model="gemini-3.5-flash", contents=prompt).text

同じ質問「今度の週末のおすすめの過ごし方を1つ提案して。」を、記憶ありとなしで投げ比べます。

記憶なし(search を挟まず、質問だけを渡した場合)の回答です。

今週末は、少し足を延ばして緑豊かな公園へピクニックに出かけてみてはいかがでしょうか。お気に入りのお弁当や本を持って、芝生の上でのんびりと風を感じながら過ごすのがおすすめです。

当たり障りのない一般論が返ります。次に、mem0 から「東京在住・猫と過ごす」という記憶を差し込んだ場合です。

今週末は、猫ちゃんのお気に入りのおやつを用意して、おうちで一緒に話題の映画やドラマを観る「おうちシネマ」を楽しんでみてはいかがでしょうか。東京の賑やかさから離れ、大好きな愛猫と温もりを感じながらのんびり過ごすのがおすすめです。

同じモデル・同じ質問でも、記憶を差し込むだけで「東京」「猫」を踏まえた回答に変わりました。モデルには手を入れていません。変えたのは、何を覚えていて何を文脈に入れるかだけです。


複数のエージェントで記憶を共有する
#

mem0 は記憶を user_id / agent_id でスコープできます。ID の振り方だけで、共有と分離を切り替えられます。

まず、営業エージェントが顧客との会話から記憶を作ります。

m.add(
    [
        {"role": "user", "content": "予算は月5万円くらいで考えています。導入は来月までにしたいです。"},
        {"role": "assistant", "content": "承知しました。月5万円のご予算、来月までの導入ですね。"},
    ],
    user_id="user-tanaka",
    agent_id="sales-agent",
)
# => "ユーザーは月額5万円程度の予算を想定しており、2026年7月までの導入を希望している。" が記憶される

次に、別のサポートエージェントが同じ顧客について検索します。検索条件の違いで結果がどう変わるかを見てください。

# (A) user_id だけで検索 → エージェントをまたいで共有
m.search("このお客様の予算は?", filters={"user_id": "user-tanaka"})
# => 営業が記録した「予算5万円/月・2026年7月までの導入」がヒット(score 0.67)

# (B) user_id + 自分の agent_id で検索 → そのエージェント専用の記憶だけ
m.search("このお客様の予算は?", filters={"user_id": "user-tanaka", "agent_id": "support-agent"})
# => 空(support-agent はまだ何も覚えていない)

user_id で引けばエージェントをまたいで記憶が共有され、agent_id まで指定すればエージェントごとに分離できます。営業が聞いた話をサポートも踏まえて応対する場合も、エージェントごとに記憶を切り分ける場合も、検索条件の設計だけで表現できます。

なお mem0 OSS では、記憶に必ず user_id / agent_id / run_id のいずれかのスコープが付きます(ソース上、少なくとも1つが必須)。どこにも紐づかない「全体共通の記憶」という概念はなく、共有は user_id などのスコープを揃えることで実現します。


運用観点でのまとめ
#

Google Cloud(Vertex AI)と pgvector で動かしてみて分かったことを、観点ごとに表にまとめます。

観点まとめ
導入の軽さadd / search だけでユーザーごとに記憶が溜まり、抽出も mem0 任せ。ただし軽いのは導入段階まで(本番は精度・PII・削除・評価の設計が要る)
認証・ガバナンスLLM・Embedder を Vertex AI で完結でき、API キーを避けて IAM/ADC でガバナンスに乗せられる(保存先の Cloud SQL は別途必要)
記憶の言語モデル依存(gemini-3.5-flash=日本語ベース、gemini-2.5-flash=英語)。そろえるなら custom_instructions で固定
コスト保存ごとに LLM 抽出+埋め込みが走る(今回の検証は数円)。本番は Cloud SQL の固定費が主なコスト要因
レイテンシadd は LLM 抽出+埋め込みを挟むため重い。チャット応答のなかで同期実行するとユーザーを待たせるので、本番では非同期化(Cloud Tasks などへの委譲)を検討
記憶のライフサイクル古い・誤った記憶の更新/削除には update() / delete() / delete_all() が用意されている。いつ消す・要約するか(長期運用での肥大化への対応)のポリシーはアプリ側の設計事項
マルチテナント分離アプリ層の user_id フィルタだけに頼らず、DB層での分離(行レベルセキュリティなど)も検討の余地がある(今回は未検証)
PII・データ消去権PII の記憶への混入対策と、ユーザーの削除要求への対応をアプリ側で設計する

メモリレイヤーは、観測(オブザーバビリティ)とも別の関心事です。エージェントの会話を1セッション内で追跡するオブザーバビリティと、セッションをまたいで事実を覚えるメモリレイヤーは役割が違い、組み合わせて使います。RAG との関係も同じで、mem0 自身が述べるとおり、知識は RAG、パーソナライズは記憶、と両方を併用するのが現実的です。

読者タイプ別の次の一歩は次のとおりです。

  • すぐ試したいエンジニア:uv add mem0ai google-genai "psycopg[binary,pool]" と上記の環境変数を設定すれば、ローカルの pgvector と Vertex AI をつないで本記事の構成をそのまま動かせます
  • Google Cloud で本番を見据える方:ベクトルDBを Cloud SQL(pgvector 拡張)に、認証をサービスアカウントに置き換えれば、同じ構成を本番想定で組めます
  • アーキテクチャを検討中の方:ファインチューニング・RAG・プロンプト・メモリレイヤーを「モデルを変えるか」「何を扱うか」で整理すると、どこを改善すべきかが見えてきます

今回は記憶を使うエージェントと、複数エージェントでの記憶共有まで確認しました。次は記憶の可視化・監査や、アプリをまたいだ記憶連携といったテーマにも踏み込みたいと思います。


関連記事(GAO ブログ)