OpenAI

[Spring] Spring Boot + OpenAI 도입기 (Chat Completions)

immersive 2024. 9. 1. 15:51

1. Chat Completions이란?

"Chat Completions" API는 사용자가 대화형 AI 모델과 상호작용할 수 있도록 OpenAI에서 제공해 주는 기능입니다. Chat Completions API는 "사용자가 입력한 대화의 맥락"에 따라 응답을 생성합니다. API는 대화의 맥락을 이해하고 이전 메시지들을 고려하여 다음 응답을 생성합니다. 여기서 "사용자가 입력한 대화의 맥락"에 주목해야 합니다!

2. 구현 개요

OpenAI 공식 문서

OpenAI의 Completions API 공식 문서를 살펴보면 위와 같이 API 호출 시 보내야 하는 데이터가 크게 두 가지가 있습니다.

  • model: 사용하고자 하는 모델의 이름
    • gpt-3.5-turbo: 2021년 10월 데이터까지 학습
    • gpt-4o-mini: 2023년 10월 데이터까지 학습 (가장 저렴하고 성능이 좋은 모델)
  • messages: "대화의 맥락"을 제공하는 데이터
    • role: 메시지의 역할 ex) system, user, assistant
      • system: 모델의 동작 방식을 지시하는 역할
      • user: 사용자가 입력하는 메시지
      • assistant: 모델이 생성하는 응답
    • content: 메시지의 실제 내용

위에서 언급한 바와 같이 Completions API는 "사용자가 입력한 대화의 맥락"에 따라 새로운 응답을 생성합니다. 따라서, 이전에 내가 입력한 채팅 내역(user), 이에 대한 응답 내역(assistant), 모델의 동작 방식을 지시하는 내역(system)을 모두 포함하여 Completions API를 호출해야 합니다.

구현 개요

위 사진과 같이 Spring Boot 프로젝트에서 OpenAI에서 제공하는 서비스를 도입할 때, Service 단에서 RestTemplate을 통해 Chat Completions API를 호출합니다.

3. 환경 설정

- application.yml

ChatGPT:
  model: gpt-4o-mini
  api-key: ${api-key}
  api-url:
    completion: https://api.openai.com/v1/chat/completions

application.yml 에 OpenAI에서 발급 받은 키와 Completions API를 호출하기 위한 엔드포인트를 명시해 줍니다.

 

- ChatGPTConfig.java

@Getter
@Configuration
public class ChatGPTConfig {

    @Value("${ChatGPT.model}")
    private String model;

    @Value("${ChatGPT.api-url.completion}")
    private String completionApiUrl;

    @Value("${ChatGPT.api-key}")
    private String openaiApiKey;

    @Bean
    public RestTemplate completionTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getInterceptors().add((request, body, execution) -> {
            request.getHeaders().add("Authorization", "Bearer " + openaiApiKey);
            return execution.execute(request, body);
        } );

        return restTemplate;
    }
}

ChatGPTConfig 클래스에서 OpenAI와 관련된 설정을 명시해 줍니다. 또한, OpenAI에게 요청을 보내기 위한 RestTemplate을 미리 정의해 줍니다. RestTemplate의 헤더에는 OpenAI에서 발급받은 api-key를 포함해야 합니다.

4. 구현

- Presentation 계층

@PostMapping("/{chatRoomId}/message/v1")
@MemberOnly
public ResponseEntity<ChatResponse> createMessageV1(
        @Auth final Accessor accessor,
        @PathVariable final Long chatRoomId,
        @RequestBody @Valid final CreateMessageRequest request
){
    log.info("memberId={}의 채팅 메시지 생성 요청이 들어왔습니다. (V1)", accessor.getMemberId());
    final ChatResponse chatMessageResponse = chatService.createMessageV1(
            accessor.getMemberId(),
            chatRoomId,
            request
    );
    return ResponseEntity.ok().body(chatMessageResponse);
}

Controller에서는 사용자 정보와 Client에서 보낸 채팅방 ID와 메시지를 토대로 서비스의 createMessageV1 메서드를 호출합니다.

 

- Service 계층

public ChatResponse createMessageV1(
        final Long memberId,
        final Long chatRoomId,
        final CreateMessageRequest request) {
    final ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
            .orElseThrow(() -> new BadRequestException(NOT_FOUND_CHAT_ROOM));

    final Member member = memberRepository.findById(memberId)
            .orElseThrow(() -> new BadRequestException(NOT_FOUND_MEMBER));

    final Message sentMessage = new Message(chatRoom, request.getMessage(), MEMBER);
    messageRepository.save(sentMessage);

    final String receivedMessage = getResponse(request.getMessage(), chatRoomId);
    messageRepository.save(new Message(chatRoom, receivedMessage, AURORA_AI));

    return ChatResponse.of(receivedMessage);
}
private String getResponse(final String prompt, final Long chatRoomId) {
    log.info("프롬프트 수행");
    CompletionRequest request;
    if(!messageRepository.existsByChatRoomId(chatRoomId)){      // 처음 텍스트 생성 (이전 채팅 내역 존재X)
        request = new CompletionRequest(config.getModel(), prompt);
    } else {    // 이전 채팅 내역 추가
        final List<CompletionMessage> chatGPTMessages = new ArrayList<>();
        final List<Message> messages = messageRepository.findAllByChatRoomId(chatRoomId);
        for(final Message message : messages){
            CompletionMessage historyMessage = null;
            if(message.getSenderType().equals(AURORA_AI)){
                historyMessage = new CompletionMessage(ASSISTANT.value(), message.getContents());
            } else if (message.getSenderType().equals(MEMBER)) {
                historyMessage = new CompletionMessage(USER.value(), message.getContents());
            }
            chatGPTMessages.add(historyMessage);
        }
        request = new CompletionRequest(config.getModel(), chatGPTMessages, prompt);
    }

    log.info("RestTemplate으로 ChatGPT API에 POST 요청");
    CompletionResponse completionResponse = config.completionTemplate().postForObject(
            config.getCompletionApiUrl(),
            request,
            CompletionResponse.class
    );

    log.info("응답 받기 성공");
    String response = completionResponse.getChoices().get(0).getMessage().getContent();
    log.info("response = {}", response);
    return response;
}

서비스 단에서는 Completions API에 보내기 위한 CompletionRequest를 정의하고 RestTemplate을 통해 Completions API를 호출하고 이에 대한 응답에서 필요한 데이터만 추출합니다.

우선, CompletionRequest를 구성하기 위해서 DB에 접근하여 이전 채팅 내역을 모두 불러오고 chatGPTMessages에 모두 적재해 줍니다.

model과 messages가 포함된 CompletionRequest를 구성하여 ChatGPTConfig 클래스에서 정의한 restTemplate을 통해 Completions API를 호출합니다.

 

- DTO

@Getter
@NoArgsConstructor
public class CompletionRequest {

    private static final String AURORA_AI_ROLE = "너는 친절한 상담가야 답변은 짧고 질문 위주로 해줘";

    private String model;
    private List<CompletionMessage> messages;

    public CompletionRequest(final String model, final String prompt){
        this.model = model;
        this.messages = new ArrayList<>();
        this.messages.add(new CompletionMessage(SYSTEM.value(), AURORA_AI_ROLE));
        this.messages.add(new CompletionMessage(USER.value(), prompt));
    }

    public CompletionRequest(final String model, final List<CompletionMessage> messages, final String prompt){
        this.model = model;
        this.messages = new ArrayList<>();
        this.messages.add(new CompletionMessage(SYSTEM.value(), AURORA_AI_ROLE));
        this.messages.addAll(messages);
        this.messages.add(new CompletionMessage(USER.value(), prompt));
    }
}

Completions API에 보낼 DTO입니다. model과 message를 명시합니다.

@Getter
@NoArgsConstructor
public class CompletionMessage {

    private String role;

    private String content;

    public CompletionMessage(final String role, final String content){
        this.role = role;
        this.content = content;
    }
}

위에서 언급한 바와 같이 Message의 세부 내용입니다. CompletionMessage는 CompletionRequest의 message를 작성하기 위한 DTO입니다.

@Getter
@NoArgsConstructor
public class CompletionResponse {

    private List<Choice> choices;

    @Getter
    @NoArgsConstructor
    public static class Choice{

        private int index;
        private CompletionMessage message;
    }
}

Completions API 가 응답으로 주는 DTO 입니다. 응답 DTO는 공식 문서의 엔드 포인트를 보고 Completions API 가 응답으로 주는 형식대로 작성해야 오류가 발생하지 않습니다.

5. 보완점

지금까지 살펴본 내용에서 볼 수 있듯이, Completions API를 호출할 때마다 DB를 접근하여 이전 채팅 내역을 모두 불러와야 하며, 모델의 동작 방식을 매번 정의해줘야 합니다. 이렇게 되면 Completions API를 호출할 때마다 이전 채팅 내역을 모두 적재해야 해서 I/O 가 늘어나며, 모델의 동작 방식 또한 매번 명시해야 해서 불필요한 토큰 사용량이 증가하게 됩니다. 다음 글에서는 이 문제점을 해결할 수 있는 Assistant API에 대해 다뤄보도록 하겠습니다.


참고 자료

- https://platform.openai.com/docs/guides/chat-completions

- https://platform.openai.com/docs/api-reference/chat (엔드 포인트) 

- https://github.com/Hongik-Grad-Project/Back-End

 

GitHub - Hongik-Grad-Project/Back-End

Contribute to Hongik-Grad-Project/Back-End development by creating an account on GitHub.

github.com

'OpenAI' 카테고리의 다른 글

[Spring] Spring Boot + OpenAI 도입기 (Assistant API)  (2) 2024.09.06