Overview
화두되고 있는 OpenAI 사의 생성형AI ChatGPT를 API를 활용하여 간단한 프롬프트를 보내고 응답받는 예시를 진행해본다. RAG나 파인튜닝 같은 개념보단, 단순 OpenAI에서 제공하는 API를 활용하는 방식이다.
1
API 클릭
Create new Secret Key
2-1
//Service Layer
WebClient webClient = WebClient.builder()
.baseUrl(url)
.defaultHeader("Authorization", "Bearer "+secret)
.build();
String bodyVal = String.format("{"
+ "\"model\": \"%s\","
+ "\"messages\": [{\"role\": \"%s\", \"content\": \"%s\"}],"
+ "\"temperature\": 0.7,"
+ "\"max_tokens\": 256,"
+ "\"top_p\": 1,"
+ "\"frequency_penalty\": 0,"
+ "\"presence_penalty\": 0,"
+ "\"stream\": true"
+ "}", GPT_MODEL, roleUser, prompt);
발급한 SecretKey, Controller단에서 넘겨주는 prompt를 Binding한다.
2-2
- 필수 항목
- messages: 대화 내용을 포함하는 메시지 목록 (배열)
- model: 사용할 모델의 ID (문자열)
- 선택적 항목
- frequency_penalty: 텍스트 반복을 줄이는 패널티 (-2.0 ~ 2.0)
- logit_bias: 특정 토큰의 출현 가능성 수정 (맵)
- logprobs: 출력 토큰의 로그 확률 반환 여부 (불리언)
- top_logprobs: 각 토큰 위치에서 가장 가능성 높은 토큰 수 (0-20 정수)
- max_tokens: 생성 가능한 최대 토큰 수 (정수)
- n: 각 입력 메시지에 대해 생성할 선택지 수 (정수)
- presence_penalty: 새로운 주제 언급 가능성 증가 패널티 (-2.0 ~ 2.0)
- response_format: 모델 출력 형식 지정 (객체)
- seed: 결정론적 샘플링을 위한 시드 값 (정수)
- service_tier: 요청 처리를 위한 지연 시간 티어 (문자열)
- stop: 토큰 생성을 중지할 시퀀스 (문자열/배열)
- stream: 부분 메시지 델타 전송 여부 (불리언)
- stream_options: 스트리밍 응답 옵션 (객체)
- temperature: 샘플링 온도 (0-2 사이 숫자)
- top_p: 핵 샘플링에 사용되는 확률 질량 (0-1 사이 숫자)
- tools: 모델이 호출할 수 있는 도구 목록 (배열)
- tool_choice: 모델이 사용할 도구 제어 (문자열/객체)
- parallel_tool_calls: 병렬 함수 호출 활성화 여부 (불리언)
- user: 최종 사용자를 나타내는 고유 식별자 (문자열)
- 더 이상 사용되지 않는 항목
- function_call: 함수 호출 제어 (문자열/객체) - tool_choice로 대체됨
- functions: 모델이 JSON 입력을 생성할 수 있는 함수 목록 (배열) - tools로 대체됨
2-3
//Service Layer
return webClient.post()
.uri("/chat/completions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(bodyVal)
.retrieve()
.bodyToFlux(String.class)
.map(chunk -> chunk.replace("data: ", "").trim())
.filter(chunk -> !chunk.equals("[DONE]"))
.map(chunk -> {
try {
JsonNode jsonNode = new ObjectMapper().readTree(chunk);
return jsonNode.at("/choices/0/delta/content").asText();
} catch (JsonProcessingException e) {
return "";
}
})
.filter(content -> !content.isEmpty());
{
"id": "chatcmpl-9exg9KPNME0pDdqReH6gYHNVfbLdA",
"object": "chat.completion",
"created": 1719549553,
"model": "gpt-3.5-turbo-0125",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "2002년 월드컵에서 한국은 준결승까지 진출하여 4위를 차지했습니다. 이는 역사상 최고 성적이며 아시아 대륙에서는 최초로 준결승까지 진출한 팀이었습니다."
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 26,
"completion_tokens": 82,
"total_tokens": 108
},
"system_fingerprint": null
}
해당 응답처럼 Json Format으로 온다. 여기서 필요한 내용은 content 내용이 결과이다. ObjectMapper를 통해 해당 내용을 파싱하여 필요한값을 추출한다.
3
// Controller 영역
@GetMapping(value = "/chatFlux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatFlux(@RequestParam String prompt){
return gptService.chatFlux(prompt);
}
produces=MediaType.TEXT_EVENT_STREAM_VALUE 설정으로 Spring은 해당 엔드포인트를 SSE 스트림으로 처리하고, 클라이언트와 서버 간의 효율적인 실시간 통신을 가능
4
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
~~~
</head>
<body>
<h1>ChatGPT Stream</h1>
<div id="chatbox"></div>
<input type="text" id="userInput" placeholder="메시지를 입력하세요...">
<button id="sendButton">전송</button>
<script th:inline="javascript">
const chatbox = document.getElementById('chatbox');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
function appendMessage(sender, message) {
const messageElement = document.createElement('p');
messageElement.innerHTML = `<strong>${sender}:</strong> ${message}`;
chatbox.appendChild(messageElement);
chatbox.scrollTop = chatbox.scrollHeight;
}
function sendMessage() {
const message = userInput.value.trim();
if (message) {
appendMessage('You', message);
userInput.value = '';
const eventSource = new EventSource(`/chatFlux?prompt=${encodeURIComponent(message)}`);
let fullResponse = '';
eventSource.onmessage = function(event) {
const newContent = event.data;
fullResponse += newContent;
chatbox.lastChild.innerHTML = `<strong>ChatGPT:</strong> ${fullResponse}`;
chatbox.scrollTop = chatbox.scrollHeight;
};
eventSource.onerror = function(error) {
console.error('EventSource failed:', error);
eventSource.close();
};
eventSource.onclose = function() {
console.log('EventSource connection closed');
};
}
}
sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
EventSource이라는 웹 API를 사용하여 실시간 응답값을 처리한다.