아래의 블로그를 참고해서 만들었다.
https://adjh54.tistory.com/397
위 블로그에 소개되어 있는 Config 클래스에 대해서만 간단히 소개하겠다.
ChatGPTConfig: OpenAI API에 인증된 HTTP 요청을 쉽게 보낼 수 있게 해주는 클래스이다.
- RestTemplate: 스프링에서 제공하는 클래스로, RESTful 웹 서비스와의 통신을 간편하게 해준다.
즉 OpenAI API와의 HTTP 통신을 수행하는 데 사용한다. - HttpHeaders: HTTP 요청 시 사용될 헤더를 설정하는 데 사용된다.
- headers.setBearerAuth(secretKey): 비밀 키를 사용하여 Bearer 토큰 인증 헤더를 설정한다.
이는 OpenAI API에 인증된 요청을 보내기 위해 필요하다. - headers.setContentType(MediaType.APPLICATION_JSON): HTTP 요청의 Type을 JSON으로 설정한다. OpenAI API가 JSON 형식의 데이터를 받기 때문에 필요하다.
- headers.setBearerAuth(secretKey): 비밀 키를 사용하여 Bearer 토큰 인증 헤더를 설정한다.
1. 질문(String) -> 답변
프론트엔드에서 String 형태의 질문을 받아주고,
백엔드에서는 GPT API를 이용해서 질문에 대한 답변을 보내주는 코드를 작성할 것이다.
아래는 해당 기능을 수행하는 Service 코드의 메서드이다.
public Map<String, Object> legacyPrompt(String question) {
log.debug("[+] 레거시 프롬프트를 수행합니다.");
CompletionDto completionDto = makePrompt(question);
// [STEP1] 토큰 정보가 포함된 Header를 가져옵니다.
HttpHeaders headers = chatGPTConfig.httpHeaders();
// [STEP5] 통신을 위한 RestTemplate을 구성합니다.
HttpEntity<CompletionDto> requestEntity = new HttpEntity<>(completionDto, headers);
ResponseEntity<String> response = chatGPTConfig
.restTemplate()
.exchange(legacyPromptUrl, HttpMethod.POST, requestEntity, String.class);
Map<String, Object> resultMap = new HashMap<>();
try {
ObjectMapper om = new ObjectMapper();
// [STEP6] String -> HashMap 역직렬화를 구성합니다.
resultMap = om.readValue(response.getBody(), new TypeReference<>() {
});
} catch (JsonProcessingException e) {
log.debug("JsonMappingException :: " + e.getMessage());
} catch (RuntimeException e) {
log.debug("RuntimeException :: " + e.getMessage());
}
return resultMap;
}
ChatGptConfig가 여기서 사용되는데, Gpt에 대한 유효한 요청 형식을 구성한다.
요청을 보내고 ResponseEntity 형태로 받은 응답을 Map 형태로 변환해서 반환한다.
makePrompt() 메서드는 아래와 같다.
private CompletionDto makePrompt(String question) {
return CompletionDto.builder()
.model("gpt-3.5-turbo-instruct")
.prompt(question)
.temperature(0.8f)
.max_tokens(1000)
.build();
}
.model은 어떤 모델을 이용할 것인지 입력해주는 것이고, 해당 요청 형식에서 가장 좋은 버전을 선택했다.
.temperature()는 0~1까지 응답의 창의력을 설정하는 것이다. 0에 가까울 수록 대답이 창의적이다.
max_tokens()는 사용할 토큰의 수를 정하는 것이다. 긴 대답일수록 token, 즉 비용이 많이 발생한다.
2. 재료 -> 음식
프론트엔드에서 String 형태의 질문을 받아주고, 특정 형식의 응답을 받아, List<음식이름> 형태로 반환한다.
아래는 해당 기능을 수행하는 Service 코드의 메서드이다.
public List<String> extractFoods(String question) {
Map<String, Object> resultMap = legacyPrompt(question);
List<String> foods = new ArrayList<>();
if (resultMap.containsKey("choices")) {
List<Map<String, String>> choices = (List<Map<String, String>>) resultMap.get("choices");
if (!choices.isEmpty()) {
String text = choices.get(0).get("text");
// 쉼표로 나누어 음식 목록을 추출하는 부분
foods = Arrays.asList(text.split(", "));
}
}
return foods;
}
legacyPrompt()에서 받은 반환형에서 GPT의 응답만 추출한다.
"음식1, 음식2, 음식3, 음식4, 음식5"형식의 응답이 나오게끔 프론트에서 질문 형식을 만들어 놓아야 한다.
추출한 응답에서 각각의 음식이름을 분리해 List<String> 형태로 저장해 반환한다.
프론트에서는 반환된 List를 버튼 형식으로 만들어서 사용자가 선택할 수 있게끔 할 것이다.
3. 음식 + 재료 -> 레시피
프론트엔드에서 String 형태의 질문을 받아주고, 특정 형식의 응답을 받아, GptRecipeResponseDto 형태로 반환한다.
아래는 해당 기능을 수행하는 Service 코드의 메서드이다.
public GptRecipeResponseDto getRecipeResponse(String question) {
Map<String, Object> resultMap = legacyPrompt(question);
GptRecipeResponseDto gptRecipeResponseDto = new GptRecipeResponseDto();
List<String> ingredients = new ArrayList<>();
List<String> instructions = new ArrayList<>();
// 결과를 RecipeResponseDto에 매핑
if (resultMap.containsKey("choices")) {
List<Map<String, String>> choices = (List<Map<String, String>>) resultMap.get("choices");
if (!choices.isEmpty()) {
String text = choices.get(0).get("text");
// "재료:" 다음 부분을 재료로 설정
int ingredientStartIndex = text.indexOf("재료:") + "재료:".length();
int recipeStartIndex = text.indexOf("레시피:");
if (ingredientStartIndex != -1 && recipeStartIndex != -1) {
String ingredientsText = text.substring(ingredientStartIndex, recipeStartIndex).trim();
String[] ingredientLines = ingredientsText.split("\n");
for (String line : ingredientLines) {
ingredients.add(line.trim());
}
gptRecipeResponseDto.setIngredients(ingredients);
// "레시피:" 다음 부분을 레시피로 설정
String recipeText = text.substring(recipeStartIndex + "레시피:".length()).trim();
String[] recipeLines = recipeText.split("\n");
for (String line : recipeLines) {
instructions.add(line.trim());
}
gptRecipeResponseDto.setInstructions(instructions);
}
}
}
return gptRecipeResponseDto;
}
legacyPrompt()에서 받은 반환형에서 GPT의 응답만 추출한다.
"재료: \n재료1 \n재료2...
레시피: \n 조리과정1 \n 조리과정2..."형식의 응답이 나오게끔 프론트에서 질문 형식을 만들어 놓아야 한다.
추출한 응답에서 각각의 재료와 조리과정을 분리해 GptRecipeResponseDto를 구성해 반환한다.
프론트에서는 반환된 Dto를 이용해 해당 음식의 레시피를 보여주고, 저장 기능에 이용할 것이다.
아래는 GptRecipeResponseDto이다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class GptRecipeResponseDto {
private List<String> ingredients;
private List<String> instructions;
}
재료와 레시피를 List 형태로 구성한다.
결과
재료 -> 음식 -> 음식 + 재료 -> 레시피의 과정이 어느정도 수행이 되었으나, 몇가지 문제가 있었다.
- 응답의 형식이 원하는대로 일관되지 않음
"음식1, 음식2, 음식3, 음식4, 음식5" 같은 반환형도 많이 나왔지만,
"1.음식1, 2.음식2, 3.음식3, 4.음식4, 5.음식5" 같이 원하는 형식에서 벗어난 결과도 많이 나옴
특히 음식 + 재료 -> 레시피 과정에서 일관되지 않은 결과를 보임
- gpt - 3.5 버전은 성능이 만족스럽지 못하므로, gpt - 4 버전을 사용하여 일관된 응답을 전송하게 하기로 함.
- 레시피가 이상함
가진 재료가 "삼겹살, 고추, 오이, 양파" 일때 오이냉국 레시피를 선택하면
삼겹살을 구워서 오이냉국에 넣어버리는 말도안되는 레시피가 나옴.
또한 레시피가 너무 구체적이지 않음- 중간에 필요없는 재료를 거르고, 필요한 재료를 추가하는 과정을 추가하기로 함. API 호출은 한번 늘어나게 될 것.
- 프론트에서 너무 많은 기능이 필요함
질문을 구성하는것도 프론트에서 해야하고,
심지어 GPT API의 응답을 그대로 전달해주다 보니 프론트에서 부담이 많이 증가함.- 프론트에서는 재료, 음식과 같은 간단한 응답만 받고, 질문을 백엔드에서 처리해주기로 함.
- GPT API의 응답에서 필요한 부분만 프론트가 원하는 방식으로 반환해 주기로 함(dto 구성 후 반환)
다음 글에서는 위의 문제점을 보완한 버전을 소개하겠다.
GPT-4: 재료->음식->레시피 기능 구현 (tistory.com)
'프로젝트 > AI명종원' 카테고리의 다른 글
레시피 CRUD 기능 구현 (0) | 2024.05.17 |
---|---|
GPT-4: 재료->음식->레시피 기능 구현 (1) | 2024.05.17 |
Roboflow Java API (0) | 2024.05.10 |
Roboflow 이미지 전처리 + 모델 훈련 (0) | 2024.05.10 |
Roboflow 선택 과정 + 이미지 수집(크롤링) (0) | 2024.05.10 |