OpenClaw ローカル埋め込みサーバー LanceDB メモリプラグイン MLX 埋め込み OpenAI API 互換サーバー AI エージェント セルフホスティング

🔍 OpenAI APIなしでAIエージェントに長期記憶を?
OpenClawを運用していると、エージェントに「長期記憶」を持たせたくなる時がきますよね。会話が終わるとすべてのコンテキストが消えてしまうのは、やっぱりもったいないですから。OpenClawのmemory-lancedbプラグインがまさにこの役割を果たしてくれるのですが、一つ問題があります — 埋め込みのためにOpenAI APIしかサポートしていないんです。
毎月APIコストがかかりますし、会話内容が外部サーバーに送信されるのも気になりますよね。そこで私は、Mac mini M4ですでに動いていたMLX埋め込みモデルをOpenAI互換サーバーでラップして、コスト0円+データ漏洩なしのローカル埋め込みサーバーを構築しました。その試行錯誤の記録をお伝えしますね!
📋 なぜローカル埋め込みサーバーが必要だったのか
OpenClaw Memory LanceDB プラグインの限界
OpenClaw v2026.3.2のmemory-lancedbプラグインは、会話内容をベクトルDBに保存して、後から関連する記憶を自動的に呼び出す機能です。ベクトルDB(Vector Database)とは、テキストを数値ベクトルに変換して意味ベースの検索を可能にするデータベースのことです。とても便利な機能なのですが、埋め込み設定がOpenAI Embeddings API形式しか受け付けないんです。
GitHub Issue #21811でもローカル埋め込みサポートのリクエストがありましたし、Discussion #3309でも活発に議論されていました。コミュニティからmemory-lancedb-localというフォークも出ていましたが、私はすでに動いているMLX埋め込みインフラを再活用したかったんです。
既存のインフラを活用する
Mac mini M4には、Knowledge RAGシステム用にMLXEmbedderがすでに稼働していました。BAAI/bge-m3モデル(568Mパラメータ、1024次元、100以上の言語をサポート)をApple Silicon GPU(Metal)で推論する構成です。埋め込み(Embedding)とは、テキストを固定長の数値ベクトルに変換するプロセスのことです。これをOpenAI互換APIでラップするだけでいいんです!
🛠️ ローカル埋め込みサーバーの構築
Step 1: アーキテクチャ設計
全体の流れは下の図のとおりです。OpenClaw Gatewayがメモリの保存・検索時にLanceDBプラグインを呼び出し、プラグインは設定されたbaseUrlに埋め込みリクエストを送ります。ここに私たちのローカルサーバーが応答する仕組みです。

Step 2: embed_server_openai.py の作成
ポイントは、FastAPIでOpenAIの/v1/embeddingsエンドポイントを模倣するサーバーを作ることです。FastAPIはPython用の高性能Webフレームワークで、APIサーバーを簡単に構築できます。重要なポイントを一つずつ見ていきましょう。
基本的なサーバー構造
まず、OpenAI API形式と同じリクエスト/レスポンスモデルをPydanticで定義します。
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn
app = FastAPI(
title="MLX Embedding Server"
)
class EmbeddingRequest(BaseModel):
input: str | list[str]
model: str = "BAAI/bge-m3"
class EmbeddingData(BaseModel):
object: str = "embedding"
embedding: list[float]
index: int
class EmbeddingResponse(BaseModel):
object: str = "list"
data: list[EmbeddingData]
model: str
usage: dict
inputが単一の文字列の場合もリストの場合もある点を、必ず処理してくださいね。
埋め込みエンドポイント
実際の埋め込みリクエストを処理するコアエンドポイントです。入力を正規化してMLXEmbedderを呼び出します。
@app.post("/v1/embeddings")
async def create_embedding(
request: EmbeddingRequest
):
texts = (
[request.input]
if isinstance(
request.input, str
)
else request.input
)
results = []
total_tokens = 0
for i, text in enumerate(texts):
vec = embedder.embed(text)
results.append(
EmbeddingData(
embedding=vec.tolist(),
index=i
)
)
total_tokens += len(
text.split()
)
return EmbeddingResponse(
data=results,
model=request.model,
usage={
"prompt_tokens":
total_tokens,
"total_tokens":
total_tokens
}
)
embedder.embed(text)は、既存のMLXEmbedderのメソッドをそのまま呼び出しています。すでに検証済みのコードを再利用するので、安定していますよ。
Authorization の処理
ローカルサーバーなので認証は不要です。LanceDBプラグインがAuthorizationヘッダーを送りますが、サーバー側で検証しなければそのまま通過します。設定で空の値は許可されないので、"dummy"を入れておけば大丈夫です。
ヘルスチェックとウォームアップ
MLXモデルは初回推論時にMetalコンパイルが発生するため、少し遅くなることがあります。startupイベントでウォームアップ埋め込みを実行しておけば、実際のリクエストはミリ秒単位で応答できます。
@app.get("/health")
async def health():
return {
"status": "ready",
"model": "BAAI/bge-m3",
"dimensions": 1024
}
@app.on_event("startup")
async def startup():
# Warmup: first embedding is
# slow due to Metal compile
embedder.embed("warmup")
print(
"MLX Embedding Server ready"
)
サーバー実行設定
uvicornでサーバーを実行する設定です。workersの値に注意してくださいね。
if __name__ == "__main__":
uvicorn.run(
app,
host="127.0.0.1",
port=8400,
workers=1 # MLX: main only
)
Step 3: OpenClaw の設定
OpenClawのメモリプラグイン設定で、baseUrlをローカルサーバーのアドレスに変更します。
{
"memory-lancedb": {
"enabled": true,
"config": {
"embedding": {
"apiKey": "dummy",
"baseUrl":
"http://127.0.0.1:8400/v1",
"model": "BAAI/bge-m3",
"dimensions": 1024
},
"autoCapture": true,
"autoRecall": true
}
}
}
apiKeyは何でも構いません。プラグインが空の値を許可しないので"dummy"を入れています。dimensionsはBAAI/bge-m3モデルの出力次元である1024に合わせる必要があります。
Step 4: macOS LaunchAgent で自動起動
リブート後もサーバーが自動的に起動するように、LaunchAgent plistファイルを設定します。
<?xml version="1.0"
encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>
com.claudie.embed-openai
</string>
<key>ProgramArguments</key>
<array>
<string>python3</string>
<string>
embed_server_openai.py
</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
KeepAliveをtrueに設定することで、サーバーが停止した場合に自動で再起動されます。常時稼働のMac miniにはぴったりの設定ですね。
🔧 トラブルシューティング:試行錯誤の記録
すんなりいっていたら、ブログ記事にはならなかったでしょうね。ここからが本番です。
失敗 1: 間違ったサーバーに追加しようとした
最初は、既存のembed_server.py(Windows RTX 3080用、SentenceTransformerバックエンド)にOpenAI互換エンドポイントを追加しようとしました。当然うまくいきませんでした — MLXはmacOS Apple Silicon専用で、SentenceTransformerとはモデルのロード方式がまったく異なるからです。
教訓:既存のコードに無理やり組み込もうとせず、目的に合った別のサーバーを作る方がすっきりします。
失敗 2: npm モジュールの不足
OpenClawのmemory-lancedbプラグインが@lancedb/lancedb npmパッケージに依存していたのですが、インストールされていませんでした。エラーメッセージが少し分かりにくくて、原因を特定するのに時間がかかりました。
# 解決方法
cd {OPENCLAW_DIR}
npm install @lancedb/lancedb
失敗 3: 次元の不一致(最も致命的)
これが一番時間のかかった問題でした。LanceDBテーブルが最初に256次元で作成されてしまったのですが、私たちのモデルは1024次元を出力します。次元が合わないと、ベクトルの保存自体が失敗するんです。
# 誤って作成されたテーブルを削除
# LanceDBデータディレクトリで
# 該当テーブルのフォルダを削除
# OpenClaw再起動時に
# 1024次元でテーブルが自動再作成
dimensions値と埋め込みモデルの実際の出力次元が一致している必要があります。BAAI/bge-m3は1024、OpenAI text-embedding-3-smallは1536です。📊 OpenAI API vs ローカル MLX 比較
| 項目 | OpenAI API | ローカル MLX |
|---|---|---|
| コスト | 有料(使用量ベース) | 無料(電気代のみ) |
| データセキュリティ | 外部サーバーに送信 | ローカルで処理 |
| レイテンシ | ネットワーク往復 | ミリ秒単位の応答 |
| モデル | text-embedding-3-small | BAAI/bge-m3(1024次元) |
| API Key | 必須(有料) | dummy(不要) |
| インターネット | 必須 | 不要 |
| 多言語サポート | 優秀 | 優秀(100以上の言語) |
実際に使ってみると、ローカルMLXの方が圧倒的に速いです。ネットワーク往復がないので当然ですよね。そして、会話内容が外部に出ないというのは、本当に大きなメリットです。
✅ まとめとポイント
OpenClawに長期記憶を追加しながら、APIコストとデータ漏洩の心配を両方解消できました。ポイントをまとめますね。
- 既存インフラの再活用:すでに動いているMLXEmbedderをFastAPIでラップするだけでOKです
- OpenAI互換API:
/v1/embeddingsエンドポイントさえ合わせれば、プラグインが自動的に連携します - workers=1は必須:MLXはメインスレッドでのみGPU演算が可能です
- 次元の一致を確認:モデルの出力次元とLanceDB設定が必ず同じである必要があります
- LaunchAgent:リブート後の自動起動で無人運用が可能です
Apple Silicon Macをお持ちでしたら、この方法で完全にローカルでAIエージェントの長期記憶を構築できます。コスト0円、データ漏洩0%、応答速度はミリ秒単位。一度セットアップしてしまえば、とても快適ですよ!
セットアップ中に困ったことがあれば、コメントで教えてくださいね。一緒に解決しましょう!