はじめに
以前の記事では、Function callingを使ってAIからの回答を定型化する方法を紹介しました。
Function callingは外部関数を呼び出す形式でJSON構造を取得できる便利な機能ですが、実はOpenAI APIにはresponse_format という、より直接的にJSON形式の回答を得られる方法が存在します。
本記事では、response_formatパラメータを用いた構造化出力の実装方法と、実際のKotlinコードでの活用例を紹介します。
Structured Outputs(構造化出力)とは
Structured Outputsは、AIモデルからの応答を確実に指定したJSON Schemaに準拠した形式で取得できる機能です。
公式ドキュメントでは以下のように説明されています:
Structured Outputs is a feature that ensures the model will always generate responses that adhere to your supplied JSON Schema.
(構造化出力は、モデルが常に指定されたJSON Schemaに準拠した応答を生成することを保証する機能です。)
Function callingとの違いは以下の通りです:
| response_format | Function Calling | |
|---|---|---|
| 用途 | AIの応答自体を構造化されたJSONで取得 | 関数呼び出しの引数として構造化データを取得 |
| 出力保証 | 必ずJSON形式 | 保証されない(ただの文章の可能性あり) |
| レスポンス形式 | choices.message.content | choices.message.function_call.arguments |
| シンプルさ | 非常にシンプル | やや複雑 |
| 高度な制御 | 不可(単一のJSON出力のみ) | 可能(複数関数からの選択など) |
| 適用シーン | とにかく手軽かつ 確実に、単一のJSONオブジェクトが欲しい場合 | 複数の外部APIや関数の中から、LLMに 適切なものを選択させたい 場合 |
response_formatパラメータの指定方法
response_formatを使用したリクエスト例を以下に示します。
POST /chat/completions
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant that provides concise answers in JSON format according to the provided schema."
},
{
"role": "user",
"content": "以下の文章に含まれる商品名と、わかればその価格を抽出して\n(対象のテキスト)"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "product_extraction",
"schema": {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "商品名"
},
"price": {
"type": "integer",
"description": "価格"
}
},
"required": ["name"],
"additionalProperties": false
}
}
},
"required": ["products"],
"additionalProperties": false
},
"strict": true
}
}
}
| response_format.type | "json_schema"を指定することで構造化出力を有効化します。 "text"を指定すると通常のテキスト応答となります。 |
|---|---|
| json_schema.name | スキーマの識別名です。わかりやすい名前を付けます。 |
| json_schema.schema | JSON Schema形式でデータ構造を定義します。 type、properties、required、additionalPropertiesなどを指定可能です。 |
| json_schema.strict | trueに設定することで、スキーマへの厳密な準拠を強制します。 |
このリクエストに対するレスポンスは以下のようになります。
{
...
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"products\":[{\"name\":\"商品A\",\"price\":120},{\"name\":\"商品B\",\"price\":150}]}"
},
"finish_reason": "stop"
}
],
...
}
Function callingと異なり、message.contentに直接JSON文字列が格納されます。
Kotlinでの実装例
Kotlinでの実装例を紹介します。以下は、OpenAI APIを使用した構造化出力の実装です。
/**
* OpenAI Chat Completions APIを呼び出し、構造化出力を取得します。
*
* @param prompt ユーザーからの入力プロンプト
* @param jsonSchema 構造化出力のJSONスキーマ定義
* @return APIからの応答JSON文字列
*/
fun callOpenAiChat(
prompt: String,
jsonSchema: Map<String, Any>
): String {
// リクエストボディの構築
val requestBody = mapOf(
"model" to "gpt-4o-mini",
"messages" to listOf(
mapOf(
"role" to "system",
"content" to "You are a helpful assistant that provides concise answers in JSON format according to the provided schema."
),
mapOf("role" to "user", "content" to prompt)
),
"response_format" to mapOf(
"type" to "json_schema",
"json_schema" to mapOf(
"name" to "structured_output",
"schema" to jsonSchema
)
)
)
// API呼び出し
val response = httpClient.post(
url = "https://api.openai.com/v1/chat/completions",
headers = mapOf("Authorization" to "Bearer $apiKey"),
body = objectMapper.writeValueAsString(requestBody)
)
// レスポンスからcontentを取得
val responseJson = objectMapper.readTree(response.body)
return responseJson["choices"][0]["message"]["content"].asText()
}
使用例
以下の文章から商品名と価格を抽出する例を示します。
// 商品情報抽出用のJSONスキーマ定義
val productSchema = mapOf(
"type" to "object",
"properties" to mapOf(
"products" to mapOf(
"type" to "array",
"items" to mapOf(
"type" to "object",
"properties" to mapOf(
"name" to mapOf(
"type" to "string",
"description" to "商品名"
),
"price" to mapOf(
"type" to "integer",
"description" to "価格"
)
),
"required" to listOf("name")
)
)
),
"required" to listOf("products")
)
// API呼び出し
val response = callOpenAiChat(
prompt = "以下の文章に含まれる商品名と、わかればその価格を抽出して\n(対象のテキスト)",
jsonSchema = productSchema
)
// 返却されるJSON文字列の例
// {"products":[{"name":"商品A","price":120},{"name":"商品B","price":150}]}
// JSONをパースして使用
val productData = objectMapper.readValue(response, ProductData::class.java)
productData.products.forEach { product ->
println("商品名: ${product.name}, 価格: ${product.price}円")
}
まとめ
本記事では、OpenAI APIのresponse_formatパラメータを使用した構造化出力について紹介しました。
Structured Outputsを使用することで、プロンプトだけに頼らず、スキーマレベルで応答形式を制御できるため、より堅牢なアプリケーション開発が可能になります。
実際のプロダクション環境では、本記事で紹介したようにオプショナルパラメータとして実装することで、柔軟性を保ちつつ構造化出力の恩恵を受けられる設計が望ましいです。