【Ollama】RESTful APIでストリーミング応答を実装する方法と「Unexpected token」エラー解決

1. 問題の概要:ストリーミング応答の取得と一般的なエラー

Ollamaはローカル環境で大規模言語モデル(LLM)を実行するための優れたツールですが、そのRESTful APIを使用してストリーミング応答を実装しようとすると、開発者はいくつかの課題に直面します。最も一般的な問題は、ストリーミングレスポンスを適切に処理できず、クライアント側でエラーが発生することです。

具体的には、以下のようなエラーメッセージがブラウザのコンソールやサーバーログに表示されることがあります:

SyntaxError: Unexpected token 'H' in JSON at position 0

または、Python `requests` ライブラリを使用している場合:

json.decoder.JSONDecodeError: Extra data: line 1 column 2 (char 1)

これらのエラーは、ストリーミングAPIから返却されるデータ形式(JSON LinesやServer-Sent Eventsなど)を正しくパースできていないことを示しています。非ストリーミングの通常リクエストは成功するにもかかわらず、`stream` パラメータを `true` に設定すると突然エラーが発生するため、初心者エンジニアを混乱させることがよくあります。

2. 原因の解説:ストリーミングレスポンスのデータ形式

OllamaのAPIでストリーミングを有効にすると、サーバーは単一のJSONオブジェクトを返すのではなく、複数のJSONオブジェクトを連続的に送信します。これが「ストリーミング」の本質です。各JSONオブジェクトは改行(`n`)で区切られ、この形式はJSON Lines(.jsonl)として知られています。

例えば、非ストリーミングリクエストでは以下のような単一のレスポンスが返されます:

{
  "model": "llama3.2",
  "created_at": "2024-01-01T00:00:00.000000Z",
  "response": "こんにちは!今日は良い天気ですね。",
  "done": true
}

一方、ストリーミングを有効にすると、以下のように複数のチャンクが送信されます:

{"model":"llama3.2","created_at":"2024-01-01T00:00:00.000000Z","response":"こ","done":false}
{"model":"llama3.2","created_at":"2024-01-01T00:00:00.100000Z","response":"ん","done":false}
{"model":"llama3.2","created_at":"2024-01-01T00:00:00.200000Z","response":"にちは","done":false}
{"model":"llama3.2","created_at":"2024-01-01T00:00:01.000000Z","response":"!","done":true}

問題は、多くのHTTPクライアントライブラリがデフォルトで単一のJSONオブジェクトのパースを想定しており、このストリーム形式を正しく処理できないことにあります。特に、`response.json()` のようなメソッドを呼び出すと、最初のチャンクのみをパースしようとして、残りのデータでエラーが発生します。

3. 解決方法:ストリーミングレスポンスの適切な処理

ストリーミングレスポンスを正しく処理するには、以下のステップに従います。

ステップ1: 基本的なAPIリクエストの構築

まず、Ollamaサーバーに対して正しいエンドポイントとパラメータでリクエストを送信します。Ollamaはデフォルトで `http://localhost:11434` で実行されています。

ステップ2: ストリーミングパラメータの設定

リクエストボディに `”stream”: true` を追加します。これがストリーミングを有効にするキーパラメータです。

ステップ3: チャンクされたレスポンスの処理

サーバーからのレスポンスをチャンクごとに読み取り、各JSONオブジェクトを個別にパースします。

ステップ4: レスポンスの組み立てと表示

パースした各チャンクから `response` フィールドを抽出し、結合して完全な応答を構築します。`done` フィールドが `true` になるまでこの処理を続けます。

4. コード例・コマンド例

Pythonでの実装例

Pythonの `requests` ライブラリを使用する場合:

import requests
import json

def stream_ollama_response(prompt, model="llama3.2"):
    url = "http://localhost:11434/api/generate"
    
    payload = {
        "model": model,
        "prompt": prompt,
        "stream": True  # ストリーミングを有効化
    }
    
    try:
        response = requests.post(url, json=payload, stream=True)
        
        # ステータスコードの確認
        if response.status_code != 200:
            print(f"エラー: ステータスコード {response.status_code}")
            print(response.text)
            return
        
        full_response = ""
        
        # ストリーミングレスポンスを1行ずつ処理
        for line in response.iter_lines():
            if line:
                # 各行をJSONとしてパース
                try:
                    chunk = json.loads(line.decode('utf-8'))
                    
                    # レスポンステキストを抽出
                    if 'response' in chunk:
                        text_chunk = chunk['response']
                        print(text_chunk, end='', flush=True)
                        full_response += text_chunk
                    
                    # ストリームの終了を確認
                    if chunk.get('done', False):
                        print()  # 改行を追加
                        break
                        
                except json.JSONDecodeError as e:
                    print(f"nJSONパースエラー: {e}")
                    print(f"問題の行: {line}")
        
        return full_response
        
    except requests.exceptions.RequestException as e:
        print(f"リクエストエラー: {e}")
        return None

# 使用例
if __name__ == "__main__":
    prompt = "日本の首都について説明してください。"
    result = stream_ollama_response(prompt)
    if result:
        print(f"nn完全な応答:n{result}")

JavaScript/Node.jsでの実装例

Node.jsでFetch APIを使用する場合:

async function streamOllamaResponse(prompt, model = "llama3.2") {
  const url = "http://localhost:11434/api/generate";
  
  const payload = {
    model: model,
    prompt: prompt,
    stream: true
  };
  
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload)
    });
    
    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let fullResponse = '';
    
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) {
        break;
      }
      
      // 受信したチャンクをテキストにデコード
      const chunkText = decoder.decode(value);
      
      // 改行で分割して各行を処理
      const lines = chunkText.split('n').filter(line => line.trim() !== '');
      
      for (const line of lines) {
        try {
          const chunk = JSON.parse(line);
          
          if (chunk.response) {
            process.stdout.write(chunk.response);
            fullResponse += chunk.response;
          }
          
          if (chunk.done) {
            console.log(); // 改行を追加
            return fullResponse;
          }
        } catch (error) {
          console.error(`JSONパースエラー: ${error}`);
          console.error(`問題の行: ${line}`);
        }
      }
    }
    
    return fullResponse;
    
  } catch (error) {
    console.error(`リクエストエラー: ${error}`);
    return null;
  }
}

// 使用例
(async () => {
  const prompt = "機械学習とは何ですか?";
  const result = await streamOllamaResponse(prompt);
  if (result) {
    console.log(`nn完全な応答:n${result}`);
  }
})();

cURLコマンドでの確認

まず、ストリーミングレスポンスがどのように見えるかを確認するには、cURLコマンドが便利です:

curl http://localhost:11434/api/generate 
  -H "Content-Type: application/json" 
  -d '{
    "model": "llama3.2",
    "prompt": "こんにちは",
    "stream": true
  }'

このコマンドを実行すると、複数のJSONオブジェクトが改行区切りで表示され、ストリーミングレスポンスの形式を直接確認できます。

5. まとめ・補足情報

OllamaのRESTful APIでストリーミング応答を実装する際の鍵は、レスポンスが単一のJSONオブジェクトではなく、改行区切りの複数JSONオブジェクト(JSON Lines形式)であることを理解することです。この形式を正しく処理するためには、HTTPクライアントのストリーミング機能を使用し、受信データを行単位でパースする必要があります。

よくある問題とその解決策

問題1: 「Unexpected token」エラーが発生する
解決策: レスポンス全体を一度にパースしようとしていないか確認してください。`response.json()` の代わりに、`response.iter_lines()`(Python)やレスポンスボディのストリーム読み取り(JavaScript)を使用します。

問題2: ストリーミングが遅い、または途中で停止する
解決策: ネットワークの問題やOllamaサーバーのリソース不足が原因の可能性があります。以下のコマンドでOllamaサーバーの状態を確認できます:

ollama ps

問題3: 特定のモデルでストリーミングが動作しない
解決策: モデルが正しくロードされているか確認します。以下のコマンドで利用可能なモデルをリスト表示し、必要に応じてモデルをプルします:

ollama list
ollama pull llama3.2  # モデルが存在しない場合

パフォーマンス最適化のヒント

1. バッファリング: 大量のテキストを処理する場合は、一定量のチャンクをバッファリングしてから表示することで、パフォーマンスを向上させられます。

2. エラーハンドリング: ネットワークの不安定さに対処するために、再接続ロジックとタイムアウト処理を実装することをお勧めします。

3. プログレス表示: 長時間実行されるクエリに対しては、プログレスバーやトークンカウンターを実装するとユーザーエクスペリエンスが向上します。

OllamaのストリーミングAPIを正しく実装することで、大規模言語モデルの応答をリアルタイムで表示するインタラクティブなアプリケーションを構築できます。この機能は、チャットボット、コード補完ツール、ライティングアシスタントなど、多くのAIアプリケーションで重要な役割を果たします。

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