CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: uv を使って手軽にフリースレッド版の CPython を試す

CPython 3.14 から、フリースレッドモード (Free-threaded mode) が正式にサポートされた 1。 フリースレッドモードでは、従来の CPython に存在した GIL (Global Interpreter Lock) の制約が無くなる。 GIL の制約が無くなると、これまでは基本的にマルチプロセスを使っていた並列処理がマルチスレッドでも書けるようになる。

ただし、フリースレッドモードにはいくつか注意点がある。 まず、CPython のバージョン 3.14 ではフリースレッドモードがデフォルトで無効になっている。 有効にするには、CPython をビルドするときにオプション (--disable-gil) で明示的に指定しなければいけない 2。 そして、フリースレッドモードが有効なときはプログラムのシングルスレッド性能が 5 ~ 10% 程度低下する。 つまり、並列処理は書きやすくなる一方で逐次処理は少し遅くなる。

今回は uv 3 を使うことで手軽にフリースレッドモードが有効な CPython を試してみる。 フリースレッドモードが有効な CPython が、本当にマルチスレッドで CPU コアを使い切れるのかを確認する。

使った環境は次のとおり。

$ sw_vers      
ProductName:        macOS
ProductVersion:     15.7.1
BuildVersion:       24G231
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M3
$ sysctl hw.logicalcpu
hw.logicalcpu: 8
$ uv -V
uv 0.9.2 (Homebrew 2025-10-10)

もくじ

下準備

まずは uv と GNU time をインストールしておく。 CPU の状況を観測する手段が何かしらあれば GNU time は無くても構わない。

$ brew install uv gnu-time

uv でインストールする Python で、フリースレッドモードが有効なビルドには名前の末尾に t がつく。 なので、まずはフリースレッドモードが有効な CPython 3.14 である 3.14t をインストールしよう。

$ uv python install 3.14t

比較対象として、フリースレッドモードが無効な通常の CPython 3.14 もインストールしておこう。

$ uv python install 3.14

サンプルコード

次に、それぞれのビルドで実行するサンプルコードを示す。 サンプルコードでは CPU の論理コア数よりも 1 少ない数のスレッドを起動してビジーループさせる。 GIL の制約が無ければ、このプログラムで CPU 1 コアよりも多いリソースを消費できるはず。

import threading
import os
import sys
import time


def _busy_loop(thread_id):
    """ビジーループする関数"""
    print(f"starting thread: {thread_id}")
    while True:
        pass


def main():
    # CPU 論理コア数より 1 少ない数のスレッドを起動する
    num_threads = os.cpu_count() - 1
    for i in range(num_threads):
        thread = threading.Thread(target=_busy_loop, args=(i,))
        # 親スレッドが止まったら子スレッドも停止する
        thread.daemon = True
        # スレッドを起動する
        thread.start()

    # メインスレッドはスリープさせる
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        sys.exit(0)


if __name__ == "__main__":
    main()

フリースレッドモードが無効なビルドで実行した場合

まずは通常の、フリースレッドモードが無効なビルドから試してみよう。

uv run コマンドに --script オプションを渡して先ほどのサンプルコードを実行する。 同時に --python オプションで実行する Python 実行環境を選ぶ。 このとき、末尾に t のつかない、フリースレッドモードが無効なビルドを指定する。 gtime コマンドを先頭につけることで、プログラムが消費する CPU のリソースに関する情報を出力できる。

$ gtime uv run --script --python 3.14 busyloop.py 
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6

実行しているのは 8 論理 CPU コアを積んだマシンなので 7 つのスレッドが走る。 その上で、リソースモニターなどを確認すると CPU 1 コア分のリソースが消費されているはず。 これは、同時に実行されるカーネルスレッドが GIL によって 1 つに制限されているため。

$ gtime uv run --script --python 3.14 busyloop.py 
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6
^C5.06user 0.04system 0:05.14elapsed 99%CPU (0avgtext+0avgdata 15312maxresident)k
0inputs+0outputs (929major+1467minor)pagefaults 0swaps

しばらく実行して満足したら Ctrl-C でプログラムの実行を止めよう。 上記の gtime コマンドの出力にも 99%CPU という表記が確認できる。

フリースレッドモードが有効なビルドで実行した場合

次は先ほどと同じことをフリースレッドモードが有効なビルドで試す。 違いは --python の引数の末尾に t をつけるだけ。

$ gtime uv run --script --python 3.14t busyloop.py
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6
^C29.49user 0.04system 0:04.31elapsed 684%CPU (0avgtext+0avgdata 18992maxresident)k
0inputs+0outputs (1002major+1707minor)pagefaults 0swaps

今度はビジーループする 7 つのスレッドで CPU 7 コア分のリソースが消費されるはず。 上記の gtime コマンドの出力にも 684%CPU という表記が確認できる。

まとめ

今回は uv を使ってフリースレッドモードが有効な CPython 3.14 を動作させてみた。 uv は Python の実行環境まで管理できるので、こういった場面で便利だと感じられる。

なお、フリースレッドモードを公式にサポートしたバージョンの CPython 3.14 は 2025-10-07 にリリースされたばかり。 そのため、フリースレッドモードが有効なビルドを実用する上では、サードパーティー製のパッケージ群の対応を待つ時間が必要なはず。

また、将来的には CPython でフリースレッドモードがデフォルトで有効になる「フェーズ 3」も計画されている 4。 マルチスレッドで並列処理が書けるのは本当に嬉しくて、実用的になるはずの将来が楽しみで仕方がない。


batt で MacBook のバッテリー充電を制御する

リチウムイオンバッテリーは、充電量が 0% か 100% に近い状態で使い続けると劣化しやすいことが知られている。 また、充放電のサイクルが増えると少しずつではあるが着実に劣化していく。 リチウムイオンバッテリーが劣化すると、製品の設計上の容量よりも電気を蓄える力が落ちて駆動時間が短くなる。

したがって、MacBook のバッテリーの寿命を延ばすためには、次のような状態にしたい。

  • バッテリーが劣化しにくい充電量を維持する
    • 特定の充電量に到達したら充電を停止する
  • バッテリーの充放電サイクルを増やさない
    • 使うときは充電ケーブルから給電し続ける (バッテリーの出力を使わない)

しかし、残念ながら上記は macOS の標準的な機能では実現することが難しい。

なお、現在の macOS にも標準で「バッテリー充電の最適化」という機能はある。 この機能を有効にすると、たまに充電が 80% でしばらく止まることがある。 しかし、この機能を使ったとしてもユーザが明示的に充電を制御することはできない。 充電ケーブルをつないだままにしていると、そのうち充電量が 100% になってしまう。

そこで、今回は batt というアプリケーションを紹介する。 このアプリケーションを使うと MacBook で明示的にバッテリーの充電を制御できるようになる。

github.com

なお、類似のアプリケーションとしては battery 1 というのもある。

使った環境は次のとおり。

$ sw_vers 
ProductName:        macOS
ProductVersion:     15.7.1
BuildVersion:       24G231
$ batt version
v0.5.3 9c9e5e3ef4b1edd93d050aaddd5f9b91ddb6c1ac

もくじ

下準備

まず、Homebrew を使って batt をインストールする。

$ brew install batt

batt のサービスを起動する。 このとき管理者権限が必要なので sudo(8) をつける。

$ sudo brew services start batt

サービスが起動すると batt コマンドが使えるようになる。

$ batt status       
Charging status:
  Allow charging: ✔ (refreshes can take up to 2 minutes)
    Your Mac will charge, but you are not plugged in yet.
  Use power adapter: ✔
    Your Mac will use power from the wall (to operate or charge), if it is plugged in.

Battery status:
  Current charge: 57%
  State: discharging
  Full capacity: 4556 mAh
  Charge rate: -9.8 W
  Voltage: 11.65 V

Battery configuration:
  Upper limit: 80%
  Lower limit: 78%
  Prevent idle-sleep when charging: ✔
  Disable charging before sleep if charge limit is enabled: ✔
  Prevent system-sleep when charging: ✘
  Allow non-root users to access the daemon: ✘
  Control MagSafe LED: ✘

バッテリーの充電量に上限を設ける

まずは、最も一般的なユースケースと考えられるバッテリーの充電量に上限を設定する方法について。 これには batt limit コマンドを使う。 何処まで充電するかをパーセンテージで引数に指定する。

$ batt limit 70

実行すると、次のように充電量の上限が反映される。

$ batt status        
Charging status:
  Allow charging: ✔ (refreshes can take up to 2 minutes)
    Your Mac will charge, but you are not plugged in yet.
  Use power adapter: ✔
    Your Mac will use power from the wall (to operate or charge), if it is plugged in.

Battery status:
  Current charge: 57%
  State: discharging
  Full capacity: 4558 mAh
  Charge rate: -3.3 W
  Voltage: 11.70 V

Battery configuration:
  Upper limit: 70%
  Lower limit: 68%
  Prevent idle-sleep when charging: ✔
  Disable charging before sleep if charge limit is enabled: ✔
  Prevent system-sleep when charging: ✘
  Allow non-root users to access the daemon: ✘
  Control MagSafe LED: ✘

なお、電源が切れている状態では制限が効かない点には注意が必要になる。

バッテリーの充電を一時的に停止する

その他に、一時的にバッテリーの充電を停止することもできる。 これには batt adapter コマンドを使う。

まず、デフォルトではアダプタが有効になっている。 現在の状況は batt adapter status コマンドで確認できる。

$ batt adapter status
INFO[2025-10-13T17:24:37+09:00] power adapter is enabled

ここで、batt adapter disable を実行すると、充電が停止する。

$ batt adapter disable
INFO[2025-10-13T17:26:34+09:00] daemon responded: "ok"                       
INFO[2025-10-13T17:26:34+09:00] successfully disabled power adapter

確認すると、次のようにアダプタが無効になっている。

$ batt adapter status 
INFO[2025-10-13T17:26:56+09:00] power adapter is disabled

batt adapter enable コマンドを実行すると、充電が再開する。

$ batt adapter enable
INFO[2025-10-13T17:27:08+09:00] daemon responded: "ok"                       
INFO[2025-10-13T17:27:08+09:00] successfully enabled power adapter

batt による制御を停止する

また、batt による制御を止めたいときは次のように batt disable すれば良い。

$ batt disable

いじょう。


Android: GitHub にある Obsidian のデータを Termux + GitHub CLI で同期する

最近、情報を記録するのに Obsidian を使い始めた。 ただ、使い始める上で問題が一つあった。 それは、複数のデバイスでデータを同期する方法について。 Obsidian は、基本的にローカルのファイルシステムで Markdown を管理する。 そのため、複数のデバイスでデータを同期したい場合には、その方法を考える必要がある。 公式には Obsidian Sync という同期のためのサービスがあるものの月額で料金がかかる。 まずはもうちょっと気軽に始めたかったので、それ以外の選択肢を検討し始めた。

そして、ひとまず GitHub にプライベートリポジトリを作って管理する方法に落ち着いた。 このやり方には、次のような利点があると思う。

  • 複数の異なるプラットフォームのデバイスで同期しやすい
  • ファイルの世代管理ができる

今回は、そのやり方で Android を使ってデータ (Vault) を同期する方法について記録しておく。 Web で事例を調べると、デバイスに SSH の秘密鍵を置くやり方が多かった。 今回は、代わりに GitHub CLI を使って PAT を発行する。 ターミナルエミュレータには Termux というアプリを使ってみた。

もくじ

下準備

まずは Android に Termux のアプリをインストールする。

play.google.com

同様に Obsidian のアプリもインストールする。

play.google.com

Termux で GitHub にログインする

Termux のアプリを開いたら GitHub CLI をインストールする。

$ pkg install gh

インストールしたら GitHub CLI で GitHub にログインする。 これには gh auth login コマンドを使う。

$ gh auth login

対話的にログインとセットアップのやり方を聞かれる。

Where do you use GitHub?
> GitHub.com

What is your preferred protocol for Git operations on this host?
> HTTPS

How would you linke to authenticate GitHub CLI?
> Login with a web browser

Login with a web browser を選択したら、ブラウザで以下の URL にアクセスする。

github.com

そして、Termux の方に表示されているワンタイムコードを入力する。

First copy your one-time code: XXXX-XXXX

入力したら Termux 経由のアクセスを承認する。

ローカルのファイルシステムを操作できるようにする

デフォルトで Termux はデバイスのファイルシステムにアクセスできない。 そこで、アクセスできるようにセットアップする。

そのために termux-setup-storage コマンドを実行する。

$ termux-setup-storage

コマンドを実行するとホームディレクトリに storage というシンボリックリンクができる。

$ ls
storage

この storage からデバイスのファイルシステムにアクセスできる。

リポジトリをクローンする

すでに既存の Obsidian 用のリポジトリがあるときは、そのリポジトリをデバイスのファイルシステムにクローンする。

たとえば storage/documents の下に repos みたいなディレクトリを作る。

$ mkdir -p storage/documents/repos
$ cd storage/documents/repos

作成したディレクトリで gh repo clone コマンドを実行してリポジトリをクローンする。 <username> には自身のアカウント名、<repo-name> にはリポジトリの名前を入れる。

$ gh repo clone <username>/<repo-name>

あるいは、まだリポジトリがないときは gh repo create コマンドで対話的に作成する。

$ gh repo create

もちろん、リポジトリを作る作業はパソコンなど操作性に優れた環境を使った方が楽だろう。

クローンしたリポジトリを Obsidian から使う

リポジトリはデバイスのファイルシステムにクローンされている。 そのため、あとは Obsidian からそのディレクトリを vault として指定するだけで利用できる。

Obsidian を開いたら「Open folder as vault」を選択して、先ほどクローンしたディレクトリを指定する。

Git プラグインをセットアップする

このままでも Termux から操作すればリポジトリを管理できる。 しかし、Obsidian の方からコミットやプッシュできた方が便利なので設定していく。

まずは Obsidian のアプリの設定を開いて「Community plugins」に移動する。 デフォルトでは Community plugins は無効になっているため、まずは有効にする。

そして Community plugins の「Browse」を選択する。 一覧の中から「Git」プラグインを探してインストールする。 インストール直後にはプラグインが無効になっているため有効にする。

有効にしたらプラグインのオプションを開く。 下の方にスクロールして「Authentication/commit author」の項目を埋めていく。

まず、「Username」や「Auther name」は GitHub のアカウント名を入れれば良い。

「Author email」は、GitHub の Web サイトを確認していれる。 GitHub の Web サイトをブラウザで開いたら「Settings > Emails」に移動する。 下の方に「Keep my email addresses private」という項目があるので、無効になっているときは有効にする。 その上で、記載されている <random>+<username>@users.noreply.github.com 的なフォーマットのメールアドレスをコピーする。 コピーした内容を、Obsidian の方の「Author email」に入れる。

「Password/Parsonal acccess token」は GitHub CLI で発行する。 Termux の方に戻って以下のコマンドを実行する。

$ gh auth token

表示されたトークンをコピーして「Password/Parsonal acccess token」にペーストする。

以上で Git プラグインをセットアップできた。

Git プラグインからファイルをプッシュする

次に Git プラグインが正常に動作することを確認する。

Obsidian のアプリの右下のハンバーガーメニューから「Open Git source control」を開く。 まずはダウンロードする感じのボタンを押下して Git リポジトリを Pull できることを確認する。 うまくいかない場合にはアプリを開き直したり、パソコンから一旦リポジトリにプッシュしたりしてみよう。 Pull できれば、Commit や Push についても問題なくできるはず。

まとめ

今回は Android で GitHub にある Obsidian の Vault を同期する方法について書いた。

自分で MCP (Model Context Protocol) を喋ってみる

Model Context Protocol (MCP) は、LLM が外部のシステムから情報を得る、または操作するのに用いられるプロトコルのひとつ。 昨今の扱われ方からすると、今後のデファクトとなりつつあるように感じられる。

今回は、そんな MCP がどういったプロトコルなのか仕様を読みながら自分で喋ってみることにした。 詳しくは後述するものの、MCP はあくまで JSON-RPC 2.0 をベースにしたプロトコルに過ぎない。 そのため、LLM とは関係なく独立して扱うことができる。

使った環境は次のとおり。 MCP Server と Client の実装には MCP 公式の Python SDK を用いた。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.6
BuildVersion:       24G84
$ uname -srm                      
Darwin 24.6.0 arm64
$ uv --version                    
uv 0.8.4 (Homebrew 2025-07-30)

もくじ

MCP について

本題に入る前に、まずは MCP の概要についてざっくりと説明していく。

なお、特に断りがない限りは執筆の時点で最新のバージョン (2025-06-18) のプロトコル仕様を元にしている。 プロトコルの仕様は、以下の Web サイトで確認できる。

modelcontextprotocol.io

アーキテクチャ

MCP のアーキテクチャには、次のようなコンポーネントが登場する。

  • MCP Server
  • MCP Client
  • MCP Host

まず、MCP Server は実際にツールを実行したりデータを提供したりするコンポーネントになっている。 そのため、MCP Server が MCP の主役といっていい。 そして MCP Client は MCP Server に対してツールの実行やデータの提供を依頼する。

MCP Host は LLM やそれを利用するアプリケーションとほぼイコールと考えて良い。 MCP Host には MCP Server (+Client) を登録する。 登録した MCP Server が提供する機能を、MCP Host の LLM が使えるようになる。 具体例を挙げると、Claude Desktop や LM Studio が MCP Host として動作する。 例として挙げた MCP Host には MCP Client の機能が同梱されている。

機能

MCP Server が提供する機能には、次のような種類がある。

  • Tools
  • Resources
  • Prompts

Tools は読んで字のごとくツールの呼び出しに使用する。 LLM はツールを呼び出すことで外部から情報を得たり、外部の世界に何らかの副作用を生じさせたりする。 たとえば外部の何らかの API の呼び出しや、データベースへのクエリの発行、計算の実行など。 これが MCP のユースケースとしてはイメージしやすいと思う。

参考: Tools - Model Context Protocol

Resources は URI を使って識別される何らかのリソースの読み取りに使用する。 LLM はリソースを読み取ることで後続の推論に必要な情報を得る。 分かりやすいところでいうとファイルシステムの読み取りがある。 たとえば file:///read/to/path のような URI を読み取ることで、そのファイルの内容が得られる。

参考: Resources - Model Context Protocol

Prompts は、LLM の推論に用いるプロンプトを得るのに使用する。 この機能は LLM が直接利用するのではなく、MCP Host やそれを操作するユーザが主に利用する機能らしい。 MCP Server から提供されたプロンプトを使って、ユーザが LLM に指示を出すイメージか。

参考: Prompts - Model Context Protocol

なお、 MCP Client も提供する機能がいくつかあるが、ここでは扱わない。

メッセージ

プロトコルとしての MCP は、メッセージングに JSON-RPC 2.0 を用いる。 つまり、MCP Server と Client の間では JSON-RPC 2.0 のメッセージがやり取りされる。

JSON-RPC 2.0 は、その名の通り JSON を表現に使った RPC (Remote Procedure Call) になっている。 仕様は以下の Web サイトで確認できる。

www.jsonrpc.org

メッセージとしては、次の 3 つが定義されている。

  • Requests
  • Responses
  • Notifications

上記で Requests と Responses は対になっている。 機能の要求が Requests で、それに対する応答が Responses になる。 Notifications は一方通行の通知で、Server と Client のいずれも他方に送信することがある。

参考: Overview - Model Context Protocol

トランスポート

MCP Server と Client の間で JSON-RPC 2.0 をやり取りするための通信路をトランスポートという。 仕様には次の 3 つのトランスポートが存在する。

  • stdio
  • Streamable HTTP
  • ​Custom

stdio は文字通り stdio (標準入出力) を使って MCP Server と Client が通信する。 典型的には MCP Server の機能がローカルホストで完結する場合や、開発の用途で用いられる。

Streamable HTTP も文字通り HTTP を使って MCP Server と Client が通信する。 この場合、MCP Server はイコール HTTP サーバになる。 MCP Server から Client に対して非同期にメッセージを送りたいときは、オプションで SSE (Server-Sent Events) を用いる。

Custom は独自に実装したトランスポートになる。 具体例を挙げると、WebSocket や UNIX ドメインソケットでメッセージをやり取りするような場合だろう。 その場合は MCP Server と Client の両方が、その独自のトランスポートに対応している必要がある。

参考: Transports - Model Context Protocol

なお、今回はトランスポートとして stdio だけを使う。

下準備

これで、必要な前提知識は大体説明できた。

次は実際に MCP Server と MCP Client を書いて動かしてみよう。 ただし、最初から自分で MCP を喋ることはしない。 まずは、Server と Client がそれぞれちゃんと動くことを確認する。

下準備として Homebrew で uv をインストールしておく。

$ brew install uv

Tools

まずは Tools から見ていこう。

MCP Server

公式の Python SDK を使って実装した MCP Server のサンプルコードを次に示す。 この MCP Server では add()server_time() という 2 つの Tools を実装している。 add は引数を取って足し算をする。 server_time は引数なしでサーバの時刻を返す。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Tools を実装した MCP サーバ"""

from datetime import datetime

from mcp.server.fastmcp import FastMCP

mcp = FastMCP()


@mcp.tool(description="足し算をするツールです")
def add(a: int, b: int) -> int:
    return a + b


@mcp.tool(description="サーバのシステム時刻を返すツールです")
def server_time() -> str:
    now = datetime.now()
    return now.isoformat(sep=" ", timespec="seconds")


if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP Client

続いて、同じように公式の SDK で実装した MCP Client のサンプルコードを以下に示す。 まずは公式の SDK を使って振る舞いを確認する。 サンプルコードは次のような流れになっている。

  • セッションを初期化する
  • 使用できる Tools のリストを得る
  • Tool を呼び出す
# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Tools を実装した MCP サーバを呼び出す MCP クライアント"""

import asyncio

from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="uv",
    args=[
        # MCP サーバのスクリプトが別のディレクトリにある場合は以下で指定する
        # "--directory",
        # "/path/to/server/script/directory",
        "run",
        "toolmcpserver.py",
    ],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # セッションを初期化する
            await session.initialize()

            # 利用できるツールの一覧を得る
            items = await session.list_tools()
            print("Available tools:", [item.name for item in items.tools])

            # ツール (add) を呼び出す
            response = await session.call_tool("add", {"a": 1, "b": 2})
            result = response.structuredContent.get("result")
            print("Tool (add) result:", result)

            # ツール (server_time) を呼び出す
            response = await session.call_tool("server_time")
            result = response.structuredContent.get("result")
            print("Tool (server_time) result:", result)


if __name__ == "__main__":
    asyncio.run(main())

それでは、実装した MCP Client のスクリプトを uv で実行してみよう。

$ uv run --script toolmcpclient.py
[07/27/25 00:55:04] INFO     Processing request of type ListToolsRequest                                               server.py:625
Available tools: ['add', 'server_time']
                    INFO     Processing request of type CallToolRequest                                                server.py:625
Tool (add) result: 3
                    INFO     Processing request of type CallToolRequest                                                server.py:625
Tool (server_time) result: 2025-07-27 00:55:04

ちゃんと MCP Server から処理に対応した結果が得られている。

自分で喋ってみる

続いては今回の本題である MCP を自分で喋ってみることにしよう。 要するに、先ほどの MCP Client を自分が代わりにやる。

それには、実装した MCP Server を uv で実行する。

$ uv run --script toolmcpserver.py

これで MCP Server と stdio トランスポートで会話できる。 stdio トランスポートの場合、それぞれのメッセージは改行で区切られる。

まずはクライアント側から initialize メソッドを呼び出す。 このメッセージで、クライアント側が対応しているプロトコルのバージョンや機能などの情報をサーバへ伝える。 なお、id は単に Requests と Responses の対応を取るためのもので、別に連番になっている必要はない。

> { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "Human", "title": "Human Powered Client", "version": "0.0.1" }}}

また、実際に試す際には先頭の > は必要ない。 これは分かりやすさのためにつけている。 以降はクライアントからサーバへのメッセージには > をつける。 また、サーバからクライアントへのメッセージには < をつける。

すると、initialize メソッドに対する返答がサーバから返ってくる。 このメッセージには、サーバが対応しているプロトコルのバージョンや機能の情報が含まれている。

< {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"FastMCP","version":"1.12.2"}}}

なお、サーバ側が出力するログの情報も本来は一緒に表示されるがここでは省略する。

次にクライアントからサーバに対して初期化が完了した旨の通知を送る。 これは先述した Notifications なので、一方通行であってサーバからの返答は無い。

> { "jsonrpc": "2.0", "method": "notifications/initialized" }

ちなみに、初期化しないまま後続のメッセージをやり取りしようとすると次のようなエラーメッセージになる。

< {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}

次に MCP Server が提供する Tools の一覧を得る。 これには tools/list メソッドを呼び出す。

> { "jsonrpc": "2.0", "id": 2, "method": "tools/list" }

すると、利用できる Tools の一覧が返答として得られる。 ここには説明や、呼び出す際の引数の情報などが含まれている。

< {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"add","description":"足し算をするツールです","inputSchema":{"properties":{"a":{"title":"A","type":"integer"},"b":{"title":"B","type":"integer"}},"required":["a","b"],"title":"addArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"integer"}},"required":["result"],"title":"addOutput","type":"object"}},{"name":"server_time","description":"サーバのシステム時刻を返すツールです","inputSchema":{"properties":{},"title":"server_timeArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"server_timeOutput","type":"object"}}]}}

実際に Tools を呼び出すには tools/call メソッドを使う。 ここでは引数のない server_time を呼んでいる。

> { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "server_time" }}

呼び出すと、次のようにサーバ側で処理された結果が返ってくる。

< {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"2025-07-27 01:23:07"}],"structuredContent":{"result":"2025-07-27 01:23:07"},"isError":false}}

引数があるものについては次のように arguments をつけて呼び出す。

> { "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "add", "arguments": { "a": 1, "b": 2 }}}

結果については先ほどと変わらない。

< {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"3"}],"structuredContent":{"result":3},"isError":false}}

これで先ほどの MCP Client と同じ流れを自分で体験できた。

Resources

次は Resources についても同じ流れを試す。

MCP Server

次の MCP Server のコードでは greeting()echo() という 2 つの Resources を実装している。 greeting は定数の文字列を返す。 echo は URI に埋め込まれた内容をそのまま返す。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Resources を実装した MCP サーバ"""

from mcp.server.fastmcp import FastMCP

mcp = FastMCP()


@mcp.resource("greeting://", description="挨拶をするリソースです")
def greeting():
    return "Hello, World!"


@mcp.resource("echo://{message}", description="エコーを返すリソースです")
def echo(message: str) -> str:
    return message


if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP Client

対応する MCP Client のサンプルコードを以下に示す。 Tools と同様に以下の流れになっている。

  • 初期化する
  • 利用できる Resources のリストを得る
  • Resources を読み取る

なお、URI に何らかの情報が含まれる Resources は Resource Templates として別物になっている。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Resources を実装した MCP サーバを呼び出す MCP クライアント"""

import asyncio

from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="uv",
    args=[
        # "--directory",
        # "/path/to/server/script/directory",
        "run",
        "resourcemcpserver.py",
    ],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # セッションを初期化する
            await session.initialize()

            # 利用できるリソースの一覧を得る
            items = await session.list_resources()
            print("Available resources:", [item.name for item in items.resources])

            # 利用できるリソーステンプレートの一覧を得る
            items = await session.list_resource_templates()
            print("Available resource templates:", [item.name for item in items.resourceTemplates])

            # リソース (greeting) を呼び出す
            response = await session.read_resource("greeting://")
            print("Resource (greeting) response:", response)

            # リソーステンプレート (echo) を呼び出す
            response = await session.read_resource("echo://hello")
            print("Resource (echo) response:", response)


if __name__ == "__main__":
    asyncio.run(main())

実装した MCP Client を実行してみよう。

$ uv run --script resourcemcpclient.py
[08/04/25 03:43:45] INFO     Processing request of type ListResourcesRequest                                           server.py:625
Available resources: ['greeting']
                    INFO     Processing request of type ListResourceTemplatesRequest                                   server.py:625
Available resource templates: ['echo']
                    INFO     Processing request of type ReadResourceRequest                                            server.py:625
Resource (greeting) response: meta=None contents=[TextResourceContents(uri=AnyUrl('greeting://'), mimeType='text/plain', meta=None, text='Hello, World!')]
                    INFO     Processing request of type ReadResourceRequest                                            server.py:625
Resource (echo) response: meta=None contents=[TextResourceContents(uri=AnyUrl('echo://hello'), mimeType='text/plain', meta=None, text='hello')]

ちゃんと Resources のリストの取得や読み取りができている。

自分で喋ってみる

次は、先ほどと同様に自分が MCP Client となって Server とやり取りしてみよう。

uv を使って MCP Server のスクリプトを起動する。

$ uv run --script resourcemcpserver.py

まずは Tools の場合と同様に初期化する。

> { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "Human", "title": "Human Powered Client", "version": "0.0.1" }}}
< {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"FastMCP","version":"1.12.2"}}}
> { "jsonrpc": "2.0", "method": "notifications/initialized" }

次に利用できる Resources の一覧を resources/list メソッドで得る。

> { "jsonrpc": "2.0", "id": 2, "method": "resources/list" }
< {"jsonrpc":"2.0","id":2,"result":{"resources":[{"name":"greeting","uri":"greeting://","description":"挨拶をするリソースです","mimeType":"text/plain"}]}}

同様に、利用できる Resource Templates の一覧を resources/templates/list で得る。

> { "jsonrpc": "2.0", "id": 3, "method": "resources/templates/list" }
< {"jsonrpc":"2.0","id":3,"result":{"resourceTemplates":[{"name":"echo","uriTemplate":"echo://{message}","description":"エコーを返すリソースです"}]}}

greeting:// の Resources を resources/read メソッドで読み取る。

> { "jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": { "uri": "greeting://" }}
< {"jsonrpc":"2.0","id":4,"result":{"contents":[{"uri":"greeting://","mimeType":"text/plain","text":"Hello, World!"}]}}

結果として定数の文字列が返ってきた。

同様に echo:// の Resource Templates を読み取る。 読み取る場合のメソッドは Resources と Resource Templates で違いはないようだ。

> { "jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": { "uri": "echo://hello" }}
< {"jsonrpc":"2.0","id":5,"result":{"contents":[{"uri":"echo://hello","mimeType":"text/plain","text":"hello"}]}}

Prompts

次は Prompts でも同じ流れを確認する。

MCP Server

以下に Prompts を実装した MCP Server のサンプルコードを示す。 サンプルコードでは summarize というプロンプトを実装している。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Prompts を実装した MCP サーバ"""

from mcp.server.fastmcp import FastMCP

mcp = FastMCP()


@mcp.prompt(description="要約に使うプロンプトです")
def summarize(topic: str) -> str:
    return f"{topic}について分かりやすく要約してください"


if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP Client

対応する MCP Client のサンプルコードを以下に示す。 Tools や Resources と同様に以下の流れになっている。

  • 初期化する
  • 利用できる Prompts のリストを得る
  • Promts を得る
# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Prompts を実装した MCP サーバを呼び出す MCP クライアント"""

import asyncio

from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="uv",
    args=[
        # "--directory",
        # "/path/to/server/script/directory",
        "run",
        "promptmcpserver.py",
    ],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # セッションを初期化する
            await session.initialize()

            # 利用できるプロンプトの一覧を得る
            items = await session.list_prompts()
            print("Available prompts:", [item.name for item in items.prompts])

            # プロンプト (summarize) を呼び出す
            response = await session.get_prompt("summarize", {"topic": "生命、宇宙、そして万物についての究極の疑問の答え"})
            print("Prompts (summarize) response:", response)


if __name__ == "__main__":
    asyncio.run(main())

上記を uv で実行してみよう。

$ uv run --script promptmcpclient.py
[08/04/25 04:12:23] INFO     Processing request of type ListPromptsRequest                                                                               server.py:625
Available prompts: ['summarize']
                    INFO     Processing request of type GetPromptRequest                                                                                 server.py:625
Prompts (summarize) response: meta=None description='要約に使うプロンプトです' messages=[PromptMessage(role='user', content=TextContent(type='text', text='生命、宇宙、そして万物についての究極の疑問の答えについて分かりやすく要約してください', annotations=None, meta=None))]

クライアントからの入力に基づいてプロンプトが得られている。

自分で喋ってみる

次は先ほどと同様に自分が MCP Client となって Server とやり取りしてみよう。

uv を使って MCP Server のスクリプトを起動する。

$ uv run --script promptmcpserver.py

まずは初期化する。

> { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "Human", "title": "Human Powered Client", "version": "0.0.1" }}}
< {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"FastMCP","version":"1.12.2"}}}
> { "jsonrpc": "2.0", "method": "notifications/initialized" }

利用できるプロンプトの一覧を得る。

> { "jsonrpc": "2.0", "id": 2, "method": "prompts/list" }
< {"jsonrpc":"2.0","id":2,"result":{"prompts":[{"name":"summarize","description":"要約に使うプロンプトです","arguments":[{"name":"topic","required":true}]}]}}

プロンプトの summarize を得る。

> { "jsonrpc": "2.0", "id": 3, "method": "prompts/get", "params": { "name": "summarize", "arguments": { "topic": "生命、宇宙、そして万物についての究極の疑問の答え" }}}
< {"jsonrpc":"2.0","id":3,"result":{"description":"要約に使うプロンプトです","messages":[{"role":"user","content":{"type":"text","text":"生命、宇宙、そして万物についての究極の疑問の答えについて分かりやすく要約してください"}}]}}

入力した内容が埋め込まれたプロンプトが得られた。

まとめ

今回は Model Context Protocol (MCP) をプロトコルという側面から扱った。 MCP は JSON-RPC 2.0 をベースにしたプロトコルで、Server / Client / Host という 3 つのコンポーネントが登場する。 今回はその中で Server と Client を実装した上で、Client になりきって自分で MCP を喋ってみた。 実際に自分でサーバとメッセージをやり取りすることで、どのようなプロトコルなのか理解を深めることができた。

OpenAI の Web API を使い始める

最近は OpenAI 互換の Web API が、LLM の機能を提供する上でデファクトに近い方法となっているように思う。 そこで、今回は本家の OpenAI の Web API を使い始めるまでの内容を自分用のメモを兼ねて残しておく。

使った環境は次のとおり。

$ sw_vers         
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm
Darwin 24.5.0 arm64
$ curl --version
curl 8.7.1 (x86_64-apple-darwin24.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.64.0
Release-Date: 2024-03-27
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM SPNEGO SSL threadsafe UnixSockets
$ python -V
Python 3.12.11
$ pip list | grep -i openai               
openai            1.90.0

もくじ

OpenAI Platform について

OpenAI の Web API を利用する際の料金体系は、ChatGPT のサブスクリプションとは分離されている。 また、Web API に関する管理は OpenAI Platform というサービスを使うことになる。

platform.openai.com

Web API を利用するには、上記のサービスにあらかじめ前払いでクレジットを登録しておく必要がある。 Web API を利用することで生じた料金は、前払いしたクレジットの中から支払われる。

Organization を作る

OpenAI Platform を利用するには、まず Organization を作成する必要があるようだ。 現時点 (2025-06-21) の UI であれば、右上にある「Start Bulding」のボタンを押下する。 特に何も入力しないでウィザードを進めると Organization name が「Personal」になる。 個人による利用であれば、それで特に問題ないはず。 また、Organization の配下にデフォルトのプロジェクトとして「Default」も同時に作られるようだ。

クレジットを追加する

前述した通り Web API はクレジットが無いと使えない。 そこで、まずは「Settings > Billing」の画面でクレジットを追加する。

platform.openai.com

「Add payment details」ボタンを押下するとクレジットカードの登録画面が出てくる。 自身のカードの情報を入力したら、追加するクレジットをドルで指定する。 クレジットが少なくなったときに自動でチャージする設定は不要であればオフにする。

API トークンを作る

Web API を呼び出す際には OpenAI Platform のアカウントとの紐づけが必要になる。 そのために、API トークンを作成する。

platform.openai.com

現時点の UI であれば右上の「Create new secret key」を押下して作成する。 API トークンは作成したタイミングで一度しか表示されない。 忘れずにパスワードマネージャなどに記録しておこう。

curl(1) から Web API を呼び出す

まずは curl(1) を使って呼び出してみる。

先ほど発行したトークンを、シェルのセッション変数に取り込む。 パスワードなどのセンシティブな情報を扱うときは read コマンドと -s オプションを用いると良い。 こうすれば入力した内容がコマンドの履歴に残らない。

$ read -s OPENAI_API_KEY

上記を実行したら、先ほど記録したトークンをターミナルにペーストする。

現時点で最もベーシックな Web API として Chat Completions を試してみよう。 Chat Completions API のリファレンスは以下にある。

platform.openai.com

なお、今だと状態 (ステート) を API 側で管理してもらえる Chat Completions with Responses という API もあるようだ。 従来の API は状態を持たないステートレスだったので、クライアント側ですべて管理する必要があった。 Responses の API では、その点が改善されているようだ。 ただし、今回は使わない。

リファレンスの内容を元に curl(1) で HTTP リクエストを作る。 先ほどセッション変数に入れたトークンは Authorization ヘッダの Bearer 以降に指定する。 model キーにはモデルの名称を入力する。 messages にはロール毎の指示を入力する。 たとえば "role": "developer" の指示は、"role": "user" の入力に関係なく従うべき内容と解釈される。

$ curl -X POST https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4.1",
    "messages": [
      {
        "role": "developer",
        "content": "あなたは親切なアシスタントです"
      },
      {
        "role": "user",
        "content": "こんにちは!"
      }
    ]
  }'

結果は次のような JSON として得られる。

{
  "id": "chatcmpl-XXX...",
  "object": "chat.completion",
  "created": 1750480953,
  "model": "gpt-4.1-2025-04-14",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "こんにちは!😊  \nどうぞ、なんでもお気軽にご相談ください。",
        "refusal": null,
        "annotations": []
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 23,
    "completion_tokens": 16,
    "total_tokens": 39,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "audio_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0,
      "audio_tokens": 0,
      "accepted_prediction_tokens": 0,
      "rejected_prediction_tokens": 0
    }
  },
  "service_tier": "default",
  "system_fingerprint": "fp_51e1070cf2"
}

なお、クレジットが無い状態で呼び出すと次のようなエラーになる。

{
    "error": {
        "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
        "type": "insufficient_quota",
        "param": null,
        "code": "insufficient_quota"
    }
}

Python から Web API を呼び出す

次は公式の Python パッケージを使って Web API を呼んでみよう。

まずは openai パッケージをインストールする。

$ pip install openai

また、先ほどトークンをセッション変数 OPENAI_API_KEY に格納してあった。 これを、プロセスが fork した後の Python プロセスからも使えるように環境変数にする。

$ export OPENAI_API_KEY

openai パッケージは環境変数 OPENAI_API_KEY が定義されていると自動的に読んでくれる。

そして、Python インタプリタを起動する。

$ python

openai.OpenAI クラスをインスタンス化する。

>>> from openai import OpenAI
>>> client = OpenAI()

openai.OpenAI#chat.completions.create() メソッドを使って Chat Completions API を呼び出す。

>>> completion = client.chat.completions.create(
...   model="gpt-4.1",
...   messages=[
...     {"role": "developer", "content": "あなたは親切なアシスタントです"},
...     {"role": "user", "content": "こんにちは!"}
...   ]
... )

レスポンスを確認すると、ちゃんと結果が得られていることが確認できる。

>>> completion
ChatCompletion(id='chatcmpl-XXX...', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='こんにちは!😊  \n何かお手伝いできることはありますか?', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))], created=1750477841, model='gpt-4.1-2025-04-14', object='chat.completion', service_tier='default', system_fingerprint='fp_51e1070cf2', usage=CompletionUsage(completion_tokens=17, prompt_tokens=23, total_tokens=40, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=0, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=0), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)))

いじょう。


[asin:B085BG8CH5:detai

Mac: LM Studio に Hugging Face Hub からダウンロードしたモデルを CLI で読み込む

LM Studio は、ローカル LLM を動かすためのソフトウェアのひとつ。

lmstudio.ai

基本的に LM Studio では、モデルをダウンロードする際には Discover タブから GUI で検索してダウンロードできる。 しかし、Hugging Face Hub 1 などから手動でダウンロードしたモデルをインポートする方法も用意されている。 今回はそのやり方について書く。

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm
Darwin 24.5.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro
$ huggingface-cli version
huggingface_hub version: 0.33.0
$ lms version  
   __   __  ___  ______          ___        _______   ____
  / /  /  |/  / / __/ /___ _____/ (_)__    / ___/ /  /  _/
 / /__/ /|_/ / _\ \/ __/ // / _  / / _ \  / /__/ /___/ /  
/____/_/  /_/ /___/\__/\_,_/\_,_/_/\___/  \___/____/___/  

lms - LM Studio CLI - v0.0.41
GitHub: https://github.com/lmstudio-ai/lmstudio-cli

もくじ

下準備

まずは Homebrew から LM Studio をインストールする。

$ brew install --cask lm-studio

インストールすると $HOME/.lmstudio 以下にディレクトリができる。 この中の bin ディレクトリには LM Studio を CLI で操作するためのバイナリが入っている。 そこで、ここにパスを通す。

$ export PATH="$PATH:$HOME/.lmstudio/bin"

必要に応じてシェルの設定ファイルなどに永続化すると良い。

パスを通すと lms コマンドが使えるようになる。

$ lms version  
   __   __  ___  ______          ___        _______   ____
  / /  /  |/  / / __/ /___ _____/ (_)__    / ___/ /  /  _/
 / /__/ /|_/ / _\ \/ __/ // / _  / / _ \  / /__/ /___/ /  
/____/_/  /_/ /___/\__/\_,_/\_,_/_/\___/  \___/____/___/  

lms - LM Studio CLI - v0.0.41
GitHub: https://github.com/lmstudio-ai/lmstudio-cli

続いて、Hugging Face Hub を操作するためのパッケージをPyPI からダウンロードする。

$ pip install "huggingface_hub[cli]"

これで huggingface-cli コマンドが使えるようになる。

$ huggingface-cli version
huggingface_hub version: 0.33.0

モデルをダウンロードする

次に Hugging Face Hub からモデルをダウンロードする。 LM Studio はデフォルトの実行ランタイムとして llama.cpp を使用する。 そのため、GGUF のモデルをダウンロードする。

$ huggingface-cli download "lmstudio-community/gemma-3-1B-it-qat-GGUF" --local-dir .

モデルをインポートする

ダウンロードしたファイルを lms import コマンドでインポートする。 このときインポートのやり方を対話的に確認される。

$ lms import gemma-3-1B-it-QAT-Q4_0.gguf

無事にインポートできると LM Studio の画面でモデルが確認できるようになるはず。

LM Studio でインポートしたモデルが確認できる

モデルの動作を確認する

サーバを起動するとモデルが WebAPI 経由で使えるようになる。

試しにモデル一覧を curl(1) で確認してみよう。

$ curl http://localhost:1234/v1/models
{
  "data": [
    {
      "id": "gemma-3-1b-it-qat",
      "object": "model",
      "owned_by": "organization_owner"
    },
    {
      "id": "text-embedding-nomic-embed-text-v1.5",
      "object": "model",
      "owned_by": "organization_owner"
    }
  ],
  "object": "list"
}

インポートした gemma-3-1b-it-qat が確認できる。

同様に completions の API も使ってみよう。

$ curl -X POST -s http://localhost:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma-3-1b-it-qat",
  "messages": [
    { "role": "user", "content": "こんにちは!" }
  ]
}'       
{
  "id": "chatcmpl-kfep0etrq6dz9fbcth25uh",
  "object": "chat.completion",
  "created": 1749874667,
  "model": "gemma-3-1b-it-qat",
  "choices": [
    {
      "index": 0,
      "logprobs": null,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "こんにちは!何かお手伝いできることはありますか? 😊 どんなことでもお気軽にご質問ください。"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 11,
    "completion_tokens": 22,
    "total_tokens": 33
  },
  "stats": {},
  "system_fingerprint": "gemma-3-1b-it-qat"
}

問題ないようだ。


Mac: Ollama でローカル LLM を動かす

Ollama 1 はローカル LLM を動かすためのソフトウェアのひとつ。 一般的なラップトップやデスクトップマシンで動かすようなユースケースが主に想定されているように思う。 LLM の機能を不特定多数に提供するというよりは、マシンを操作しているユーザ自身が使用するイメージだろう。 似たようなユースケースで用いられるソフトウェアには、他にも LM Studio 2 などがある。 今回は、そんな Ollama を Mac から使う方法についてメモしておく。

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm                
Darwin 24.5.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro
$ ollama --version
ollama version is 0.9.0

もくじ

下準備

まず、Ollama は Homebrew を使ってインストールできる。

$ brew install --formula ollama

サーバを起動する

Ollama を利用するには、何をするにもまずはサーバのインスタンスを立ち上げる必要がある。

そのためには、Homebrew でインストールした場合には Ollama をサービスとして起動すれば良い。

$ brew services start ollama

あるいは、フォアグラウンドで実行したいときは単に ollama serve コマンドを叩いても良い。

$ ollama serve

設定を変更する

Ollama は、OLLAMA_ から始まる名前のシェル変数・環境変数を使って動作を変更できる。

たとえば、Homebrew のサービスからサーバを起動する場合には launchctl setenv で設定を変更する。 以下ではグローバルなコンテキスト長を 32768 トークンに変更している。

$ launchctl setenv OLLAMA_CONTEXT_LENGTH 32768

あるいは ollama serve コマンドで起動しているときは、単純にセッション変数として指定すれば良い。

$ OLLAMA_CONTEXT_LENGTH=32768 ollama serve

モデルをダウンロードする

続いてモデルをダウンロードするには ollama pull コマンドを使う。

以下では例として Gemma3 の 1B モデルを指定している。

$ ollama pull gemma3:1b
pulling manifest 
pulling 7cd4618c1faf: 100% ▕██████████████████▏ 815 MB                         
pulling e0a42594d802: 100% ▕██████████████████▏  358 B                         
pulling dd084c7d92a3: 100% ▕██████████████████▏ 8.4 KB                         
pulling 3116c5225075: 100% ▕██████████████████▏   77 B                         
pulling 120007c81bf8: 100% ▕██████████████████▏  492 B                         
verifying sha256 digest 
writing manifest 
success

Ollama が公式が提供しているモデルについては以下の Web ページで検索できる。

ollama.com

ダウンロードしたモデルは ollama ls コマンドで確認できる。

$ ollama ls           
NAME         ID              SIZE      MODIFIED    
gemma3:1b    8648f39daa8f    815 MB    6 hours ago

モデルの情報を得る

モデルの詳しい情報は ollama show コマンドで確認できる。

$ ollama show gemma3:1b
  Model
    architecture        gemma3     
    parameters          999.89M    
    context length      32768      
    embedding length    1152       
    quantization        Q4_K_M     

  Capabilities
    completion    

  Parameters
    stop           "<end_of_turn>"    
    temperature    1                  
    top_k          64                 
    top_p          0.95               

  License
    Gemma Terms of Use                  
    Last modified: February 21, 2024    
    ...                                 

ターミナルでモデルと対話する

ollama run コマンドを使うと、ターミナルを使ってモデルと対話できる。

$ ollama run gemma3:1b
>>> こんにちは!
こんにちは!何かお手伝いできることはありますか?😊 

どんなことでも構いません。例えば:

*   質問に答える
*   文章を作成する
*   アイデアを出す
*   情報検索をする

など、お気軽にお申し付けください。

なお、ollama ps コマンドを使うと、現在どのモデルがメモリ上で動作しているか確認できる。

$ ollama ps                     
NAME         ID              SIZE      PROCESSOR    UNTIL              
gemma3:1b    8648f39daa8f    2.2 GB    100% GPU     4 minutes from now

OpenAI-like な WebAPI を利用する

Ollama は OpenAI-like な Web API を提供している。 この Web API を通してモデルの機能を利用できる。

たとえば completions の API を curl(1) で叩いてみよう。 API はループバックアドレスの 11434 ポートで提供されている。

$ curl -s http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma3:1b",
  "messages": [
    { "role": "user", "content": "ご機嫌いかがですか?" }
  ]
}' | jq .

すると JSON で結果が得られる。

$ curl -s http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma3:1b",
  "messages": [
    { "role": "user", "content": "ご機嫌いかがですか?" }
  ]
}' | jq .
{
  "id": "chatcmpl-578",
  "object": "chat.completion",
  "created": 1749812263,
  "model": "gemma3:1b",
  "system_fingerprint": "fp_ollama",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "元気です、ありがとう! 😊 あなたはいかがですか? \n\n何かお手伝いできることはありますか?\n"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 24,
    "total_tokens": 40
  }
}

すべての API のドキュメントは以下の Web ページで確認できる。

github.com

そして、API を通して様々なツールと連携できる。 連携できるツールは以下のページにまとめられている。

github.com

必要に応じて、先ほど叩いた API のエンドポイントをツールに設定することで連携が可能になる。

不要になったモデルを削除する

LLM のファイルはサイズが大きいので、不要になったときは消したくなることもある。

そんなときは ollama rm コマンドを使って削除できる。

$ ollama rm gemma3:1b 
deleted 'gemma3:1b'

いじょう。