(Spring) 취향 기반 향수 추천 서비스 - 9. Chat GPT API 연동 (향수 스토리 만들기)

김준석·2023년 5월 11일
0

향수 추천 서비스

목록 보기
10/21

목표

졸업 작품 메인 기능 3개 외에 Sub기능으로 Chat Gpt를 연동하여 사용자만의 스토리를 만들어 주려고 구현했다.

Chat Gpt란?

Open Ai에서 만든 자연 언어 처리 모델로, GPT-3.5 언어 기술을 기반으로 하여, 어떤 텍스트가 주어졌을 때 다음 텍스트가 무엇인지까지 예측하며 글을 생성할 수 있는 모델을 사용한다.

Api Key 발급


api key 발급 사이트
해당 사이트에 접속하여 Api-key를 발급받으면 된다. 당연하게도 api key는 노출되면 안되므로 보안을 확실히 해야한다.

Completion 모델 사용

대략 400Byte 이내의 스토리를 생성할 것이라서 completion 모델을 사용하였다.


해당 EndPoint로 요청을 보내면 된다.

Request Body

Body에는 여러가지가 있는데,
나는 model, prompt, maxTokens, temperature, topP 를 사용하였다.

  • model : Gpt 모델중 어떤 것을 사용할 지 정하는 변수이다. 나는 text-davinci-003을 사용하였다.
  • max_tokens : 내용을 생성할때 쓸 토큰의 최댓값을 정하는 것이다.
  • temperature - 0~2 사이의 값으로 설정하며, 생성된 텍스트에서 다음 단어를 선택하는 데 영향을 미치는 랜덤성의 정도를 결정한다.
  • top_p : 생성된 텍스트에서 모델이 고려할 후보 단어 집합의 크기를 결정하는 값이다. 0~1사이의 값으로 설정한다.

Config

ChatGptConfig.java

@Configuration
public class ChatGptConfig {
    public static final String AUTHORIZATION = "Authorization";
    public static final String BEARER = "Bearer ";
    public static final String MODEL = "text-davinci-003";
    public static final Integer MAX_TOKEN = 400;
    public static final Double TEMPERATURE = 0.0;
    public static final Double TOP_P = 1.0;
    public static final String MEDIA_TYPE = "application/json; charset=UTF-8";
    public static final String URL = "https://api.openai.com/v1/completions";

}

config는 다음과 같이 설정을 하였다.

Request Dto

ChatGptRequest.java

@Getter
public class ChatGptRequest implements Serializable {

    private String model;
    private String prompt;
    @JsonProperty("max_tokens")
    private Integer maxTokens;
    private Double temperature;
    @JsonProperty("top_p")
    private Double topP;

    public ChatGptRequest(){}
    @Builder
    public ChatGptRequest(String model, String prompt, Integer maxTokens, Double temperature, Double topP) {
        this.model = model;
        
        this.prompt = prompt;
        this.maxTokens = maxTokens;
        this.temperature = temperature;
        this.topP = topP;
    }
}

gpt Server에 요청을 보내게 설정한 Dto이다.
변수명을 다르게 설정할 시 요청이 제대로 가지 않는다.

  • PerfumeStoryRequest.java
@Getter
public class PerfumeStoryRequest implements Serializable {
    private String name;

    private String scentAnswer;

    private String moodAnswer;

    private String seasonAnswer;

    private String styleAnswer;

    public PerfumeStoryRequest(){}

    @Builder
    public PerfumeStoryRequest(String name, String scentAnswer, String moodAnswer, String seasonAnswer, String styleAnswer) {
        this.name = name;
        this.scentAnswer = scentAnswer;
        this.moodAnswer = moodAnswer;
        this.seasonAnswer = seasonAnswer;
        this.styleAnswer = styleAnswer;
    }

    public String toPromptString() {
        return "제가 제시할 단어는 다음과 같습니다. " +
                "사람 이름: " + name +
                ", 향수 노트: " + scentAnswer +
                ", 분위기: " + moodAnswer +
                ", 계절: " + seasonAnswer +
                ", 스타일: " + styleAnswer + "." +
                "해당 5가지의 단어들을 조합하여 400byte 이내의 짧은 스토리를 만들어주세요";
    }
}
  • Gpt 서버로 어떤 내용을 보낼지에 관한 RequestDto이다. 이름, 향, 무드, 계절, 옷스타일을 담아 조합하여 스토리를 생성할 것이다.

Response Dto

  • Choice.java
@Getter
public class Choice implements Serializable {
    private String text;
    private Integer index;
    @JsonProperty("finish_reason")
    private String finishReason;

    public Choice(){}

    @Builder
    public Choice(String text, Integer index, String finishReason) {
        this.text = text;
        this.index = index;
        this.finishReason = finishReason;
    }
}

Gpt Server로부터 choice 객체에 질문에 대한 응답값이 들어온다.

  • ChatGptResponse.java
@Getter
@NoArgsConstructor
public class ChatGptResponse implements Serializable {
    private String id;
    private String object;
    private LocalDate created;
    private String model;
    private List<Choice> choices;


    @Builder
    public ChatGptResponse(String id, String object,LocalDate created, String model, List<Choice> choices) {
        this.id = id;
        this.object = object;
        this.created = created;
        this.choices = choices;
        this.model = model;
    }
}

앞의 Choice객체를 List에 담아서 응답받는다.

Service

  • PerfumeStoryService.java
@Service
public class PerfumeStoryService {
    @Value("${gpt.apiKey}")
    private String API_KEY;
    private static RestTemplate restTemplate = new RestTemplate();

    public HttpEntity<ChatGptRequest> createHttpEntity(ChatGptRequest chatGptRequest) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType(ChatGptConfig.MEDIA_TYPE));
        headers.add(ChatGptConfig.AUTHORIZATION, ChatGptConfig.BEARER + API_KEY);
        return new HttpEntity<>(chatGptRequest, headers);
    }

    public ChatGptResponse getResponse(HttpEntity<ChatGptRequest> chatGptRequest) {
        ResponseEntity<ChatGptResponse> responseEntity = restTemplate.postForEntity(
                ChatGptConfig.URL,
                chatGptRequest,
                ChatGptResponse.class);
        if (isGptCannotResponse(responseEntity)) {
            throw new GptCannotMakeStoryException();
        }
        return responseEntity.getBody();
    }

    public ChatGptResponse askQuestionToChatGpt(PerfumeStoryRequest perfumeStoryRequest) {
        return this.getResponse(
                this.createHttpEntity(
                        ChatGptRequest.builder()
                                .model(ChatGptConfig.MODEL)
                                .prompt(perfumeStoryRequest.toPromptString())
                                .maxTokens(ChatGptConfig.MAX_TOKEN)
                                .temperature(ChatGptConfig.TEMPERATURE)
                                .topP(ChatGptConfig.TOP_P)
                                .build()));
    }

    public boolean isGptCannotResponse(HttpEntity<ChatGptResponse> chatGptResponseEntity) {
        if (chatGptResponseEntity.getBody().getChoices().isEmpty() || chatGptResponseEntity.getBody().getChoices() == null) {
            return true;
        }
        return false;
    }
}
  • Api Key는 .yml 파일에서 따로 관리해줬다.
  • createHttpEntity()를 통해 ContentType와 Authorization Bearer을 담아 헤더를 생성했다.
  • getResponse를 통해 responseBody를 생성하였다.
  • aksQuestionToChatGpt는 Gpt에 request를 보내고 응답을 받는 메서드이다

Controller

@Slf4j
@RestController
@RequestMapping("/perfume")
public class PerfumeStoryController implements PerfumeStoryControllerDocs {
    private final PerfumeStoryService perfumeStoryService;

    public PerfumeStoryController(PerfumeStoryService perfumeStoryService) {
        this.perfumeStoryService = perfumeStoryService;
    }

    @PostMapping("/make-story")
    public ResponseEntity<ChatGptResponse> makePerfumeStory(@RequestBody final PerfumeStoryRequest perfumeStoryRequest) {
        log.info("Chat GPT에게 향수 스토리 생성 요청, 질문 내용 : {}",perfumeStoryRequest.toPromptString());
        return ResponseEntity.ok(perfumeStoryService.askQuestionToChatGpt(perfumeStoryRequest));
    }

}

테스트 결과

느낀 점

공식 문서에 설명이 잘 나와있어서 개발하는 데 어렵지 않았다. 근데 변수명을 맞추지 않아서 계속 Choice가 null로 들어왔었다 ㅜ.. 새로운 개념이나 기술들을 거부감 없이 잘 받아들일 수 있는 덕목을 가지는 것이 좋은 개발자가 되는 길이라고 생각한다! 화이팅구리

profile
기록하면서 성장하기!

0개의 댓글