이 게시물을 읽기 전 JSON에 대해 잘 모르겠다면 이전 게시물을 참고하길 바란다.
우리가 사용하는 OpenAI 모델은 웹 서버와의 통신을 용이하게 하기 위해 구조화된 출력(Structured Outputs)을 사용해야 했다.
구조화된 출력(Structured Outputs)은 LLM의 답변을 지정된 JSON 형식에 맞게 생성시켜주는 기능이다. 아래의 형태처럼 일반적인 긴 줄글의 답변이 아닌 유효한 JSON 형식의 답변으로 모델이 생성할 수 있다.
{
"steps": [
{
"explanation": "Subtract 31 from both sides to isolate the term with x.",
"output": "8x + 31 - 31 = 2 - 31"
},
{
"explanation": "This simplifies to 8x = -29.",
"output": "8x = -29"
},
{
"explanation": "Divide both sides by 8 to solve for x.",
"output": "x = -29 / 8"
}
],
"final_answer": "x = -29 / 8"
}
OpenAI API에서 구조화된 출력은 gpt-4o로 시작되는 모델에서만 사용이 가능하다.
원래는 JSON 모드를 통해 답변을 출력하다가 더 정확하고 유효한 JSON 답변을 출력하기 위해서 구조화된 출력을 사용하기 시작했다.구조화된 출력 기능은 개발자가 제공한 스키마와 일치하도록 제한하고, 복잡한 스키마를 더 잘 이해하도록 모델을 훈련시킨 결과이다.
OpenAI가 구조화된 출력을 평가한 결과 gpt-4o-2024-08-06 모델에서 100%의 신뢰성을 가진 출력을 얻을 수 있었다고 한다.

OpenAI의 Structured Outputs에 대해서는 아래의 문서를 참고하면 더 구체적인 개념과 사용법을 알 수 있다.
구조화된 출력을 사용하는 경우는 두가지 인데 하나는 함수 호출을 사용할 때와 JSON 스키마 응답 형식을 사용할 때이다. 나는 함수 호출은 다른 방식을 이용했고, 모델이 함수의 작동 결과를 참고하여 답변을 생성할 때 JSON 형식으로 생성하도록 사용하였다.
먼저 모델이 출력할 때 따라야 하는 JSON 스키마로서 데이터 구조를 정의해줘야 한다. 정의하는 방법은 SDK를 사용하여 Pydantic을 사용할 수 있지만 나는 사용자 질문에 따라 답변하는 JSON 값이 미묘하게 다르기 때문에 더 정확한 출력 유도를 위해서 JSON 스키마 여러개를 직접 정의하였다.
myFormat = {
"type": "json_schema",
"json_schema" : {
"name" : "A_unique_answer",
"schema" : {
"type": "object",
"properties": {
"response" : {
"type": "string",
"description" : "put a short sentence that gives information about the places the user like"
},
"additionalInfo": {
"type": "array",
"items": {
"type": "object",
"properties" : {
"name" : {
"type": "string",
"description" : "The name of the place"
},
"explanation" : {
"type": "string",
"description" : "A brief description of the place."
}
},
"additionalProperties": False,
"required" : ["place_name", "explanation"]
}
}
},
"additionalProperties": False,
"required" : ["response", "additionalInfo"]
},
"strict" : True
}
구조화된 출력을 사용하기 위해서는 모든 필드를 required로 지정해야 하고 strict는 True로 설정하여 제공된 스키마에 출력을 일치하도록 맞춘다. JSON 스키마에 정의되지 않은 추가적인 키/값 생성을 막기 위해서 additionalProperties를 False로 설정하였다.
이렇게 스키마를 정의하고 myFormat 변수에 저장하였다.
messages = [
{"role":"system", "content": "당신은 사용자의 취향을 파악하여 사용자가 좋아할 만한 장소를 추천해주는 봇입니다."},
{"role": "user", "content":f"{reference}"}
]
response = client.chat.completions.create(
model=model,
messages=messages,
response_format=myFormat
)
위에서 정의한 스키마를 OpenAPI를 호출할 때 response_format 파라미터에 넣어준다. 이렇게 넣어주면 내가 지정한 JSON 형식으로 답변을 생성한다. Pydantic 객체로 출력 구조를 정의했으면 client.chat.completions.parse() 메소드를 이용해 JSON 응답을 내가 만든 객체로 변환해 가져올 수 있다.
answer = response.choices[0].message.content
print(answer)
출력)
'{
"response" : "사용자가 좋아할 만한 장소를 알려드립니다.",
"additionalInfo" :
[
{
"name" : "하늘하늘 카페",
"explanation" : "따뜻한 무드의 카페로 가족과 함께 방문하기 좋은 곳입니다."
},
{
"name" : "바다의 울림",
"explanation" : "사용자는 귀여운 물품을 좋아하기 때문에 바다와 관련된 물품을 파는 곳에서 다양한 굿즈를 얻을 수 있습니다."
},
...
]
}'
위의 방법대로 답변을 확인해 봤을 때, 지정한 JSON 형식의 문자열로 답변이 잘 생성된 것을 알 수 있었다.
def change_to_dict(answer):
try:
response_dict = json.loads(answer, strict=False)
return response_dict
except Exception as e:
print(f"Error escaping JSON strings: {e}")
return response
생성한 JSON 문자열을 json.loads() 함수를 이용해 파이썬 딕셔너리로 변환할 수 있다. 이때, 유효하지 않은 JSON 형식의 데이터일 경우 오류가 발생하는데 Structured Outputs로 생성한 답변은 오류가 발생하지 않았다.