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)を呼び出すと:
- クエリを同じモデルで埋め込む
- ベクタ検索で、最も類似したメモリ上位k件を返す
- グラフクエリでは、知識グラフ上でキーワードの重なりに基づくウォークを行い、クエリとはテキスト的に類似していない可能性がある関連事実を掘り起こす
- 重み付きランカーが両者を統合する:
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
これを使って何か作っている方、または同じ問題にぶつかって別のやり方で解決した方がいれば、ぜひコメントで教えてください。心から聞いてみたいです。




