前回までの記事で、MacBook Air M4にOllamaを導入し、Open WebUIで快適に使う環境を整え、Gemma 3を3サイズで比較しました。本記事ではOllamaのAPI活用編として、PythonからOllamaのAPIを叩いてオリジナルのチャットCLIを作成する手順を解説します。最終的にはストリーミング表示、モデル切り替え、persona(system prompt)切り替え、会話保存などを備えた実用的なCLIまで仕上げます。
OllamaのAPIについて
Ollamaは起動と同時に localhost:11434 でAPIサーバーを自動的に立ち上げます。Open WebUIもこのAPIを利用してOllamaと通信しています。自作のアプリやスクリプトからも同じAPIを叩けるため、ローカルLLMを自分のツールやワークフローに組み込めます。
主なエンドポイントは以下のとおりです。
| エンドポイント | 用途 |
|---|---|
POST /api/generate | 単発のテキスト生成(会話履歴なし) |
POST /api/chat | チャット形式の対話(messages配列で履歴を保持) |
POST /api/embeddings | テキストのベクトル化(RAG等で使用) |
GET /api/tags | インストール済みモデル一覧 |
GET /api/ps | 現在ロード中のモデル一覧 |
本記事では対話アプリを作るので /api/chat を中心に扱います。
まずはcurlでAPIの動作を確認する
実装に入る前に、curlで直接APIを叩いて感触を確かめます。
curl http://localhost:11434/api/generate -d '{
"model": "gemma3:12b",
"prompt": "日本の首都はどこですか?",
"stream": false
}'
"stream": false を指定すると、全部生成し終わってからJSONで一括返却されます。レスポンスは以下のような形式です。
{
"model": "gemma3:12b",
"created_at": "2026-05-20T07:33:25.302446Z",
"response": "日本の首都は東京都です。\n",
"done": true,
"done_reason": "stop",
"total_duration": 7306910500,
"load_duration": 6158475417,
"prompt_eval_count": 15,
"prompt_eval_duration": 354117334,
"eval_count": 8
}
response フィールドに生成テキスト、その他にもメタ情報が含まれます。total_duration などの時間情報はナノ秒単位です。
なお、初回呼び出し時は load_duration(モデルをメモリに読み込む時間)が大きな割合を占めますが、2回目以降は既にロード済みのモデルが再利用されるため、load_duration はほぼ0になります。
Pythonの作業環境を準備する
プロジェクト用のディレクトリを作成し、仮想環境をセットアップします。
cd ~/works
mkdir ollama-chat-cli
cd ollama-chat-cli
# 仮想環境の作成と有効化
python3 -m venv .venv
source .venv/bin/activate
プロンプトの先頭に (.venv) が表示されれば仮想環境が有効化されています。
ステップ1: requestsで最小実装
まずはOllamaの公式ライブラリを使わず、requests ライブラリだけでAPIを直接叩く実装から始めます。仕組みを理解するための足場づくりです。
pip install requests
chat.py を作成します。
import requests
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "gemma3:12b"
def chat(messages):
"""Ollamaに会話履歴を送って応答を取得する"""
response = requests.post(
OLLAMA_URL,
json={
"model": MODEL,
"messages": messages,
"stream": False,
},
)
response.raise_for_status()
return response.json()["message"]["content"]
def main():
print(f"=== Ollama Chat CLI ({MODEL}) ===")
print("終了するには 'exit' または Ctrl+C を入力してください")
print()
messages = []
while True:
try:
user_input = input("あなた> ").strip()
except (KeyboardInterrupt, EOFError):
print("\n終了します")
break
if not user_input:
continue
if user_input.lower() in {"exit", "quit", "bye"}:
print("終了します")
break
messages.append({"role": "user", "content": user_input})
print("AI> ", end="", flush=True)
try:
reply = chat(messages)
except requests.RequestException as e:
print(f"\nエラー: {e}")
messages.pop()
continue
print(reply)
print()
messages.append({"role": "assistant", "content": reply})
if __name__ == "__main__":
main()
実行します。
python chat.py
実行例は以下のようになります。
=== Ollama Chat CLI (gemma3:12b) ===
終了するには 'exit' または Ctrl+C を入力してください
あなた> 私の名前は深尾です
AI> 深尾さん、こんにちは!深尾さんというお名前を伺えて嬉しいです。何かお手伝いできることはありますか?
あなた> 私の名前を覚えていますか?
AI> はい、深尾さんのお名前は覚えています。先ほど教えていただきました。
messages リストに会話履歴を溜めて毎回全部送ることで、前の発言を踏まえた応答が成立します。Ollamaのチャット用APIはステートレスなので、履歴管理はクライアント側の責務です。
ステップ2: 公式ライブラリ+全機能を盛り込んだ完成版
仕組みが分かったので、Ollama公式のPythonライブラリを使って完成版を作ります。公式ライブラリを使うと、ストリーミング処理やエラーハンドリングが簡潔に書けます。
pip install ollama
完成版の chat.py は以下のとおりです。
"""
Ollama Chat CLI
- ストリーミング表示
- モデル切り替え(起動時引数)
- system promptで性格付け(プリセット切り替え)
- 対話中コマンド(/help, /reset, /system, /save)
"""
import argparse
from datetime import datetime
from pathlib import Path
import ollama
# system promptのプリセット
PERSONAS = {
"default": "あなたは親切で有能なアシスタントです。日本語で丁寧に答えてください。",
"kansai": "あなたは関西弁で話すフレンドリーなアシスタントです。語尾は「〜やで」「〜やん」などを自然に使ってください。",
"expert": "あなたは技術分野の専門家です。正確で簡潔に、根拠を示しながら答えてください。不確かなことは「分からない」と明言してください。",
"concise": "あなたは要点だけを伝えるアシスタントです。前置きや謝辞は省き、必要最小限の言葉で答えてください。",
"teacher": "あなたは小学生にも分かるように説明するのが得意な先生です。難しい言葉は避け、たとえ話を交えて答えてください。",
}
def stream_chat(model: str, messages: list) -> str:
"""ストリーミングでOllamaに送信し、応答を逐次表示しながら全文を返す"""
full_response = ""
stream = ollama.chat(model=model, messages=messages, stream=True)
for chunk in stream:
content = chunk["message"]["content"]
print(content, end="", flush=True)
full_response += content
print()
return full_response
def print_help():
print()
print("=== コマンド一覧 ===")
print("/help このヘルプを表示")
print("/reset 会話履歴をリセット(system promptは維持)")
print("/system 利用可能なpersonaを表示")
print("/system NAME personaを切り替え(履歴もリセット)")
print("/save 現在の会話をMarkdownファイルに保存")
print("/exit 終了(exit, quit, bye でも可)")
print()
def save_conversation(messages: list, model: str, persona: str):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"chat_{timestamp}.md"
path = Path(filename)
lines = [
f"# Ollama Chat Log",
f"",
f"- 日時: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"- モデル: `{model}`",
f"- Persona: `{persona}`",
f"",
"---",
"",
]
for msg in messages:
role = msg["role"]
content = msg["content"]
if role == "system":
lines.append(f"## System Prompt\n\n> {content}\n")
elif role == "user":
lines.append(f"## あなた\n\n{content}\n")
elif role == "assistant":
lines.append(f"## AI\n\n{content}\n")
path.write_text("\n".join(lines), encoding="utf-8")
print(f"保存しました: {path.absolute()}")
def build_initial_messages(persona: str) -> list:
return [{"role": "system", "content": PERSONAS[persona]}]
def main():
parser = argparse.ArgumentParser(description="Ollama Chat CLI")
parser.add_argument(
"-m", "--model",
default="gemma3:12b",
help="使用するモデル名(デフォルト: gemma3:12b)",
)
parser.add_argument(
"-p", "--persona",
default="default",
choices=PERSONAS.keys(),
help=f"persona({', '.join(PERSONAS.keys())})",
)
args = parser.parse_args()
model = args.model
persona = args.persona
messages = build_initial_messages(persona)
print(f"=== Ollama Chat CLI ===")
print(f"モデル: {model}")
print(f"Persona: {persona}")
print("コマンド一覧は /help、終了は /exit")
print()
while True:
try:
user_input = input("あなた> ").strip()
except (KeyboardInterrupt, EOFError):
print("\n終了します")
break
if not user_input:
continue
if user_input.startswith("/"):
parts = user_input.split(maxsplit=1)
cmd = parts[0].lower()
if cmd in {"/exit", "/quit", "/bye"}:
print("終了します")
break
if cmd == "/help":
print_help()
continue
if cmd == "/reset":
messages = build_initial_messages(persona)
print("会話履歴をリセットしました")
continue
if cmd == "/system":
if len(parts) < 2:
print(f"利用可能なpersona: {', '.join(PERSONAS.keys())}")
print(f"現在: {persona}")
else:
new_persona = parts[1].strip()
if new_persona not in PERSONAS:
print(f"未知のpersona: {new_persona}")
print(f"利用可能: {', '.join(PERSONAS.keys())}")
else:
persona = new_persona
messages = build_initial_messages(persona)
print(f"personaを '{persona}' に切り替え、履歴をリセットしました")
continue
if cmd == "/save":
if len(messages) <= 1:
print("保存する会話がありません")
else:
save_conversation(messages, model, persona)
continue
print(f"未知のコマンド: {cmd}(/help で一覧表示)")
continue
if user_input.lower() in {"exit", "quit", "bye"}:
print("終了します")
break
messages.append({"role": "user", "content": user_input})
print("AI> ", end="", flush=True)
try:
reply = stream_chat(model, messages)
except ollama.ResponseError as e:
print(f"\nOllamaエラー: {e}")
messages.pop()
continue
except Exception as e:
print(f"\nエラー: {e}")
messages.pop()
continue
messages.append({"role": "assistant", "content": reply})
if __name__ == "__main__":
main()
完成版の使い方
起動時の引数でモデルとpersonaを指定できます。
# デフォルト(gemma3:12b、defaultペルソナ)
python chat.py
# モデル指定
python chat.py -m gemma3:4b
# persona指定(関西弁モード)
python chat.py -p kansai
# 組み合わせ
python chat.py -m gemma3:27b -p expert
# ヘルプ
python chat.py -h
対話中に使えるコマンドは以下のとおりです。
| コマンド | 機能 |
|---|---|
/help | ヘルプを表示 |
/reset | 会話履歴をリセット(system promptは維持) |
/system | 利用可能なpersona一覧を表示 |
/system NAME | personaを切り替え(履歴もリセット) |
/save | 会話をMarkdownファイルに保存 |
/exit | 終了 |
用意したpersona
system promptを切り替えるだけで、同じモデルでも応答の傾向が大きく変わります。今回は5種類のpersonaを用意しました。
- default: 親切で丁寧な標準アシスタント
- kansai: 関西弁で話す
- expert: 技術分野の専門家、根拠を示す
- concise: 要点のみ、最小限の言葉
- teacher: 小学生にも分かるように、たとえ話を交える
PERSONAS 辞書に追加するだけで自由に増やせます。
実際の動作例
関西弁モードで動かしてみた例です。
あなた> /system kansai
personaを 'kansai' に切り替え、履歴をリセットしました
あなた> いつも有り難う!
AI> おおきに!こちらこそいつも感謝やで!何かお手伝いできること、あったら遠慮なく言ってやん!😊
たった1つのsystem promptで、ここまで自然に性格が変わるのがLLMの面白いところです。Gemma 3 12Bは「おおきに」「〜やで」「〜やん」といった関西弁の語尾を自然に使い分けており、絵文字まで添えてくれました。
実装のポイント
messagesの構造(OpenAI互換)
Ollamaのチャット用APIで使う messages は、OpenAIのChat Completion APIとほぼ同じ構造です。
[
{"role": "system", "content": "あなたは親切なアシスタントです"},
{"role": "user", "content": "こんにちは"},
{"role": "assistant", "content": "こんにちは!"},
{"role": "user", "content": "今日はどんな日?"},
]
役割は system / user / assistant の3種類。system はモデルへの指示で、配列の先頭に1つ置くのが一般的です。
ストリーミング処理
ollama.chat() に stream=True を渡すと、トークンごとに分割されたチャンクをイテレータで受け取れます。各チャンクの message.content を逐次出力することで、ChatGPT風の「文字が少しずつ出てくる」表示が実現できます。
stream = ollama.chat(model=model, messages=messages, stream=True)
for chunk in stream:
content = chunk["message"]["content"]
print(content, end="", flush=True)
full_response += content
print(..., end="", flush=True) の flush=True が重要で、これがないとバッファリングされてストリーミングの効果が消えてしまいます。
エラー時の履歴管理
通信失敗時は失敗したユーザーメッセージを履歴から削除しています。
messages.append({"role": "user", "content": user_input})
try:
reply = stream_chat(model, messages)
except Exception as e:
print(f"\nエラー: {e}")
messages.pop() # 失敗時は履歴から消す
continue
これをしないと、エラー後の会話で「対応するassistantメッセージが無いuserメッセージ」が履歴に残り、以降のやりとりが不自然になります。
まとめ
本記事では以下の内容を扱いました。
- OllamaのAPIエンドポイントの概要
- curlで直接APIを叩いて動作確認
- requestsライブラリを使った最小実装
- 公式ライブラリ(ollama)を使った完成版の作成
- ストリーミング表示、モデル切り替え、persona切り替え、会話保存などの実装
- system promptで応答の性格が大きく変わる体験
Ollamaは起動するだけでAPIが立ち上がるため、ローカルLLMを自分のツールに組み込むハードルが非常に低いことが分かりました。Pythonライブラリも整っており、100行強のコードで実用的なチャットCLIが作れます。
ここから先の応用としては、ファイル読み込みからの要約、RAG(検索拡張生成)、構造化出力(JSON返却)、複数モデルの並列実行など、用途に応じてさまざまな展開が可能です。クラウドAPIと違って従量課金もないため、思いついたツールをすぐに試せるのもローカルLLMの強みです。


コメント