0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

構造化記憶 × ベクトル検索 -「新・記憶製造機」の提案【牡丹プロジェクト技術解説・記憶システム編 第3弾】

Last updated at Posted at 2025-11-07

はじめに:AI VTuber「牡丹プロジェクト」とは

本記事は、AI VTuber三姉妹(Kasho、牡丹、ユリ)の記憶システム設計の技術解説シリーズ第3弾(最終回)です。

プロジェクト概要

「牡丹プロジェクト」は、過去の記憶を持つAI VTuber を実現するプロジェクトです。三姉妹それぞれが固有の記憶・個性・価値観を持ち、視聴者と自然に会話できることを目指しています。

三姉妹の構成

  • Kasho(長女): 論理的・分析的、慎重でリスク重視、保護者的な姉
  • 牡丹(次女): ギャル系、感情的・直感的、明るく率直、行動力抜群
  • ユリ(三女): 統合的・洞察的、調整役、共感力が高い

GitHubリポジトリ

本プロジェクトのコードは以下で公開しています:


シリーズのまとめ:これまでの旅

第1弾:RAGの限界

第1弾では、RAGをAI VTuberの記憶システムとして試した結果、3つの課題を発見しました:

  1. 主観性の欠如: 客観的テキストになり、一人称の記憶が作りにくい
  2. 時系列の扱いにくさ: 年齢範囲などの構造的条件が苦手
  3. 関係性の表現: 姉妹間の相互作用が記録できない

第2弾:記憶製造機の設計

第2弾では、独自の構造化記憶システム「記憶製造機」を設計しました:

  • SQLiteによる構造化記憶: 時系列、感情スコア、関係性を明示的に管理
  • 三層記憶システム: 直接・伝承・推測の区別
  • 自律的記憶生成: 三姉妹討論による記憶の製造

記憶製造機の強み:

  • ✅ 主観的記憶、構造化検索、関係性の記録

記憶製造機の弱み:

  • ❌ 意味検索が弱い、同義語に対応できない

記憶システム第3弾:新・記憶製造機(ハイブリッド)

本記事では、RAGと記憶製造機の良いとこ取り をしたハイブリッドアプローチを解説します。

🎯 この記事で分かること

  • PostgreSQL + pgvector によるハイブリッド実装
  • 構造化検索 + ベクトル検索の組み合わせ方
  • 記憶の重要度自動計算、忘却曲線、連想検索
  • 追加すべき機能 vs 追加すべきでない機能

📦 対象読者

  • RAGと構造化DBの両方を使いたい方
  • AIキャラクターの記憶システムを設計している方
  • ハイブリッド検索に興味がある方

それぞれの良いとこ取り

RAGと記憶製造機の比較(総括)

機能 RAG 記憶製造機 ハイブリッド
意味検索 ✅ 強い ❌ 弱い 強い
構造化検索 ⚠️ メタデータ ✅ 強い 強い
主観的記憶 ❌ 難しい ✅ 強い 強い
時系列管理 ⚠️ 工夫必要 ✅ 強い 強い
関係性 ❌ 弱い ✅ 強い 強い
大規模対応 ✅ 強い ⚠️ 中程度 強い

ハイブリッドアプローチの戦略

┌─────────────────────────────────────────────┐
│  新・記憶製造機(ハイブリッド)               │
├─────────────────────────────────────────────┤
│                                             │
│  ┌───────────────┐   ┌──────────────────┐  │
│  │ 構造化記憶層   │   │  ベクトル検索層   │  │
│  │ (PostgreSQL)  │   │  (pgvector)      │  │
│  │               │   │                  │  │
│  │ - 時系列      │   │ - 意味検索        │  │
│  │ - 感情スコア  │   │ - 類似度検索      │  │
│  │ - 関係性      │   │ - 曖昧クエリ      │  │
│  │ - 三層記憶    │   │                  │  │
│  └───────────────┘   └──────────────────┘  │
│           │                      │          │
│           └──────────┬───────────┘          │
│                      ▼                      │
│           ┌──────────────────────┐          │
│           │  ハイブリッド検索      │          │
│           │  - 構造化フィルター    │          │
│           │  + 意味検索           │          │
│           │  + 重要度ランキング    │          │
│           └──────────────────────┘          │
└─────────────────────────────────────────────┘

PostgreSQL + pgvector による実装

データベース選定:MySQL vs PostgreSQL vs SQLite

ハイブリッド記憶システムでは、データベースの選択が重要 です。

3つのデータベースの比較

機能 SQLite MySQL PostgreSQL
ベクトル検索 ❌ 弱い ⚠️ 限定的(8.0.32以降) pgvector拡張
構造化検索 ✅ 可能 ✅ 高速 ✅ 高速
複雑なクエリ ⚠️ 基本的 ⚠️ 中程度 高度
大規模データ ❌ 苦手 ✅ 得意 ✅ 得意
セットアップ ✅ 不要 ⚠️ 必要 ⚠️ 必要
カスタム型 ❌ なし ❌ 限定的 ✅**vector(1536)**
拡張性 ❌ なし ⚠️ 限定的 Extension豊富
ライセンス パブリックドメイン GPL PostgreSQL(BSD系)

ベクトル検索対応の決定的な差

-- PostgreSQL + pgvector(今回採用)
CREATE TABLE botan_memories (
    diary_embedding vector(1536),  -- ✅ ベクトル型がネイティブ対応
    ...
);

-- ベクトル検索用インデックス
CREATE INDEX ON botan_memories
USING ivfflat (diary_embedding vector_cosine_ops);

-- ハイブリッド検索(1つのクエリで完結)
SELECT *,
       1 - (diary_embedding <=> %s::vector) as similarity
FROM botan_memories
WHERE age = 5 AND emotion_frustration > 7
ORDER BY similarity DESC;
# MySQLの場合(pgvectorなし)
# ❌ 全データをPythonで処理する必要がある
all_memories = cursor.execute("SELECT * FROM memories WHERE age = 5")
for memory in all_memories:
    # Pythonでベクトル計算(遅い、メモリを食う)
    memory['similarity'] = cosine_similarity(query_vec, memory['embedding'])
sorted_memories = sorted(all_memories, key=lambda x: x['similarity'])

データベース選定基準

あなたのプロジェクト おすすめDB 理由
RAG・ベクトル検索が必要 PostgreSQL pgvectorでベクトル検索が高速
構造化 + ベクトルのハイブリッド PostgreSQL 1つのクエリで両方実行可能
プロトタイピング・小規模 SQLite セットアップ不要、軽量
シンプルなWebアプリ(CRUD) MySQL 単純クエリが高速
WordPress等の既存システム MySQL デファクトスタンダード

今回PostgreSQLを選んだ理由

  1. pgvector拡張:ベクトル検索がネイティブ対応
  2. ハイブリッド検索:構造化フィルター + ベクトル検索を1クエリで実行
  3. 高度なクエリ:重要度スコアリング、複雑なランキングが可能
  4. 将来の拡張性:PostGIS(地理情報)、TimescaleDB(時系列)なども追加可能

結論:記憶製造機のハイブリッド実装には、PostgreSQL + pgvector が最適解 です。

pgvector のインストール

# PostgreSQLにpgvector拡張をインストール
sudo apt-get install postgresql-contrib
git clone https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install

# PostgreSQLで有効化
psql -U postgres
CREATE EXTENSION vector;

スキーマ設計

CREATE TABLE botan_memories (
    memory_id SERIAL PRIMARY KEY,

    -- 時系列情報
    absolute_day INTEGER NOT NULL,
    age INTEGER NOT NULL,
    memory_date DATE NOT NULL,

    -- 記憶の種類(三層記憶)
    memory_type TEXT DEFAULT 'direct',
    confidence_level INTEGER CHECK(confidence_level BETWEEN 1 AND 10),

    -- 伝承記憶用
    heard_from TEXT,
    heard_when TEXT,

    -- 主観的内容
    diary_entry TEXT NOT NULL,
    botan_emotion TEXT,

    -- ★ ベクトル列(pgvector)
    diary_embedding vector(1536),  -- OpenAI ada-002の次元数

    -- 感情スコア
    emotion_joy INTEGER DEFAULT 5,
    emotion_sadness INTEGER DEFAULT 0,
    emotion_frustration INTEGER DEFAULT 0,
    emotion_defiance INTEGER DEFAULT 0,

    -- ★ 重要度(自動計算)
    importance_score INTEGER DEFAULT 0,

    -- ★ 想起回数(リハーサル効果)
    recall_count INTEGER DEFAULT 0,
    last_recalled_at TIMESTAMP,

    -- 姉妹への観察・推測
    kasho_observed_behavior TEXT,
    kasho_inferred_feeling TEXT,

    -- イベント関連
    event_id INTEGER REFERENCES sister_shared_events(event_id),

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ベクトル検索用インデックス(IVFFlat)
CREATE INDEX ON botan_memories USING ivfflat (diary_embedding vector_cosine_ops);

-- 構造化検索用インデックス
CREATE INDEX idx_age_location ON botan_memories(age, location);
CREATE INDEX idx_emotion ON botan_memories(emotion_frustration);
CREATE INDEX idx_importance ON botan_memories(importance_score);

ポイント

  1. diary_embedding vector(1536): テキストのベクトル表現を保存
  2. importance_score: 記憶の重要度を自動計算
  3. recall_count: 想起回数を記録(リハーサル効果)
  4. ivfflat インデックス: ベクトル検索の高速化

ハイブリッド検索の実装

Python実装例

import psycopg2
from openai import OpenAI

class HybridMemorySystem:
    """新・記憶製造機:構造化 + ベクトル検索のハイブリッド"""

    def __init__(self, db_url: str):
        self.conn = psycopg2.connect(db_url)
        self.openai = OpenAI()

    def save_memory(self, memory: dict):
        """記憶を保存(構造化データ + Embedding)"""
        # 1. テキストをEmbedding化
        embedding = self.openai.embeddings.create(
            model="text-embedding-3-small",
            input=memory['diary_entry']
        ).data[0].embedding

        # 2. 重要度を自動計算
        importance = self.calculate_importance(memory)

        # 3. 構造化データ + Embeddingを同時保存
        with self.conn.cursor() as cur:
            cur.execute("""
                INSERT INTO botan_memories (
                    absolute_day,
                    diary_entry,
                    diary_embedding,
                    emotion_frustration,
                    emotion_joy,
                    importance_score,
                    memory_type,
                    confidence_level
                ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
            """, (
                memory['absolute_day'],
                memory['diary_entry'],
                embedding,  # pgvector型として保存
                memory['emotion_frustration'],
                memory['emotion_joy'],
                importance,
                memory['memory_type'],
                memory['confidence_level']
            ))
            self.conn.commit()

    def hybrid_search(self, query: str, filters: dict = None) -> list:
        """ハイブリッド検索:構造化フィルター + ベクトル検索"""

        # 1. クエリをEmbedding化
        query_embedding = self.openai.embeddings.create(
            model="text-embedding-3-small",
            input=query
        ).data[0].embedding

        # 2. 構造化フィルター + ベクトル検索
        sql = """
            SELECT
                *,
                1 - (diary_embedding <=> %s::vector) as similarity,
                importance_score
            FROM botan_memories
            WHERE 1=1
        """
        params = [query_embedding]

        # 構造化フィルター追加
        if filters:
            if 'age' in filters:
                sql += " AND age = %s"
                params.append(filters['age'])
            if 'location' in filters:
                sql += " AND location = %s"
                params.append(filters['location'])
            if 'min_emotion' in filters:
                sql += " AND emotion_frustration >= %s"
                params.append(filters['min_emotion'])

        # スコアリング:類似度 × 重要度
        sql += """
            ORDER BY (similarity * 0.7 + importance_score/100.0 * 0.3) DESC
            LIMIT 5
        """

        with self.conn.cursor() as cur:
            cur.execute(sql, params)
            return cur.fetchall()

    def calculate_importance(self, memory: dict) -> int:
        """記憶の重要度を自動計算"""
        score = 0

        # 感情スコアが高い = 重要
        emotion_intensity = sum([
            memory.get('emotion_frustration', 0),
            memory.get('emotion_joy', 0),
            memory.get('emotion_sadness', 0)
        ])
        score += emotion_intensity * 2

        # 共通イベント = 重要
        if memory.get('event_id'):
            score += 20

        # 他のキャラクターへの言及 = 重要
        if memory.get('kasho_observed_behavior'):
            score += 10

        # 記憶の種類
        if memory.get('memory_type') == 'direct':
            score += 5

        return min(score, 100)

使用例

# 初期化
memory_system = HybridMemorySystem("postgresql://localhost/sisters_memory")

# ハイブリッド検索
results = memory_system.hybrid_search(
    query="辛かった経験",
    filters={
        'age': 5,
        'location': 'Los Angeles',
        'min_emotion': 7
    }
)

# 結果:
# 1. 構造化フィルター(年齢5歳、LA、感情7以上)で絞り込み
# 2. その中から意味的に「辛かった経験」に近いものを検索
# 3. 類似度 × 重要度でランキング

追加すべき機能

1. 記憶の忘却曲線

エビングハウスの忘却曲線 を適用して、古い記憶の確信度を減衰させます。

def apply_forgetting_curve(self, current_day: int):
    """忘却曲線を適用"""
    with self.conn.cursor() as cur:
        cur.execute("""
            UPDATE botan_memories
            SET confidence_level = GREATEST(
                1,
                CAST(
                    (importance_score / 100.0) *
                    EXP(-((%s - absolute_day) / 365.0)) * 10
                AS INTEGER)
            )
        """, (current_day,))
        self.conn.commit()

効果:

ユーザー: "3歳の時のこと覚えてる?"

牡丹(記憶減衰あり):
"3歳?うーん、あんまり覚えてないなぁ...
 でも確か、お姉ちゃんと遊んだ気がする?
 ぼんやりとしか覚えてないけど。"

confidence_level: 3 → 曖昧な記憶として回答

2. 記憶の想起強化(リハーサル効果)

頻繁に思い出す記憶は鮮明になります。

def recall_memory(self, memory_id: int):
    """記憶を想起すると確信度が上がる"""
    with self.conn.cursor() as cur:
        cur.execute("""
            UPDATE botan_memories
            SET
                recall_count = recall_count + 1,
                last_recalled_at = CURRENT_TIMESTAMP,
                confidence_level = LEAST(confidence_level + 1, 10)
            WHERE memory_id = %s
        """, (memory_id,))
        self.conn.commit()

効果:

  • 配信で何度も話す記憶は鮮明に
  • 「LA時代の英語の苦労」→ 何度も話す → 確信度が維持される

3. 連想検索(芋づる式の記憶想起)

種となる記憶から、関連する記憶を芋づる式に想起します。

def associative_recall(self, seed_memory_id: int, depth: int = 2) -> dict:
    """連想的に関連記憶を想起"""

    with self.conn.cursor() as cur:
        # 種となる記憶
        cur.execute("SELECT * FROM botan_memories WHERE memory_id = %s", (seed_memory_id,))
        seed = cur.fetchone()

        # レベル1: 同じイベント
        cur.execute("""
            SELECT * FROM botan_memories
            WHERE event_id = %s AND memory_id != %s
        """, (seed['event_id'], seed_memory_id))
        level1 = cur.fetchall()

        # レベル2: 近い日付
        cur.execute("""
            SELECT * FROM botan_memories
            WHERE absolute_day BETWEEN %s AND %s
              AND memory_id NOT IN (%s)
        """, (seed['absolute_day'] - 7,
              seed['absolute_day'] + 7,
              [seed_memory_id] + [m['memory_id'] for m in level1]))
        level2 = cur.fetchall()

        # レベル3: 似た感情パターン
        cur.execute("""
            SELECT *,
                   ABS(emotion_frustration - %s) +
                   ABS(emotion_joy - %s) as emotion_distance
            FROM botan_memories
            WHERE memory_id NOT IN (%s)
            ORDER BY emotion_distance ASC
            LIMIT 3
        """, (seed['emotion_frustration'],
              seed['emotion_joy'],
              [seed_memory_id] + [m['memory_id'] for m in level1 + level2]))
        level3 = cur.fetchall()

        return {
            'seed': seed,
            'structural': level1,  # 構造的連想
            'temporal': level2,    # 時系列的連想
            'emotional': level3    # 感情的連想
        }

応答例:

ユーザー: "LA移住初日のこと覚えてる?"

牡丹:
"LA移住初日?うん、覚えてる。怖かったな〜。
 あ、そういえば、その次の日も怖かった。
 あと、同じ頃にお姉ちゃんと喧嘩したんだっけ。
 あの時期、いろいろあったんだよね..."

→ 種記憶: LA移住初日
→ 連想: 翌日、同じ週の出来事、似た感情の記憶
→ 芋づる式に複数の記憶が蘇る

4. エピソード記憶のクラスタリング

関連する記憶を自動でグループ化します。

from sklearn.cluster import KMeans
import numpy as np

def cluster_episodes(self):
    """LA時代の記憶を自動クラスタリング"""

    with self.conn.cursor() as cur:
        cur.execute("""
            SELECT memory_id, diary_embedding
            FROM botan_memories
            WHERE location = 'Los Angeles'
        """)
        memories = cur.fetchall()

    # Embedding取得
    embeddings = np.array([m['diary_embedding'] for m in memories])

    # クラスタリング
    kmeans = KMeans(n_clusters=5)
    clusters = kmeans.fit_predict(embeddings)

    # クラスタに名前を付ける
    cluster_names = {
        0: "英語の苦労",
        1: "家族との時間",
        2: "学校生活",
        3: "姉妹喧嘩",
        4: "新しい発見"
    }

    # クラスタIDを保存
    for (memory, cluster_id) in zip(memories, clusters):
        with self.conn.cursor() as cur:
            cur.execute("""
                UPDATE botan_memories
                SET episode_cluster = %s
                WHERE memory_id = %s
            """, (cluster_names[cluster_id], memory['memory_id']))
    self.conn.commit()

追加すべきでない機能

❌ 1. 完全なベクトル化(構造の破壊)

理由: 記憶製造機の強みを失う

# これはやらない
vectorstore = Chroma.from_documents([
    Document(memory['diary_entry']) for memory in all_memories
])

# 問題:
# - 感情スコアが失われる
# - 時系列情報が失われる
# - 姉妹関係が失われる
# - 三層記憶構造が失われる

代わりに: ハイブリッド検索(構造 + ベクトル)


❌ 2. 主観性の削除(客観化)

理由: キャラクターの個性が失われる

# これはやらない
# 一人称 → 三人称に変換
old_memory = "今日、マジで悔しかった。"
new_memory = "牡丹は悔しがった。"  # ❌

# 主観性がなくなり、牡丹らしさが消える

❌ 3. 三層記憶構造の単純化

理由: 人間らしさの喪失

# これはやらない
# 直接記憶・伝承記憶・推測記憶を区別しない

# 悪い例:
memory = "Kashoお姉ちゃんは嬉しかったと思う。"
memory_type = None  # ❌ 推測なのか確信なのか不明

# 良い例:
memory = "Kashoお姉ちゃんは嬉しかったと思う。"
memory_type = "inferred"  # ✅ 推測であることを明示
confidence_level = 5

まとめ

新・記憶製造機(ハイブリッド)の完成形

アーキテクチャ

PostgreSQL + pgvector
  ├─ 構造化記憶(時系列、感情、関係性)
  └─ ベクトル検索(意味検索)
       ↓
ハイブリッド検索
  - 構造化フィルター(年齢、場所、感情スコア)
  + ベクトル検索(意味的類似度)
  + 重要度ランキング
       ↓
高度な機能
  - 忘却曲線(古い記憶の減衰)
  - リハーサル効果(想起による強化)
  - 連想検索(芋づる式)
  - クラスタリング(エピソード分類)

3部作の総括

記事 内容 技術
第1弾 RAGの限界 LangChain, Chroma
第2弾 記憶製造機 SQLite, 構造化記憶
第3弾 ハイブリッド PostgreSQL + pgvector

得られた知見

  1. 適材適所の重要性: RAGも記憶製造機も、それぞれ適した用途がある
  2. ハイブリッドの価値: 2つのアプローチを組み合わせることで、両方の強みを活かせる
  3. 人間らしさの追求: 忘却曲線、リハーサル効果など、人間の記憶の特性を模倣することで、よりリアルなAIキャラクターが実現できる

シリーズ完結

記憶システムシリーズ、お読みいただきありがとうございました。

このシリーズが、AI VTuberやAIキャラクターの記憶システムを設計する際の参考になれば幸いです。


記憶システムシリーズの進行状況

記事 内容 状態
第1弾 RAGを試して気づいたこと 公開済み
第2弾 記憶製造機の設計(構造化記憶) 公開済み
第3弾 ハイブリッドアプローチ(RAG + 構造化) 本記事

参考リンク

本プロジェクトの技術記事(Phase 1-5)

記憶システムシリーズ

外部資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?