마이데이터 에이전트 개발 토이 프로젝트
https://github.com/sm0514sm/mymydata/tree/master
소개
프로젝트 개요
마이데이터 공식 문서를 기반으로 정책과 기술 사양에 대한 질문에 답변할 수 있는 AI 에이전트를 개발했다. 이 프로젝트의 주요 목표는 사용자들이 마이데이터 관련 정보를 쉽고 빠르게 얻을 수 있도록 하는 것이었다.
주요 기능:
마이데이터 관련 질문에 대한 AI 기반 응답
실시간 채팅 기능
다중 채널 지원
동시성 처리를 통한 효율적인 요청 처리
Spring AI를 선택한 이유
Spring AI를 선택한 이유는 다음과 같다:
통합 용이성: Spring 생태계와의 원활한 통합이 가능했다.
RAG 지원: Retrieval Augmented Generation (RAG) 패턴을 쉽게 구현할 수 있었다.
다양한 AI 모델 지원: OpenAI, Hugging Face 등 다양한 AI 모델을 지원한다.
벡터 데이터베이스 통합: 다양한 벡터 데이터베이스와의 통합이 용이했다.
확장성: 새로운 AI 기능을 쉽게 추가하고 확장할 수 있는 구조를 제공한다.
이러한 이유로 Spring AI는 마이데이터 에이전트 개발에 적합한 프레임워크였고, 프로젝트의 요구사항을 효과적으로 충족시킬 수 있었다.
Spring AI 기반 마이데이터 에이전트 구현
프로젝트 구조 설명
프로젝트는 Spring Boot를 기반으로 구성되었으며, 주요 패키지는 다음과 같다:
com.sangminlee.mymydata.config
: 애플리케이션 설정 클래스com.sangminlee.mymydata.service
: 비즈니스 로직을 담당하는 서비스 클래스com.sangminlee.mymydata.repository
: 데이터 접근 계층com.sangminlee.mymydata.vo
: 값 객체(Value Object) 클래스com.sangminlee.mymydata.views
: 사용자 인터페이스 관련 클래스
주요 컴포넌트 소개
ChatService
ChatService는 AI 모델과의 상호작용을 담당한다. 주요 기능은 다음과 같다:
사용자 메시지 처리 및 AI 응답 생성
채널별 대화 컨텍스트 관리
함수 호출 기능 지원
@Service
public class ChatService {
// ... 생략 ...
public void answerMessage(String channelId, String message) {
ChatResponse chatResponse = chatClient.prompt()
.user(message)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, channelId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))
.call()
.chatResponse();
// ... 응답 처리 ...
}
}
MessageService
MessageService는 메시지의 저장 및 검색을 담당한다. 주요 기능은 다음과 같다:
메시지 히스토리 관리
실시간 메시지 스트리밍
메시지 삭제 기능
@Service
public class MessageService {
// ... 생략 ...
public Flux<List<Message>> getLiveMessages(String channelId) {
return sink.asFlux()
.filter(m -> m.channelId().equals(channelId))
.buffer(Duration.ofMillis(500));
}
}
ChannelService
ChannelService는 채널 관리를 담당한다. 주요 기능은 다음과 같다:
채널 생성 및 조회
채널 목록 관리
채널별 최근 메시지 관리
@Service
public class ChannelService {
// ... 생략 ...
public List<Channel> getAllChannels() {
return channelRepository.findAll();
}
public void createChannel(String name) {
channelRepository.save(name);
}
}
이러한 주요 컴포넌트들이 상호 작용하며 마이데이터 에이전트의 핵심 기능을 구현했다.
OpenAI 통합
Spring AI를 통해 OpenAI의 강력한 언어 모델을 프로젝트에 쉽게 통합할 수 있다. 이 섹션에서는 OpenAI API 연동 방법과 모델 선택 및 파라미터 튜닝에 대해 설명한다.
OpenAI API 연동
Spring AI는 OpenAI와의 연동을 위한 자동 구성을 제공한다. 이를 사용하기 위해 먼저 필요한 의존성을 추가해야 한다.
dependencies {
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
}
그 다음, application.yml
파일에 OpenAI API 키와 기본 설정을 추가한다.
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4
temperature: 0.7
이제 OpenAiChatClient
를 주입받아 사용할 수 있다.
@Service
public class OpenAiService {
private final OpenAiChatClient chatClient;
public OpenAiService(OpenAiChatClient chatClient) {
this.chatClient = chatClient;
}
public String generateResponse(String prompt) {
return chatClient.call(prompt);
}
}
모델 선택 및 파라미터 튜닝
OpenAI는 다양한 모델을 제공하며, 각 모델은 고유한 특성을 가지고 있다. Spring AI를 통해 이러한 모델을 쉽게 선택하고 파라미터를 튜닝할 수 있다.
모델 선택
모델 선택은 application.yml
에서 기본값을 설정하거나 런타임에 변경할 수 있다.
OpenAiChatOptions options = OpenAiChatOptions.builder()
.withModel("gpt-4")
.build();
String response = chatClient.call(new Prompt("질문", options));
주요 파라미터 튜닝
Temperature: 출력의 무작위성을 조절한다. 높을수록 더 창의적인 응답을, 낮을수록 더 일관된 응답을 생성한다.
Max Tokens: 생성될 최대 토큰 수를 지정한다.
Top P: Nuclear sampling이라고도 불리는 이 파라미터는 모델이 고려할 확률 분포의 상위 비율을 결정한다.
Frequency Penalty: 같은 단어나 구문의 반복을 줄이는 데 사용된다.
OpenAiChatOptions options = OpenAiChatOptions.builder()
.withModel("gpt-4")
.withTemperature(0.7f)
.withMaxTokens(500)
.withTopP(0.9f)
.withFrequencyPenalty(0.1f)
.build();
ChatResponse response = chatClient.call(new Prompt("질문", options));
이러한 파라미터를 조정하여 마이데이터 에이전트의 응답을 최적화할 수 있다. 예를 들어, 정책 관련 질문에는 낮은 temperature를, 창의적인 제안이 필요한 경우에는 높은 temperature를 사용할 수 있다.
Spring AI의 유연한 설계 덕분에 OpenAI 모델을 쉽게 통합하고 fine-tuning할 수 있으며, 이는 마이데이터 에이전트의 성능과 사용자 경험을 크게 향상시킬 수 있다.
RAG(Retrieval Augmented Generation) 구현
VectorStore 설정 및 활용
RAG 구현의 핵심은 효율적인 VectorStore 설정과 활용이다. Spring AI는 다양한 VectorStore 구현체를 제공하며, 이 프로젝트에서는 SimpleVectorStore를 사용했다.
VectorStore 설정
@Configuration
public class VectorStoreConfig {
private final EmbeddingModel embeddingModel;
@Value("${app.vectorstore.path:./simple-vectorstore.json}")
private String vectorStorePath;
public VectorStoreConfig(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
@Bean
@ConditionalOnProperty(name = "app.vectorstore.target", havingValue = "simple", matchIfMissing = true)
SimpleVectorStore simpleVectorStore() throws IOException {
var vectorStore = new SimpleVectorStore(embeddingModel);
File vectorStoreFile = new File(vectorStorePath);
if (vectorStoreFile.exists()) {
// 기존 VectorStore 파일이 있으면 로드
vectorStore.load(vectorStoreFile);
} else {
// 새로운 VectorStore 생성 및 데이터 추가
vectorStore.add(processPdfDocuments());
vectorStore.save(vectorStoreFile);
}
return vectorStore;
}
// PDF 문서 처리 메서드
private List<Document> processPdfDocuments() throws IOException {
// PDF 문서 처리 로직
// ...
}
}
이 설정에서 주목할 점:
EmbeddingModel
을 주입받아 SimpleVectorStore를 초기화한다.기존 VectorStore 파일이 있으면 로드하고, 없으면 새로 생성한다.
@ConditionalOnProperty
어노테이션을 사용해 설정에 따라 VectorStore 구현체를 선택할 수 있게 했다.
VectorStore 활용
VectorStore를 활용하여 RAG를 구현하는 핵심 부분은 QuestionAnswerAdvisor
를 사용하는 것이다. 이를 통해 사용자 질문과 관련된 문서를 검색하고 AI 모델에 제공할 수 있다.
@Service
public class ChatService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public ChatService(ChatClient.Builder builder, VectorStore vectorStore) {
this.vectorStore = vectorStore;
this.chatClient = builder
.defaultAdvisors(
new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults())
)
.build();
}
public String answerQuestion(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
이 구현의 주요 포인트:
QuestionAnswerAdvisor
를 ChatClient의 기본 advisor로 설정한다.VectorStore
를QuestionAnswerAdvisor
에 주입하여 관련 문서를 검색할 수 있게 한다.사용자 질문에 대해 RAG를 적용하여 답변을 생성한다.
Spring AI의 VectorStore 추상화 덕분에 다른 VectorStore 구현체로 쉽게 전환할 수 있다. 예를 들어, 프로덕션 환경에서는 Chroma나 Postgres와 같은 더 강력한 VectorStore로 전환할 수 있다.
문서 처리 및 임베딩 과정
다음으로 RAG 구현의 중요한 부분은 문서를 처리하고 임베딩하는 과정이다. Spring AI는 이를 위한 다양한 도구와 추상화를 제공한다.
문서 처리
먼저, PDF 문서를 처리하기 위해 PagePdfDocumentReader
를 사용했다. 이 클래스는 PDF 문서를 읽고 Spring AI의 Document
객체로 변환한다.
@Component
public class DocumentProcessor {
@Value("${app.resources}")
private List<String> pdfUrls;
public List<Document> processPdfDocuments() throws IOException {
// PDF 설정
PdfDocumentReaderConfig pdfReaderConfig = PdfDocumentReaderConfig.builder()
.withPageTopMargin(2)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withLeftAlignment(true).build())
.withPagesPerDocument(1)
.build();
// 텍스트 분할 설정
TextSplitter textSplitter = new TokenTextSplitter(300, 250, 5, 10000, true);
return pdfUrls.stream()
.map(url -> new PagePdfDocumentReader(url, pdfReaderConfig))
.map(PagePdfDocumentReader::get)
.map(textSplitter)
.flatMap(Collection::stream)
.toList();
}
}
주요 포인트:
PdfDocumentReaderConfig
를 사용하여 PDF 읽기 설정을 커스터마이즈한다.TokenTextSplitter
를 사용하여 긴 문서를 AI 모델의 컨텍스트 윈도우에 맞는 크기로 분할한다.스트림 API를 사용하여 여러 PDF 문서를 효율적으로 처리한다.
임베딩 과정
문서 처리 후, 각 Document
객체를 벡터로 임베딩해야 한다. 이를 위해 EmbeddingModel
을 사용한다.
@Service
public class EmbeddingService {
private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
public EmbeddingService(EmbeddingModel embeddingModel, VectorStore vectorStore) {
this.embeddingModel = embeddingModel;
this.vectorStore = vectorStore;
}
public void embedAndStoreDocuments(List<Document> documents) {
List<float[]> embeddings = documents.stream()
.map(doc -> embeddingModel.embed(doc.getContent()))
.toList();
// 문서와 임베딩을 VectorStore에 저장
for (int i = 0; i < documents.size(); i++) {
Document doc = documents.get(i);
float[] embedding = embeddings.get(i);
vectorStore.add(List.of(new Document(doc.getContent(), doc.getMetadata(), embedding)));
}
}
}
주요 포인트:
EmbeddingModel
을 사용하여 각 문서의 내용을 벡터로 변환한다.생성된 임베딩과 원본 문서를
VectorStore
에 저장한다.
전체 프로세스 통합
문서 처리와 임베딩 과정을 VectorStoreConfig
에 통합하여 애플리케이션 시작 시 자동으로 실행되도록 할 수 있다.
@Configuration
public class VectorStoreConfig {
private final EmbeddingModel embeddingModel;
private final DocumentProcessor documentProcessor;
private final EmbeddingService embeddingService;
// 생성자 주입
@Bean
@ConditionalOnProperty(name = "app.vectorstore.target", havingValue = "simple", matchIfMissing = true)
SimpleVectorStore simpleVectorStore() throws IOException {
var vectorStore = new SimpleVectorStore(embeddingModel);
File vectorStoreFile = new File(vectorStorePath);
if (vectorStoreFile.exists()) {
vectorStore.load(vectorStoreFile);
} else {
List<Document> documents = documentProcessor.processPdfDocuments();
embeddingService.embedAndStoreDocuments(documents);
vectorStore.save(vectorStoreFile);
}
return vectorStore;
}
}
이 구현을 통해 애플리케이션은 시작 시 자동으로 PDF 문서를 처리하고, 임베딩을 생성하여 VectorStore에 저장한다. 이렇게 준비된 데이터는 RAG 시스템에서 효과적으로 활용될 수 있다.
Spring AI의 추상화 덕분에 다양한 문서 형식과 임베딩 모델을 쉽게 교체하거나 확장할 수 있다. 예를 들어, 나중에 다른 문서 형식을 추가하거나 더 성능이 좋은 임베딩 모델로 전환하는 것이 용이하다.
질의 응답 시스템 구축
RAG 기반의 질의 응답 시스템은 사용자의 질문에 대해 관련 문서를 검색하고, 이를 바탕으로 AI 모델이 답변을 생성하는 방식으로 작동한다. Spring AI를 사용하여 이러한 시스템을 효과적으로 구축할 수 있다.
ChatClient 설정
먼저, ChatClient
를 설정하여 RAG 기능을 활성화한다.
@Configuration
public class ChatConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel, VectorStore vectorStore) {
return ChatClient.builder(chatModel)
.defaultSystem("금융위원회의 금융분야 마이데이터 기술 비서입니다. 주어진 컨텍스트를 바탕으로 질문에 답변하겠습니다.")
.defaultAdvisors(
// 채팅 기록을 유지하기 위한 MessageChatMemoryAdvisor
new MessageChatMemoryAdvisor(new InMemoryChatMemory()),
// RAG를 위한 QuestionAnswerAdvisor
new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults().withTopK(3)),
// 로깅을 위한 SimpleLoggerAdvisor
new SimpleLoggerAdvisor()
)
.build();
}
}
주요 포인트:
defaultSystem
을 사용하여 AI 모델의 기본 동작을 설정한다.MessageChatMemoryAdvisor
를 통해 대화 컨텍스트를 유지한다.QuestionAnswerAdvisor
를 사용하여 RAG 기능을 활성화한다.topK
를 3으로 설정하여 가장 관련성 높은 3개의 문서를 검색한다.SimpleLoggerAdvisor
를 추가하여 디버깅과 모니터링을 용이하게 한다.
질의 응답 서비스 구현
다음으로, 실제 질의 응답을 처리할 서비스를 구현한다.
@Service
public class QAService {
private final ChatClient chatClient;
public QAService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String askQuestion(String conversationId, String question) {
return chatClient.prompt()
.user(question)
.advisors(a -> a
// 대화 ID를 설정하여 대화 컨텍스트 유지
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
// 검색할 최대 토큰 수 설정
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
)
.call()
.content();
}
public Flux<String> askQuestionStream(String conversationId, String question) {
return chatClient.prompt()
.user(question)
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
)
.stream()
.content();
}
}
주요 포인트:
askQuestion
메서드는 동기식으로 답변을 반환한다.askQuestionStream
메서드는 Reactive Streams를 사용하여 답변을 스트리밍한다.CHAT_MEMORY_CONVERSATION_ID_KEY
를 사용하여 대화별로 컨텍스트를 유지한다.CHAT_MEMORY_RETRIEVE_SIZE_KEY
로 검색할 대화 기록의 최대 토큰 수를 설정한다.
컨트롤러 구현
마지막으로, 이 서비스를 사용할 RESTful API 컨트롤러를 구현한다.
@RestController
@RequestMapping("/api/qa")
public class QAController {
private final QAService qaService;
public QAController(QAService qaService) {
this.qaService = qaService;
}
@PostMapping
public ResponseEntity<String> askQuestion(@RequestBody QuestionRequest request) {
String answer = qaService.askQuestion(request.getConversationId(), request.getQuestion());
return ResponseEntity.ok(answer);
}
@PostMapping("/stream")
public Flux<String> askQuestionStream(@RequestBody QuestionRequest request) {
return qaService.askQuestionStream(request.getConversationId(), request.getQuestion());
}
// QuestionRequest 내부 클래스 정의
@Data
static class QuestionRequest {
private String conversationId;
private String question;
}
}
이 컨트롤러는 동기식과 스트리밍 방식의 질의 응답 엔드포인트를 모두 제공한다.
성능 및 확장성 고려사항
캐싱: 자주 묻는 질문에 대한 답변을 캐싱하여 응답 시간을 단축할 수 있다.
비동기 처리: 긴 응답 시간이 예상되는 경우, 비동기 처리를 통해 서버 리소스를 효율적으로 사용할 수 있다.
벡터 검색 최적화:
SearchRequest
의withSimilarityThreshold
메서드를 사용하여 유사도 임계값을 설정하면, 검색 정확도와 속도를 조절할 수 있다.모니터링:
SimpleLoggerAdvisor
를 통해 로깅된 정보를 분석하여 시스템 성능을 모니터링하고 개선점을 찾을 수 있다.
이렇게 구현된 RAG 기반 질의 응답 시스템은 마이데이터 관련 질문에 대해 정확하고 상세한 답변을 제공할 수 있다. Spring AI의 유연한 구조 덕분에 필요에 따라 다양한 AI 모델이나 벡터 저장소로 쉽게 전환할 수 있으며, 시스템의 성능과 정확도를 지속적으로 개선할 수 있다.
결론 및 향후 계획
프로젝트 회고
마이데이터 에이전트 개발 프로젝트를 통해 Spring AI의 강력함과 유연성을 직접 경험했다. 이 프로젝트는 AI 기술을 실제 비즈니스 문제 해결에 적용하는 과정에서 많은 학습과 도전의 기회를 제공했다.
주요 성과:
RAG 시스템 구현으로 정확하고 맥락에 맞는 응답 생성
실시간 채팅 기능으로 사용자 경험 향상
성능 최적화를 통한 시스템 안정성 확보
Spring AI를 활용한 개발의 장단점
장점:
쉬운 통합: Spring 생태계와의 원활한 통합으로 개발 시간 단축
추상화 수준: 다양한 AI 모델과 벡터 저장소에 대한 높은 수준의 추상화 제공
확장성: 새로운 AI 기능을 쉽게 추가하고 확장할 수 있는 구조
성능: 비동기 처리와 리액티브 프로그래밍 지원으로 높은 성능 달성
단점:
학습 곡선: Spring AI의 새로운 개념과 API에 익숙해지는 데 시간 소요
문서화: 프로젝트 초기 단계에서 일부 기능에 대한 문서화 부족
의존성 관리: 빠르게 발전하는 AI 기술로 인한 의존성 버전 관리의 어려움
만약한다면 향후 개선 방향
다국어 지원: 현재 한국어 중심의 시스템을 다국어로 확장하여 글로벌 사용자 지원
멀티모달 기능 강화: 텍스트뿐만 아니라 이미지, 음성 데이터도 처리할 수 있도록 기능 확장
public Mono<String> processMultimodalInput(String text, byte[] image) {
return Mono.fromCallable(() -> {
var userMessage = new UserMessage(text,
List.of(new Media(MimeTypeUtils.IMAGE_PNG, image)));
return chatClient.call(new Prompt(userMessage)).getResult().getOutput().getContent();
}).subscribeOn(Schedulers.boundedElastic());
}
AI 모델 fine-tuning: 마이데이터 도메인에 특화된 모델 fine-tuning으로 응답 품질 향상
보안 강화: 개인정보 보호를 위한 추가적인 보안 메커니즘 구현
// 예제 코드
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
return http.build();
}
}
- 성능 모니터링 시스템: 상세한 성능 지표를 수집하고 분석하는 모니터링 시스템 구축
이러한 개선을 통해 마이데이터 에이전트의 기능과 성능을 더욱 향상시키고, 사용자에게 더 나은 경험을 제공할 수 있을 것이다.
참고 링크
프로젝트 github: https://github.com/sm0514sm/mymydata/tree/master
spring ai doc: https://docs.spring.io/spring-ai/reference/
Subscribe to my newsletter
Read articles from sangminLee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by