「messages配列を自分で組み立てようとしたが、どう管理すればいいか分からず止まってしまった」
そんな状況に置かれたエンジニアは多いはずです。この記事では、OllamaのChat APIを使ったマルチターン会話の実装方法を、Pythonコードを中心に解説します。
LangChainなどのフレームワークを使わずに、messages配列の構造・会話履歴の管理・コンテキスト上限への対処・ストリーミング応答・エラー処理の実装まで、本番で使える形に仕上げます。
Ubuntu Server上にOllamaがインストール済みの環境を前提とします。
この記事のポイント
・Ollamaの/api/chatはmessages配列で会話全履歴を毎回送信する設計
・Pythonでマルチターン会話を実装するにはrequestsライブラリだけで十分
・deque(maxlen=N)でターン数を制限し、コンテキスト上限を超えないよう管理する
・タイムアウト・リトライ・ストリーミングの実装で本番品質に仕上げる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
OllamaのチャットAPIとシングルターンの違い
Ollamaには2種類のエンドポイントがあります。/api/generate(シングルターン)と/api/chat(マルチターン)です。シングルターンは1回の問いに1回の答えを返すだけで、前の会話を引き継ぎません。 チャットAPIは
messages配列を渡すことで、過去のやり取りをモデルに読み込ませて続きを生成します。配列に含まれるのは
system(挙動の指示)・user(利用者の発言)・assistant(モデルの回答)の3種類のroleです。Ollama側でセッションを保持する仕組みはなく、会話を続けるには毎回すべての履歴を送信する必要があります。 これは設計の核心です。「前の会話を覚えさせたい」なら、前の会話全体をリクエストに含めます。
何も設定しなければ毎回新しい会話として扱われます。
まずcurlで動作を確認します。
# /api/chat へのリクエスト例(会話履歴を2ターン含む) $ curl -s http://localhost:11434/api/chat \ -H "Content-Type: application/json" \ -d '{ "model": "llama3.3:8b-instruct-q4_K_M", "messages": [ {"role": "system", "content": "あなたはLinuxの専門家です。"}, {"role": "user", "content": "sudoとは何ですか?"}, {"role": "assistant", "content": "sudoは一般ユーザーが特定のコマンドをroot権限で実行する仕組みです。"}, {"role": "user", "content": "sudoersファイルはどこにありますか?"} ], "stream": false }' | python3 -c "import sys,json; print(json.load(sys.stdin)['message']['content'])" /etc/sudoers です。visudo コマンドで安全に編集します。
messages配列に前のuser発言とassistant発言を入れているため、モデルはsudo関連の文脈を持ったまま回答します。
Ubuntu ServerでローカルLLMを構築する方法でサーバーを構築済みであれば、このコマンドをそのまま試せます。 roleは会話内で必ず
userとassistantが交互に並ぶ順序にします。systemは会話の先頭に1回だけ配置します。順序が崩れるとモデルが誤った文脈で回答するため、追加の際は順序を意識します。
Pythonで基本的なマルチターン会話を実装する
最小構成のPythonスクリプトから始めます。ライブラリはrequestsだけです。Ubuntu ServerにPython 3.8以上とpipが入っていれば追加インストールは不要なケースがほとんどです。
まずrequestsのインストール状況を確認します。 ### 1. requestsのインストール確認
# python3 と requests のバージョンを確認する $ python3 --version Python 3.11.9 $ pip3 show requests 2>/dev/null | grep Version || pip3 install requests Version: 2.31.0
chat.pyとして保存します。
#!/usr/bin/env python3 import requests OLLAMA_URL = "http://localhost:11434/api/chat" MODEL = "llama3.3:8b-instruct-q4_K_M" SYSTEM_PROMPT = "あなたはLinuxサーバー管理の専門家です。正確で実用的な回答をしてください。" def chat(history: list, user_message: str) -> str: history.append({"role": "user", "content": user_message}) payload = { "model": MODEL, "messages": history, "stream": False, } response = requests.post(OLLAMA_URL, json=payload, timeout=120) response.raise_for_status() assistant_message = response.json()["message"]["content"] history.append({"role": "assistant", "content": assistant_message}) return assistant_message def main(): history = [{"role": "system", "content": SYSTEM_PROMPT}] print("チャットを開始します(終了は Ctrl+C)") while True: user_input = input("You: ").strip() if not user_input: continue reply = chat(history, user_input) print(f"AI: {reply}\n") if __name__ == "__main__": main()
$ python3 chat.py チャットを開始します(終了は Ctrl+C) You: cronとは何ですか? AI: cronはLinuxで定期的なタスクを自動実行するスケジューラです... You: どこに設定ファイルがありますか? AI: cronの設定ファイルは以下の場所にあります。/etc/crontab...
historyリストにuserとassistantの発言を交互に追加しているのが実装の要点です。historyに前回の全会話が入っているため、モデルはcron関連の文脈を持ったまま2問目に答えます。
ただしこの実装では、会話を重ねるほどhistoryリストが無制限に大きくなります。コンテキストが増えると推論が遅くなり、やがてコンテキスト上限を超えます。
ターン数を制限する設計が次のステップです。
会話履歴を管理するクラスの設計
ターン数を上限付きで管理するには、collections.dequeのmaxlenが便利です。deque(maxlen=N)は、N+1件目が入った時点で最古の要素を自動的に削除します。これをクラスに組み込むと、履歴の増加を制御できます。 ### 1. ターン数制限付きのチャットクラス
from collections import deque import requests class OllamaChat: def __init__(self, model: str, system_prompt: str, max_turns: int = 10): self.model = model self.system_prompt = system_prompt # (user発言, assistant発言) の組を max_turns 件保持する self._turns = deque(maxlen=max_turns) def _build_messages(self) -> list: messages = [{"role": "system", "content": self.system_prompt}] for user_msg, assistant_msg in self._turns: messages.append({"role": "user", "content": user_msg}) messages.append({"role": "assistant", "content": assistant_msg}) return messages def ask(self, user_message: str) -> str: messages = self._build_messages() messages.append({"role": "user", "content": user_message}) payload = { "model": self.model, "messages": messages, "stream": False, "options": {"num_ctx": 4096}, } resp = requests.post( "http://localhost:11434/api/chat", json=payload, timeout=120, ) resp.raise_for_status() assistant_reply = resp.json()["message"]["content"] self._turns.append((user_message, assistant_reply)) return assistant_reply
$ python3 -c " from ollama_chat import OllamaChat c = OllamaChat( model='llama3.3:8b-instruct-q4_K_M', system_prompt='Linuxの専門家です。', max_turns=5, ) print(c.ask('SELinuxとは何ですか?')) print(c.ask('Enforcingモードに設定するコマンドは?')) " SELinuxは強制アクセス制御(MAC)を提供するLinuxのセキュリティ機構です... setenforce 1 で Enforcing モードに切り替えます...
max_turns=5で最大5ターン分の履歴を保持します。5ターンを超えた段階で、
dequeが自動的に古いターンから削除します。_build_messages()でsystemプロンプトを毎回先頭に追加するため、会話が削れても指示は維持されます。
max_turnsの適切な値はモデルと用途によって変わります。技術的なQ&Aなら5~10ターン、長い説明が必要な手順解説なら3~5ターンが目安です。
次のセクションでコンテキスト使用量を計測しながら調整します。
コンテキスト上限とnum_ctxの設定
num_ctxはモデルが一度に読み込めるトークン数の上限です。Ollamaのデフォルト値は2048前後で、長い会話ではすぐに上限に達します。
optionsフィールドで明示的に設定することが重要です。
### 1. 現在のnum_ctx設定を確認する
# 現在のモデル設定を確認する $ curl -s http://localhost:11434/api/show \ -H "Content-Type: application/json" \ -d '{"name": "llama3.3:8b-instruct-q4_K_M"}' \ | python3 -c " import sys, json d = json.load(sys.stdin) params = d.get('parameters', '') found = False for line in params.splitlines(): if 'num_ctx' in line: print('num_ctx設定:', line) found = True break if not found: print('num_ctx: デフォルト(2048)') " num_ctx: デフォルト(2048)
prompt_eval_countでプロンプトに使ったトークン数を確認できます。
$ python3 -c " import requests, json payload = { 'model': 'llama3.3:8b-instruct-q4_K_M', 'messages': [ {'role': 'system', 'content': 'Linuxの専門家です。'}, {'role': 'user', 'content': 'ファイルのパーミッションについて詳しく教えてください。'} ], 'stream': False, 'options': {'num_ctx': 4096} } resp = requests.post('http://localhost:11434/api/chat', json=payload, timeout=120) d = resp.json() print('プロンプトトークン数:', d.get('prompt_eval_count', 0)) print('応答トークン数:', d.get('eval_count', 0)) " プロンプトトークン数: 42 応答トークン数: 318
prompt_eval_countがnum_ctxの8割を超えたら、ターン数を削減するタイミングです。num_ctxを引き上げる場合はoptions: {"num_ctx": 8192}のように指定します。ただし
num_ctxを大きくするほどVRAMを消費するため、利用可能なVRAMに合わせて調整します。
8Bクラスのモデルなら8192まで設定できる環境が多いです。70Bモデルは量子化タグ
-q4_K_Mを使わないと大きなnum_ctxを確保しにくくなります。モデルの選定についてはローカルLLMのモデルを比較する方法を参照してください。Llama3.3・Mistral・Gemma 3など、用途に合ったモデルを選ぶことでコンテキスト効率も変わります。
ストリーミング応答で体感速度を上げる
stream: falseでは全文生成が終わるまで応答が返りません。長い回答では待ち時間が30秒を超えることがあります。
stream: trueにすると、生成されるたびにトークンを逐次受信できます。
### 1. ストリーミングの実装
import requests import json def ask_stream(messages: list, model: str) -> str: payload = { "model": model, "messages": messages, "stream": True, "options": {"num_ctx": 4096}, } full_response = "" with requests.post( "http://localhost:11434/api/chat", json=payload, stream=True, timeout=180, ) as resp: resp.raise_for_status() for line in resp.iter_lines(): if not line: continue chunk = json.loads(line) token = chunk.get("message", {}).get("content", "") print(token, end="", flush=True) full_response += token if chunk.get("done"): break print() return full_response
$ python3 -c " import sys sys.path.insert(0, '.') from ask_stream import ask_stream messages = [ {'role': 'system', 'content': 'Linuxの専門家です。'}, {'role': 'user', 'content': 'iptablesの基本的な使い方を教えてください。'} ] result = ask_stream(messages, 'llama3.3:8b-instruct-q4_K_M') print('--- 合計文字数:', len(result)) " iptablesはLinuxのパケットフィルタリングツールです...(リアルタイム表示) --- 合計文字数: 412
resp.iter_lines()で1行ずつ受信し、chunk["message"]["content"]にトークンが入ります。chunk["done"]がTrueになった時点で生成完了です。戻り値に全文を返すことで、マルチターン実装の
historyへの追加にも使えます。
stream: trueとstream: falseの実装を使い分けるには、askメソッドにstream引数を追加します。インタラクティブなターミナルではストリーミング、バッチ処理では非ストリーミングと切り替えると、どちらの用途にも対応できます。
タイムアウトとエラー処理で本番品質に仕上げる
本番環境ではネットワーク断・モデルの過負荷・サーバー再起動が起こります。requestsのtimeout引数だけでは不十分な場面もあり、リトライと例外処理の設計が必要です。社内サーバーで複数人が利用する場面では特に重要です。 ### 1. リトライ付きの実装
import time import requests def ask_with_retry( messages: list, model: str, max_retries: int = 3, retry_wait: float = 2.0, ) -> str: last_error = None for attempt in range(1, max_retries + 1): try: payload = { "model": model, "messages": messages, "stream": False, "options": {"num_ctx": 4096}, } resp = requests.post( "http://localhost:11434/api/chat", json=payload, timeout=120, ) resp.raise_for_status() return resp.json()["message"]["content"] except requests.exceptions.Timeout: last_error = f"タイムアウト(試行{attempt}回目)" except requests.exceptions.ConnectionError: last_error = f"接続エラー(試行{attempt}回目)" except requests.exceptions.HTTPError as e: last_error = f"HTTPエラー {e.response.status_code}(試行{attempt}回目)" if e.response.status_code < 500: break # 4xx はリトライしない if attempt < max_retries: time.sleep(retry_wait * attempt) raise RuntimeError(f"応答取得に失敗しました: {last_error}")
GET /api/tagsへのリクエストが使えます。
$ python3 -c " import requests try: resp = requests.get('http://localhost:11434/api/tags', timeout=5) models = [m['name'] for m in resp.json()['models']] print('稼働中。利用可能モデル:', models) except requests.exceptions.ConnectionError: print('Ollamaサーバーに接続できません。systemctl status ollama で確認してください。') " 稼働中。利用可能モデル: ['llama3.3:8b-instruct-q4_K_M']
5xxエラー(サーバー側の問題)のみリトライすることで、不正なリクエストを繰り返さない設計です。
retry_wait * attemptで待機時間を指数的に伸ばし、負荷の高い状況での再試行を分散できます。
情シス担当がOllamaを社内に展開する場面では、このような堅牢な実装が不可欠です。社内導入の背景については社内でChatGPTが使えないときの代替手段をあわせて参照してください。
よくあるトラブルと対処
**前の会話を覚えていない**messages配列に過去のターンが入っていないのが原因です。historyリストにuserとassistantの発言を交互に追加しているか確認します。
# messages配列の中身を確認するデバッグコマンド $ python3 -c " import json messages = [ {'role': 'system', 'content': 'Linuxの専門家'}, {'role': 'user', 'content': '質問1'}, {'role': 'assistant', 'content': '回答1'}, {'role': 'user', 'content': '質問2'}, ] print('メッセージ数:', len(messages)) for m in messages: print(m['role'], ':', m['content'][:30]) " メッセージ数: 4 system : Linuxの専門家 user : 質問1 assistant : 回答1 user : 質問2
num_ctxが不足してコンテキストがあふれた可能性があります。options: {"num_ctx": 8192}に引き上げ、それでも止まる場合はmax_turnsを小さくします。prompt_eval_countで使用トークン数を計測してから調整すると原因を特定しやすくなります。
**モデルが英語で回答する**systemプロンプトに「日本語で回答してください」を明示します。
Llama3.3・Mistral・Gemma 3はいずれも日本語対応ですが、プロンプトで指定する方が安定します。 **HTTPError 404が返る**
エンドポイントを確認します。チャットAPIは
/api/chatで、/api/generateは別のエンドポイントです。接続先のポート番号も確認します。デフォルトは11434ですが、環境によって変わる場合があります。
# Ollamaが正常に稼働しているか確認する $ curl -s http://localhost:11434/api/tags | python3 -c " import sys, json d = json.load(sys.stdin) for m in d.get('models', []): print(m['name']) " llama3.3:8b-instruct-q4_K_M
典型的なケースでは、複数モデルが同時にVRAMをロードしている状態です。
ollama psで現在ロード中のモデルと使用VRAMを確認し、不要なモデルをollama stop モデル名で解放してから再試行します。VRAMの割り当てが不安定な場合は、
OLLAMA_MAX_LOADED_MODELS=1を/etc/systemd/system/ollama.service.d/override.confに設定すると同時ロード数を制限できます。
まとめ
OllamaのチャットAPIでマルチターン会話を実装するポイントをまとめます。| 実装内容 | 要点 |
|---|---|
| エンドポイント | /api/chatにmessages配列を送信する |
| 履歴管理 | user/assistantを交互に追加。deque(maxlen=N)でターン数を制限する |
| num_ctx設定 | options.num_ctxを明示する。prompt_eval_countで使用量を確認する |
| ストリーミング | stream: trueとiter_lines()で逐次表示する |
| エラー処理 | Timeout/ConnectionErrorをキャッチしてリトライ。4xxは即断する |
| サーバー確認 | 起動時にGET /api/tagsで200が返るか確認する |
LangChainやLlamaIndexに移行する際も、内部の仕組みを把握しているとデバッグの速さが違います。
20年以上のサーバー管理経験から見ても、ブラックボックスを減らして動作を把握することが安定運用の基本です。
ローカルLLMのPython連携を2日間のハンズオンで体験する
この記事で解説したマルチターン会話の実装は、実際に手を動かしながら試すことで理解が深まります。実機GPU環境で手を動かしながら習得したい方向けに、「ローカルAIマスターセミナー」を開催しています。
少人数(最大8名)ZOOMハンズオン形式で実施しています。
・Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド
・社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢
・ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:OllamaのAPIをOpenTelemetryで計装する方法|PythonアプリのリクエストをJaegerでトレースしてレスポンス遅延の原因を特定する手順
- この記事の属するカテゴリ:ローカルLLMへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます