LLM API呼び出し: temperature、top_p、max tokens

1. 概要

LLM APIを呼び出す際に、プロンプトの他に、テキスト生成パラメータをいくつか送信します。最も一般的な3つのパラメータは temperature、top_p、max_tokens です。これらは、応答が安定か創造的か、どのくらい長いかを決定します。

この記事では、小さなJavaシミュレーションを使って各パラメータのメカニズムを説明します。その後、Spring AIで設定して切断された応答を検出します。最後に、各タイプのタスクに適した値を選べるようになります。

2. モデルがトークンをどのように選択するか

各ステップで、LLMはその次に表示される可能性のあるすべてのトークンのスコア(ロジット)を計算します。softmax関数はこれらのロジットを確率に変換します。その後、モデルはその分布に従ってトークンをサンプリングします。

temperature と top_p はモデルの「知識」を変更しません。これらはサンプリングステップにのみ干渉します。

2.1. Temperature: 分布をシャープにするか平坦にするか

温度TのsoftMax公式:

p_i = exp(z_i / T) / Σ exp(z_j / T)

以下のコードは、「chiếc nón lá màu …」という文の次の単語を選択しているモデルをシミュレートしています:

import java.util.*;

public class SamplingDemo {

    static final List<String> TOKENS = List.of("xanh", "đỏ", "vàng", "tím");
    static final double[] LOGITS = {2.0, 1.0, 0.5, 0.1};

    static double[] softmax(double[] logits, double temperature) {
        double[] exp = new double[logits.length];
        double sum = 0;
        for (int i = 0; i < logits.length; i++) {
            exp[i] = Math.exp(logits[i] / temperature);
            sum += exp[i];
        }
        for (int i = 0; i < exp.length; i++) {
            exp[i] /= sum;
        }
        return exp;
    }

    public static void main(String[] args) {
        for (double t : new double[]{0.5, 1.0, 1.5}) {
            double[] p = softmax(LOGITS, t);
            StringBuilder line = new StringBuilder("T=" + t + ": ");
            for (int i = 0; i < p.length; i++) {
                line.append(String.format(Locale.ROOT, "%s=%.2f ", TOKENS.get(i), p[i]));
            }
            System.out.println(line);
        }
    }
}

softmax 関数は各ロジットを temperature で割ってから exp を取ります。main 関数は3つのtemperature レベルで分布を出力します:

T=0.5: xanh=0.83 đỏ=0.11 vàng=0.04 tím=0.02
T=1.0: xanh=0.57 đỏ=0.21 vàng=0.13 tím=0.09
T=1.5: xanh=0.46 đỏ=0.24 vàng=0.17 tím=0.13

結果はtemperatureの効果を明確に示しています:

  • T が低い (0.5): トークン「xanh」が83%を占めます。モデルはほぼ常に最も一般的な答えを選択します。応答は安定していて予測可能です。
  • T が高い (1.5): 確率がより均等に分布しています。「tím」は2%から13%に増加します。応答はより多様ですが、トピックから外れやすくなります。
  • T = 0: 理論的には、確率が最も高いトークンを常に選択します(greedy)。

注意: temperature = 0 は、呼び出し間で完全に同じ結果を保証しません。GPU上の浮動小数点計算とプロバイダのバッチ処理方法により、わずかな違いが生じる可能性があります。LLMの実際の出力と文字列を正確に比較するテストを書かないでください。

2.2. Top_p: 低確率のテールをカットする

top_p (nucleus sampling) は別の方法で動作します。モデルはトークンを降順の確率でソートします。合計確率がしきい値pに達する最小のグループを保持し、そのグループ内からのみサンプリングします。

SamplingDemoに次のメソッドを追加します:

static List<String> nucleus(double[] probs, double topP) {
    Integer[] idx = {0, 1, 2, 3};
    Arrays.sort(idx, (a, b) -> Double.compare(probs[b], probs[a]));

    List<String> kept = new ArrayList<>();
    double cumulative = 0;
    for (int i : idx) {
        kept.add(TOKENS.get(i));
        cumulative += probs[i];
        if (cumulative >= topP) {
            break;
        }
    }
    return kept;
}

nucleus(softmax(LOGITS, 1.0), 0.8) を呼び出すと、以下が返されます:

[xanh, đỏ, vàng]

「xanh」と「đỏ」の合計は0.785で、まだ0.8に達していません。したがって「vàng」が追加され、合計は0.914になります。トークン「tím」は除外され、temperatureがどんなに高くても選択されません。

2つのパラメータの主な違い:

  • temperature は分布の形を変更します。
  • top_p は選択が許可されたトークンセットを変更します。

注意: ほとんどのプロバイダは、2つのパラメータのいずれか一方のみを調整し、もう一方をデフォルト値(top_p = 1.0)のままにしておくことを勧めます。両方を同時に調整すると、どの変更が結果を引き起こしているのかが不明確になります。

3. Max tokens: 出力長の制限

max_tokens はモデルが応答で生成できるトークンの最大数です。これはターゲット長ではありません。モデルはこの数に「達するために書こう」とはしません。制限に達すると、文の途中でも即座に停止します。

理解する必要がある3つのポイントがあります:

  1. トークンは単語ではありません。 英語では、1トークンは約3/4の単語です。ベトナム語のダイアクリティクスは同じコンテンツ量に対してより多くのトークンを消費する傾向があります。
  2. コストはトークン単位で計算されます。 max_tokens はコストと遅延の上限です。
  3. コンテキストウィンドウには入力と出力の両方が含まれます。 プロンプトがコンテキストウィンドウのほぼ全体を使用している場合、出力に残される部分は非常に少なくなります。

切断されると、APIは stop の代わりに length の finish_reason を返します。OpenAI形式のAPIからの簡潔なレスポンスは以下のようになります:

{
  "choices": [{
    "message": { "role": "assistant", "content": "Nón lá Huế được làm từ lá cọ non, phơi" },
    "finish_reason": "length"
  }],
  "usage": { "prompt_tokens": 42, "completion_tokens": 20, "total_tokens": 62 }
}

max_tokens = 20 のため、コンテンツは途中で停止しました。コードが content のみを読む場合、ユーザーに気づかずに不完全な文を表示することになります。

4. Spring AIで設定する

このセクションでは、Spring AI を使用してSpring Boot 3.xからOpenAIを呼び出します。

4.1. 依存関係とデフォルト設定

pom.xml にBOMとstarterを追加します:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

アプリケーション全体のデフォルト値は application.properties にあります:

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
spring.ai.openai.chat.options.temperature=0.3
spring.ai.openai.chat.options.max-tokens=300

オーバーライドされないリクエストは、temperature = 0.3と最大300トークンの出力を使用します。APIキーは環境変数から取得され、ファイルにはハードコードされていません。

4.2. タスクタイプごとのパラメータプロファイル

各タスクは異なるパラメータセットが必要です。コード全体に「magic numbers」を分散させる代わりに、それらをenumにまとめます:

import org.springframework.ai.openai.OpenAiChatOptions;

public enum GenerationProfile {

    EXTRACTION(0.0, 1.0, 200),      // データ抽出、分類
    SUPPORT_REPLY(0.4, 1.0, 400),   // 顧客対応
    MARKETING_COPY(0.9, 1.0, 600);  // 商品説明、キャッチコピーの作成

    private final double temperature;
    private final double topP;
    private final int maxTokens;

    GenerationProfile(double temperature, double topP, int maxTokens) {
        this.temperature = temperature;
        this.topP = topP;
        this.maxTokens = maxTokens;
    }

    public OpenAiChatOptions toOptions() {
        return OpenAiChatOptions.builder()
                .temperature(temperature)
                .topP(topP)
                .maxTokens(maxTokens)
                .build();
    }
}

各定数は1つのタスクタイプを説明しています。EXTRACTIONは一貫性のある結果が必要なため、temperatureは0です。MARKETING_COPYは多様性が必要なため、0.9を使用します。3つすべてがtop_p = 1.0を保持しており、つまりセクション2.2で推奨されているようにtemperatureのみを調整しています。

4.3. リクエストごとのパラメータオーバーライドと切断検出

Spring Bootは自動的に ChatClient.Builder beanを設定します。これをサービスに注入します:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.stereotype.Service;

@Service
public class ProductDescriptionService {

    private final ChatClient chatClient;

    public ProductDescriptionService(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("あなたはNón Lá Shopのコピーライターです。最大3文で書いてください。")
                .build();
    }

    public String describe(String productName) {
        ChatResponse response = chatClient.prompt()
                .user(u -> u.text("商品の説明を書く: {name}")
                            .param("name", productName))
                .options(GenerationProfile.MARKETING_COPY.toOptions())
                .call()
                .chatResponse();

        Generation result = response.getResult();
        String finishReason = result.getMetadata().getFinishReason();

        if ("length".equalsIgnoreCase(finishReason)) {
            throw new TruncatedResponseException(
                "説明は" + response.getMetadata().getUsage().getCompletionTokens() + "トークン後に切断されました");
        }
        return result.getOutput().getText();
    }
}

各部分を分析します:

  • .options(...) はこのリクエストのみで application.properties の値をオーバーライドします。
  • .chatResponse() はメタデータを含む完全なレスポンスを返し、.content() のような文字列だけではありません。
  • getFinishReason() は停止理由を示します。Spring AIは LENGTH を大文字で返す可能性があるため、コードは equalsIgnoreCase を使用します。
  • getUsage().getCompletionTokens() は実際の出力トークン数を示します。この数字はログに記録し、コストを追跡するのに役立ちます。

例外は次のように簡単に定義されます:

public class TruncatedResponseException extends RuntimeException {
    public TruncatedResponseException(String message) {
        super(message);
    }
}

この例外が発生した場合、2つの選択肢があります。1つ目は max_tokens を増やして再試行することです。2つ目はプロンプトを厳しくして、より短い応答をリクエストすることです。2番目の方法は通常、より安価で、より簡潔な結果が得られます。

5. テスト

LLMの実際の出力を文字列比較でテストすべきではありません。テストすべきはあなた自身のロジック: パラメータプロファイルとシミュレーションアルゴリズムです。

import org.junit.jupiter.api.Test;
import org.springframework.ai.openai.OpenAiChatOptions;

import static org.assertj.core.api.Assertions.assertThat;

class GenerationProfileTest {

    @Test
    void extractionProfile_shouldBeDeterministicAndShort() {
        OpenAiChatOptions options = GenerationProfile.EXTRACTION.toOptions();

        assertThat(options.getTemperature()).isZero();
        assertThat(options.getTopP()).isEqualTo(1.0);
        assertThat(options.getMaxTokens()).isEqualTo(200);
    }

    @Test
    void marketingProfile_shouldBeMoreCreativeThanSupport() {
        assertThat(GenerationProfile.MARKETING_COPY.toOptions().getTemperature())
                .isGreaterThan(GenerationProfile.SUPPORT_REPLY.toOptions().getTemperature());
    }

    @Test
    void lowerTemperature_shouldConcentrateProbabilityOnTopToken() {
        double[] cold = SamplingDemo.softmax(SamplingDemo.LOGITS, 0.5);
        double[] hot = SamplingDemo.softmax(SamplingDemo.LOGITS, 1.5);

        assertThat(cold[0]).isGreaterThan(hot[0]);
        assertThat(cold[3]).isLessThan(hot[3]);
    }

    @Test
    void nucleus_shouldDropLowProbabilityTail() {
        double[] probs = SamplingDemo.softmax(SamplingDemo.LOGITS, 1.0);

        assertThat(SamplingDemo.nucleus(probs, 0.8))
                .containsExactly("xanh", "đỏ", "vàng");
    }
}

最初の2つのテストは、重要なプロファイルのパラメータが誤って変更されないことを保証します。後の2つのテストはセクション2で説明された動作を確認します: 低いtemperatureは確率を最上位のトークンに集中させ、top_p = 0.8はトークン「tím」を除外します。

6. よくあるエラーと使用すべきではない場合

精度が必要なタスクに高いtemperatureを使用する。 JSON抽出、チケット分類、またはSQL生成は一貫した結果が必要です。0.8以上のtemperatureは、形式エラーと幻覚情報のリスクを増加させます。

低いtemperatureが「ハルシネーション」を防ぐと思う。 低いtemperatureは、モデルが最も可能性の高いと考える答えを選ぶようにするだけです。モデルが答えを知らない場合、それでも自信を持って間違ったことを述べる可能性があります。ハルシネーションを減らすには、正しいコンテキスト(RAG)を提供し、出力を検証する必要があります。

コスト削減のためにmax_tokensを設定しすぎる。 応答が切断されて再度呼び出す必要がある場合、何も節約できません。プロンプト(「最大3文」)で長さを制限し、max_tokensはセーフティネットの役割のみを果たします。

finish_reason を確認しない。 これが最も一般的なエラーです。ユーザーは短い文を見ますが、ログには何も記録されません。

パラメータを別のモデルにそのまま持ち込む。 各プロバイダには独自の慣例があります:

  • 異なるtemperature範囲。OpenAIは0~2を使用し、Anthropicは0~1を使用するため、同じ値1.0でも異なる意味を持ちます。
  • Reasoning model(OpenAIのo-seriesなど)はtemperatureとtop_pを拒否する可能性があります。
  • 一部のモデルは max_tokens の代わりに max_completion_tokens が必要です。Spring AIは OpenAiChatOptions.builder().maxCompletionTokens(...) 経由でこのフィールドをサポートします。

注意: Reasoning modelの場合、内部の「思考」トークンも出力制限にカウントされます。制限を設定しすぎると、finish_reason = lengthで空のコンテンツを受け取る可能性があります。

7. 結論

この記事では、LLM APIを呼び出す際の3つの重要なパラメータについて説明しました:

  • temperature は確率分布の鋭さを変更します。低い値は安定した結果をもたらし、高い値は多様な結果をもたらします。
  • top_p はトークン候補セットを制限します。この2つのパラメータのいずれか一方のみを調整する必要があり、両方を同時に調整しないでください。
  • max_tokens は長さとコストの上限です。常に finish_reason を確認して、切断された応答を検出してください。

Spring AIを使用すると、application.properties のデフォルト値を設定し、.options(...) でリクエストごとにオーバーライドします。GenerationProfile のようなenumにパラメータセットを集約すると、コードが読みやすく、テストしやすく、別のモデルに切り替えるときに調整しやすくなります。

llmspring-aispring-bootjavaopenai