150行で自己進化するメモリエージェントを構築する方法

150行で自己進化するメモリエージェントを構築する方法 Claude Code
Photo by Ecliptic Graphic on Unsplash

AIエージェントに「記憶」を持たせたい——そう考えたことはありませんか。チャットの文脈を覚えている、過去の会話から学習する、そして自分自身の記憶構造を最適化していく。そんな自己進化型のメモリエージェントを、わずか150行のPythonコードで実装できることをご存知でしょうか。本記事では、DEV Communityで公開された実践的なチュートリアルを基に、メモリアーキテクチャの基本概念と実装方法を解説します。外部依存なし、コピー&ペーストですぐに動かせるスケルトンコードです。

この記事のポイント

  • 150行のPythonで自己進化メモリエージェントを実装
  • 内部ループ(ランタイム処理)と外部ループ(アーキテクチャ進化)の2層構造
  • 4つの「部屋」(エンコード/ストア/検索/管理)で関心を分離
  • 実際のLLMやベクトルDBに差し替え可能な拡張設計

2つのループを理解する

このメモリエージェントの核心は、2つの異なるループで動作することです。

内部ループ(ランタイム) は、通常の記憶操作を担当します。テキストを受け取り、ベクトルに変換(エンコード)し、ストレージに保存(ストア)し、関連する記憶を検索(リトリーブ)し、古い記憶を整理(マネージ)します。これは一般的なRAG(Retrieval-Augmented Generation)システムと同様の処理フローです。

外部ループ(進化) は、メモリシステム自体を最適化します。定期的にパフォーマンス指標——検索成功率やメモリサイズなど——を評価し、設定パラメータを調整します。例えば、検索結果の上位何件を返すか(top_k)、類似度の閾値(sim_threshold)、古い記憶を削除する確率(decay_prob)などが動的に変化します。

この2層構造により、エージェントは運用しながら自分自身を改善していきます。検索がうまくいかない期間が続けば、より多くの結果を返すようにtop_kを増やす。メモリが肥大化すれば、decay_probを上げて積極的に整理する。人間が手動でチューニングしなくても、エージェント自身が最適なバランスを見つけていきます。

4つの「部屋」による関心分離

このアーキテクチャでは、メモリ操作を4つの独立した「部屋」に分離しています。

Encode(エンコード部屋): テキストをベクトル表現に変換します。デモでは文字頻度ベクトルという単純な方法を使用していますが、実際のプロダクションではOpenAIのEmbedding APIや、sentence-transformersなどのライブラリに差し替えます。

Store(ストア部屋): 記憶を永続化します。デモではPythonのリストを使用していますが、PineconeやWeaviateなどのベクトルデータベース、あるいはFaissのようなローカルインデックスに置き換えられます。

Retrieve(検索部屋): 意味的に類似したアイテムを見つけます。コサイン類似度を計算し、閾値を超えるものを上位k件返します。この検索の精度が、エージェント全体の性能を左右します。

Manage(管理部屋): 減衰(decay)を適用して古い記憶を削除します。すべての記憶を永遠に保持することは現実的ではないため、重要度や鮮度に基づいて整理する仕組みが必要です。

各部屋が独立しているため、一部だけを高機能な実装に差し替えることが容易です。

コードの核心部分

以下は、このアーキテクチャの中核となるPythonコードの抜粋です。

class Memory:
    def __init__(self):
        self.items = []
        self.config = {
            "top_k": 3,
            "sim_threshold": 0.5,
            "decay_prob": 0.1
        }
        self.stats = {"hits": 0, "misses": 0}

class Agent:
    def __init__(self):
        self.memory = Memory()

    def handle_task(self, query: str):
        # 内部ループ: encode -> retrieve -> process -> store
        embedding = self.encode(query)
        relevant = self.retrieve(embedding)
        response = self.process(query, relevant)
        self.store(query, embedding)
        self.manage()  # decay
        return response

    def evolve(self):
        # 外部ループ: パフォーマンスに基づいて設定を調整
        hit_rate = self.memory.stats["hits"] / max(1, sum(self.memory.stats.values()))
        if hit_rate < 0.3:
            self.memory.config["top_k"] += 1
        if len(self.memory.items) > 100:
            self.memory.config["decay_prob"] += 0.05

handle_taskメソッドが内部ループを実装し、evolveメソッドが外部ループを実装しています。非常にシンプルな構造ですが、この骨格を理解すれば、より洗練されたメモリシステムを構築する基盤が得られます。

実践してみよう

このスケルトンコードは、依存関係なしで即座に実行できます。

# ファイルを保存して実行
python self_evolving_agent.py

元記事のDEV Communityページには、完全なコードが掲載されています。150行程度のコンパクトなコードなので、全体を読み通すのに時間はかかりません。

実際のプロダクションで使用する場合は、以下の部分を差し替えることを検討してください。

# fake_embed() を実際のEmbedding APIに置き換え
def encode(self, text: str) -> List[float]:
    # OpenAI Embeddings
    response = openai.Embedding.create(input=text, model="text-embedding-ada-002")
    return response['data'][0]['embedding']

# メモリストレージをベクトルDBに置き換え
def store(self, text: str, embedding: List[float]):
    # Pineconeに保存
    self.index.upsert([(str(uuid.uuid4()), embedding, {"text": text})])

差し替える部分が明確に分離されているため、段階的にプロダクション品質に近づけていくことができます。

知っておくと便利なTips

  • decay_probの調整: 減衰確率を高くしすぎると重要な記憶も失われます。最初は低めに設定し、メモリ肥大化が問題になってから徐々に上げていく方が安全です。

  • 検索ヒット率のモニタリング: 外部ループの進化は、検索ヒット率に大きく依存します。この指標をログに残し、時系列で可視化することで、システムの健全性を把握できます。

  • ハイブリッドアプローチ: ベクトル類似度検索だけでなく、キーワード検索(BM25など)と組み合わせるハイブリッド検索も効果的です。両方の結果をマージすることで、検索精度が向上します。

  • メモリの重要度スコア: 単純な減衰だけでなく、各記憶に重要度スコアを付与し、重要なものを優先的に保持する仕組みを追加すると、より賢い記憶管理が可能になります。

まとめ

自己進化するメモリエージェントは、複雑そうに見えて実はシンプルな構造で実装できます。内部ループで記憶を処理し、外部ループで設定を最適化する——この2層構造を理解すれば、様々な応用が可能です。

150行のスケルトンコードは、学習用としてだけでなく、実際のプロトタイプの出発点としても使えます。fake_embed()を本物のEmbedding APIに、リストをベクトルDBに置き換えていけば、プロダクション品質のメモリシステムへと発展させることができます。AIエージェントに記憶を持たせたいと考えている方は、ぜひこのコードを出発点として実験してみてください。


📎 元記事: https://dev.to/narnaiezzsshaa/build-a-self-evolving-memory-agent-in-150-lines-lad

コメント

タイトルとURLをコピーしました