pakages-to-scan에 해당하는 경로를 넣게되면 해당 경로를 상위폴더로 두고 하위폴더를 전부 탐색하니 생각하여 경로를 설정하도록 하자. 필자는 프로젝트 전체를 경로로 넣었다.
API가 RESTFUL하게 통신을 주고받기 때문에 json타입으로 기본 설정한다.
나머지는 기본 url을 사용할건지 등의 기본 설정이기에 해당 설정을 따른다.
Config 파일 추가
Bean에 등록하여 서버 실행 시 참조에 오류가 없도록한다.
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@OpenAPIDefinition(
info = @Info(
title = "LLM Service API",
version = "1.0",
description = "LLM Service API")
)
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new io.swagger.v3.oas.models.info.Info()
.title("LLM Service API")
.version("1.0")
.description("LLM Service API"));
}
}
@OpenAPIDefinition 어노테이션과 OpenAPI를 정의하여 Swagger의 제목과 버젼 설명을 명세한다.
이후 각 Controller, DTO, Entity, ResponseEntity(디폴트 응답 틀)에 설명을 추가한다.
Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/llm")
@Tag(name = "Chat-GPT(LLM) 서비스", description = "LLM 관련 API")
public class LLMController {
private final LLMService llmService;
@Operation(summary = "코딩테스트 문제 생성 API", description = "난이도(1~5), 알고리즘, 추가 사항을 기반으로 코딩 테스트를 생성하는 API")
@PostMapping("/code-generating")
public ResponseEntity<LLMResponseDTO.CodeGenerateResponse> codeGenerator(@RequestBody LLMRequestDTO.codeGeneratingInfo request) {
return llmService.codeGenerator(request);
}
@PostMapping("/chat")
public ResponseEntity<LLMResponseDTO> chat(@RequestBody LLMRequestDTO.chatMessage request) {
return llmService.chat(request);
}
}
DTO
@Builder
@Getter
@AllArgsConstructor
@Schema(name = "LLMResponseDTO", description = "LLM 응답 양식", title = "LLMResponseDTO [LLM 응답 양식]")
public class LLMResponseDTO {
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Schema(name = "CodeGenerateResponse", description = "코딩테스트 생성 응답", title = "CodeGenerateResponse [코딩테스트 생성 응답]")
public static class CodeGenerateResponse{
@Schema(name = "title", description = "문제 제목", example = "혼자 남지 않기 위한 최적의 경로 찾기")
private String title;
@Schema(name = "algorithm", description = "알고리즘", example = "DP")
private String algorithm;
@Schema(name = "content", description = "문제 내용", example = "<h3>혼자 남지 않기 위한 최적의 경로 찾기</h3>\n\n<p>이 문제는 '혼자 남지 않기 위한 최적의 경로 찾기'를 목표로 합니다. 주어진 2차원 격자에서 특정 조건을 만족하며 목적지에 도달하기 위한 행동을 결정해야 합니다. 주어진 조건을 만족하는 함수를 작성하고, 격자를 탐색하는 중 최대한의 안전성을 확보해야 합니다.</p>\n\n<h4>문제 설명:</h4>\n<p>이 문제는 특정 시작점에서 목적지까지 '혼자 남지 않기' 위한 최적의 경로를 찾아야 합니다. 2차원 격자에서는 특정 위치로 이동할 때 1의 스탭이 소요되며, 각 위치에서 움직일 수 있는 최대 경로 수를 제한하여 주어질 것입니다. 주어진 조건하에서 최단 경로를 결정해야 하며, 경로에 존재하는 위험 요소를 피하면서 격자를 탐색해야 합니다.</p>\n\n<h4>문제 제한 사항:</h4>\n<ul>\n<li>격자 크기: M x N (1 ≤ M, N ≤ 100)</li>\n<li>시작점: 출발 위치는 (0,0)으로 고정되어 있습니다.</li>\n<li>목적지: 목적지는 항상 오른쪽 아래 모서리 (M-1, N-1)입니다.</li>\n<li>위험 요소는 격자 내 임의의 위치에 존재할 수 있으며, 이를 피하는 것이 중요합니다.</li>\n</ul>\n\n<h4>입력 예시:</h4>\n<pre><code>\n5 5\n0 0 0 0 0\n0 -1 0 -1 0\n0 0 0 -1 0\n0 -1 -1 0 -1\n0 0 0 0 0\n</code></pre>\n\n<h4>출력 예시:</h4>\n<pre><code>\n7\n</code></pre>\n\n<h4>입출력 예 설명:</h4>\n<p>주어진 격자에서 (0,0)에서 (4,4)까지 가장 적은 스탭으로 이동하기 위한 경로는 7스탭 입니다. 중간에 -1은 이동할 수 없는 위치이며, 다른 안전한 경로를 찾아야 합니다.</p>")
private String content;
@Schema(name = "difficulty", description = "난이도", example = "3")
private String difficulty;
@Schema(name = "additionalNotes", description = "추가 사항", example = "문제의 조건을 만족하는 최소의 시간 복잡도를 갖는 알고리즘을 작성해줘.(can be null)")
private String additionalNotes;
@Schema(name = "testCases", description = "테스트 케이스 목록(front 필요 없을 확률이 높음.)")
private List<TestCaseSpec> testCases;
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Schema(name = "TestCaseSpec", description = "테스트 케이스 명세", title = "TestCaseSpec [테스트 케이스 명세]")
public static class TestCaseSpec {
@JsonProperty("input")
@Schema(name = "input", description = "입력값", example = "5 5\n0 0 0 0 0\n0 -1 0 -1 0\n0 0 0 -1 0\n0 -1 -1 0 -1\n0 0 0 0 0")
private String input;
@Schema(name = "expectedOutput", description = "예상 출력값", example = "7\n")
@JsonProperty("expected_output")
private String expectedOutput;
}
public static LLMResponseDTO.CodeGenerateResponse of(String jsonResponse) {
ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode rootNode = objectMapper.readTree(jsonResponse);
JsonNode problemInfoNode = rootNode.path("problem_info").get(0);
List<TestCaseSpec> testCases = objectMapper.readValue(
rootNode.path("test_cases").toString(),
objectMapper.getTypeFactory().constructCollectionType(List.class, TestCaseSpec.class)
);
return LLMResponseDTO.CodeGenerateResponse.builder()
.title(rootNode.path("title").asText())
.content(rootNode.path("content").asText())
.algorithm(problemInfoNode.path("algorithm").asText())
.difficulty(problemInfoNode.path("difficulty").asText())
.additionalNotes(problemInfoNode.path("additional_notes").asText())
.testCases(testCases)
.build();
} catch (Exception e) {
throw new RuntimeException("Failed to parse JSON response"+ e.getMessage(), e);
}
}
ResponseEntity
@Data
@AllArgsConstructor
@Schema(name = "ResponseEntity", description = "전체 API 응답", title = "ResponseEntity [전체 API 응답]")
public class ResponseEntity<T> {
@Schema(name = "statusCode", description = "응답 코드", example = "200")
private Integer statusCode;
@Schema(name = "message", description = "응답 메시지", example = "OK")
private String message;
@Schema(name = "data", description = "응답 데이터")
private T data;
public static <T> ResponseEntity<T> success(T data) {
return new ResponseEntity<>(HttpStatus.OK.value(), "OK", data);
}
public static <T> ResponseEntity<T> error(Integer statusCode, String message, T data) {
return new ResponseEntity<>(statusCode, message, data);
}
}
이후 서버를 실행시킨 뒤, localhost:8080/swagger-ui/index.html# 로 접속한다.
메인 페이지
상세 페이지 RequestResponse
이렇게 정의하여, 프론트에게 API 명세를 껴안아 주면 그때부터 완전한 프로젝트 협업의 시작이 되겠다.
먼저 필자는 코딩테스트 블로그를 만들고, 코딩테스트와 관련된 내용을 chatGPT에 질문하고, 코딩테스트 문제를 생성하는 서비스를 만드려고 한다.
사용하는 유저 혹은 문제 마다 다 다른 대화내용일텐데, chatGPT를 사용하는 것처럼 질의응답 내용을 다 파악하여 답할 수 있을까?
결론은 그렇지 않다.
해당 내용을 기억하지 못하기 떄문에, 기존 대화 내용을 프롬프트에 담아 제공을 해준다.
이를 통해 chatGPT로 하여금 이전 내용을 숙지하여 의도에 맞는 답을 전달해줄 수 있도록 할 수 있는것이다. (물론 토큰 비용은 더 나가겠지만.. 좋은 서비스를 제공하기 위해서는 감안해야 할 듯 하다.)
따라서 나는 경우를 세가지로 나누게 되었다.
신규 사용자의 첫 질문
기존 사용자의 첫 코딩테스트 질문
기존 사용자의 연속된 코딩테스트 질문
경우마다 다 다르게 나누어 프롬프트를 설정한다.
프롬프트는 다음과 같다.
해당 언어를 주력 기술로 가지고 있는 사용자
기존 대화에서 현재 질문한 것과 가장 유사한 질문 및 가장 유사하지 않은 질문을 제시한다.
캐시 메모리를 이용하여, 가장 최근 두 질의응답을 저장해 이를 반환한다.
그렇다면 해당 행동은 RAG라고 볼 수 있을까?
RAG(Retrieval-Augmented Generation)는 외부의 정보를 검색하거나 불러와서 생성 모델의 입력으로 제공하는 방식으로, 본질적으로 검색(검색 및 불러오기) + 생성 접근 방식을 의미한다.
유의미한 결과를 도출하기 위해 정보를 다듬어 다시 제공한다는 점에서 RAG라고 볼 수 있다. 물론 주로 사용되고 있는 기술이 외부의 정보를 한데 모아 정보를 통합하여 질문하는 방식이다. 허나 유창한 기술을 사용하지 않더라도 (원칙에 부합하기도 하고) 사용자 입장에서 더 좋은 결과를 획득할 수 있다면 그것도 올바른 방법이지 않을까 필자는 생각한다.
물론 2번 프롬프트에서 BERT모델을 사용하여 벡터화 한 값을 통해 유사한 질문과 가장 유사하지 않은 질문을 제시하기는 한다.
그렇다면 RAG를 어떻게 사용하게 되는지 확인해 보자.
Retrieval (검색 및 불러오기)
사용자가 이전에 했던 질문과 답변, 문제 정보 등을 DB(2번 프롬프트)나 캐시(3번 프롬프트)에서 불러온다.
필요한 경우 중요한 부분만 요약하여 정보를 효율적으로 구성한다.(2번 프롬프트)
Augmentation(정보 강화)
불러온 대화 이력이나 문제와 관련된 정보를 프롬프트에 추가하여, 모델이 이전 대화 맥락을 인식할 수 있게 한다.
요약된 이력, 사용자 성향, 특정 질문 패턴 등이 프롬프트에 포함되므로 사용자 맞춤형 답변을 생성하도록 도와준다.
Generation(응답 생성)
최종적으로 강화된 프롬프트를 모델에 입력하여 답변을 생성한다.
이때, 모델은 전달된 프롬프트에 포함된 정보를 참고하여 맥락에 맞는 답변을 하게된다.
이렇게 발전에 흐름에 전지전능하다고 생각되는 GPT도 다 해주는 건 아니기에, 해당내용을 참고하여 API를 잘 다뤄보기 바란다.
const answer = [5, 6, 3, 2];
const value = '4652'
let strike = 0;
let ball = 0;
answer.forEach((element, index) => {
const answerIndex = value.indexOf(element);
if(answerIndex === index) strike++;
}}
console.log(strike+"개 맞췄습니다")
//return 2개 맞췄습니다.
forEach는 for문 도는 거 처럼 행동 닉값 함 ㅇㅇ, element와 index를 순서로 매개변수를 받을 수 있다. 배열의 처음부터 끝까지 순회하며, 콜백함수 형식으로 사용해야 한다.
const array = [5, 6, 3, 2];
const result = array.map((element, index) => {
return element * 2;
})
//return undefined, result (4) [10, 12, 6, 4]
map함수도 마찬가지로 element와 index를 순서로 매개변수를 받고, 배열의 처음부터 끝까지 순회하며 콜백함수 형식으로 사용해야하는 것은 같지만, return을 주어 그 값을 배열에 mapping 할 수 있다.
const array = Array(9).fill(1).map((el,idx) => idx +1)
array.splice(2,3) // index가 2인곳 부터 3개 빼온다.
//return (3) [3, 4, 5], array (6) [1, 2, 6, 7, 8, 9]
let a = array.slice(4)
//return (2) 8, 9
let b = array.slice(4,5)
//return 8, slice(startIndex, EndIndex-1)
이런식으로 응용할 수 있겠다.
Array 함수를 통해 길이가 9이고, 1~9까지의 값을 가지는 array를 선언했다.
splice 함수를 통해 index가 2인곳 부터 3개를 반환했고
slice 함수를 통해 a에 인덱스가 4인곳 부터 따로 배열을 저장했으며
slice 함수를 통해 b에 인덱스가 4인곳 부터 5-1인곳 까지 배열을 저장했다.
조그마한 정리
배열 정렬은 그냥 검색해라.
setTimeout( 콜백함수, 시간(밀리초))
스코프 - 범위 : var은 함수 스코프, let은 블록 스코프
이벤트 관리 EventListener
태그.addEventListener('click', 고차함수())); // 마우스 버튼 클릭 시 함수 행동
태그.removeEventlistener('click',고차함수())); // 변수 사용하는 것이 더 좋다
/*
mouseover 마우스를 태그 위에 올리면 발생
mouseout 마우스가 태그 바깥으로 벗어나면 발생
mousedown 마우스버튼을 누르고 떼기 전, 태그를 드래그할 떄 사용
mouseup 드래그한 태그를 드랍할 떄 사용
focus 태그에 포커스가 갔을 때 발생
blur 태그가 포커스에서 벗어났을 떄 발생
keypress 키를 누르는 순간 시작 누르고 있는 동안 계속 발생
keydown 키를 누를 때 발생
key up 키를 뗄 때 발생
load 페이지 상 모든 요소(파일)의 다운로드가 완료되었을 때 발생
resize 브라우저 창 크기 조절 시 발생
scroll 스크롤바 드래그 혹은 키보드(up, down)를 사용하거나 휠을 사용해서 웹페이지를 스크롤 할 때 발생
페이지에 스크롤바 없으면 이벤트 발생 안함.
unload 링크 클릭하여 다른페이지 이동 혹은 브라우저 탭 닫을 떄 브라우저 창을 닫을 때 이벤트 발생
change 상태가 변경되었을 때 발생
*/
위의 식은 잘못되었다. 왜냐면 함수와 함수간 비교는 객체와 객체간 비교로 볼 수 있는데, 이 상황에서 객체간 비교는 항상 false가 나온다. 객체의 호출은 재생성과 똑같다고 생각하면 되기에 그런데, 변수는 조금 다르다. 그렇기에 함수의 값을 변수에 담아 비교해보도록 하자. 그러면 같다고 할 수 있으니께롱