0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[OpenAI]structured outputについて調べてみた

Posted at

function callingとresponse_formatの違いは?

function calling

モデルをアプリケーションの機能やツールに接続する際に使用します。例えば、データベースクエリや UI 操作などのアプリケーション機能をモデルから呼び出す場合に適しています。

response_format

モデルがユーザーに直接応答する際の出力構造を指定するために使用します。例えば、数学の家庭教師アプリで、モデルの回答を特定の JSON 形式で構造化したい場合に適しています。

対応モデル

Structured Outputs(対応モデル)

OpenAIの最新モデル(gpt-4o-mini-2024-07-18以降)で利用可能な機能です。指定したJSON Schemaに厳密に従った出力を生成します。response_formatパラメータでスキーマを指定することで、型安全性が保証され、必須キーの欠落や無効な列挙値の生成を防ぎます。これにより、出力の検証が簡素化され、より信頼性の高いAPIレスポンスが得られます。

JSONモード(非対応モデルの代替策)

古いモデル(gpt-4-turbo以前)で使用できる機能です。response_format: { "type": "json_object" }を指定することで、有効なJSON形式の出力を生成します。ただし、厳密なスキーマ準拠は保証されません。システムメッセージやユーザーメッセージで出力構造を説明し、明示的にJSONフォーマットを指示する必要があります。出力の検証とエラー処理が重要となります。

実際に送ってみる

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
)

const openaiAPIEndpoint = "https://api.openai.com/v1/chat/completions"

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type RequestBody struct {
	Model          string    `json:"model"`
	Messages       []Message `json:"messages"`
	ResponseFormat struct {
		Type       string          `json:"type"`
		JSONSchema json.RawMessage `json:"json_schema"`
	} `json:"response_format"`
}

type ResponseChoice struct {
	Message struct {
		Role    string          `json:"role"`
		Content json.RawMessage `json:"content"`
		Refusal *string         `json:"refusal,omitempty"`
	} `json:"message"`
	FinishReason string `json:"finish_reason"`
}

type APIResponse struct {
	Choices []ResponseChoice `json:"choices"`
}

type JSONSchemaWrapper struct {
	Name   string        `json:"name"`
	Schema WeatherSchema `json:"schema"`
}

type WeatherSchema struct {
	Type       string                    `json:"type"`
	Properties map[string]SchemaProperty `json:"properties"`
	Required   []string                  `json:"required"`
}

type SchemaProperty struct {
	Type string   `json:"type"`
	Enum []string `json:"enum,omitempty"`
}

func createWeatherSchema() JSONSchemaWrapper {
	return JSONSchemaWrapper{
		Name: "weather_response",
		Schema: WeatherSchema{
			Type: "object",
			Properties: map[string]SchemaProperty{
				"location":    {Type: "string"},
				"temperature": {Type: "number"},
				"unit": {
					Type: "string",
					Enum: []string{"C", "F"},
				},
				"conditions": {Type: "string"},
			},
			Required: []string{"location", "temperature", "unit", "conditions"},
		},
	}
}

func main() {
	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		fmt.Println("Please set the OPENAI_API_KEY environment variable.")
		return
	}

	weatherSchema := createWeatherSchema()
	weatherSchemaJSON, err := json.Marshal(weatherSchema)
	if err != nil {
		fmt.Printf("Error marshalling weather schema: %v\n", err)
		return
	}

	reqBody := RequestBody{
		Model: "gpt-4o-2024-08-06",
		Messages: []Message{
			{
				Role:    "system",
				Content: "You are a helpful assistant designed to output JSON about weather conditions.",
			},
			{
				Role:    "user",
				Content: "What's the weather like in Tokyo today?",
			},
		},
		ResponseFormat: struct {
			Type       string          `json:"type"`
			JSONSchema json.RawMessage `json:"json_schema"`
		}{
			Type:       "json_schema",
			JSONSchema: weatherSchemaJSON,
		},
	}

	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		fmt.Printf("Error marshalling request: %v\n", err)
		return
	}

	req, err := http.NewRequest("POST", openaiAPIEndpoint, bytes.NewBuffer(jsonData))
	if err != nil {
		fmt.Printf("Error creating request: %v\n", err)
		return
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+apiKey)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("Error sending request: %v\n", err)
		return
	}
	defer resp.Body.Close()

	// Check the status code
	if resp.StatusCode != http.StatusOK {
		fmt.Printf("API returned non-200 status code: %d\n", resp.StatusCode)
		body, _ := ioutil.ReadAll(resp.Body)
		fmt.Printf("Response body: %s\n", string(body))
		return
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("Error reading response: %v\n", err)
		return
	}

	// Print the raw response for debugging
	fmt.Printf("Raw API response:\n%s\n", string(body))

	var apiResp APIResponse
	err = json.Unmarshal(body, &apiResp)
	if err != nil {
		fmt.Printf("Error parsing response: %v\n", err)
		return
	}

	if len(apiResp.Choices) > 0 {
		choice := apiResp.Choices[0]

		// Handle different finish reasons
		switch choice.FinishReason {
		case "stop":
			fmt.Println("Received structured output. Writing to file...")

			// The content is a JSON string, so we need to unmarshal it twice
			var jsonString string
			err := json.Unmarshal(choice.Message.Content, &jsonString)
			if err != nil {
				fmt.Printf("Error parsing JSON string: %v\n", err)
				return
			}

			var structuredOutput map[string]interface{}
			err = json.Unmarshal([]byte(jsonString), &structuredOutput)
			if err != nil {
				fmt.Printf("Error parsing structured output: %v\n", err)
				return
			}

			// Then, marshal it back to JSON with indentation
			prettyJSON, err := json.MarshalIndent(structuredOutput, "", "  ")
			if err != nil {
				fmt.Printf("Error formatting JSON: %v\n", err)
				return
			}

			// Write to file
			err = ioutil.WriteFile("structured_output.json", prettyJSON, 0644)
			if err != nil {
				fmt.Printf("Error writing to file: %v\n", err)
				return
			}
			fmt.Println("Structured output written to structured_output.json")
		case "length":
			fmt.Println("Warning: The response was truncated due to token limit.")
		case "content_filter":
			fmt.Println("Warning: The response was filtered due to content restrictions.")
		default:
			fmt.Printf("Unexpected finish reason: %s\n", choice.FinishReason)
		}

		// Handle refusals
		if choice.Message.Refusal != nil {
			fmt.Printf("The model refused to generate a response: %s\n", *choice.Message.Refusal)
		}
	} else {
		fmt.Println("No choices in the API response.")
	}
}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?