## ChatGPT 클라이언트 ### ChatGPT API 토큰 환경 변수 등록 ```powershell # 관리자 권한으로 실행 setx CHATGPT_API_TOKEN "[토큰]" refreshenv ``` - 토큰을 하드 코딩해서 입력하면 **안** 된다. Git을 통해 협업할 때 따로 무시하도록 설정도 해야 하고, 누군가 지나가다가 내 화면을 보는 순간 토큰을 알 수 있기 때문이다. - 좋은 대안 중 하나는 환경 변수로 토큰 값을 저장하고 런타임에서 토큰 값을 가져 오는 것이다. - ChatGPT API 토큰 발급 방법은 [여기](https://rfriend.tistory.com/794)에 잘 정리되어 있다. ### Config 추가 #### ChatGPTConfig ```java public class ChatGPTConfig { static public final String MODEL = "gpt-3.5-turbo"; static public final String URL = "https://api.openai.com/v1/chat/completions"; static public final float TEMPERATURE = 0.7F; static public final String ROLE = "user"; } ``` - ChatGPT API로 요청을 보낼 때 설정할 값들이 많다. 그런 값들을 일괄적으로 관리해야 나중에 수정하기 편할 것이다. 그러니 따로 `ChatGPTConfig`를 만들어 관리하자. #### ChatGPTClientConfig ```java @Configuration public class ChatGPTClientConfig { @Bean public WebClient webClient(){ return WebClient.builder() .baseUrl(ChatGPTConfig.URL) .defaultHeaders( httpHeaders -> { httpHeaders.add("Authorization", "Bearer " + System.getenv("CHATGPT_API_TOKEN")); httpHeaders.add("Content-Type", "application/json"); } ).build(); } } ``` - ChatGPT에 요청을 보낼 때 토큰이나 url 등의 설정 값은 모두 같다. 그렇다면 굳이 매번 `WebClient` 인스턴스를 만들 이유가 없다. 그러니 빈으로 등록하고 주입 받아 사용하자. ### DTO #### ChatGPTRequest ##### ChatGPT API 요청 바디 예시 ```json { "model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Say this is a test!"}], "temperature": 0.7 } ``` - `ChatGPTRequest`는 ChatGPT API 요청 바디를 자바 클래스로 만든 것이다. 따라서 코드를 구현하기 전에 미리 ChatGPT API 요청 바디의 구조를 파악해야 한다. 위는 ChatGPT API 요청 바디의 예시다. - `messages` 이외에는 따로 특이한 건 없다. `messages`는 리스트 안에 object 형식으로 `role`과 `content` 값을 받는다. ##### 코드 ```java @Getter @EqualsAndHashCode @ToString public class ChatGPTRequest { final private String model = ChatGPTConfig.MODEL; final private float temperature = ChatGPTConfig.TEMPERATURE; @Getter @ToString @EqualsAndHashCode @AllArgsConstructor static class ChatGPTMessage{ private String role; private String content; } private final List<ChatGPTMessage> messages = new ArrayList<>(); public ChatGPTRequest(String content){ messages.add(new ChatGPTMessage(ChatGPTConfig.ROLE, content)); } } ``` - `ChatGPTMessage`는 위 예시 JSON에서 `{"role": "user", "content": "Say this is a test!"}` 와 같은 Object에 해당한다. ChatGPT API에 요청을 보낼 때 이외에는 사용할 일이 없는 클래스이니 내부 클래스로 선언한다. ### Client #### ChatGPTClient ##### ChatGPT API 응답 바디 예시 ```json {     "id": "chatcmpl-XXX",     "object": "chat.completion",     "created": XXX,     "model": "gpt-3.5-turbo-0613",     "choices": [         {             "index": 0,             "message": {                 "role": "assistant",                 "content": "This is a test!" // 이게 답변이다.             },             "finish_reason": "stop"         }     ],     "usage": {         "prompt_tokens": 13,         "completion_tokens": 5,         "total_tokens": 18     },     "system_fingerprint": null } ``` - `ChatGPTClient`는 요청을 보낸 다음 응답 바디에서 ChatGPT의 답변만 추출해야 하므로, ChatGPT API 응답 바디의 구조를 파악해야 한다. 위는 ChatGPT API 응답 바디의 예시다. - `choices`는 리스트기 때문에 `choices` > `message`가 아니라 `choices` > `0` > `message` > `content`로 접근해야 한다는 점을 주의하자. ##### 클래스 생성 및 의존성 주입 ```java @Component public class ChatGPTClient { @Autowired private WebClient webClient; @Autowired private PromptTemplateService service; } ``` - [[7. Spring WebClient를 통한 Client 구현#ChatGPTClientConfig|WebClient]]처럼 `ChatGPTClient`도 인스턴스를 매번 생성할 이유가 없으므로 빈으로 등록한다. - `PromptTemplateService`은 템플릿을 통한 프롬프트 생성 기능을 사용하기 위해 주입한다. - 컨트롤러가 아닌데 서비스를 주입하는 게 바람직한지 고민해봤다. 내 생각은 '괜찮다'이다. 왜냐하면 `ChatGPTClient` 는 컨트롤러에서만 호출하기 때문에 컨트롤러와 같은 **인터페이스 레이어**에 있기 때문이다. ##### `chat` ```java public String chat(String content) throws JsonProcessingException { return webClient.post() .bodyValue(new ChatGPTRequest(content)) .retrieve() .bodyToMono(JsonNode.class) .map( jsonNode -> jsonNode .path("choices") .path(0) .path("message") .path("content") .asText() ) .block(); } ``` - API 요청을 보내고 `jackon`의 `JsonNode`를 통해 응답 속 `content`를 파싱하여 문자열로 반환하는 메서드다. `WebClient` 사용법은 [여기](https://gngsn.tistory.com/154)에 잘 정리되어있다. ##### `chatByTemplate` ```java public String chatByTemplate(String templateId, Map<String, String> map) throws TemplateException, IOException { return chat(service.generatePromptByTemplate(templateId, map)); } ``` - `PromptTemplateService`를 이용하여 템플릿으로 프롬프트를 생성한 다음 API 요청을 보내는 메서드다. ## [[4-2. 클라이언트 테스트 코드|테스트 코드]] ![[4-2. 클라이언트 테스트 코드#`ChatGPTClientTest` 작성]]