GoodJob 프로젝트에서도 사용해봤지만 사용법이 어렵지않고 프롬프트만 작성한다면 높은 퍼포먼스를 보여주고 유연하게 사용가능 한 api 입니다. 사용자 정보(신장, 체중, 나이, 성별,활동량 등..)에 따른 유연한 정보가 필요했습니다. 물론 bmi 로 계산하는 방법 등 여러가지가 있지만 이러한 단순 계산 정보는 gpt를 활용한다면 다른 서비스에도 적용가능하고, 유연하게 전환할 수 있다고 생각했습니다.
여러가지 방법이 있습니다.
우리가 gpt 를 사용하는것처럼 이어서 말해줘 와 같이 전에 주고받았던 응답들을 저장하는방법, 1회용으로 정보를 저장하지 않고 입력받는 방법 gpt 4, 3.5터보 등 버전도 여러가지가 있을텐데 이는 필요한 정보의 수준에 따라 자유롭게 선택하시면 됩니다.
저희 프로젝트에선 복잡하지않고 단순 정보를 받아오는 수준이기때문에 저렴한 3.5터보를 사용 했습니다.
// gpt
implementation 'com.theokanning.openai-gpt3-java:api:0.12.0'
implementation 'com.theokanning.openai-gpt3-java:service:0.12.0'
@Slf4j
@Configuration
public class GptConfig {
public final static String MODEL = "gpt-3.5-turbo";
public final static double TOP_P = 1.0;
public final static int MAX_TOKEN = 2000;
public final static double TEMPERATURE = 1.0;
public final static Duration TIME_OUT = Duration.ofSeconds(300);
@Value("${openAi.secretKey}")
private String token;
@Bean
public OpenAiService openAiService() {
return new OpenAiService(token, TIME_OUT);
}
public ResponseEntity<String> requestGPT(String prompt) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + token);
JSONObject requestBody = new JSONObject();
requestBody.put("model", MODEL);
List<Map<String, String >> messages = new ArrayList<>();
Map<String , String > map = new HashMap<>();
map.put("role", "user");
map.put("content", prompt);
messages.add(map);
requestBody.put("messages", messages);
requestBody.put("temperature", 0.7);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toString(), headers);
RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForEntity(GPT_BASE_URL, entity, String.class);
}
}
RestTemplate 를 사용해서 Body값에 넣어 전달합니다.
public class Prompt {
public static String generateFoodNutrient(String foodName) {
String prompt =
"""
You are a nutritionist who knows very well about the nutrients in food
Answer as you know, even if you don't elaborate
Do not use '', "", (), [] or emojis in all content.
Tell me the nutritional ingredients for one serving of %s
Tell me based on g in the order of carbohydrates, proteins, and fats
The answer is in json form
Just send it to only number
""";
return String.format(prompt, foodName);
}
public static String generatePersonalData(PersonalDataDto personalDataDto) {
String prompt =
"""
You are a very professional sports coach
Don't use "-" or "~" in your answer, just send one average and don't say anything else.
The answer is sent in json format as follows. json value is only a number. {carbohydrate:, protein:, fat:}
'%d' years old, '%.1f'cm, '%.1f'kg, female, '%s' and does not consider basal metabolic rate
""";
return String.format(prompt, personalDataDto.getAge(), personalDataDto.getHeight(), personalDataDto.getWeight(), personalDataDto.getActivity());
}
}
제가 원하는 내용을 전달받기위해 원하는 리턴값과 내용을 프롬프트로 작성하였습니다.
@Component
@RequiredArgsConstructor
public class ChatGptAPIManager {
@Value("${openAi.secretKey}")
private String secretKey;
private final GptConfig gptConfig;
private final JSONParser parser = new JSONParser();
private final ObjectMapper objectMapper = new ObjectMapper();
public FoodVo generateNutrient(String foodName) throws ParseException, JsonProcessingException {
ResponseEntity<String> response = gptConfig.requestGPT(Prompt.generateFoodNutrient(foodName));
// 응답 처리
String content = getMessageContent(response);
JSONObject contentObject = (JSONObject) parser.parse(content);//
Long carbohydrates = (Long) contentObject.get("carbohydrates");
Long proteins = (Long) contentObject.get("proteins");
Long fats = (Long) contentObject.get("fats");
return new FoodVo(foodName, carbohydrates, proteins, fats);
}
public void generatePersonalData(PersonalDataDto personalDataDto) throws ParseException, JsonProcessingException {
ResponseEntity<String> response = gptConfig.requestGPT(Prompt.generatePersonalData(personalDataDto));
String content = getMessageContent(response);
JSONObject contentObject = (JSONObject) parser.parse(content);
Long carbohydrate = (Long) contentObject.get("carbohydrate");
Long protein = (Long) contentObject.get("protein");
Long fat = (Long) contentObject.get("fat");
}
private String getMessageContent(ResponseEntity<String> gptResponse) throws ParseException, JsonProcessingException {
JSONObject body = (JSONObject) parser.parse(gptResponse.getBody());
JSONArray o = (JSONArray) body.get("choices");
JSONObject o1 = (JSONObject) o.get(0);
JSONObject o2 = (JSONObject) o1.get("message");
JsonNode rootNode = objectMapper.readTree(o2.toString());
return rootNode.path("content").asText();
}
}
해당 요청을 받으면 Dto, Vo 를 생성하여 리턴해줍니다.
@Service
@RequiredArgsConstructor
public class GptService {
private final ChatGptAPIManager chatGptAPIManager;
public FoodVo generateNutrient(String foodName) {
try {
return chatGptAPIManager.generateNutrient(foodName);
} catch (ParseException | JsonProcessingException e) {
throw new GptException(JSON_PARSE_EXCEPTION.getMsg());
} catch (HttpClientErrorException e) {
throw new GptException(GPT_MANY_REQUEST.getMsg());
}
}
public void generatePersonalData(PersonalDataDto personalDataDto) {
try {
chatGptAPIManager.generatePersonalData(personalDataDto);
} catch (ParseException | JsonProcessingException e) {
throw new GptException(JSON_PARSE_EXCEPTION.getMsg());
} catch (HttpClientErrorException e) {
throw new GptException(GPT_MANY_REQUEST.getMsg());
}
}
}
@RestController
@RequiredArgsConstructor
public class TestController {
private final GptService gptService;
@GetMapping("/test")
public void test() {
gptService.generateNutrient("자장면");
}
}
제가 원하는 자장면에 대한 영양성분을 얻어오는걸 확인할 수 있습니다.
단순 음식의 영양성분을가져오는거라 오차범위가 적을거라 생각했는데 생각보다 오차범위가 컸습니다. 실무에 적용시키기엔 아쉬운 결과 였습니다.
외부 API를 사용하기때문에 제공해주는 응답속도를 개선하기 위해선 등록하고싶은 내용이 많을때 이를 비동기처리로 한다면 더욱 향상될것입니다.
다음 리팩토링시 비동기 처리로 했을때 유의미한 결과 값이 나오는지 테스트해보며 비동기처리 유무를 확인하겠습니다.