OpenAI Chat Completions と Spring AI

最終更新: 2025-04-20 05:43:09 +0800 +0800
tags: Ai | Llm | Openai | Spring-Ai | Api | Json
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

OpenAI Chat Completions API は、会話型言語モデル向けに広く実装されている JSON over HTTP インターフェースです。クライアントはメッセージと生成オプションの順序付きリストを送信し、サーバーはメタデータとトークン使用量を含む 1 つ以上のアシスタントメッセージを返します。

基本的な構造は次のとおりです。

text
HTTP client
  -> POST /v1/chat/completions
  -> request JSON containing messages and options
  -> model inference
  <- response JSON containing choices and usage

多くのゲートウェイやモデルサーバーは、基盤モデルが OpenAI でホストされていない場合でも、OpenAI 互換エンドポイントを実装しています。そのため、この形式は事実上の相互運用 API として有用ですが、HTTP、JSON-RPC、MCP のような独立した正式標準ではありません。互換プロバイダーによって、サポートするフィールド、モデル、制限、エラー形式、拡張機能が異なる場合があります。

Spring AI は、このワイヤー形式の上に、Java アプリケーション向けのプロバイダー中立なモデルを提供します。アプリケーションコードは Prompt、Message、ChatOptions、ChatResponse を扱い、OpenAI または OpenAI 互換アダプターがこれらのオブジェクトとプロバイダーの JSON の相互変換を行います。

HTTP リクエスト #

簡略化したリクエストは次のとおりです。

http
POST /v1/chat/completions HTTP/1.1
Host: api.example.com
Authorization: Bearer ...
Content-Type: application/json

{
  "model": "gpt-4.1",
  "messages": [
    {
      "role": "system",
      "content": "Answer concisely."
    },
    {
      "role": "user",
      "content": "Explain optimistic locking."
    }
  ],
  "temperature": 0.3,
  "max_tokens": 1000,
  "stream": false
}

model と messages は、実行対象と、そのモデルが参照する会話コンテキストを識別します。残りのフィールドは生成を制御します。一部のゲートウェイでは、リクエスト本文で model を要求する代わりに、設定、デプロイメント名、URL、またはヘッダーによってモデルを選択します。

メッセージ #

messages は順序付きです。その順序自体がプロンプトの一部になります。

json
[
  {
    "role": "system",
    "content": "You are a support assistant."
  },
  {
    "role": "user",
    "content": "Where is order 42?"
  },
  {
    "role": "assistant",
    "content": "I need to look up that order."
  },
  {
    "role": "user",
    "content": "It belongs to account 7."
  }
]

一般的なロールは次のとおりです。

RoleMeaning
systemアプリケーションの指示と動作
developer新しいモデル API がサポートする開発者向け指示
userユーザーが提供した入力
assistant以前に生成された、または新たに生成されたモデル出力
toolアプリケーションによるツール呼び出しの結果

テキストは通常、文字列として表現されます。マルチモーダルをサポートするモデルでは、テキスト、画像、音声、その他のメディアを含む型付きコンテンツパートの配列を受け付ける場合があります。

エンドポイントは通常ステートレスです。呼び出し元が会話 ID を再利用しただけでは、モデルは以前の HTTP リクエストを記憶しません。会話を継続するには、クライアントが選択した過去のメッセージを再度送信する必要があります。

text
load conversation history
  -> append current user message
  -> send all selected messages
  -> receive assistant message
  -> save new user and assistant messages

モデルのコンテキスト制限内に収めるため、履歴は切り詰めたり、要約したり、外部ストレージから取得したりできます。

生成オプション #

頻繁に実装されるリクエストフィールドには次のものがあります。

FieldPurpose
modelモデルまたはデプロイメント識別子
temperatureサンプリングのランダム性を制御
top_pnucleus sampling のしきい値
max_tokens旧式または互換 API で生成トークン数を制限
max_completion_tokens一部のモデルで使用される新しい完了トークン数の上限
n候補選択肢の数
frequency_penalty頻繁に使用されたトークンの繰り返しを抑制
presence_penaltyまだ使用されていないトークンの導入を促進
stop設定されたシーケンスで生成を停止
stream完全なレスポンスを 1 つ返すか、チャンクをストリームするかを選択
response_formatサポートされる場合に JSON またはスキーマ制約付き出力を要求
toolsモデルが要求できる関数を記述
tool_choice選択可能なツール、またはツールを選択するかどうかを制約

プロバイダーのサポートと意味は異なります。ある互換エンドポイントが受け付けるフィールドが、別のエンドポイントでは拒否、無視、改名されたり、異なる意味で解釈されたりする場合があります。同じエンドポイントの背後にあるモデルのバージョンによっても異なる可能性があります。

サンプリングオプションも相互作用します。アプリケーションでは、評価なしに temperature と top_p の両方を変更するのではなく、通常はいずれか一方を調整します。

HTTP レスポンス #

ストリーミングしないレスポンスを簡略化すると、次のようになります。

json
{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1784778339,
  "model": "gpt-4.1",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Optimistic locking detects concurrent updates..."
      },
      "finish_reason": "stop",
      "logprobs": null
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 18,
    "total_tokens": 43
  }
}

主なフィールドは次のとおりです。

FieldMeaning
idプロバイダーが生成したリクエストまたは完了の識別子
objectレスポンスタイプ。通常は chat.completion
createdプロバイダーが割り当てた Unix タイムスタンプ
model結果を生成したモデル
choicesアシスタント出力の候補
usage入力、出力、合計トークンの使用量
system_fingerprintプロバイダー側のモデル設定を示すオプションの識別子

各 choice には通常、index、アシスタントの message、finish_reason、およびオプションの対数確率が含まれます。一般的な終了理由は次のとおりです。

  • stop: 通常の完了
  • length: 出力またはコンテキストの上限に到達
  • tool_calls: モデルが 1 つ以上のツールを要求
  • content_filter: 安全性フィルターによって生成が停止

HTTP レスポンスが成功したからといって、回答が正しいとは限りません。成功は、プロバイダーがリクエストを受け付け、構文的に有効なモデル結果を生成したことを意味します。

プロバイダー拡張 #

OpenAI 互換であることは、バイト単位で同一であることを意味しません。たとえば次のような違いがあります。

  • Azure 互換レスポンスには prompt_filter_results や content_filter_results が含まれる場合があります。
  • ゲートウェイがレイテンシーチェックポイント、ルーティング情報、キャッシュ統計、サービス階層のメタデータを追加する場合があります。
  • トークンの詳細で、推論、音声、キャッシュ、受理された予測、拒否された予測のトークンを区別する場合があります。
  • エラーレスポンスとレート制限ヘッダーはプロバイダーごとに異なります。

可能な場合、アプリケーションは未知のフィールドを無視し、すべてのプロバイダー拡張にドメインモデルを結合しないようにします。拡張が必要な場合は、プロバイダー固有の設定またはメタデータとして明示的に表現します。

スキーマ内のツール呼び出し #

リクエストにはツール定義を含めることができます。

json
{
  "model": "gpt-4.1",
  "messages": [
    {
      "role": "user",
      "content": "What is the status of order 42?"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "find_order",
        "description": "Look up an order by ID",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": {
              "type": "string",
              "description": "Canonical order ID"
            }
          },
          "required": [
            "order_id"
          ]
        }
      }
    }
  ]
}

モデルは関数を実行しません。代わりに、構造化されたリクエストを含むアシスタントメッセージを返せます。

json
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_123",
      "type": "function",
      "function": {
        "name": "find_order",
        "arguments": "{\"order_id\":\"42\"}"
      }
    }
  ]
}

ホストアプリケーションは引数を検証し、ローカルまたはリモートの処理を呼び出し、後続のリクエストで結果を送信します。

json
{
  "role": "tool",
  "tool_call_id": "call_123",
  "content": "{\"status\":\"SHIPPED\"}"
}

したがって、通常の通信は次のようになります。

text
model request with tool schemas
  <- assistant tool-call request
application executes tool
  -> model request with tool result
  <- final assistant answer

ツール呼び出しには、少なくとも 2 回のモデルとの往復が必要になることがよくあります。認可、検証、冪等性、タイムアウト、副作用について責任を持つのはモデルではなくアプリケーションです。

ストリーミングレスポンス #

"stream": true の場合、サーバーは通常、増分的な chat.completion.chunk オブジェクトをデータとして含む Server-Sent Events を返します。

text
data: {"object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"Optimistic"}}]}

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":" locking"}}]}

data: [DONE]

チャンクには完全なメッセージではなく差分が含まれます。クライアントは、提供される場合、コンテンツ、ツール呼び出しの断片、終了理由、使用量を集約する必要があります。ストリーミングは最初に表示可能な出力が得られるまでの時間を短縮しますが、合計レイテンシーやトークンコストを必ずしも削減するわけではありません。

Spring AI のオブジェクトモデル #

Spring AI は、アプリケーション向けのチャット概念とプロバイダーのワイヤー DTO を分離します。主な型は次のとおりです。

Spring AI typeResponsibility
ChatClientプロンプト、Advisor、ツール、出力変換向けの流暢なアプリケーション API
ChatModelチャットモデルを呼び出すプロバイダー中立インターフェース
Prompt順序付きメッセージとリクエストオプション
Message会話内の 1 エントリを表す共通インターフェース
SystemMessagesystem ロールの指示
UserMessageuser ロールの入力
AssistantMessageアシスタントのテキストとオプションのツール呼び出し
ToolResponseMessageアシスタントのツール呼び出しに対応する結果
ChatOptions移植可能なリクエストオプション
OpenAiChatOptionsOpenAI アダプター使用時の OpenAI 固有オプション
ChatResponse生成結果とメタデータを含むプロバイダー中立レスポンス
Generationアシスタント候補の 1 つ
ChatResponseMetadataモデル、使用量、レート制限、プロバイダーのメタデータ
Usage入力、出力、合計トークン数

低レベルのアプリケーションフローは次のとおりです。

java
ChatOptions options = OpenAiChatOptions.builder()
    .model("gpt-4.1")
    .temperature(0.3)
    .maxTokens(1000)
    .build();

Prompt prompt = new Prompt(
    List.of(
        new SystemMessage("Answer concisely."),
        new UserMessage("Explain optimistic locking.")
    ),
    options
);

ChatResponse response = chatModel.call(prompt);
AssistantMessage answer = response.getResult().getOutput();

ChatClient は、より高レベルな流暢な同等 API を提供します。

java
ChatResponse response = chatClient.prompt()
    .system("Answer concisely.")
    .user("Explain optimistic locking.")
    .options(options)
    .call()
    .chatResponse();

call() は同期的な終端操作です。プロンプト構築メソッドはリクエストを設定するだけであり、call() が Advisor チェーンとモデルアダプターを呼び出して完了を待機します。代わりに stream() はリアクティブストリームを返します。

Spring AI によるリクエストのシリアライズ #

Spring AI は通常、Prompt.toString() を HTTP 本文としてシリアライズしません。プロバイダーアダプターがマッピングを実行します。

text
Spring AI Prompt
  messages:
    SystemMessage
    UserMessage
    AssistantMessage
    ToolResponseMessage
  options:
    ChatOptions
       |
       v
OpenAI adapter
  OpenAI request DTOs
  role/content conversion
  option and tool-schema conversion
       |
       v
JSON serializer
       |
       v
POST /v1/chat/completions

Spring AI OpenAI 統合では、トランスポート型は OpenAiApi 配下に表現され、リクエスト、メッセージ、choice、completion の構造を含みます。アプリケーションでは通常、これらのトランスポート DTO を直接構築するのではなく、移植可能な型を使用すべきです。

レスポンスは逆方向に進みます。

text
HTTP response JSON
  -> OpenAI response DTOs
  -> AssistantMessage
  -> Generation
  -> ChatResponseMetadata and Usage
  -> ChatResponse

この境界があるため、ChatClientRequest をログに記録すると Java のレコードまたは Prompt 表現が表示される一方、HTTP ワイヤーログには具体的な OpenAI 互換 JSON が表示されます。前者は変換前のプロバイダー中立リクエストであり、後者は変換後にシリアライズされたトランスポート本文です。

Advisor と会話メモリ #

ChatMemory は OpenAI のワイヤースキーマの一部ではありません。これはアプリケーション側の抽象化です。メモリ Advisor はモデル呼び出し前にメッセージを読み込み、呼び出し後に新しいメッセージを保存します。

java
ChatMemory memory = MessageWindowChatMemory.builder()
    .maxMessages(20)
    .build();

ChatClient client = builder
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(memory).build()
    )
    .build();

各リクエストでは会話キーを指定します。

java
ChatResponse response = client.prompt()
    .user(question)
    .advisors(spec -> spec.param(
        ChatMemory.CONVERSATION_ID,
        conversationId
    ))
    .call()
    .chatResponse();

概念的には、Advisor は次の処理を実行します。

text
ChatMemory.get(conversationId)
  -> add historical messages to Prompt
  -> ChatModel.call(Prompt)
  -> ChatMemory.add(current user and assistant messages)

その結果、シリアライズされた messages 配列には、選択された会話ウィンドウ全体が含まれる場合があります。会話 ID、ストレージ、保持期間、アクセス制御はホストアプリケーションに属し、Chat Completions API はそれらを定義しません。

Spring AI のツールシリアライズ #

Spring AI は実行可能な機能を ToolCallback として表現します。アノテーション付きの Java メソッドを検出してコールバックに変換できます。

java
public class OrderTools {

    @Tool(description = "Look up an order by ID")
    public OrderStatus findOrder(
            @ToolParam(description = "Canonical order ID") String orderId) {
        return orderService.find(orderId);
    }
}

オブジェクトを登録します。

java
String answer = chatClient.prompt()
    .user("Where is order 42?")
    .tools(orderTools)
    .call()
    .content();

これにより、Spring AI は次の処理を行います。

  1. アノテーション付きメソッドを検査する。
  2. ToolCallback を作成する。
  3. 引数の JSON Schema を導出する。
  4. コールバック定義をプロバイダーの tools 配列にマッピングする。
  5. 返された tool_calls を AssistantMessage.ToolCall の値にマッピングする。
  6. 自動ツール実行が有効な場合、ホストアプリケーション内でコールバックを呼び出す。
  7. 次のモデルリクエスト向けに、結果をツールメッセージとしてシリアライズする。

LLM が認識するのは、名前、説明、スキーマだけです。JVM をリフレクションしたり、任意のメソッドを呼び出したりすることはできません。

2 つの表現のロギング #

Spring AI の Advisor ロギングと HTTP ワイヤーロギングは、異なる問いに答えます。

SimpleLoggerAdvisor などの Advisor は、次の内容を表示できます。

  • それ以前の Advisor がリクエストを変更した後の Spring AI メッセージ
  • ChatOptions
  • 会話 ID などの Advisor コンテキスト
  • プロバイダー中立の ChatResponse

リクエストが Java の toString() 出力のように見えるのは、最終的な HTTP 本文ではなく ChatClientRequest を記録しているためです。

HTTP クライアントロギングでは、次の内容を表示できます。

  • URL とヘッダー
  • 最終的にシリアライズされた JSON リクエスト
  • プロバイダー固有のフィールドとデフォルト値
  • 生の JSON レスポンス
  • ストリーミングチャンク

ワイヤーロギングは、プロバイダーアダプターがトランスポート DTO を作成した後に発生します。したがって、シリアライズやプロバイダー互換性を診断する適切な層です。ただし危険性もあります。メッセージ、取得したドキュメント、ツール結果、認証ヘッダー、Cookie、個人データがログに現れる可能性があります。完全なペイロードロギングは、管理された開発環境でのみ有効化し、秘匿化と保持期間の制限を適用してください。

互換性の境界 #

OpenAI 互換エンドポイントは、同一の動作を保証するものではなく、プロバイダー契約として扱うべきです。次の項目を検証してください。

  • エンドポイントパスと認証
  • サポートされるメッセージロールとコンテンツ型
  • モデル名とデプロイメント選択
  • リクエストオプションの名前と制限
  • ツール呼び出しと構造化出力のサポート
  • ストリーミングイベント形式
  • トークン使用量の計算
  • 安全性および拒否フィールド
  • エラー本文と HTTP ステータスコード
  • プロバイダー固有の拡張

Spring AI は、これらの詳細をモデルアダプター内に保持することで、アプリケーションの結合を減らします。ただし、互換性のないプロバイダー動作を同一にすることはできません。サポートする各ゲートウェイとモデルを、個別の実行パスとしてテストしてください。

Advisor、RAG、ベクトルストア、メモリリポジトリ、可観測性、本番アーキテクチャを含む、より広範なアプリケーションモデルについては、Spring AI を参照してください。

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)