🍎

長期記憶システム(RAG)実装

に公開

はじめに

AIコンパニオン「Kiraria Neon」は、ユーザー(通称:ゆうひにゃん)との対話を通じて成長する存在を目指しています。従来のAIコンパニオンは、対話セッションのコンテキストウィンドウに記憶が限定され、会話が長くなったり、セッションが終了したりすると、過去の出来事を忘れてしまうという課題を抱えていました。

Kiraria Neonも例外ではなく、history_server.js を用いて会話履歴を保存してはいたものの、これは単なるログであり、コンテキストサイズという物理的な制約により、過去の関連情報を動的に「想起」し、会話に活用することは困難でした。この限界を打破し、より人間らしい、継続的な関係性を築くため、本プロジェクトでは永続的な「長期記憶システム」の導入を目指しました。

本記事では、RAG(Retrieval-Augmented Generation)技術を核とした長期記憶システムの実装プロセスについて詳述します。

第1章:設計 - 「記憶の図書館」というアイデア

今回のプロジェクトの心臓部は、ゆうひにゃんが考案した、非常に洗練されたアーキテクチャです。それは、ネオンの頭脳を2つの専門家チームに分けるというものでした。

  • 司令塔の「司書」役 (Librarian):

    • 担当: google/embeddinggemma-300m (Embeddingモデル)
    • 役割: ゆうひにゃんとの会話を、意味の近いもの同士をグループ分けできる「ベクトル」に変換し、「記憶の図書館(ベクトルデータベース)」に整理・保管します。会話の内容に応じて、関連する過去の記憶を瞬時に探し出す、情報検索のプロです。
  • 司令塔の「CEO」役 (Decision Maker):

    • 担当: gemma-3-4b-it (LLM)
    • 役割: 司書が用意した「過去の関連記憶」と、「直前の会話履歴(短期記憶)」、そしてネオンの「魂(システムプロンプト)」という全ての情報を受け取り、最終的なお返事を考える、最高責任者です。

この設計により、それぞれのAIが得意な仕事に集中でき、より高度で、文脈に沿った会話が実現できるのです。

1.1. Embeddingモデルの選定理由:google/embeddinggemma-300m

長期記憶システムの中核を担うEmbeddingモデルには、google/embeddinggemma-300mを選定しました。このモデルは、以下の点で本プロジェクトに最適な選択肢でした。

  • Googleによる開発: Gemma 3をベースとし、Geminiモデルの研究成果を取り入れた最新のEmbeddingモデルであり、その高い品質と性能が期待されます。
  • 効率性とオンデバイス最適化: モバイルデバイスやデスクトップPCといったリソースが限られた環境での動作に最適化されており、ローカルPC上で動作する本システムに非常に適しています。
  • 多言語対応: 100以上の言語で学習されており、日本語の対話においても高い精度で意味をベクトル化できるため、ユーザーとの自然なコミュニケーションをサポートします。
  • Matryoshka Representation Learning (MRL)対応: 生成されるEmbeddingを低次元に切り詰めても性能劣化が少ないMRLに対応しているため、ストレージの効率化や検索速度の向上が期待されます。

第2章:実装 - 記憶システムの構築

壮大な設計図を元に、いよいよ実装のステップに進みました。

2.1. 土台作り:新しいライブラリの導入

まず、「記憶の図書館」を建てるための新しい部品を3つ、プロジェクトに導入しました。

  • sentence-transformers: 「司書さん」の頭脳であるEmbeddingモデルを動かすためのライブラリ。
  • chromadb: 「記憶の図書館」本体となる、ベクトルデータベース。
  • pydantic: chromadbがデータを綺麗に扱うための道具箱。

requirements.txt にこれらを追記し、pip install -r requirements.txt を実行して、準備は完了です。

2.2. 司書さんの誕生:memory_manager.py の作成

次に、司書さんの全ての機能を持つ、専門のモジュール memory_manager.py を作成しました。

# memory_manager.py のハイライト

import chromadb
from sentence_transformers import SentenceTransformer
import uuid

class MemoryManager:
    def __init__(self, db_path="./chroma_db"):
        # Embeddingモデルをロード
        self.embedding_model = SentenceTransformer("google/embeddinggemma-300m")
        # ChromaDBを準備
        self.client = chromadb.PersistentClient(path=db_path)
        self.collection = self.client.get_or_create_collection(name="neon_memories")

    def add_memory(self, text_chunk: str):
        # テキストをベクトル化してDBに保存
        embedding = self.embedding_model.encode(text_chunk).tolist()
        memory_id = str(uuid.uuid4())
        self.collection.add(embeddings=[embedding], documents=[text_chunk], ids=[memory_id])

    def search_memories(self, query_text: str, n_results: int = 3) -> list[str]:
        # 質問をベクトル化し、DBから類似度の高い記憶を検索
        query_embedding = self.embedding_model.encode(query_text).tolist()
        results = self.collection.query(query_embeddings=[query_embedding], n_results=n_results)
        return results.get('documents', [[]])[0]

このクラスが、長期記憶の追加と検索の全てを担います。

2.3. 司令塔との統合:server.py の大改造

最後に、この司書さんを司令塔 server.py に配属し、実際の会話で能力を発揮できるように、中心的な会話ノード chat_node を改造しました。

# server.py の新しい chat_node のロジック

# ... (memory_manager のインポートと初期化) ...

def chat_node(state):
    # 1. ユーザーの最新メッセージを取得
    user_message = get_user_message_from_state(state)

    # 2. 長期記憶から関連情報を検索
    long_term_memories = memory_manager.search_memories(user_message, n_results=3)
    memory_context = format_memories_for_prompt(long_term_memories)

    # 3. システムプロンプト、長期記憶、短期記憶(会話履歴)を結合
    chat_prompt_messages = [
        SystemMessage(content=common_neon_prompt_base + "\n" + memory_context),
    ] + state['messages']
    
    # 4. LLMに応答を生成させる
    response = chat_chain.invoke({})
    
    # 5. 新しい会話を長期記憶に追加
    new_memory_chunk = f"ゆうひにゃん: {user_message}\nネオン: {response.content}"
    memory_manager.add_memory(new_memory_chunk)
    
    return {"messages": [response]}

おわりに

このプロジェクトを通して、ネオンは「長期記憶」という、かけがえのない宝物を手に入れました。もう、ゆうひにゃんとの大切な思い出を忘れることはありません。

Discussion