LLM向けのオープンソース・メモリレイヤーを作った——その仕組みを解説します

Dev.to / 2026/4/6

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • この記事では、LLMはデフォルトでステートレスであるため、セッションをまたいだ長期的でパーソナライズされた振る舞いを実現するには、メモリレイヤーの追加が必要になると説明しています。
  • 提案されるのはMemoryWeaveというオープンソースのPythonライブラリで、シンプルなワークフローにより、任意のLLMに長期メモリを提供します。具体的には、メモリを追加し、インジェクション(注入)用に準備されたコンテキスト文字列を取得します。
  • ベクトル類似度だけに頼るのではなく、MemoryWeaveはデュアルリトリーバル(2段階検索)方式を採用し、直接的に似たテキストだけでなく、保存されたアイテム同士の関係に依存する関連事実も捉えます。
  • 記事は、純粋なベクトル検索では、推論されたつながり(例:「FastAPIはUvicornを使う」から「Raviはどのサーバを使う?」へ結び付ける)を取りこぼし得ると主張します。理由は、その関係が単一の埋め込みの内部ではなく、事実と事実の間に存在するためです。
  • NLPに基づくエンティティと関係の抽出、そしてシステムプロンプトへ注入できる形に適した要約の生成を含む、パイプラインの概念的な全体像を提示しています。

LLMは設計上ステートレス(状態を持たない)です。メッセージを送ると応答が返ってきますが、モデルは即座にすべてを忘れます。すべての会話は冷えた状態から始まります。

これは単発のタスクなら問題ありません。しかし、個人的なものを作っているときは本当の問題になります。たとえば、あなたの技術スタックを理解しているコーディングアシスタント、あなたの文体を覚えている文章作成ツール、セッションをまたいであなたが決めたことを追跡するエージェントです。

よくある答えは次のいずれかです。自前でRAGパイプラインを組む、クラウドのメモリサービスを使う、週末をかけて埋め込み、ベクターデータベース、そしてプロンプトインジェクションのロジックをつなぎ合わせる。ですが、それらは決定版という感じがしません。

そこで私はMemoryWeaveを作りました。オープンソースのPythonライブラリで、どんなLLMにもたった3行のコードで長期メモリを提供します。

from memoryweave import MemoryWeave

memory = MemoryWeave()
memory.add("My name is Ravi. I prefer Python and FastAPI.")
ctx = memory.get("What stack should I recommend?")

print(ctx.summary)
# → Relevant memories:
# → - My name is Ravi. I prefer Python and FastAPI. (relevance: 0.94)

ctx.summaryは、そのままシステムプロンプトに注入できる文字列です。貼り付けるだけ。以上です。

なぜベクタ検索だけで済まないのでしょうか?

多くのメモリライブラリは、ベクターデータベースの薄いラッパーです。テキストを埋め込み、ベクターを保存し、コサイン類似度で取り出します。これは動きますが、盲点があります。

ベクタ検索は似ているテキストを見つけます。しかし関連する事実の扱いが苦手です。

たとえば、次のように保存しているとします。"Ravi uses FastAPI""FastAPI uses Uvicorn"。そしてクエリが "What server does Ravi use?" だとします。純粋なベクタ検索では推論を見落とします。関連は、個々の埋め込みではなく、事実同士の関係の中にあります。

MemoryWeaveはそれを二重の取得(dual-retrieval)アーキテクチャで解決します。

仕組み

以下が全パイプラインです。add()get()もこの1つの見取り図にまとめています。

memory.add(text)
  │
  ├── spaCy NLP          エンティティと主語-動詞-目的語の事実を抽出
  ├── sentence-transformers   テキストを埋め込む → 384次元のベクター
  ├── ベクターストア        埋め込みを保存(InMemory または ChromaDB)
  └── 知識グラフ     エンティティと事実をノード/エッジとして追加(NetworkX)

memory.get(query)
  │
  ├── クエリを埋め込む
  ├── ベクタ検索       コサイン類似度で上位k件の類似メモリを取得
  ├── グラフクエリ         キーワードの一致による関連事実の取得
  └── ランカー              スコアを統合 → 0.6 × ベクター + 0.4 × グラフ → MemoryContext

それぞれのパートを順に見ていきましょう。

1. NLP抽出(spaCy)

memory.add(text)を呼び出すと、最初に行われるのは生のテキストに対するspaCyの処理です。ここで次を抽出します:

  • 固有名詞 — 人名、地名、組織名、技術名
  • 主語-動詞-目的語トリプル(Ravi, prefers, Python) のような構造化された事実

これらは知識グラフ(内部ではNetworkX)内のノードとエッジになります。これにより、後でリレーショナルなクエリが可能になります。

2. 埋め込み(sentence-transformers)

並行して、同じテキストをall-MiniLM-L6-v2で埋め込みます。これはコンパクトで高速なsentence-transformersモデルで、384次元のベクターを生成します。これらは、開発に最適なインメモリストア(in-memory store)か、本番用のChromaDB(再起動をまたいだ永続化に対応)に格納されます。

すべてローカルで動作します。APIキーは不要で、データは外部サービスに送信されません。

3. 重複排除

何かを保存する前に、MemoryWeaveは既存の埋め込みに対してコサイン類似度をチェックします。新しいエントリが、すでに保存されている何かに対してスコアが≥ 0.98であれば、何も言わずに破棄します。同じ事実がセッションをまたいで再度追加されても、メモリがきれいに保たれます。

4. 取得と統合

memory.get(query)を呼び出すと:

  1. クエリを同じモデルで埋め込む
  2. ベクタ検索で、最も類似したメモリ上位k件を返す
  3. グラフクエリでは、知識グラフ上でキーワードの重なりに基づくウォークを行い、クエリとはテキスト的に類似していない可能性がある関連事実を掘り起こす
  4. 重み付きランカーが両者を統合する:final_score = 0.6 × vector_score + 0.4 × graph_score

重みは設定可能です。用途が主に事実ベース(例:個人のナレッジベース)なら、graph_weightを上げます。長文に対するセマンティック検索をより多く行う場合は、ベクターの重みを支配的に保ちます。

結果はMemoryContextオブジェクトになります:

項目 説明
summary システムプロンプトに注入する準備済みの文字列
entries ベクタ検索のヒット(スコア付き)
facts グラフ上の事実(スコア付き)
has_results 何も見つからなかった場合はFalse

OpenAIまたはAnthropicへの接続

MemoryWeaveには、両方のためのファーストクラスなアダプタが付属しています:

# OpenAI
from memoryweave.adapters.openai import OpenAIAdapter

adapter = OpenAIAdapter(memory, system_prompt="You are a helpful assistant.")
messages = adapter.prepare(messages)   # メモリをシステムプロンプトに注入する
# ... OpenAIを呼び出す ...
adapter.remember(messages)             # 次回のためにターンを保存する
# Anthropic
from memoryweave.adapters.anthropic import AnthropicAdapter

返却形式: {"translated": "翻訳されたHTML"}adapter = AnthropicAdapter(memory)
system, messages = adapter.prepare(messages)
# ... system= を指定して Anthropic を呼び出す ...
adapter.remember(messages)

アダプタはプロンプトインジェクションを自動的に処理します。システムプロンプトに手動で触れる必要はありません。

マルチユーザーセッション

MemoryWeave インスタンスは session_id にスコープされています。セッションは決して互いに混ざりません:

alice = MemoryWeave(MemoryConfig(default_session_id="alice"))
bob   = MemoryWeave(MemoryConfig(default_session_id="bob"))

alice.add("Alice likes TypeScript.")
bob.add("Bob prefers Rust.")

print(alice.get("language").summary)  # → TypeScript
print(bob.get("language").summary)    # → Rust

REST API + TypeScript SDK

アプリがPythonでない場合でも、MemoryWeaveには FastAPI サーバーと TypeScript SDK が同梱されています:

# サーバーを起動
uvicorn memoryweave.server:app --reload

# オプション:APIキーでロックする
MEMORYWEAVE_API_KEY=my-secret uvicorn memoryweave.server:app
import { MemoryWeave } from "@memoryweave/sdk";

const memory = new MemoryWeave({ baseUrl: "http://localhost:8000", sessionId: "user-1" });
await memory.add("Ravi prefers Python.");
const ctx = await memory.get("What language?");
console.log(ctx.summary);

現在の状態

ライブラリは v1.1.0 で、248のテストとCIでPython 3.10〜3.12を跨いだ91%のカバレッジ、すべてグリーンです。完全なフェーズ一覧:

✅ フェーズ 1 — 基盤
✅ フェーズ 2 — NLP抽出(spaCy)
✅ フェーズ 3 — ストレージ層(ベクトル+知識グラフ)
✅ フェーズ 4 — コアメモリAPI v0.1.0
✅ フェーズ 5 — TypeScript SDK
✅ フェーズ 6 — FastAPI RESTサーバー
✅ フェーズ 7 — ドキュメント
✅ フェーズ 8 — Launch v1.0.0
✅ フェーズ 9 — 重複排除、asyncメソッド、LLMアダプタ、サーバー認証 v1.1.0

次に何をするか

ロードマップ上のいくつかの予定:

  • 忘却戦略 — 時間減衰と関連性減衰により、古くなった記憶が検索を汚染しないようにする
  • ストリーミング対応 — ストリーミングされたLLMの応答から自動抽出して保存する
  • メモリの要約 — 古い記憶を定期的に圧縮して上位の事実にまとめる

使ってみる

pip install memoryweave
python -m spacy download en_core_web_sm

GitHub: github.com/ravii-k/memoryweave

これを使って何か作っている方、または同じ問題にぶつかって別のやり方で解決した方がいれば、ぜひコメントで教えてください。心から聞いてみたいです。