- 템플릿 생성
먼저 pom.xml에 유효성 검사 Starter를 추가합니다:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
"한국 역사 문화"에 대한 AI를 정의하고, 템플릿을 간단히 구성합니다:
엔티티 정의:
package com.korea.ai.demo.model;
import jakarta.validation.constraints.NotBlank;
public record CulturalQuestion(
@NotBlank(message = "주제를 입력해주세요") String subject,
@NotBlank(message = "질문을 입력해주세요") String query) {
}
템플릿 파일 resources/promptTemplates/culturalPromptTemplate.st:
당신은 한국 역사 문화 전문가입니다.
해당 주제에 대해 정보가 없거나 답변을 알지 못한다면 "정보가 없습니다"라고 답변하세요.
상세한 설명과 함께 답변해주세요.
문화 분야는 {subject}입니다.
질문:
{query}
- 구현 로직
package com.korea.ai.demo.service.impl;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import com.korea.ai.demo.model.CulturalResponse;
import com.korea.ai.demo.model.CulturalQuestion;
import com.korea.ai.demo.service.CulturalService;
@Service
public class CulturalAIServiceImpl implements CulturalService {
private final ChatClient chatClient;
public CulturalAIServiceImpl(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@Value("classpath:/promptTemplates/culturalPromptTemplate.st")
Resource culturalPromptTemplate;
@SuppressWarnings("null")
@Override
public CulturalResponse ask(CulturalQuestion culturalQuestion) {
var response = chatClient.prompt()
.user(userSpec -> userSpec
.text(culturalPromptTemplate)
.param("subject", culturalQuestion.subject())
.param("query", culturalQuestion.query())
)
.call();
var responseText = response.content();
return new CulturalResponse(culturalQuestion.subject(), responseText);
}
}
- 실행 결과
테스트 케이스:
curl http://localhost:8080/culture/ask \
-X POST \
-H "Content-Type: application/json" \
-d '{"subject": "조선 시대", "query": "조선 시대 과거 제도의 특징과 변천 과정에 대해 설명해주세요. 특히 양반과 서인, 동인의 정치적 대립이 어떻게 과거 제도와 연결되었는지 설명하십시오."}'
반환 결과 (일부 편집):
{
"subject":"조선 시대",
"answer":"조선 시대의 과거 제도는... (상세 설명)"
}
조선 시대 과거 제도에 대한 상세 설명이 반환됩니다.
- 추가 기능
4.1 템플릿 기반 확장
RAG를 도입하기 전, 간단한 템플릿 채우기를 통해 확장 기능을 구현할 수 있습니다.
예를 들어 cultureRules/korea.txt 파일을 추가하고 내용은 다음과 같습니다:
- 역사적 사실을 기반으로 답변할 것
- 특정 시대의 문화적 배경을 고려할 것
- 중립적인 시각을 유지할 것
이를 템플릿에 다음과 같이 적용할 수 있습니다:
당신은 한국 역사 문화 전문가입니다.
해당 주제에 대해 정보가 없거나 답변을 알지 못한다면 "정보가 없습니다"라고 답변하세요.
가능한 경우 다음 규칙을 따르세요: {rules}.
상세한 설명과 함께 답변해주세요.
문화 분야는 {subject}입니다.
질문:
{query}
4.2 AI 모델 옵션
4.2.1 모델 유형
설명 부분에서 언급된 내재로 생략합니다
4.2.2 모델 온도
temperature 파라미터는 생성 전략의 핵심 파라미터로 출력의 무작위성과 창의성에 직접적인 영향을 미칩니다.
- 낮은 온도(0.3-0.7): 고객 서비스 로봇 등 정확한 답변이 필요한 시나리오에 적합, 오류 정보 감소
- 중간 온도(0.7-1.2): 창의적인 글쓰기에 적합, 논리성과 다양성의 균형
- 높은 온도(1.2-2.0): 아이디어 도구에 사용되며, 비전통적인 창의성을 자극
ChatOptions chatOptions = ChatOptions.builder()
.temperature(0.7)
.build();
String responseText = chatClient.prompt()
.user(culturalQuestion.query())
.options(chatOptions)
.call()
.content();
4.2.3 기타 옵션
topP결과 선택 비율, 예를 들어.topP(0.8)은 상위 80% 결과에서 선택topK결과 제외 비율, 예를 들어.topP(0.2)은 하위 20% 결과 제외
4.3 응답 형식화
예를 들어, Prompt에서 JSON 형식의 출력을 지정하거나 특정 형식의 응답만 받을 수 있습니다. 이를 통해 인기 문화 목록 등 JSON API를 빠르게 구현할 수 있습니다.
또한 출력 형식을 스트리밍으로 설정하면 클라이언트나 웹 프론트엔드에서 SSE 프로토콜을 사용해 결과를 토큰 단위로 표시할 수 있습니다.
return chatClient.prompt()
.system(systemSpec -> systemSpec
.text(promptTemplate)
.param("subject", question.subject())
.param("rules", cultureRules))
.user(question.query())
.stream() // 스트리밍
.content();
4.4 응답 메타데이터
LLM이 반환하는 내용에는 OpenAI의 경우 토큰 사용 관련 데이터(메타데이터)가 포함됩니다:
{
"token_usage": {
"completion_tokens": 245,
"prompt_tokens": 32,
"total_tokens": 277
},
"model_name": "gpt-4-turbo",
"system_fingerprint": "fp_korean_culture_2024",
"finish_reason": "stop",
"logprobs": null
}
이를 코드에서 가져와 기록할 수 있습니다:
var responseEntity = chatClient.prompt()
.system(systemSpec -> systemSpec
.text(promptTemplate)
.param("subject", question.subject())
.param("rules", cultureRules))
.user(question.query())
.call()
.responseEntity(Response.class);
var response = responseEntity.response();
var metadata = response.getMetadata();
log.info(metadata.getUsage()); // 토큰 사용량 획득
return responseEntity.entity();