はじめに
LLM、便利ですよね。
ただ、クラウドの LLM を使っていると料金や各種制限がどうしても気になってきます(貧乏性なので…)
特にプログラム経由であれこれ実験し始めると、実行回数がとんでもないことになりそうで、まずはローカルで試したいと思う方も多いのではないでしょうか。
最近、私自身もローカルLLMを使っていろいろと試しているので、導入時に行った手順を備忘録も兼ねてまとめておくことにしました。
環境
- Windows11
- GeForceRTX 3060(12GB)
- python3.13
・GPU/CUDAについて
この後いれるLMStudioはCUDAを内包しているようで、別途 CUDA Toolkit を手動でインストールする必要はないようです。
ただ、このPCは別件で既にCUDAを入れているので確認できていません…。
CUDAを入れる場合は以下の過去記事を参考にしてください。
[2025年版] Windows11における機械学習+GPU環境の構築(TensorFlow+WSL2、PyTorch)
1. LM Studio
ローカルLLMを管理するツールです。
もう一つ有名なツールでOllamaがありますが、今のところGUIの見やすさでLM Studioをよく使っています。
1-1. インストール
以下の公式からインストールします。
以下初回起動時の選択です。
1. GetStarted
2. CHoose your level
今回はPython経由で使うのでDeveloperを選びます。
後から変更できるのでそこまで気にしなくても大丈夫です。
各モードをざっくり説明すると以下です。
- User: チャット用途のみ(UIがシンプルで詳細画面が隠れる)
- PowerUser: LLMを調整しながら使う用(詳細画面表示)
- Developer:プログラム経由でLMStudioを使う用
3. 最初のモデルのインストール
モデルのダウンロードフォルダを変更してからDLしたいので、右上にあるskipで飛ばします。
デフォルトで問題ないならダウンロードしても大丈夫です。
1-2. 日本語化
右下の小さい歯車 → Settingsタブ → Generalタブ → Languageの項目から日本語(Beta)を選択。
1-3. LM Studioの見方
左に大きく4つのアイコンがあり、これがメイン機能です。
- 吹き出しマーク(チャット)
ChatGPTみたいにそのままチャットする機能です。 - コンソールマーク(開発者)
API用で、APIサーバを立てて外部からLMStudioにアクセスする機能を提供します。
また、ロードされているモデルや実行ログもここで確認できます。 - フォルダマーク(マイモデル)
ダウンロードされているモデルの管理場所です。 - 検索マーク(検索)
モデルを検索してダウンロードできます。
1-4. その他初期設定
モデルのダウンロードフォルダの変更
モデルは結構サイズをとります。(基本GB単位)
もし別ドライブに保存したいなどあれば変更しておきましょう。
左のフォルダアイコン → モデルディレクトリ → 変更
ヘッドレスモード
LMStudioを閉じても起動したままにしたい場合に設定します。
右下の小さい歯車 → Settingsタブ → Developerタブ → ローカルLLMサービスを有効
APIサーバの起動と各種設定
APIサーバを起動するとLMStudio以外のアプリ(Python等)からアクセスできるようになります。
左のコンソールアイコンを選択 → Statusの横のトグルを切り替え
ServerSettingsでサーバの細かい設定ができます。
個人的に気になった内容は以下です。
- ローカルネットワークで提供: Offにします。Onにすると別のPCからアクセスが可能になります。
- ジャストインタイムモデルロード: Onにします。Onだとモデルがロードされていないと自動的にロードしてくれます。
Offだとモデルがロードされていない場合に例外が出ます。 - 未使用のJITロードモデルを自動的にアンロード:使ってないモデルをアンロードしてくれます。初期値は60分です。
- 最後のJITロードモデルのみを保持:Offにします。Onのままだとモデルが1個しかロードされず、2個目を使う場合に1個目がアンロードされます。
2. モデルの検索
モデルの検索ですが今の時代たくさんのモデルがあり、どのモデルか決めるだけでも結構大変です。
検索の一例として私のやり方を書いておきます。
1. モデルファミリーの選定
まずChatGPTに聞いてある程度モデルに目星をつけます。
例えば以下です。
いくつか例
LMStudioで使う日本語と英語の翻訳に特化した最新のモデルを教えて
LMStudioでText-to-Textの最新のモデルを教えて
LMStudioでOCRが得意な最新のモデルを教えて
ポイントは "最新" と聞いて検索させ、古い情報で答えさせない事です。
すると以下のように特徴も含めていくつか候補を上げてくれます。
2. LMStudioで検索
次にLMStudioでモデル名を検索します。
基本的にはファミリー名で大きく検索をかけて、その後細かい違いのモデルを見ていく形です。
今回は「Gemma 3」を見てみます。
LMStudioで左の検索アイコンから「Gemma 3」を検索にかけます。
モデルは"GGUF"の中から選びます。
・GGUFについて
GGUFは、主にローカル環境でLLMを動かすためのモデル形式です。
多くの場合、すでに量子化されており、ダウンロードしてそのまま実行する用途に向いています。
一方で、PyTorchを使ったファインチューニングやLoRAの追加など、モデルを学習・改造する用途には向いておらず、そういった場合はGGUFになる前の元のモデルを使う必要があります。
モデル名について
モデル名はある程度意味があります。
- 例1:lmstudio-community/gemma-3-1B-it-qat-GGUF
- lmstudio-community: 配布者
- gemma-3: gemmaシリーズの第3世代
- 1B: Bやbはパラメータ数を指します。これは10億(1 Billion)パラメータ
- it(Instruction-Tuned): チャット用途向け
- qat(Quantization-Aware Training): 量子化前提モデル
- GGUF: llama.cpp 系の標準モデルフォーマット
- 例2: unsloth/Qwen3-30B-A3B-Thinking-2507-GGUF
- unsloth: 配布者
- Qwen3: Qwenシリーズの第3世代
- 30B: 300億(30Billon)パラメータ
- A3B(Active 3B): Mixture of Experts(MoE)モデルを採用
- Thinking: チャットより推論を重視した学習
- 2507: リリース日
- GGUF
基本はパラメータ数を見ることが多く、多いほど精度は良くなりますが時間がかかります。
目安は以下です。
- 1B前後: 軽量モデル。
- 7B前後: 実用ライン。CPUだと数秒~数十秒。GPUだとQ4で約6GBほどのメモリを使用。
- 13B前後: GPU実用ライン。GPUで数秒~数十秒。GPUだとQ4で約10-12GBほどのメモリを使用。
- 30B以上: 時間より精度を求めるライン。100Bとかになると、そもそもメモリが足りずに動かせなかったりする。
量子化について
量子化とは、モデル内で使われている数値の型(bit幅や表現)をより小さいものに変換して、モデルを軽量化する技術です。
学習時は主に float32(32bit)が使われますが、推論時に int8(8bit)や int4(4bit)へ量子化することで、サイズの削減と速度の向上を図っています。
GGUFモデルは量子化方法が選べるモデルもあり、ダウンロードオプションの所をクリックすると選択できます。
(基本は変えなくてもいいと思います)
量子化の文字の意味は"Q{bit数}_{方式}"です。
- 例1: Q3_K_L
- Q3: 3bit量子化
- K: K-quant(新世代の量子化方式、精度が高い)
- L: Large(同bit数の中で精度寄り)
- 例2: Q4_K_M
- Q4: 4bit量子化
- K: K-quant
- M: Medium(精度とサイズのバランス)
後はいいね数とダウンロード数あたりを参考にしつつモデルを決めます。
モデルが決まったらダウンロードをします。
3. Pythonでの利用(litellm)
Pythonのインストールは省略します。
LLMとの連携はlitellmを使います。
他の方法として試したのは以下です。
- requestで直接HTTPを叩く → 実装がナイーブすぎて大変だった…
- langchain → ブラックボックスが多く、どう動いているかが分かりにくかった
ある程度やる事が決まっている場合はlangchainがいいと思います。
しかし色々試したい場合や動作の理解を優先する場合はlitellmがいい感じでした。
litellmの導入は以下です。
pip install litellm
サンプルコードは以下です。
(モデルは lmstudio-community/gemma-3-1B-it-qat-GGUFを使用)
import time
import litellm
messages = [
{
"role": "system",
"content": "あなたは日本語で簡潔に答えるアシスタントです。",
},
{
"role": "user",
"content": "Pythonで辞書をソートする方法は?",
},
]
model = "lmstudio-community/gemma-3-1B-it-qat-GGUF"
t0 = time.time()
res = litellm.completion(
model="lm_studio/" + model,
base_url="http://127.0.0.1:1234/v1",
messages=messages,
)
text = res["choices"][0]["message"]["content"]
print(text)
print(f"推論時間 : {time.time() - t0:.3f}s")
print("入力トークン数:", res["usage"]["prompt_tokens"])
print("出力トークン数:", res["usage"]["completion_tokens"])
実行するとLMStudio側でモデルがロードされるはずです。
2回目以降はロードの時間が省略されるのですぐに結果が返ってきます。
ちなみにlitellmは簡単にLLM先を変更できるので必ずしもLMStudioである必要はありません。
参考までに他の場合をいくつか書いておきます。
・ollamaの場合
res = litellm.completion(
model="ollama/" + model,
base_url="http://127.0.0.1:11434",
messages=messages,
)
・OpenAIの場合
import os
os.environ["OPENAI_API_KEY"] = "your-api-key"
res = litellm.completion(
model="gpt-4o", # OpenAIはprefix不要
messages=messages,
)
・Geminiの場合
import os
os.environ["GEMINI_API_KEY"] = "your-api-key"
res = litellm.completion(
model="gemini/gemini-pro",
messages=messages,
)
メッセージの書き方
messagesは OpenAI Chat Completion API と同じ書き方になります。
基本は上記サンプルコードのように、role と content を持つ辞書のリストで構成され、上から順番に発言が進行する会話ログとして扱われます。
content は発言内容、role は発言の立場を表し、代表的な役割は以下のとおりです。
- role
- system: ルール・制約・人格・出力フォーマット(最初に置かれることが多い)
- user: ユーザー入力
- assistant: 過去のモデル出力(会話継続用)
厳密に守る必要はないですが、この構造を前提にモデルは学習されているため、従わない場合は精度が落ちる可能性があります。
最後に画像を入力する方法と出力フォーマットを固定する方法を書いて終わります。
マルチモーダル(画像)の利用
画像を入力する場合は以下のようにメッセージに画像を埋め込みます。
微妙に書き方に派生がありそうですが、とりあえず動いたものを書いておきます。
(モデルは Qwen/Qwen3-VL-8B-Instruct-GGUF を使用)
import base64
from urllib.request import urlopen
import cv2 # pip install opencv-python
import litellm
import numpy as np
from PIL import Image # pip install pillow
# --- サンプルでこの記事の上にある、LMStudio の GetStarted のキャプチャ画像
img_cv = np.asarray(Image.open(urlopen("https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/f675cab3-733c-49ee-9fde-b23ccd531af4.png")).convert("RGB"))
# img_cv = cv2.imread("a.png") # ローカルの画像を使う場合
# --- 画像をbase64にエンコード
def cv2_image_to_base64(image: np.ndarray, ext: str = ".png") -> str:
success, buffer = cv2.imencode(ext, image)
if not success:
raise ValueError("Failed to encode image")
encoded: str = base64.b64encode(buffer).decode("utf-8")
return encoded
img_base64 = cv2_image_to_base64(img_cv)
# 1枚の場合のメッセージ例
messages = [
{"role": "system", "content": "画像を説明して"},
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": f"data:image/png;base64,{img_base64}",
}
],
},
]
# 複数画像の場合のメッセージ例
# 画像の説明はお好み(なくても認識する)
messages = [
{"role": "system", "content": "画像を説明して"},
{
"role": "user",
"content": [
{"type": "text", "text": "【1枚目】"},
{"type": "image_url", "image_url": f"data:image/png;base64,{img_base64}"},
{"type": "text", "text": "【2枚目】"},
{"type": "image_url", "image_url": f"data:image/png;base64,{img_base64}"},
],
},
]
# 呼び出し方は同じだが、モデルがVisionに対応している必要がある
res = litellm.completion(
model="lm_studio/Qwen/Qwen3-VL-8B-Instruct-GGUF",
base_url="http://127.0.0.1:1234/v1",
messages=messages,
)
text = res["choices"][0]["message"]["content"]
print(text)
出力フォーマットの固定
LLMの出力は通常、自然言語によるテキストですが、プログラムから利用する場合には、構造化された形式の方が嬉しいケースがほとんどです。
litellm では OpenAI 互換の JSON Schema 出力に対応しており、あらかじめ schema を指定することで、出力形式を任意の構造に厳密に制約できます。
(JSON Schema の仕様については、公式サイト https://json-schema.org/ を参照)
JSON Schema は以下みたいな感じで記載します。
{
"type": "json_schema",
"json_schema": {
"name": "result",
"schema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "生成されたテキスト"
},
},
"required": ["text"],
"additionalProperties": False,
},
},
}
ただこれを毎回書きおこすのは大変なので、私は以下のように BaseModel で表現して使っています。
import litellm
from pydantic import BaseModel
# ---------------------------
# BaseModelでスキーマを定義
# ---------------------------
class PurchaseItem(BaseModel): # 購入商品
name: str # 商品名
quantity: int # 商品の個数
section: str # 商品が売ってるコーナー名
class ShoppingResult(BaseModel): # 買い物の結果
items: list[PurchaseItem]
# ---------------------------
# 出力形式をプロンプトで指定する
# ---------------------------
messages = [
{
"role": "system",
"content": f""" かごに入れたものを分類してください。
# 出力形式(厳守・JSONのみ)
{ShoppingResult.model_json_schema()}
# 禁止事項
- JSON構造を変更しないでください
""",
},
{
"role": "user",
"content": "青果コーナーでトマトと玉ねぎを選び、次に食品コーナーで牛乳と食パン2個をかごに入れて店内を回った。",
},
]
# ---------------------------
# 実行
# ---------------------------
res = litellm.completion(
# --- 精度が良くなかったので gemma-3 1B -> Qwen3 VL-8B に変更しています
model="lm_studio/Qwen/Qwen3-VL-8B-Instruct-GGUF",
# model="lm_studio/lmstudio-community/gemma-3-1B-it-qat-GGUF",
base_url="http://127.0.0.1:1234/v1",
messages=messages,
# ---------------------------
# schemaを指定、schemaはBaseModel形式から変換
# ---------------------------
response_format={
"type": "json_schema",
"json_schema": {
"name": "query",
"schema": ShoppingResult.model_json_schema(),
},
},
)
text = res["choices"][0]["message"]["content"]
print(text) # debug
# ---------------------------
# 出力をBaseModel形式に変換
# ---------------------------
result = ShoppingResult.model_validate_json(text)
for item in result.items:
assert isinstance(item.quantity, int) # quantityはint型になってる
print(item.section, item.name, item.quantity)
・出力結果
# ユーザの入力内容
青果コーナーでトマトと玉ねぎを選び、次に食品コーナーで牛乳と食パン2個をかごに入れて店内を回った。
# gemma-3 1B の結果
野菜 トマト 3
野菜 玉ねぎ 1
# Qwen3-VL-8B の結果
青果コーナー トマト 1
青果コーナー 玉ねぎ 1
食品コーナー 牛乳 1
食品コーナー 食パン 2
出力をBaseModel形式に変換するとVSCodeも型が認識でき、コード補完や型チェックをしてくれるので便利です。
また、boolやint,float等はわざわざ文字列から変換する必要がなくなります。
おわりに
複数のモデルを試すという点では、ローカルLLMは気軽に扱えてとてもいいですね。
チャットのように過去の発言を保持して参照したり、MCPやRAGといった最近一般的になりつつあるやり取りを行う場合、litellmではナイーブに実装する必要があるため、用途によってはlangchainのほうが向いているかもしれません。
(もしかすると、私が把握していないだけでlitellmにあるかも…)
また最近では、プロンプト自体を生成・改善していくアプローチも増えてきています。
システムプロンプトや BaseModel を自動生成するなど、まだまだ工夫の余地はありそうです。
とりあえず最初に動かしてみるところまでは、この記事で一通りまとめられたと思います。
少しでも誰かの参考になれば幸いです。












