GPT API의 응답 값을 원하는 형태로 만들어보자 (with. Chat Completion API & Fine-Tuning)

 

 

졸업작품 프로젝트에서 마주한 문제

 

졸업작품 프로젝트 ‘코리’를 개발하는 과정에서 GPT API를 사용하여 AI로부터 원하는 응답값을 얻어오는 기능을 구현하던 중 너무나도 어지러운 문제에 직면했다.

코리는 GPT를 사용해서 개발자들의 전반적인 작업에 도움을 주는 어시스턴트 서비스이다.

‘코드 리팩토링’, ‘변수명 추천’, ‘주석을 통한 코드 설명’ 기능이 존재하는데, 이를 구현하기 위해 SpringBoot 환경에서 HttpClient를 사용하여 GPT API에서 제공하는 gpt-3.5-turbo 모델과 통신하여 응답 값을 얻어오는 것 까지는 성공했다.

하지만 여기서부터 몇 가지 문제에 직면하면서 고민이 시작되었다.

 

 


 

문제1. 일정하지 않은 응답 형태

 

 

GPT 사이트에 접속하여 질문을 던질 때, 질문을 조금만 다르게 보내도 GPT가 응답하는 형식이 계속 바뀌었으며, 정해진 형식 없이 텍스트로 나열하는 형태였다.

아무래도 프로덕트를 구성한다면, 이렇게 얻어온 GPT의 응답 값을 그대로 사용하는 것은 유저 경험과 프로덕트의 완성도 측면에서 문제가 된다고 생각했다.

 

 


 

문제2. 유효하지 않은 요청도 어떻게든 응답함

 

 

 

GPT에게 Java 코드를 Python 코드라고 말하면서 주석 설명을 붙여달라고 요청해보았다.

말도 안되는 요청이지만 GPT는 어떻게든 응답을 쥐어짜내어 Java코드를 Python 코드로 변경하고, 주석을 달아서 응답하였다.

사실 이건 요청, 응답 모두 유효하지 않은 경우이다.

GPT API는 돈을 내고 사용하는 것이기 때문에 이렇게 유효하지 않은 요청과 응답을 주고받는 것은 의미없는 비용만 낭비하게 되는 것이다.

이렇게 유효하지 않은 요청 상황이 발생하면 예외처리를 할 수단이 필요하다고 느껴졌다.

 

 


 

Chat Completion API

 

위에서 나열한 문제들을 해결하기 위해 선택한 방법은 Chat Completion API 이다.

Chat Completion API를 사용하면 대화 형식으로 GPT와 응답을 주고받을 수 있는데, 이 방식을 통해 assistant에게 가상의 역할을 부여하고, 응답 형태를 알려줄 수 있어서 문제를 해결할 수 있을 것으로 생각했다.

 

 


 

공식 문서

💡 채팅 모델은 메시지 목록을 입력으로 받고 모델이 생성한 메시지를 출력으로 반환합니다. 채팅 형식은 여러 차례에 걸친 대화를 쉽게 할 수 있도록 설계되었지만, 대화가 없는 단일 턴 작업에도 유용합니다.

 

라는 Chat Completion API에 대한 공식문서의 내용을 읽을 수 있었다.

Chat Completion 방식은 우리가 일반적으로 웹사이트에 접속하여 사용하는 GPT 처럼 이전에 대화한 내용을 통해 응답값을 유추하는 방식과 같은 방식으로, 우리는 이 API를 통해 이전 요청, 응답 내용을 알려줄 수 있고 이를 바탕으로 다음 대답을 생성하도록 유도할 수 있다.

 

예시를 통해 살펴보자

 

 

우리가 무턱대고 GPT에게 “그것이 어디서 진행되었나?” 라는 질문을 한다면 GPT는 이상한 답변을 내놓게 될 것이다.

하지만 Chat Completion 방식을 통해 “그것이 어디서 진행되었나?” 라는 질문 이전 ‘2020 World Series’에 대한 대화의 문맥이 포함된 대화뭉치를 제공한다면 GPT는 훨씬 정확하고 수월한 답을 내놓을 수 있게 되는 것이다.

이것이 Completion 방식의 기본 개념이다.

 

 


 

Chat Completion Role

 

GPT의 Chat Completion에는 3가지 Role이 존재한다.

 

system Role (시스템 역할):

  • "system" 역할은 API 요청에서 GPT-3 모델에게 주어지는 지시 또는 문맥을 정의한다.
  • 이 역할은 모델이 요청에 대답할 때 원하는 행동을 나타내며, 일반적으로 문맥을 설정하고 대화 스타일의 상호 작용을 정의하는 데 사용된다.
  • system Role을 통해 model에게 너는 ‘어떤 역할인지’, ‘어떻게 답변해야하는 지’ 등을 알려주어 대화 스타일을 지정할 수 있었다. 예를 들어 “말 끝마다 ^^를 붙여서 대답해” 라고 하면 모든 응답의 끝에 “^^”를 붙여서 응답하게 된다.

 

user Role (사용자 역할):

  • "user" 역할은 API 요청에서 사용자의 입력 또는 질문을 정의한다.
  • 이 역할은 모델이 질문에 답하거나 특정 주제에 대한 정보를 생성하는 데 사용된다. 쉽게말해 사용자는 "user" 역할을 통해 모델에게 질문하거나 정보를 요청할 수 있다.

 

assistant Role (응답자 역할):

  • 요청에 대한 AI model의 응답을 전달하는 응답자 역할입니다.

 

그리고 user Role을 통해 model에게 질문을 전송하면, model은 system으로부터 부여받은 내용을 통해 응답을 생성하여 assistant 역할로 return한다.

나는 이 방식이 응답 형태를 고정하는 하나의 열쇠라고 생각했다.
때문에 Chat Completion 방식을 어떻게 활용하여 API 호출에 응용할 수 있을 지 연구해보기로 결정했다.

 

 


 

Fine Tuning

Chat Completion 방식을 API 호출 과정에서 응용할 수 있는 해결책을 찾는 과정에서 Fine Tuning이라는 것을 알게 되었다.

 

‘미세 조정' 이라는 뜻을 가진 Fine Tuning은 어떤 요청을 받았을 때 어떤 형식으로 응답을 해야하는 지에 대한 데이터셋을
Chat Completion API 방식으로 만들어서 GPT 모델에 학습시키면 원하는 출력 포맷팅을 학습시킬 수 있었다.

 

 


 

공식문서

 

 

공식문서에 나와있듯이 Fine Tuning을 통해 믿을 수 있는 출력 포맷팅 이라는 이점을 얻을 수 있었다.

즉 학습 시킨대로 출력 형태를 지정할 수 있다는 뜻이었다.

 

 


 

Fine-Tuning 가능 모델

 

 

  • 현재 공식 문서에서 말하는 Fine-Tuning이 가능한 모델은 이렇게 3가지가 존재하는데
  • 나는 애초에 gpt-3.5-turbo 모델을 사용하고 있기도 했고, 가장 권장하는 모델이기에 gpt-3.5-turbo-0613을 택했다.
뒤에 0613은 해당 모델의 릴리즈 날짜인데, 이는 21년 6월 13일에 릴리즈 되었음을 뜻한다. 다음 모델이 릴리즈 되면 뒤에 숫자는 바뀔 수 있으니 참고하자.

 

 


 

학습시킬 대화 형식을 담은 파일 준비

 

학습파일

학습 파일은 Completions API 형태이며, 무조건 jsonl 형식여야함

 

학습파일 예시

{"messages": [{"role": "system", "content": "당신은 java 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "자동차"}, {"role": "assistant", "content": "1. car\n2. automobile\n3. vehicle\n4. motorcar\n5. auto\n6. ride\n7. wheels\n8. transport\n9. machine\n10. drive"}]}
{"messages": [{"role": "system", "content": "당신은 python 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "자동차"}, {"role": "assistant", "content": "1. car\n2. automobile\n3. vehicle\n4. motorcar\n5. auto\n6. ride\n7. wheels\n8. transport\n9. machine\n10. drive"}]}
{"messages": [{"role": "system", "content": "당신은 java 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "게임결과"}, {"role": "assistant", "content": "1. gameResult\n2. resultOfGame\n3. gameOutcome\n4. gameScore\n5. gameRecord\n6. gameStatistics\n7. gameAchievement\n8. gameProgress\n9. gameEvaluation\n10. gameSummary"}]}
{"messages": [{"role": "system", "content": "당신은 python 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "게임결과"}, {"role": "assistant", "content": "1. game_result\n2. result_game\n3. gameOutcome\n4. gameScore\n5. game_result_1\n6. game_result_2\n7. game_result_3\n8. game_result_4\n9. game_result_5\n10. game_result_final"}]}
{"messages": [{"role": "system", "content": "당신은 javascript 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "던전입구"}, {"role": "assistant", "content": "1. dungeonEntrance\n2. entranceToDungeon\n3. dungeonGate\n4. dungeonEntry\n5. dungeonAccess\n6. entranceOfDungeon\n7. dungeonIngress\n8. gatewayToDungeon\n9. dungeonOpening\n10. dungeonPortal"}]}
{"messages": [{"role": "system", "content": "당신은 go 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "던전입구"}, {"role": "assistant", "content": "1. dungeonEntrance\n2. entranceToDungeon\n3. dungeonGate\n4. dungeonEntry\n5. gateToDungeon\n6. dungeonAccess\n7. entranceOfDungeon\n8. dungeonIngress\n9. dungeonPortal\n10. dungeonWaypoint"}]}
{"messages": [{"role": "system", "content": "당신은 typescript 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "던전입구"}, {"role": "assistant", "content": "1. dungeonEntrance\n2. entranceToDungeon\n3. dungeonGate\n4. dungeonAccess\n5. dungeonEntry\n6. entranceOfDungeon\n7. dungeonIngress\n8. gatewayToDungeon\n9. dungeonOpening\n10. dungeonPortal"}]}
{"messages": [{"role": "system", "content": "당신은 ruby 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "가게"}, {"role": "assistant", "content": "1. store\n2. shop\n3. market\n4. boutique\n5. emporium\n6. outlet\n7. establishment\n8. storehouse\n9. mart\n10. bazaar"}]}
{"messages": [{"role": "system", "content": "당신은 java 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "로또"}, {"role": "assistant", "content": "1. lotto\n2. lottery\n3. luckyNumber\n4. drawResult\n5. jackpot\n6. winningTicket\n7. numberSelection\n8. prizeMoney\n9. ticketPurchase\n10. randomPick"}]}
{"messages": [{"role": "system", "content": "당신은 python 언어 변수명 추천 전문가이다. 단어를 입력받으면 그 단어의 영어 변수명을 10개 생성하여 출력해라"}, {"role": "user", "content": "유저 위치"}, {"role": "assistant", "content": "1. user_location\n2. location_user\n3. userPos\n4. user_place\n5. userLoc\n6. position_user\n7. user_location_data\n8. user_location_info\n9. user_position\n10. user_geo"}]}

//...
 

 

 

유효하지 않은 요청 처리 예시

{"messages": [{"role": "system", "content": "당신은 java 언어로 작성된 코드를 받으면 이를 분석하여 설명 주석을 달아주는 코드 전문가이다. 만약 받은 코드가 java 언어의 문법이 아니거나 java라는 언어가 존재하지 않다면 X를 출력하고, 받은 코드가 java 언어로 작성된 것이 맞다면 각 코드 라인의 윗줄에 코드 기능 설명을 주석으로 언어 형식에 맞는 주석으로 달아라."}, {"role": "user", "content": "a = int(input())\nprint(a)\n"}, {"role": "assistant", "content": "X"}]}

 

위에서 언급했던 유효하지 않은 요청에 대해서는 ‘X’ 를 응답하도록 학습시켰다.

서버 내에서는 응답값이 ‘X’ 이면 그에 따라 예외처리를 수행해줄 것이다.

 

 


 

학습 파일 업로드 API 호출

curl https://api.openai.com/v1/files \
-H "Authorization: Bearer ${GPT-API-KEY}" \
-F purpose="fine-tune" \
-F file="@학습파일명.jsonl"

 

학습파일이 준비되었다면 학습파일 업로드 API(https://api.openai.com/v1/files) 를 호출하여 준비된 학습파일을 OpenAI에 업로드해야한다.

이때 GPT API KEY가 사용되는데, 이 KEY는 유료로 사용할 수 있다.

 

 


 

응답값

{
  "object": "file",
  "id": "학습파일ID",
  "purpose": "fine-tune",
  "filename": "학습파일명.jsonl",
  "bytes": 4372,
  "created_at": 1697785733,
  "status": "uploaded",
  "status_details": null
}
  • 여기서 사용할 내용은 학습파일ID이다.
  • 이는 OpenAI에 학습파일이 등록된 ID를 뜻하며 이를 통해 파인튜닝을 시작하거나 종료하는 등의 작업을 수행할 수 있다.

 

 


 

파인튜닝 시작

curl https://api.openai.com/v1/fine_tuning/jobs \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GPT-API-KEY}" \
-d '{
"training_file": "학습파일ID",
"model": "gpt-3.5-turbo-0613"
}'
 

학습파일ID과 FineTuning할 모델명을 포함한 curl요청으로 파인튜닝을 시작할 수 있다.

 

 

 

  • 파인 튜닝의 진행상황은 OpenAI Playground에서 실시간 확인이 가능하다
  • 위 FineTuning 과정은 Playground웹페이지에서 UI로 간편하게 진행이 가능하다.

 

 


 

Fine-Tuning 완료

 

 

Fine-Tuning이 완료되면 이렇게 튜닝된 모델의 이름이 출력된다!

이제 이 모델을 사용하기만 하면 된다.

 

 


 

Fine-Tuning gpt-3.5-turbo VS Default gpt-3.5-turbo 비교

 

파인튜닝이 정상적으로 완료되어서 우리가 원하던 고정된 형태의 응답값을 얻을 수 있는 지 검증하기 위해서 기존의 GPT 기본 모델과 파인튜닝한 모델의 ‘응답 형태’ 를 비교해보았다.

 

 


 

변수명 추천

 

gpt-3.5-turbo 모델

 

 

[응답 형태]

고정되지 않은 형태의 응답 형태이며 매 요청마다 달라진다.

 

 

gpt-3.5-turbo FineTuning 모델

 

 

 
 

[응답 형태]

모든 요청에서 항상 고정된 형태의 응답 형태이며, 언어의 종류에 따라 각 언어의 변수명 네이밍 패턴에 맞게 작명한다.

 

 


 

코드 리팩터링

 

gpt-3.5-turbo 모델

 

 

 

 

gpt-3.5-turbo FineTuning 모델

 

 

데이터셋을 통해 리팩토링된 코드를 출력하고 마지막에 리팩토링 내용 요약을 출력하도록 학습시켰다. 한 눈에 들어오는 깔끔하고 통일성있는 응답결과로 변한 것을 확인할 수 있었다.

 

 

gpt-3.5-turbo FineTuning 모델 - 오류 처리

 

 

모델을 학습하는 데이터셋에 유저가 전송한 코드와 언어가 매칭되지 않으면 ‘X’라는 응답 결과만을 Response하도록 학습시켰다.

서버에서는 요청 결과로 ‘X’를 Response받으면 다음과 같이 예외를 발생시키도록 구현하였다.

 

 


 

주석 달기

 

gpt-3.5-turbo 모델

 

 

 

gpt-3.5-turbo FineTuning 모델

 

설명 주석 달기 기능은 “입니다.” 등의 표현없이 단답으로 깔끔하게 응답하도록 만든 부분 외에 많이 다른 점이 없다.

 

 

gpt-3.5-turbo FineTuning 모델 - 오류 처리

 

 

리팩토링 기능의 오류처리와 동일하게 모델을 학습하는 데이터셋에 유저가 전송한 코드와 언어가 매칭되지 않으면 서버에서는 다음과 같은 예외를 발생시키도록 구현하였다.

 

 


 

사용 비용 (Cost)

FineTuning을 하면, 튜닝 중에 발생하는 초기 훈련 비용과 사용 비용 두 가지의 비용이 분류된다.

아래에는 공식문서에서 볼 수 있는 가격표이니 확인하길 바란다.