NIIが公開した LLM-jp-4 を、手元の MacBook Pro M4 Max 128GB でローカル実行し、Ollama の OpenAI互換API 経由で既存のバッチ処理から使えるかを検証しました。
結論から言うと、8B は十分実用的です。32B-A3B も、このクラスのマシンならローカルで動きます。ただし、今回の GGUF + Ollama 構成では 32B-A3B の出力品質が安定せず、現時点では 8B のほうが実務上は扱いやすいという結果になりました。
背景
今回やりたかったことは、単にローカルで対話することではなく、以下の構成を作ることでした。
LLM-jp-4をローカルサーバとして起動する- 他のクライアントソフトウェアから使えるようにする
- 既存の抽出バッチから
OpenAI互換APIとして呼び出す
用途は、日本語OCRテキストから表情・感情表現を抽出するバッチ処理です。
結論
今回の時点での実務的な判断は以下です。
MacBook Pro M4 Max 128GBならLLM-jp-4 8Bは十分動くLLM-jp-4 32B-A3Bも量子化版ならローカルで実際に動くOpenAI互換APIをすぐ使いたいならOllamaが最短- ただし
LLM-jp-4の公式配布をそのまま使うのではなく、今回はGGUF変換版を使う - 抽出品質の観点では、今回の設定では
32B-A3Bより8Bのほうが安定していた
ここは重要です。LLM-jp-4 の公式公開は Hugging Face 上の transformers 向けですが、ローカルで Ollama から使うため、今回は GGUF 変換版を使いました。そのため、厳密には「公式実装そのまま」ではありません。
使用した環境
- マシン:
MacBook Pro M4 Max 128GB - 実行基盤:
Ollama 0.20.3 - API:
http://localhost:11434/v1/ - モデル:
llm-jp-4-8b-thinking-Q4_K_M.gguf
8B の Q4_K_M は約 5.3GB です。ローカルのテスト用途にはかなり扱いやすいサイズでした。
モデルの導入
Ollama は Homebrew で入れました。
brew install ollama
brew services start ollama
次に GGUF をダウンロードし、Modelfile を作成します。
curl -L --fail -o ~/git/llm/ollama/models/llm-jp-4-8b-thinking-Q4_K_M.gguf \
'https://huggingface.co/mmnga-o/llm-jp-4-8b-thinking-gguf/resolve/main/llm-jp-4-8b-thinking-Q4_K_M.gguf?download=true'
FROM /Users/yourname/git/llm/ollama/models/llm-jp-4-8b-thinking-Q4_K_M.gguf
PARAMETER num_ctx 8192
PARAMETER temperature 0.7
ollama create llm-jp-4-8b-q4 -f ~/git/llm/ollama/modelfiles/llm-jp-4-8b-q4.Modelfile
これで Ollama の OpenAI互換API から llm-jp-4-8b-q4 として呼べます。
OpenAI互換APIとして使う
接続先は以下です。
base_url = "http://localhost:11434/v1/"
api_key = "ollama"
Python からは普通に openai SDK で呼べます。
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1/",
api_key="ollama",
)
response = client.chat.completions.create(
model="llm-jp-4-8b-q4",
messages=[
{"role": "user", "content": "これはテストです。JSONだけ返してください。"}
],
)
print(response.choices[0].message.content)
バッチ抽出の試験用スクリプト
今回は既存の本番コードを直接書き換えず、試験用スクリプトを別で用意しました。
- 入力:
/Users/nakamura/git/ndl/face/data/886000 - 出力: JSON
- 呼び出し先: ローカル
OllamaAPI
最初は「JSON配列だけ返せ」とプロンプトで指示していましたが、実際には余計な文字列や ... が混ざり、パースに失敗しました。
そこで以下を適用しました。
response_formatによるJSON Schema制約temperature=0- ストリーミングログによる進捗表示
- チャンク単位の計測
なぜ構造化出力が必要だったか
この種の抽出では、プロンプトだけで「JSONのみ返して」と言っても崩れます。特にローカルモデルでは、
- 余計な説明文が付く
- コードフェンスが付く
- 一部のフィールドが省略される
- フィールドの値が想定外になる
ということが普通に起きます。
今回のスクリプトでは response_format で JSON Schema を渡し、最低限の安定性を確保しました。これで「すぐ壊れる状態」からは脱しました。
実測
まず 1ページ で試しました。
chunk 1/1 pages=8-8 chars=1116
first_token=1.2s
streaming=5.0s chars=287
completed=5.1s response_chars=290
extracted=1
1ページ なら、今回の条件では約 5秒 でした。
次に 5ページ を回しました。最初は chunk_size=2500 で見えづらかったので、chunk_size=1200 で小さく切り、進捗が見えるようにしました。
chunk 1/5 pages=8-8 chars=1116
first_token=1.2s
streaming=5.0s chars=287
completed=5.1s response_chars=290
extracted=1
chunk 2/5 pages=9-9 chars=836
first_token=1.0s
completed=3.6s response_chars=302
extracted=1
chunk 3/5 pages=10-10 chars=1113
first_token=2.1s
streaming=5.0s chars=256
completed=7.1s response_chars=356
extracted=1
chunk 4/5 pages=11-11 chars=869
first_token=1.7s
streaming=5.0s chars=365
streaming=10.0s chars=867
streaming=15.0s chars=1402
completed=15.8s response_chars=1457
extracted=5
chunk 5/5 pages=12-12 chars=777
first_token=1.6s
completed=4.3s response_chars=262
extracted=1
saved=/Users/nakamura/git/llm/outputs/886000_extraction_test_5pages.json
total=9
この結果から分かるのは、5ページ にしたからといって単純に 5秒 x 5 にはならない、ということです。
時間が伸びる主な理由は以下です。
- ページ数ではなく、実際のテキスト内容で難しさが変わる
- JSON Schema に合わせた構造化出力は遅くなりやすい
- 1つのチャンクで抽出件数が増えると、その分だけ応答も長くなる
実際、page 11 のチャンクだけ 15.8秒 かかりました。
出力品質
フォーマットとしては JSON で保存できましたが、内容はまだ荒いです。
たとえば以下のような問題がありました。
surfaceやcontextが...になる項目があるpagesが空配列になる項目があるbody_partに想定外の値が出るcharacterが長すぎたりノイズを含んだりする
つまり、8B で「APIとして使える」ことは確認できましたが、抽出品質まで含めて即本番投入という段階ではありません。
32B-A3Bは現実的か
私の結論は「このマシンなら現実的」です。
ただし、8B でも連続推論時にはファンが回ります。32B-A3B の Q4_K_M はさらに重いので、短時間の比較テストには向いていても、静かで快適とは言いにくいはずです。
実際に 32B-A3B Q4_K_M もローカルに導入し、1ページ だけ抽出を試しました。
chunk 1/1 pages=8-8 chars=1116
first_token=7.9s
streaming=12.9s chars=246
completed=13.0s response_chars=251
extracted=1
このログだけ見ると「遅いが動く」です。ただし、問題は内容でした。出力は JSON としては返るものの、surface や context が本文に根拠づかない文字列になり、pages が空になるケースがありました。
たとえば以下のような応答でした。
[
{
"surface": "あやま後から〓〓さいづちあたぶお才槌頭ハちいてほ%",
"context": "あやま後から〓〓さいづちあたぶお才槌頭ハちいてほ%",
"emotion": "その他",
"body_part": "涙",
"valence": "ambiguous",
"intensity": "low",
"character": "あやま後から〓〓さいづちあたぶお才槌頭ハちいてほ%",
"pages": []
}
]
さらに制約を強めて再試行すると、今度は以下のような不自然な文字列が surface に入りました。
[
{
"surface": "/c/co/n/t/e/x/t/",
"context": "/c/co/n/t/e/x/t/",
"emotion": "悲しみ・哀れ",
"body_part": "口",
"valence": "ambiguous",
"intensity": "low",
"character": "ださいづちあたぶお才槌頭ハちいてほ%",
"pages": [8]
}
]
つまり、今回の GGUF + Ollama 構成では 32B-A3B は「動く」が、「抽出品質が良い」とは言えませんでした。
用途別には以下のように考えるのがよさそうです。
- プロンプト調整、API接続確認、小規模バッチ:
8B - 精度比較、少量の本気評価:
32B-A3BただしGGUF/Ollamaではなく、できれば公式 Hugging Face 実装で再検証 - 長時間の大量処理: できれば外部GPU
今回の結果を見ると、32B-A3B の評価は GGUF + Ollama だけで決めないほうがよさそうです。ローカル API 用には 8B を使い、32B-A3B は Transformers ベースで別経路から試すのが妥当だと感じました。
まとめ
MacBook Pro M4 Max 128GB で LLM-jp-4 をローカル実行し、Ollama の OpenAI互換API から使う構成は十分成立します。
今回の時点での整理は以下です。
8BはローカルAPIサーバとして十分使える- 構造化出力を使わないと、抽出バッチはかなり不安定
5ページ程度でも、チャンクによっては15秒前後かかる32B-A3Bもこのクラスの Mac なら動く- ただし今回の
GGUF + Ollamaでは、抽出品質は8Bのほうが安定していた
ローカルで日本語向けLLMを検証したい場合、LLM-jp-4 はかなり有力な選択肢だと思います。ただし、抽出系タスクでは「動く」と「品質が十分」は別です。今回の範囲では、ローカル API 用の現実解は 8B で、32B-A3B は公式 Transformers 実装でもう一段検証する価値がある、という結論になりました。
参考
- NII のリリース: https://www.nii.ac.jp/news/release/2026/0403.html
- 公式
LLM-jp-4 8B thinking: https://huggingface.co/llm-jp/llm-jp-4-8b-thinking GGUF変換版8B: https://huggingface.co/mmnga-o/llm-jp-4-8b-thinking-ggufGGUF変換版32B-A3B: https://huggingface.co/mmnga-o/llm-jp-4-32b-a3b-thinking-gguf- Ollama OpenAI compatibility: https://docs.ollama.com/api/openai-compatibility
- Ollama Structured Outputs: https://docs.ollama.com/capabilities/structured-outputs