AI

Azure OpenAI Structured Outputs

지기(ZIGI) 2024. 11. 7. 14:36

Today Key : structured, outputs, azure, openai, ai, 구조화, 출력, chatgpt, gpt



이번 포스팅에서는 지난 8월 6일에서 ChatGPT에  추가 된  API에서의 Structured Outputs에 대한 내용입니다. 

Structured Outputs이 무엇인지 간단하게 살펴보고, 사용 예시를 몇 가지 정리했습니다.


Structured outputs

Structured outputs는 API를 통해서 ChatGPT 모델 사용 시, 
출력 값을 개발자가 사전에 제공한 JSON Schema에 따라 모델이 일관된 형식의 답변을 생성하는 기능입니다. 
이를 통해 함수 호출이나, 비정형 데이터에서 구조화돈 데이터 추출, 복잡한 다단계 워크플로우 구축에 활용 가능합니다. 
 

지원 모델 및 API 버전

Azure OpenAI에서 Structured outputs는 GPT-4o(2024-08-06) 모델에서만 지원되며, 
API는 2024-08-01-preview 버전부터 사용이 가능합니다.

 


Structured outputs 사용 예시-1

pydanticd의 BaseModel을 사용해서, ChatGPT가 생성할 데이터의 구조를 만듭니다.
아래 예시에서는 CalendarEvent 클래스를 BaseModel을 상속해서 
name, date, participants 라는 데이터를 갖도록 만듭니다. 
 

chat.completion.parse 함수를 사용해서, gpt-4o를 호출하면서

response_format에 앞서 BaseModel을 상속해서 만든 CalendarEvent를 지정합니다.

 

 

from pydantic import BaseModel
from openai import AzureOpenAI

client = AzureOpenAI(
  azure_endpoint = "Azure Endpoint",
  api_key="API Key",
  api_version="2024-08-01-preview"
)

class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]

completion = client.beta.chat.completions.parse(
    model="gpt-4o", 
    messages=[
        {"role": "system", "content": "주어진 내용에서 행사 정보를 추출합니다."},
        {"role": "user", "content": "앨리스와 밥은 금요일에 과학 박람회에 갈 예정입니다."},
    ],
    response_format=CalendarEvent,
)

event = completion.choices[0].message.parsed

print(event)

 

이제 메시지를 살펴보면, 

System Message로는 주어진 내용에서 행사 정보를 추출하도록 되어 있고, 

User Message는 행사와 관련된 문장인 ' 앨리스와 밥은 금요일에 과학 박람회에 갈 예정입니다' 가 있습니다.

출력된 결과 값을 확인하면 response_format으로 지정한 CalendarEvent의 데이터 구조에 맞춰서

행사명(name), 날짜(Date), 참가자(participants) 행태로 구조화된 답변이 된 것을 볼 수 있습니다.

[실행 결과]

 

 

이번에는 문장을 조금더 복잡하게 테스트를 해보겠습니다. 

System Message에서는 추출을 한국어로 하도록 추가하였습니다.

def parse_chat(query: str) -> CalendarEvent:
    completion = client.beta.chat.completions.parse(
        model="gpt-4o", #
        messages=[
            {"role": "system", "content": "주어진 내용에서 행사 정보를 추출합니다.추출은 한국어로 합니다."},
            {"role": "user", "content": query},
        ],
        response_format=CalendarEvent,
    )
    return completion.choices[0].message.parsed

 

행사와 관련된 User Message에는 참가자와 요일에 대한 정보를 조금 헛갈릴 수 있도록 다음과 같이 정했습니다.

'앨리스와 밥과 존은 화요일에 카페에서 만나 목요일에 엑스포를 가기로 결정했는 데, 밥은 빼고 2명은 참석하기로 했다.'

query = "앨리스와 밥과 존은 화요일에 카페에서 만나 목요일에 엑스포를 가기로 결정했는 데, 밥은 빼고 2명은 참석하기로 했다."
event = parse_chat(query)
print(event)

 

이번에도 처음보다는 조금 더 복잡한 문장으로 메시지를 주었으나, 

다음과 같이 구조화 된 형태로 잘 찾아낸 것을 볼 수 있었습니다.

[실행 결과]

 


Structured outputs 사용 예시-2

Structured outputs을 사용해서, Tool 사용을 할 수도 있습니다. 
Tool에 사용 할 데이터 구조를 선언하기 위해서 BaseModel을 상속해서 
CurrentTime과 SearchBing이라는 클래스를 만들었습니다.
 
tools에는 각 클래스를 추가합니다.
그리고, 실제 동작할 Function을 만드는 데, 여기에서는 동작 구조만 볼 것이기 때문에
임의 값을 리턴하도록 함수를 선언했습니다.
from pydantic import BaseModel
from openai import AzureOpenAI

client = AzureOpenAI(
  azure_endpoint = "Azure Endpoint",
  api_key="API Key",
  api_version="2024-08-01-preview"
)

class CurrentTime(BaseModel):
    city: str

class SearchBing(BaseModel):
    search_word: str

tools = [openai.pydantic_function_tool(CurrentTime), openai.pydantic_function_tool(SearchBing)]

def getCurrentTime(city: str) -> str:
    return f"현재 {city}의 시간은 10시입니다."

def getSearchResult(search_word: str) -> str:
    return f"{search_word}에 대한 검색 결과입니다."

 

Message에서는 워싱턴의 시간은 몇 시인지 확인하도록 하였고, 

chat.completions에서 위에서 생성한 tools를 추가하였습니다.

그리고, 결과 값에서 Fuction의 name과 arguments를 확인해보면, 

messages = []
messages.append({"role": "system", "content": "당신은 유용한 도우미입니다. 제공된 도구를 사용하여 사용자를 지원하세요."})
messages.append({"role": "user", "content": "워싱턴의 시간은 몇시인간요?"})

response = client.chat.completions.create(
    model="gpt-4o", # replace with the model deployment name of your gpt-4o 2024-08-06 deployment
    messages=messages,
    tools=tools
)

func_name = response.choices[0].message.tool_calls[0].function.name
func_args = json.loads(response.choices[0].message.tool_calls[0].function.arguments)

print(f"Function Name : {func_name}")
print(f"Function Arguments : {func_args}")

 

앞서 BaseModel을 상속해서 만든 Class 중에서 현재 시간과 관련이 있는 CurrentTime 클래스 이름과

해당 클래스엔 선언된 city 라는 데이터에 User Message에서 물었던 '워싱턴'이라는 도시가 있는 것을 볼 수 있습니다.

간단하게, Class로 어떤 함수를 호출 할 것인지 확인하여

argments로 받아온 값으로 함수를 호출할 수 있습니다.

if func_name == "CurrentTime":
    print(getCurrentTime(func_args["city"]))
elif func_name == "SearchBing":
    print(getSearchResult(func_args["search_word"]))

 

함수 호출에 대한 결과도 잘 나오는 것을 볼 수 있습니다.

 

이번에는 User Message를 검색과 관련된 함수를 호출하도록 변경하였습니다.

messages = []
messages.append({"role": "system", "content": "당신은 유용한 도우미입니다. 제공된 도구를 사용하여 사용자를 지원하세요."})
messages.append({"role": "user", "content": "gpt search가 어떤거야?"})

response = client.chat.completions.create(
    model="gpt-4o", # replace with the model deployment name of your gpt-4o 2024-08-06 deployment
    messages=messages,
    tools=tools
)

func_name = response.choices[0].message.tool_calls[0].function.name
func_args = json.loads(response.choices[0].message.tool_calls[0].function.arguments)

print(f"Function Name : {func_name}")
print(f"Function Arguments : {func_args}")

 

이번에는 SearchBing이라는 클래스를 가져오고, 

해당 SearchBing에 선언된 데이타인, search_word에

User Message에 포함된 GPT Search라는 값을 매핑하여 잘 가져온 것을 볼 수 있습니다.

 

동일하게 class명으로 호출할 함수명을 확인하여, 호출할 수 있습니다.

if func_name == "CurrentTime":
    print(getCurrentTime(func_args["city"]))
elif func_name == "SearchBing":
    print(getSearchResult(func_args["search_word"]))

 

결괏 값도 정상적으로 잘 호출된 것을 볼 수 있습니다.

 


Structured outputs 사용 예시-3

Structured outputs는 비구조화 데이터를 구조화 된 형태로 출력하기 위해서도 사용 할 수 있습니다. 

논문의 내용을 '제목', '저자', '요약', '키워드' 형태로 정리하려고 합니다.

BaseModel을 상속해서,  '제목', '저자', '요약', '키워드' 를 저장하기 위한 구조를 선언합니다.

System Message에서는 비정형 데이터를 구조화 하도록 적절하게 선언해주었습니다.

User Message는 논문의 내용을 갖고 있는 변수있습니다.

웹페이지나, 문서 파일 등에서 눈문의 내용을 가져오시면 됩니다.

response_format 값에 위에서 선언한 ResearchPaperExtraction을 연결해 줍니다.

class ResearchPaperExtraction(BaseModel):
    title: str
    authors: list[str]
    abstract: str
    keywords: list[str]

system_msg = """당신은 구조화된 데이터 추출 전문가입니다.
연구 논문의 비구조화된 텍스트를 제공받으면, 이를 주어진 구조로 변환해야 합니다.
주어진 구조로 변환 시에 내용은 한국어로 정리합니다."""

completion = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": system_msg},
        {"role": "user", "content": paper_text}
    ],
    response_format=ResearchPaperExtraction,
)

research_paper = completion.choices[0].message.parsed

print(f"논문 제목 : {research_paper.title}")
print(f"논문 저자 : {research_paper.authors}")
print(f"논문 요약 : {research_paper.abstract}")
print(f"논문 키워드 : {research_paper.keywords}")

 

이제 결괏 값을 확인해 보면, 텍스트 형태의 논문이 

'제목, '저자', '요약', '키워드' 형태로 잘 구조화된 형태로 가져올 수 있음을 확인 할 수 있습니다.

 

 


Structured outputs 사용 예시-4

Structured outputs를 사용해서 Chain of Thought(CoT)와 같은 방식을 구현할 수도 있습니다.

Chain of Thought를 위해서 Model이 생각하는 단계를 Step이라는 클래스로 생성합니다. 

그리고, Step을 리스트 형태로 값고 있는 MathReasoning 클래스를 만들어서, 

이를 response_format 형태로 사용하여, 

모델이 추론식에 단계적으로 풀어나갈 수 있도록 구조를 잡고, 최종 답을 찾아내게 합니다.

class Step(BaseModel):
    explanation: str
    output: str

class MathReasoning(BaseModel):
    steps: list[Step]
    final_answer: str

completion = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "당신은 유용한 수학 교사입니다. 사용자에게 단계별로 해결 방법을 안내하세요.모든 답변은 한국어로 해주세요."},
        {"role": "user", "content": "8x + 7 = -23을 어떻게 풀 수 있나요?"}
    ],
    response_format=MathReasoning,
)

math_reasoning = completion.choices[0].message.parsed

print(math_reasoning)

 

생성된 답변을 확인해보면 다음과 같이 

User Message의 계산 문제를 단계적으로 하나씩 해결해 나간 것을 볼 수 있습니다.

 


Structured outputs 사용 예시-5

Structured outputs에서 CoT 예시와 같이 중첩을 사용하여 데이터 구조를 선언 할 수도 있습니다.
 - 최대 5 Level, 100개의 속성 
 
아래 예시에서는
Profile이라는 구조를 선언 할 때, Community_Title이라는 데이터 구조를 List형태로 갖도록 했습니다.
긜고. Profile을 response_format으로 선언합니다.
class Community_Title(BaseModel):
    company : str
    title_name : str

class Profile(BaseModel):
    name: str
    community_title: List[Community_Title]
    blog : str

def parse_profile(query: str) -> Profile:
    completion = client.beta.chat.completions.parse(
        model="gpt-4o", #
        messages=[
            {"role": "system", "content": "사용자 프로파일을 만드는 유용한 도우미입니다.추출은 한국어로 합니다."},
            {"role": "user", "content": query},
        ],
        response_format=Profile,
    )
    return completion.choices[0].message.parsed

 

User Message로 사용 할 query 값에 사용자에 대한 소개를 작성하여, 

Structured outputs를 활용하여, 답변을 생성한 후

Structured outputs로 선언한 구조대로 출력을 해봅니다.

query = """ZIGI라는 닉네임을 가진 고재성은
zigispace.net 블로그를 운영하고 있습니다.
커뮤니티 활동으로는 Microsoft의 MVP와, AWS의 Community Builder를 하고 있습니다.
연락을 위해서는 010-1234-5678,
zigi@zigispace.net 으로 하시면 됩니다."""
profile = parse_profile(query)

print(f"Name : {profile.name}")
for title in profile.community_title:
    print("Community Title")
    print(f"- Company : {title.company}")
    print(f"- Title : {title.title_name}")
print(f"Blog : {profile.blog}")

 

그러면, 다음과 같이 원하는 구조대로 답변이 출력되는 것을 볼 수 있으며

Community Title의 경우에는 중첩된 구조대로 잘 구조화 된 것도 확인 할 수 있습니다.