1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MiroFishをOllama(ローカルLLM)+SQLiteで無料で動かす

1
Last updated at Posted at 2026-05-07

はじめに

MiroFishはマルチエージェント社会シミュレーションフレームワークで、エージェント同士が相互に情報を共有しながら集合知を形成します。
面白そうなのでローカルで動かそうとしたのですが、デフォルトの構成では OpenAI API(有料)と Zep Cloud(グラフDBサービス、Enterprise版限定)が必要でした。

本記事では以下の2点を置き換えて、完全無料・ローカルで動かすまでの手順と詰まったポイントをまとめます。

元の構成 置き換え後
OpenAI API Ollama(ローカルLLM)
Zep Cloud(有料) SQLite + 自作互換レイヤー

動作環境

項目 内容
OS Tahoe 26.4
マシン MacBook Pro (Apple M5 Pro / 48GB RAM)
Python 3.12(重要:3.14は不可)
パッケージマネージャ uv
LLM Ollama + llama3.1

今回は少し古いllama 3.1を使っています。


1. 事前準備

Ollamaのインストールと起動

# インストール(公式サイトからDLするか brew で入れる)
brew install ollama

# モデルのダウンロードと起動
ollama pull llama3.3:70b
ollama serve

http://localhost:11434 でOpenAI互換のAPIが使えるようになります。

uvのインストール

MiroFishはパッケージマネージャに uv を使っています。

curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.zshrc  # または ~/.bashrc

uvについてはこちら


2. リポジトリのクローンと依存関係のインストール

git clone https://github.com/666ghj/MiroFish.git
cd MiroFish

Python 3.14では動きません

uv sync 時に以下のエラーが出ました。

error: Failed to build `tiktoken==0.7.0`

MiroFishが依存する camel-ai 0.2.78tiktoken 0.7.0 を固定しており、これは Python 3.14でビルドできません。Python 3.12を指定して解決します。

brew install python@3.12
uv sync --python 3.12

また、tiktokenのビルドには Rust が必要です。入っていない場合は先にインストールしてください。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env

3. .envの設定

MiroFish/.env を作成します。

# Ollama(OpenAI互換エンドポイント)
LLM_API_KEY=ollama
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL_NAME=llama3.1

# Zepは後述の自作レイヤーに置き換えるので適当な値でOK
ZEP_API_KEY=local

4. Zep Cloudの置き換え(最大の山場)

問題

MiroFishはグラフ型メモリDBとして Zep Cloud を使っています。アカウントを作成してAPIキーを取得しようとしたところ…

APIの利用はEnterpriseプランのみ

普通の無料アカウントでは使えません。

解決策:SQLiteで互換レイヤーを自作

Zep SDKの呼び出し箇所を調べると、すべて client.graph.node.* / client.graph.edge.* というネスト構造のインターフェースを通じていることがわかりました。この構造を同じままSQLiteで実装すれば、既存コードの変更を最小限に抑えられます。

backend/app/utils/local_graph_store.py を新規作成します。

import json, sqlite3, threading, uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

DB_PATH = Path(__file__).parent.parent / "data" / "mirofish_graph.db"

class _NodeObj:
    __slots__ = ("uuid_", "uuid", "name", "labels", "summary", "attributes", "created_at")
    def __init__(self, uuid_, name, labels, summary, attributes, created_at=None):
        self.uuid_ = self.uuid = uuid_
        self.name = name
        self.labels = labels
        self.summary = summary
        self.attributes = attributes
        self.created_at = created_at

class _EdgeObj:
    __slots__ = (
        "uuid_", "uuid", "name", "fact",
        "source_node_uuid", "target_node_uuid",
        "created_at", "valid_at", "invalid_at", "expired_at",
        "attributes", "episodes", "fact_type",
    )
    def __init__(self, uuid_, name, fact, source_node_uuid, target_node_uuid,
                 created_at=None, valid_at=None, invalid_at=None, expired_at=None):
        self.uuid_ = self.uuid = uuid_
        self.name = name
        self.fact = fact
        self.fact_type = name
        self.source_node_uuid = source_node_uuid
        self.target_node_uuid = target_node_uuid
        self.created_at = created_at
        self.valid_at = valid_at
        self.invalid_at = invalid_at
        self.expired_at = expired_at
        self.attributes = {}
        self.episodes = []

class _EpisodeObj:
    processed = True
    def __init__(self, uuid_):
        self.uuid_ = self.uuid = uuid_

SQLiteのテーブル定義は以下の通りです。

def _init_schema():
    with _open() as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS nodes (
                uuid        TEXT PRIMARY KEY,
                graph_id    TEXT NOT NULL,
                name        TEXT NOT NULL,
                labels      TEXT NOT NULL DEFAULT '[]',
                summary     TEXT NOT NULL DEFAULT '',
                attributes  TEXT NOT NULL DEFAULT '{}',
                created_at  TEXT
            );
            CREATE TABLE IF NOT EXISTS edges (
                uuid               TEXT PRIMARY KEY,
                graph_id           TEXT NOT NULL,
                name               TEXT NOT NULL DEFAULT '',
                fact               TEXT NOT NULL DEFAULT '',
                source_node_uuid   TEXT NOT NULL,
                target_node_uuid   TEXT NOT NULL,
                created_at         TEXT,
                valid_at           TEXT,
                invalid_at         TEXT,
                expired_at         TEXT
            );
            CREATE TABLE IF NOT EXISTS episodes (
                uuid        TEXT PRIMARY KEY,
                graph_id    TEXT NOT NULL,
                data        TEXT NOT NULL,
                ep_type     TEXT NOT NULL DEFAULT 'text',
                processed   INTEGER NOT NULL DEFAULT 1,
                created_at  TEXT
            );
            CREATE INDEX IF NOT EXISTS idx_n_graph ON nodes(graph_id);
            CREATE INDEX IF NOT EXISTS idx_e_graph ON edges(graph_id);
            CREATE INDEX IF NOT EXISTS idx_n_name  ON nodes(graph_id, name);
        """)

Zep SDKと同じネスト構造のAPIクラスを実装し、最後にシングルトンとして公開します。

class LocalGraphStore:
    """Zep(api_key=...) の drop-in replacement"""
    _lock = threading.Lock()
    _instance = None

    def __new__(cls):
        with cls._lock:
            if cls._instance is None:
                inst = object.__new__(cls)
                _init_schema()
                inst.graph = _GraphAPI()
                cls._instance = inst
        return cls._instance

既存サービスの修正

Zepを使っていた4ファイルの __init__ を書き換えるだけで済みました。

# 変更前
from zep_cloud.client import Zep
self.client = Zep(api_key=Config.ZEP_API_KEY)

# 変更後
from ..utils.local_graph_store import LocalGraphStore
self.client = LocalGraphStore()

対象ファイル:

  • services/graph_builder.py
  • services/zep_tools.py
  • services/zep_entity_reader.py
  • services/zep_graph_memory_updater.py

5. ページング処理の修正

utils/zep_paging.py はZep SDKのAPIを直接呼んでいたため、LocalGraphStoreに合わせて書き直しました。

def fetch_all_nodes(client, graph_id, page_size=500, max_items=5000, **_) -> list:
    nodes, cursor = [], None
    while True:
        batch = client.graph.node.get_by_graph_id(
            graph_id, limit=page_size, uuid_cursor=cursor
        )
        if not batch:
            break
        nodes.extend(batch)
        if len(nodes) >= max_items or len(batch) < page_size:
            break
        cursor = getattr(batch[-1], "uuid_", None) or getattr(batch[-1], "uuid", None)
        if not cursor:
            break
    return nodes[:max_items]

6. 詰まったポイント:エンティティが0件になる競合状態

症状

シミュレーションを実行すると以下のログが出て先に進めませんでした。

グラフエンティティの読み取り: 完了、合計0エンティティ
没有找到符合条件的实体

原因

元の実装では、add_batch() でエピソードを追加した後、バックグラウンドスレッドでOllamaにエンティティ抽出を依頼していました。

# 元のコード(問題あり)
threading.Thread(
    target=_extract_and_store, args=(graph_id, data), daemon=True
).start()

一方、graph_builder.py_EpisodeObj.processed == True を確認してすぐに次の処理へ進むため、Ollamaが抽出を終える前にシミュレーションがエンティティを読みに行ってしまっていました。

修正

add_batch() 内の抽出処理を同期実行に変更しました。

# 修正後
def add_batch(self, *, graph_id: str, episodes) -> List[_EpisodeObj]:
    # ... エピソードをDBに保存 ...

    # 同期実行することで、呼び出し元がentityを読みに行く前に抽出が完了する
    combined = "\n\n---\n\n".join(texts[:5])
    _extract_and_store(graph_id, combined)
    return results

Ollamaのレスポンスを待ってから処理が進むため、初回の起動は数十秒かかりますが、0件問題が解消されました。


7. 起動方法

# バックエンド(MiroFish/backend/ で実行)
uv run python run.py

# フロントエンド(MiroFish/ で実行)
npm run frontend

成果物

image.png

今回は明日からコーヒーが禁止されるとどうなる?という題材で話し合ってもらいました。


まとめ

課題 解決策
Python 3.14でtiktokenがビルドできない uv sync --python 3.12 で3.12を指定
Zep CloudがEnterprise限定 SQLite + 互換インターフェースを自作
エンティティ抽出の競合状態(0件問題) バックグラウンドスレッド→同期処理に変更
OpenAI APIが有料 Ollamaのllama3.1をローカルで実行

ローカルLLMで動かすと、APIコストを気にせず試行錯誤できるのは大きなメリットです。


参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?