【Ollama】RESTful APIストリーミング応答が失敗する?実装方法とよくあるエラー解決策

問題の概要:Ollama APIストリーミング応答の実装エラー

Ollamaはローカル環境で大規模言語モデル(LLM)を実行するための優れたツールですが、そのRESTful APIを使用してストリーミング応答を実装しようとすると、開発者、特にAI開発初心者から中級者によく以下のような問題が発生します。

典型的なエラーシナリオとしては:

  • ストリーミングリクエストを送信しても、応答が一括で返され、単語や文節ごとの逐次表示が実現できない
  • Connection closed unexpectedly」や「stream interrupted」といったエラーメッセージが表示される
  • クライアント側(Python requests, JavaScript fetch, cURLなど)でストリーミングデータを正しくパースできない
  • 長時間の応答生成中にタイムアウトが発生する

例えば、Pythonのrequestsライブラリでシンプルな実装を試みた場合:

import requests
import json

response = requests.post('http://localhost:11434/api/generate',
                         json={
                             "model": "llama3.2",
                             "prompt": "AIの未来について教えてください。",
                             "stream": True  # ストリーミングを有効化
                         },
                         stream=True)

for line in response.iter_lines():
    if line:
        print(line.decode('utf-8'))

このコードを実行しても、期待通りに単語が逐次表示されず、すべての生成が完了してからまとめて出力される、あるいは以下のようなエラーが発生することがあります:

Error: stream interrupted
または
requests.exceptions.ChunkedEncodingError: ("Connection broken: IncompleteRead(...)", IncompleteRead(...))

原因の解説:なぜストリーミングが失敗するのか?

1. クライアント側のストリーミング処理の不備

最も一般的な原因は、クライアント側でHTTPストリーミング応答を正しく処理できていないことです。Ollama APIはstream: trueパラメータが設定されると、Server-Sent Events (SSE) 形式ではなく、改行区切りのJSONストリームを返します。各チャンクは完全なJSONオブジェクトであり、"response"フィールドに生成されたテキストの一部が含まれます。クライアントがこの形式を正しく解析・処理しないと、ストリーミングは機能しません。

2. ネットワークバッファリングとタイムアウト設定

HTTPクライアントライブラリやオペレーティングシステムのネットワークスタックは、効率化のためにデータをバッファリングすることがあります。これにより、小さなチャンクが即座にアプリケーションに渡されず、ストリーミングの「リアルタイム感」が損なわれます。また、長時間の生成処理では、デフォルトのタイムアウト設定が短すぎて接続が切断される可能性があります。

3. Ollamaサーバー側の設定やモデル問題

使用しているモデルによっては、ストリーミング生成のサポートが完全でない場合があります。また、Ollamaサーバーのメモリ不足や、同時実行リクエストの競合もストリーミングを不安定にさせる要因となります。

解決方法:ステップバイステップでの実装

ステップ1: Ollamaサーバーの起動確認

まず、Ollamaサーバーが正しく起動していることを確認します。ターミナルで以下のコマンドを実行:

# Ollamaサービスの状態確認(システムによってコマンドが異なります)
ollama serve
# 別のターミナルでサーバーが応答するかテスト
curl http://localhost:11434/api/tags

正常に起動していれば、インストール済みのモデルリストがJSON形式で返されます。

ステップ2: Pythonでの正しいストリーミング実装

以下は、requestsライブラリを使用した完全なストリーミングクライアントの実装例です:

import requests
import json

def stream_ollama_response(prompt, model="llama3.2", host="localhost", port=11434):
    """
    Ollama APIを使用してストリーミング応答を取得する関数
    """
    url = f"http://{host}:{port}/api/generate"
    
    payload = {
        "model": model,
        "prompt": prompt,
        "stream": True,  # ストリーミングを有効化
        "options": {
            "temperature": 0.7,
            "num_predict": 512  # 生成する最大トークン数
        }
    }
    
    headers = {
        "Content-Type": "application/json",
    }
    
    try:
        # stream=Trueを設定してストリーミングレスポンスを取得
        response = requests.post(url, 
                                 json=payload, 
                                 headers=headers, 
                                 stream=True,
                                 timeout=60)  # タイムアウトを長めに設定
        
        response.raise_for_status()  # HTTPエラーを例外として発生
        
        full_response = ""
        
        # ストリーミングデータの処理
        for line in response.iter_lines():
            if line:
                # 各行はJSONオブジェクトとしてデコード
                decoded_line = line.decode('utf-8')
                try:
                    data = json.loads(decoded_line)
                    
                    # 応答テキストの部分を取得
                    if "response" in data:
                        chunk = data["response"]
                        print(chunk, end="", flush=True)  # 逐次表示
                        full_response += chunk
                    
                    # 最終的な統計情報(オプション)
                    if data.get("done", False):
                        print(f"nn[生成完了] 総トークン数: {data.get('total_duration', 0)/1e9:.2f}秒")
                        
                except json.JSONDecodeError as e:
                    print(f"nJSONデコードエラー: {e}")
                    print(f"問題の行: {decoded_line}")
                    
        return full_response
        
    except requests.exceptions.RequestException as e:
        print(f"リクエストエラー: {e}")
        return None

# 使用例
if __name__ == "__main__":
    prompt = "Pythonで機械学習モデルを訓練する基本的なステップを説明してください。"
    print("質問:", prompt)
    print("n回答:", end=" ")
    
    result = stream_ollama_response(prompt, model="llama3.2")

ステップ3: JavaScript/Fetch APIでの実装

Webアプリケーションで使用する場合のJavaScript実装例:

async function streamOllamaResponse(prompt, model = 'llama3.2') {
    const response = await fetch('http://localhost:11434/api/generate', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            model: model,
            prompt: prompt,
            stream: true
        })
    });
    
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let fullResponse = '';
    
    try {
        while (true) {
            const { done, value } = await reader.read();
            
            if (done) {
                console.log('ストリーミング完了');
                break;
            }
            
            // 受信したチャンクをテキストにデコード
            const chunk = decoder.decode(value);
            
            // 改行で分割(Ollamaは各行が独立したJSON)
            const lines = chunk.split('n').filter(line => line.trim() !== '');
            
            for (const line of lines) {
                try {
                    const data = JSON.parse(line);
                    
                    if (data.response) {
                        // UIに逐次表示
                        document.getElementById('response-output').textContent += data.response;
                        fullResponse += data.response;
                    }
                    
                    if (data.done) {
                        console.log('生成完了:', data);
                    }
                } catch (e) {
                    console.error('JSONパースエラー:', e, '行:', line);
                }
            }
        }
    } catch (error) {
        console.error('ストリーミングエラー:', error);
    }
    
    return fullResponse;
}

// 使用例
document.getElementById('ask-button').addEventListener('click', async () => {
    const prompt = document.getElementById('question-input').value;
    document.getElementById('response-output').textContent = '';
    await streamOllamaResponse(prompt);
});

ステップ4: cURLでの直接テスト

クライアントコードの問題を切り分けるために、まずcURLで直接テストします:

# 基本的なストリーミングリクエスト
curl -X POST http://localhost:11434/api/generate 
  -H "Content-Type: application/json" 
  -d '{
    "model": "llama3.2",
    "prompt": "こんにちは、調子はどうですか?",
    "stream": true
  }'

# 詳細なデバッグ情報を表示(-Nオプションでバッファリングを無効化)
curl -N -X POST http://localhost:11434/api/generate 
  -H "Content-Type: application/json" 
  -d '{"model": "llama3.2", "prompt": "テスト", "stream": true}'

コード例・コマンド例:高度な設定とトラブルシューティング

エラー処理を強化した実装

import requests
import json
import time

class OllamaStreamClient:
    def __init__(self, host="localhost", port=11434):
        self.base_url = f"http://{host}:{port}"
        self.session = requests.Session()
        
    def generate_stream(self, prompt, model="llama3.2", **kwargs):
        """強化されたストリーミング生成メソッド"""
        url = f"{self.base_url}/api/generate"
        
        payload = {
            "model": model,
            "prompt": prompt,
            "stream": True,
            "options": kwargs.get("options", {})
        }
        
        # リトライ設定
        max_retries = 3
        retry_delay = 2
        
        for attempt in range(max_retries):
            try:
                response = self.session.post(
                    url,
                    json=payload,
                    stream=True,
                    timeout=kwargs.get("timeout", 300)  # 5分タイムアウト
                )
                
                response.raise_for_status()
                
                for line in response.iter_lines(decode_unicode=True):
                    if line:
                        try:
                            data = json.loads(line)
                            yield data  # ジェネレータとしてデータを返す
                        except json.JSONDecodeError:
                            continue  # 不完全なJSONはスキップ
                
                break  # 成功したらループを抜ける
                
            except (requests.exceptions.ConnectionError, 
                    requests.exceptions.Timeout) as e:
                if attempt < max_retries - 1:
                    print(f"接続エラー、{retry_delay}秒後にリトライ... ({attempt+1}/{max_retries})")
                    time.sleep(retry_delay)
                    retry_delay *= 2  # 指数バックオフ
                else:
                    raise Exception(f"最大リトライ回数に達しました: {e}")
                    
    def close(self):
        self.session.close()

# 使用例
client = OllamaStreamClient()

try:
    for chunk in client.generate_stream("AIの倫理について教えてください。", model="llama3.2"):
        if "response" in chunk:
            print(chunk["response"], end="", flush=True)
        if chunk.get("done"):
            print(f"nn統計: {chunk}")
finally:
    client.close()

よくあるエラーと解決策

# エラー1: "model not found"
# 解決策: モデルがインストールされているか確認
ollama list
# モデルをプル(ダウンロード)
ollama pull llama3.2

# エラー2: "context length exceeded"
# 解決策: コンテキストウィンドウを調整
payload = {
    "model": "llama3.2",
    "prompt": prompt,
    "stream": True,
    "options": {
        "num_ctx": 4096  # コンテキスト長を拡大
    }
}

# エラー3: ストリーミングが遅い/途切れる
# 解決策: ネットワーク設定とモデルパラメータ調整
payload = {
    "model": "llama3.2",
    "prompt": prompt,
    "stream": True,
    "options": {
        "num_predict": 100,  # 生成数を制限
        "temperature": 0.8,
        "top_p": 0.9,
        "repeat_penalty": 1.1
    }
}

まとめ・補足情報

OllamaのRESTful APIを使用したストリーミング応答の実装で重要なポイントは以下の通りです:

  1. クライアント側の適切なストリーミング処理:Ollamaは改行区切りのJSONストリームを返すため、クライアントは行ごとにJSONをパースする必要があります。
  2. ネットワークバッファリングの管理stream=Trueパラメータ(requestsの場合)や適切なタイムアウト設定が必須です。
  3. エラー処理とリトライメカニズム:長時間の生成処理ではネットワークの不安定性を考慮した設計が必要です。
  4. モデルとパラメータの適切な選択:使用するモデルと生成パラメータはストリーミングの安定性に影響します。

さらに高度なユースケースとして、複数のストリーミング接続を同時に管理したり、プログレスバーを実装したり、生成途中でキャンセルする機能を追加することも可能です。OllamaのストリーミングAPIを正しく実装することで、対話型AIアプリケーションのユーザー体験を大幅に向上させることができます。

最後に、Ollamaの公式ドキュメントやGitHubリポジトリのIssueも貴重な情報源です。特定のモデルやバージョンでの問題は、コミュニティですでに解決策が議論されていることが多いため、問題に遭遇した場合は積極的に情報を探してみてください。

この記事は役に立ちましたか?