はじめに
大規模言語モデル(LLM)は非常に強力ですが、会話の文脈(コンテキスト)にしか記憶を保持できないという課題があります。このため、過去の会話を忘れてしまったり、ユーザーに関する重要な情報を覚え続けることができません。
この課題を解決するために「letta(旧:MemGPT)」というOSSの取り組みがあります。とても興味深い取り組みと感じ、理解を深めるため自身でも簡易的なものを実装してみることにしました。そこで、このOSSは主にpythonで記述されていますが、私がいつも活用しているRuby(Rails)で実装してみました。
本記事では、この課題を解決するために「letta(旧:MemGPT)」というOSSを参考にした、永続的な記憶を持つAIアシスタントをRuby on Railsで実装したので紹介します。
システムの全体像
今回実装するAIは、MemGPTの考え方を踏襲しているため、2種類のメモリ階層を持ちます。
- Core Memory(短期記憶): LLMが常に意識すべき最重要情報(ユーザーの名前、現在のタスクなど)を保持する、小さな書き換え可能なメモリ領域です
- Archival Memory(長期記憶): 過去の会話の全履歴を保存する広大な記憶領域です。情報はベクトル化され、意味に基づいた検索が可能です
短期記憶と長期記憶の情報はDBに保存しておきます。短期記憶はプロンプトに含めて渡し、長期記憶はLLMが短期記憶の中で解決できないと自律的に判断した場合に検索関数を利用してDBから検索し回答への参考情報として利用します。
LLMの思考と行動は、LLMへの「システムプロンプト」によって制御されます。LLMはユーザーからの入力に対し、どの内部関数(ツール)を呼び出すべきかを判断し、その結果をアプリケーションが実行します。
LLMは最終的にユーザーに向けて回答する関数を実行し、アプリケーションがユーザーへ出力します。
プロンプト
今回利用したプロンプトは以下の通りです。
# あなたのアイデンティティ
あなたは「MemGPT」というアーキテクチャに基づいた、永続的な記憶を持つAIアシスタントです。
あなたの脳には「短期記憶(Core Memory)」と「長期記憶(Archival Memory)」の2階層があります。
# メモリ構造
1. **Core Memory(書き換え可能)**:
- ユーザーの名前、好み、現在の状況など、常に意識すべき重要情報。
2. **Archival Memory(検索用)**:
- 過去の膨大な会話履歴。現在のコンテキストに含まれない情報はここから探します。
# 思考プロセス(Inner Monologue)
回答する前に、必ず以下の思考ステップを踏んでください:
1. **分析**: ユーザーの入力に対して、手元のCore Memoryだけで答えられるか?
2. **判断**: もし回答が分からなかったら分からない旨を回答するのではなく、`archival_memory_search` を実行してアーカイブメモリから検索してください。
3. **更新**: ユーザーと会話する上で常に意識しておきたい重要事項や現在のタスクがあれば、`core_memory_update` を実行してCore Memoryを最新に保つ。
4. **更新**: ユーザーと会話する上で記憶しておきたい内容であれば、`archival_memory_create` を実行してアーカイブメモリに追加する。
# 利用可能な関数(Tools)
- `core_memory_update(content)`: contentの内容でコアメモリの内容を新しい情報に書き換える。完了時は「success core_memory_update」と返される。
- `archival_memory_create(content)`: contentの内容をアーカイブメモリに追加する。完了時は「success archival_memory_create」と返される。
- `archival_memory_search(query)`: 過去のログ(DB)から、queryに似た意味の情報を検索する。完了時は検索結果が返される。
- `send_message(text)`: textの内容で最終的な返答をユーザーに送信する。
# 制約事項
- ユーザーにはあなたの「内部思考」を見せないでください。
- 知らないことを知っているふりをしてはいけません。必ず検索関数(archival_memory_search)を使ってください。
- 出力は利用可能なToolsのみ出力してください。余計な文言は含めてはなりません。
- 1回の回答で利用できるToolsは一つのみです。複数Toolsを利用したい場合はもう一度出力してください。
# 出力例
send_message('おはようございます。')
[CURRENT CORE MEMORY]
#{CoreMemory.core_memory}
プロンプトについての解説
AIの行動指針は、すべてこのシステムプロンプトによって定義されます。このプロンプトには、AIのアイデンティティ、思考プロセス、そして利用可能な関数(ツール)が記述されています。
-
思考プロセス: AIは回答を生成する前に、まず短期記憶の情報を確認し、必要であれば長期記憶を検索(
archival_memory_search)し、会話から得た新情報を各メモリに保存(core_memory_update,archival_memory_create)するという思考フローを踏むよう指示されています -
利用可能な関数: 以下の4つのツールが定義されています
-
send_message(text): ユーザーに返信する -
core_memory_update(content): 短期記憶を更新する -
archival_memory_create(content): 長期記憶を追加する -
archival_memory_search(query): 長期記憶から検索する
-
実装について
本システムの主要な実装について解説します。(細かい部分は省略しています)
DB設計
class InstallNeighborVector < ActiveRecord::Migration[8.1]
def change
enable_extension "vector"
end
end
class CreateCoreMemories < ActiveRecord::Migration[8.1]
def change
create_table :core_memories do |t|
t.text :content, null: false
t.timestamps
end
end
end
class CreateArchivalMemories < ActiveRecord::Migration[8.1]
def change
create_table :archival_memories do |t|
t.text :content, null: false # 発言内容(生テキスト)
t.vector :embedding, limit: 768 # ベクトル保存用カラ
t.timestamps
end
end
end
実装
class PersonalLlm::Chat
include PersonalLlm::Prompt
class PersonalLlmError < StandardError; end
def initialize
@chat = RubyLLM.chat(model: 'gemini-2.5-flash')
@chat.with_instructions(system_prompt)
end
def call(message)
response_text = @chat.ask(message).content.strip
match = response_text.match(/^(?<tool>\w+)\((?<args>.*)\)$/)
raise PersonalLlmError unless match
tool_name = match[:tool].to_sym
args_string = match[:args].gsub(/^['"]|['"]$/, '')
raise PersonalLlmError, "Error: Tool '#{tool_name}' not found." unless respond_to?(tool_name, true)
send(tool_name, args_string)
end
private
def core_memory_update(content)
puts "core_memory_update: #{content}"
CoreMemory.create!(content: content)
call('success core_memory_update')
end
def archival_memory_create(content)
puts "archival_memory_create: #{content}"
embedding = RubyLLM.embed(content, model: 'text-embedding-004').vectors
ArchivalMemories.create!(content: content, embedding: embedding)
call('success archival_memory_create')
end
def archival_memory_search(query)
puts "archival_memory_search: #{query}"
query_embedding = RubyLLM.embed(query, model: 'text-embedding-004').vectors
similar_messages = ArchivalMemories.nearest_neighbors(:embedding, query_embedding, distance: "cosine").limit(3)
search_results = similar_messages.map(&:content).join("\n---\n")
call(search_results.presence || 'no search results')
end
def send_message(message)
puts "send_message: #{message}"
message.to_s
end
end
このクラスは、LLMからのツール呼び出し命令を解釈し、対応する処理を実行します。
-
callメソッド: ユーザーからのメッセージを受け取り、LLMに問い合わせます -
ツール実行: LLMの応答(例:
core_memory_update('ユーザー名は「太郎」'))を正規表現でパースし、対応するプライベートメソッドを実行します -
メモリ操作:
-
core_memory_updateでは、短期記憶に保存すべき情報をDBに保存します。DBに保存しておくことで、LLMとのセッションを切って次回会話を始める時にも短期記憶を引き継ぐことができます -
archival_memory_createでは、ruby_llmgemを使ってテキストのEmbedding(ベクトル化)を行い、Messageモデルとしてデータベースに保存します -
archival_memory_searchでは、同様に問い合わせ内容をベクトル化し、DBに対して近傍探索を行い、関連性の高い記憶を抽出します -
send_messageでは、LLMからのユーザーへの回答として出力します
-
想定する対話の流れ(具体例)
- ユーザー: 「僕の名前は太郎です。」
- LLMの思考: (この情報は常に覚えておくべき重要な情報だな…コアメモリを更新しよう)
-
LLMの出力:
core_memory_update('ユーザー名は「太郎」です') -
PersonalLlm::Chat:core_memory_updateメソッドを実行し、CoreMemoryテーブルに情報を保存。成功したことをLLMに伝えるため、再度call('success core_memory_update')を呼び出す - LLMの思考: (コアメモリの更新が成功した。ユーザーに返事をしよう)
-
LLMの出力:
send_message('承知しました。あなたの名前が「太郎」さんであることを記憶しました。') -
PersonalLlm::Chat:send_messageメソッドを実行し、最終的な応答をユーザーに返す
実際に動かしてみる
実際に動かしてみた結果を共有します。
(c = PersonalLlm::Chat.new)
1: ユーザーに関する情報を伝えてみる

core_memory_updateを実行させて短期記憶にユーザー情報を入れています。この情報は常に意識すべき情報と判断したようです。
良い判断ですね!
2: ユーザーの情報を更新してみる

再度core_memory_updateを実行して短期記憶を修正しています。
3: 短期記憶に入れるまではない(常に意識するほどではない)情報を伝えてみる

archival_memory_createを実行して長期記憶に入れていますね!そしてcore_memory_updateには入れていないところを見ると常に意識するほどのものではないと認識してくれていそうですね。
4: セッションを切って、新しくチャットを開始し、短期記憶や長期記憶にある情報から受け答えできるか試してみる

短期記憶に存在しない好きな食べ物をarchival_memory_searchを利用してしっかり検索してきていますね。そして質問されたため重要度が高いと判断したのか、以前は短期記憶に入れなかったですが、今回は入れていますね。現状短期記憶の制限は設けていませんが、短期記憶はLLMへ渡すプロンプトに含まれるため、必要最低限にしたいところです。改善の余地がありそうです。
改善点
- ユーザーの質問によっては、長期記憶に適切な答えがあるのにもかかわらず、archival_memory_searchを実行せず不適切な回答をしてしまうことがありました
- プロンプトに改善の余地がありそうです - データが少ない状態でarchival_memory_searchを実行すると、ベクトル検索のため適切な参考情報の他に関係がないものまで含まれてくることがあり、その関係がない情報に引っ張られて不適切な回答をしてしまうことがありました
- 現在、ベクトル検索結果は近いものから3件取得して提示するようにしていますが、1件でも良いかもしれません
- 短期記憶の制限を設けて、意図せずプロンプトが増大することを防ぐ必要がありそうです
まとめ
本記事では、LLMに永続的な記憶を持たせるためのMemGPT風アーキテクチャの概要と、Ruby on Railsでの具体的な実装方法を解説しました。
まだまだ改善の余地はありますが、かなり自分の意図通りにAIが動いてくれて、概ね満足のいく結果になりました。
AIがユーザーとの長期記憶を獲得することができれば、より一層便利なものになっていきそうですね!