PythonとLetta (MemGPT) 入門:自己管理型・長期記憶を持つAIエージェントの構築

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

午前2時、コンテキストウィンドウの上限到達によるVIPボット障害

午前2時15分、オンコール用Slackチャンネルに赤色の警告アラートが鳴り響きました。HTTP 400 - ContextWindowExceededError。原因は、あるVIPクライアントが3週間にわたって送信し続けた180件以上の技術要件カスタマイズメッセージでした。プロンプトに蓄積されたトークン数がgpt-4oの上限である128kトークンを超過し、ボットは即座に応答不能に陥りました。

これは本番環境(プロダクション)における典型的な課題です。LLMは本質的にステートレス(stateless)であり、各リクエストは過去のやり取りから完全に独立しています。しかしユーザー側は、何日にもわたる会話の後でも、ボットが細かいコンテキストをすべて記憶していることを期待します。

AIエージェントのメモリ保持における3つのアーキテクチャ比較

その夜、チームは現状を打破するために広く用いられている3つのアプローチを再検証しました。

  • 1. スライディングウィンドウ(Sliding Window): 直近のk往復の会話(通常10〜20件)のみを保持し、プロンプトに直接埋め込む手法。それ以前の古いメッセージはすべて切り捨てられます。
  • 2. ベクトルDB + RAG(会話履歴のセマンティック検索): 会話の各ターンをベクトルデータベース(Qdrantやpgvectorなど)に保存。新たな入力があるたびに埋め込み(embedding)を生成し、類似度の高い上位k件をコンテキストへ注入する手法。
  • 3. OSライクな階層型メモリ(Letta / MemGPT): コンピュータのハードウェア階層を模倣したアーキテクチャ。Core MemoryをRAMのように扱い、Archival MemoryやRecall MemoryをSSDストレージのように位置づけます。エージェント自身が内部ツールコールを通じて、メモリの読み取り、書き込み、削除、更新を自律的に決定します。

実践比較:RAGの落とし穴とLettaの強み

1. スライディングウィンドウ:低コスト・実装容易だが深刻な「物忘れ」

このアプローチは極めて迅速に実装可能で、Redis内のListやQueueがあれば成立します。しかし、15〜20往復もすると、プロジェクトの予算、技術スタック、要件定義といった初期の重要情報がコンテキストウィンドウから完全に消失します。ボットは初期に確認済みの内容を初歩的に再質問し始め、ユーザー体験を著しく損ないます。

2. ベクトルDB RAG:キーワード一致には強いが時系列の変更に弱い

RAGは膨大なデータ量を保存できます。しかし、状態の上書き(state mutation)の処理が極めて苦手です。具体例を見てみましょう。

  • 先週のユーザーの発言:「AWS RDS上でPostgreSQL 14を運用しています」
  • 本日のユーザーの発言:「システムをCockroachDBへ移行完了しました」

通常のRAGによるベクトル検索では、類似度スコアが近い両方の情報が同時にヒットしてしまいます。LLMは相反する2つの情報が含まれたプロンプトを受け取ることになり、どちらが現在の実態なのか判断できずハルシネーションを起こします。

3. Letta:3階層の自己管理型メモリアーキテクチャ

Letta(UCバークレー校の研究プロジェクトMemGPTから発展)は、メモリを以下の3つの領域に分割することでこの課題を根本から解決します。

  • Core Memory(RAM): humanブロック(ユーザー情報)とpersonaブロック(ボットのペルソナ)で構成。全リクエストのコンテキストウィンドウ内に常に直接配置され、コスト最適化のためにサイズ(通常数千トークン程度)が制限されます。
  • Recall Memory: 全メッセージを時系列順に記録したログ。フィルタリングやページネーションに対応。
  • Archival Memory(ストレージ): 容量無制限の長期保存領域。埋め込みベクトルによるセマンティック検索に対応。

決定的な違いは、Lettaのエージェントが「メモリ自己編集権限(Memory Editing Tools)」を持っている点にあります。ユーザーがスタックの変更を伝えると、エージェントは即座にcore_memory_replace関数を呼び出し、Core Memory上のデータを新しい内容で上書きします。開発者がcronジョブやSQLクエリを追加で書く必要は一切ありません。

実践導入:PythonでLettaエージェントを構築する

以下の手順に沿って、ローカルからサーバーまで対応可能な自己更新メモリを持つエージェントを構築します。

ステップ1:Letta SDKのインストールと環境構築

クリーンな仮想環境を作成し、lettaパッケージをインストールします。

python3 -m venv letta-env
source letta-env/bin/activate

pip install letta

ステップ2:APIキーの設定とLettaサーバーの起動

Lettaはクライアント・サーバーモデルで動作します。Lettaサーバーは状態管理、データベース接続(デフォルトはローカルのSQLite、本番環境ではPostgreSQL)、バックグラウンドタスクのオーケストレーションを担います。

export OPENAI_API_KEY="sk-proj-your-api-key-here"

# ローカルバックグラウンドサーバーを起動(デフォルトポート: 8283)
letta server

ステップ3:エージェントの初期化と初期Core Memoryの設定

agent_demo.pyを作成します。サーバーに接続し、エージェントの初期情報を定義します。

from letta import create_client

# localhost:8283 で稼働中のLettaサーバーに接続
client = create_client()

# persona と human の2つのCore Memoryブロックを持つエージェントを作成
agent_state = client.create_agent(
    name="devops_support_agent",
    memory={
        "persona": "私はインフラ支援を担当するSREエンジニアです。簡潔な回答を心がけ、実行コマンドとログからの根本原因分析を優先します。",
        "human": "クライアント名はNam、DevOpsリードを務めており、GCP上で40ノードの本番Kubernetesクラスタを運用中。"
    }
)

print(f"エージェントの初期化に成功しました! ID: {agent_state.id}")

ステップ4:メモリの自動上書き機能の検証

連続して2件のメッセージを送信し、エージェントが情報の変化を検知して自身のRAM(Core Memory)を自動更新するか検証します。

from letta import create_client

client = create_client()
# ステップ3で取得したエージェントIDを指定
agent_id = "agent-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# メッセージ1: 新しいインフラ移行情報を伝える
response = client.send_message(
    agent_id=agent_id,
    role="user",
    message="チームでKubernetesを完全に廃止し、全サービスをHashiCorp Nomadへ移行完了しました!"
)

for msg in response.messages:
    # LLMがメモリツールの呼び出しを決定した際の内部推論思考を出力
    if hasattr(msg, 'message_type') and msg.message_type == 'reasoning_message':
        print(f"[LLMの推論]: {msg.reasoning}")
    elif hasattr(msg, 'text') and msg.text:
        print(f"[エージェント]: {msg.text}")

# メッセージ2: ボットの記憶状態をテストする
response2 = client.send_message(
    agent_id=agent_id,
    role="user",
    message="現在の私のメインインフラ構成は何ですか?"
)

for msg in response2.messages:
    if hasattr(msg, 'text') and msg.text:
        print(f"[エージェント]: {msg.text}")

ステップ5:Core Memoryを直接取得して確認

テキストの返答だけでなく、APIを直接呼び出してデータベース内に保存されている実データを確認します。

current_memory = client.get_core_memory(agent_id=agent_id)
print("\n=== DB内のCore Memoryデータ ===")
for block in current_memory.blocks:
    print(f"[{block.label}]: {block.value}")

コンソールログを確認すると、humanブロックがHashiCorp Nomadの情報に自動更新されていることが確認できます。この全プロセスは、バックグラウンドでのcore_memory_replaceツールコールによって自律的に行われます。OpenAIへ送信される各リクエストのコンテキストウィンドウは常に2,000〜3,000トークン前後に一定に保たれるため、コンテキストウィンドウのあふれを根絶できると同時に、会話履歴全体を詰め込む手法と比較してAPIトークンコストを70〜80%削減できます。

Share: