はじめに:AI VTuber「牡丹プロジェクト」とは
本記事は、AI VTuber三姉妹(Kasho、牡丹、ユリ)の記憶システム設計の技術解説シリーズ第3弾(最終回)です。
プロジェクト概要
「牡丹プロジェクト」は、過去の記憶を持つAI VTuber を実現するプロジェクトです。三姉妹それぞれが固有の記憶・個性・価値観を持ち、視聴者と自然に会話できることを目指しています。
三姉妹の構成
- Kasho(長女): 論理的・分析的、慎重でリスク重視、保護者的な姉
- 牡丹(次女): ギャル系、感情的・直感的、明るく率直、行動力抜群
- ユリ(三女): 統合的・洞察的、調整役、共感力が高い
GitHubリポジトリ
本プロジェクトのコードは以下で公開しています:
- リポジトリ: https://github.com/koshikawa-masato/AI-Vtuber-Project
- 主要機能: 記憶生成システム、三姉妹決議システム、LangSmith統合、VLM対応
シリーズのまとめ:これまでの旅
第1弾:RAGの限界
第1弾では、RAGをAI VTuberの記憶システムとして試した結果、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を選んだ理由
- pgvector拡張:ベクトル検索がネイティブ対応
- ハイブリッド検索:構造化フィルター + ベクトル検索を1クエリで実行
- 高度なクエリ:重要度スコアリング、複雑なランキングが可能
- 将来の拡張性: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);
ポイント
- diary_embedding vector(1536): テキストのベクトル表現を保存
- importance_score: 記憶の重要度を自動計算
- recall_count: 想起回数を記録(リハーサル効果)
- 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 |
得られた知見
- 適材適所の重要性: RAGも記憶製造機も、それぞれ適した用途がある
- ハイブリッドの価値: 2つのアプローチを組み合わせることで、両方の強みを活かせる
- 人間らしさの追求: 忘却曲線、リハーサル効果など、人間の記憶の特性を模倣することで、よりリアルなAIキャラクターが実現できる
シリーズ完結
記憶システムシリーズ、お読みいただきありがとうございました。
このシリーズが、AI VTuberやAIキャラクターの記憶システムを設計する際の参考になれば幸いです。
記憶システムシリーズの進行状況
| 記事 | 内容 | 状態 |
|---|---|---|
| 第1弾 | RAGを試して気づいたこと | ✅ 公開済み |
| 第2弾 | 記憶製造機の設計(構造化記憶) | ✅ 公開済み |
| 第3弾 | ハイブリッドアプローチ(RAG + 構造化) | ✅本記事 |
参考リンク
本プロジェクトの技術記事(Phase 1-5)
- Phase 1: LangSmithマルチプロバイダートレーシング
- Phase 2: VLM実装ガイド
- Phase 3: LLM as a Judge実装ガイド
- Phase 4: 三姉妹討論システム
- Phase 5: センシティブ判定システム