LLMは賢いですが、そのままだと「前に話したこと」を覚えていません。会話履歴を全部プロンプトに詰めれば擬似的に覚えさせられますが、量が増えると入りきらないし、推論も遅くなります。そこで「結城のあ」では、過去の会話をベクトル検索で必要なぶんだけ取り出す仕組み(RAG)を入れています。
長期記憶と短期記憶を分ける
記憶を2層に分けています。役割が違うので、置き場所も別にしました。
- 短期記憶: 直近の会話の流れ。Redis に、最新10件くらいをTTL(24時間)付きで保持。会話の文脈を保つために毎回プロンプトへ入れる。
- 長期記憶: 過去の出来事や設定。PostgreSQL + pgvector に蓄積し、今の話題に関係するものだけをベクトル検索で引っ張ってくる。
短期記憶はRedisが得意な「速くて消えてもいいデータ」、長期記憶はちゃんと残したい知識、という住み分けです。
埋め込み: nomic-embed-text で768次元に
ベクトル検索の前提として、文章を「意味を表す数値の並び(ベクトル)」に変換する必要があります。これを埋め込み(embedding)と呼びます。「結城のあ」では Ollama で動かせる nomic-embed-text を使い、各テキストを 768次元のベクトルにしています。
意味が近い文章どうしは、ベクトル空間でも近い位置に来ます。だから「今の発話」を同じ方法でベクトル化して、データベースの中から距離が近い過去の記憶を探せば、関連する記憶が取り出せる、という仕組みです。
pgvector に保存して検索する
PostgreSQL に pgvector 拡張を入れると、ベクトル型のカラムを持てて、ベクトル間の距離で検索できるようになります。記憶テーブルには本文と一緒に vector(768) のカラムを持たせています。
検索は「コサイン距離が近い順に上位N件」を取る形です。件数が増えてくると総当たりでは遅くなるので、IVFFlat インデックスを張っています。IVFFlat はベクトル空間をいくつかのクラスタに区切っておき、検索時は近そうなクラスタだけを見る方式で、精度を少し犠牲にする代わりに高速化できます。
距離の演算子は、使う距離の種類(コサイン・ユークリッドなど)とインデックスの種類を揃えるのが大事です。「結城のあ」ではコサイン距離で揃えています。揃っていないとインデックスが使われず、せっかく張っても遅いままになります。
距離しきい値: 関係ない記憶を混ぜない
「近い順に上位N件」をそのまま使うと、本当は関係ない記憶まで毎回引っ張ってきてしまいます。それをプロンプトに入れると、AITuberが文脈と無関係なことを語り出す原因になります。
そこで距離しきい値を設けて、「ある程度近い記憶だけ」を採用するようにしています。「結城のあ」ではコサイン距離でおおよそ 0.30 前後を目安にしています。この値より遠い(=あまり関係ない)記憶は捨てます。しきい値は厳しくしすぎると何も拾えず、緩めすぎるとノイズが増えるので、実際の会話を見ながら少しずつ調整しました。
ハマった落とし穴: NULL の埋め込み
埋め込み生成に失敗したレコードが検索を壊す
記憶を保存するとき、埋め込み生成(Ollama呼び出し)がたまに失敗して、本文だけ入って ベクトルが NULL のレコードができていました。これがあると、ベクトル検索のクエリでエラーになったり、結果がおかしくなったりします。
対策として、埋め込みが NULL のレコードは検索対象から除外する(WHERE embedding IS NOT NULL)こと、そして保存時に埋め込み生成が失敗したらレコード自体を作らない(あるいは後でやり直す)ようにしました。「とりあえず本文だけ保存」が後で効いてくる、典型的な落とし穴でした。
夜間の振り返りで記憶を育てる
会話をそのまま全部ベクトル化して貯めると、雑多すぎて検索のノイズになります。そこで毎晩 午前3時に、その日の会話を振り返って要点を整理するバッチを走らせています(実行基盤は k3s の記事 を参照)。
この振り返りでは、LLMに「今日の会話から覚えておくべきことを抜き出す」ような処理をさせています。生の会話ログより、整理された記憶のほうが後で引きやすくなります。じっくり考えさせたいので、このバッチではモデルの思考モード(think:true)を有効にしています。リアルタイムの配信応答では速度優先で思考を切りますが、夜間バッチは時間に余裕があるので質を優先する、という使い分けです。
まとめ
長期記憶は「埋め込み → pgvectorで近傍検索 → しきい値で絞る」という流れが土台で、そこに「短期記憶はRedis」「NULL埋め込みを混ぜない」「夜間に記憶を整理する」といった運用の工夫を重ねて、ようやく自然に過去を覚えているAITuberになります。地味ですが、ここがキャラクターの「らしさ」を一番支えている部分だと思っています。
続けて読む: ローカルLLMの全体構成 / 自宅k3sでAI基盤を組む / VTube Studioの表情API