본문 바로가기
dev/spring

Spring AI를 사용한 AI 어시스턴트 구현 RAG - Part 2 (with OpenAI)

by igooo 2024. 10. 3.
728x90

개요

Spring AI에 첫 번째 부분에서 Spring AI를 LLM과(대규모 언어 모델) 통합하는 기본 사항에 대하여 구현했다. 사용자 지정 ChatClient를 생성하여 애플리케이션에서 제공하는 함수를 호출하에 사용자 프롬프트에 응답하는 기능에 대하여 구현했다. 

Part 2에서는 일반적인 함수 호출 방식의 제약에 맞지 않은 대규모 데이터 세트를 처리할 수 있는 기술인 RAG를(Retrieval-Augmented Generation) 사용하여 검색 기능을 살펴보고, RAG를 사용하여 어떻게 AI를 애플리케이션과 통합할 수 있는지 알아본다.

 

RAG(Retrieval-Augmented Generation) 

RAG(Retrieval-Augmented Generation)는 대규모 언어 모델의 출력을 최적화하여 응답을 생성하기 전에 학습 데이터 소스 외부의 신뢰할 수 있는 지식 베이스를 참조하도록 하는 프로세스다. RAG를 사용하면 LLM을 실시간 데이터 검색과 통합하여 정확하고 문백적으로 관련성이 있는 텍스트를 생성한다. 예제에서 RAG는 TODO에 내용을 임베딩 형태로(텍스트를 고차원 벡터로 변환) 변환하고 Vector Store에 저장하고 기존의 텍스트 기반 검색이 아닌 의미론에(semantics) 기반한 효율적인 유사성 검색을(similarity searches) 사용하여 검색한다.

 

이미지 검색 참고 : https://blog.igooo.org/116

 

기존 검색 시스템과 차이점

  • Database : LIKE 구문을 사용한 텍스트 검색은 실제 저장된 데이터에 일치하는 문자이 있어야 검색이 가능하다.
    • ex) I have child. -> children으로 검색하는 경우 검색 불가
  • 검색엔진 : 형태소 분석을 통한 텍스트 검색으로 복수형, 동의어(ex 버스카드 -> 버카) 등의 처리는 가능하지만 수동으로 정의가 필요하고, 최종적으로는 형태소 분석을 통해 나온 단어가 포함된 문서가 검색된다.
    • ex) 버스 -> '교통 카드'로 검색하면 검색 결과를 얻을 수 없다.
  • RAG : 임베딩을 기반으로 하는 의미 검색을 사용하면 "교통 카드"는 버스와 관련이 있음을 인식하고 명시적으로 버스가 언급되지 않았음에도 불구하고 일치하는 항목으로 반환될 수 있다.

임베딩이 정확한 키워드에만 의종하는 것이 아니라 근본적인 의미를 포착하는 방식을 보여준다.

아래 예제를 통해 RAG를 사용한 검색 구현 방법에 대하여 살펴본다.

 

Getting started

프로젝트는 기존 Part 1에 프로젝트를(https://blog.igooo.org/150) 그대로 사용하면서 RAG 기능만 추가함으로 이전 Post를 참고한다.

 

데이터 임베딩(Embedding the Test Data)

임베딩된 데이터를 저장할 Vector Store를 Bean으로 먼저 정의한다. Database처럼 외부 솔루션을 사용가능하지만 예제에서는 데이터가 많지 않아 Spring AI에서 제공하는 SimpleVectorStore를 사용한다. Spring AI에서 지원하는 Vector Store 목록은 https://docs.spring.io/spring-ai/reference/api/vectordbs.html에서 확인 가능하다.

@Configuration
class AIChatClientConfig {
	......

	@Bean
	VectorStore vectorStore(EmbeddingModel embeddingModel) {
		return new SimpleVectorStore(embeddingModel);
	}

	......
}

 

VectorStore를 생성했으니 VectorStore에 데이터를 임베드한다.

@Configuration
class AIEmbeddingDataConfig {
	private final TodoRepository todoRepository;
	private final VectorStore vectorStore;

	AIEmbeddingDataConfig(TodoRepository todoRepository, VectorStore vectorStore) {
		this.todoRepository = todoRepository;
		this.vectorStore = vectorStore;
	}

	@EventListener
	public void loadTodoDataToVectorStoreOnStartup(ApplicationStartedEvent event) {
		var todoList = this.todoRepository.findAll();

		var todoAsJson = convertListToJsonResource(todoList);
		var reader = new JsonReader(todoAsJson);

		var documentList = reader.get();
		this.vectorStore.add(documentList);
	}

	private Resource convertListToJsonResource(List<Todo> todoList) {
		var objectMapper = new ObjectMapper();
		objectMapper.registerModule(new JavaTimeModule());

		try {
			// Convert List<Vet> to JSON string
			var json = objectMapper.writeValueAsString(todoList);

			// Convert JSON string to byte array
			var jsonBytes = json.getBytes();

			// Create a ByteArrayResource from the byte array
			return new ByteArrayResource(jsonBytes);
		} catch (JsonProcessingException e) {
			e.printStackTrace();
			return null;
		}
	}
}

 

  1. @EventListener Spring에서 발생하는 Event를 처리하는 어노테이션으로 ApplicationStartedEvent가 발생하면, 즉 애플리케이션이 실행이 완료되면 이벤트를 처리한다. 
  2. todo 전체 목록을 조회하여 convertListToJsonResource()를 호출하여 전체 데이터를 Json으로 변환 후 Bytes 형태의 Resource 객체로 변환한다.
  3. JsonReader를 사용하여 AI에서 사용하는 Document(org.springframework.ai.document) 객체로 변환한다.
  4. 변환된 객체를 VectorStore에 저장한다.

애플리케이션을 실행하면 TODO 전체 목록이 하나씩 임베딩되어 VectorStore에 저장하는 로그를 확인할 수 있다.

......
....SimpleVectorStore   : Calling EmbeddingModel for document id = 4aa442c3-f49d-48e8-a383-ce8a59a791e0
....SimpleVectorStore   : Calling EmbeddingModel for document id = 3f15972f-86e6-4dd5-9505-b483ba05a97f
....SimpleVectorStore   : Calling EmbeddingModel for document id = 26d41806-dfff-4705-8e49-58393c2fd074
....VectorStoreConfig   : vector store loaded with 150 documents

 

Spring Petclinic 예제에서는 임베딩 데이터를 Json 파일로 저장하고, 애플리케이션이 실행될때 Json 파일이 있으면 Json 파일을 바로 VectorStore에 로드하여 테스트 데이터를 여러 번 임베딩하지 않도록 적용했다. 아래 코드 참고
@EventListener
	public void loadVetDataToVectorStoreOnStartup(ApplicationStartedEvent event) throws IOException {
		Resource resource = new ClassPathResource("vectorstore.json");

		// Check if file exists
		if (resource.exists()) {
			// In order to save on AI credits, use a pre-embedded database that was saved
			// to disk based on the current data in the h2 data.sql file
			File file = resource.getFile();
			((SimpleVectorStore) this.vectorStore).load(file);
			logger.info("vector store loaded from existing vectorstore.json file in the classpath");
			return;
		}

 

유사성 검색(Similatity Search)

VectorStore에 데이터가 저장되면 SearchRequest 객체를 사용하여 유사 검색을 한다.

검색한 검색어와 인접한 데이터를 검색하여 사용자에게 응답 하도록 구현한다.

@Service
class AIDataProvider {
	private final UserRepository userRepository;
	private final VectorStore vectorStore;

	public AIDataProvider(UserRepository userRepository, VectorStore vectorStore) {
		this.userRepository = userRepository;
		this.vectorStore = vectorStore;
	}

	......

	public List<String> findAllTodo(TodoRequest todoRequest) throws JsonProcessingException {
		var objectMapper = new ObjectMapper();
		objectMapper.registerModule(new JavaTimeModule());

		var todoAsJson = objectMapper.writeValueAsString(todoRequest.todo());

		var searchRequest = SearchRequest.from(SearchRequest.defaults()).withQuery(todoAsJson).withTopK(5);
		if (todoRequest.todo() == null) {
			searchRequest = searchRequest.withTopK(10);
		}

		var topMatches = this.vectorStore.similaritySearch(searchRequest);
		var results = topMatches.stream().map((document) -> document.getContent()).toList();
		return results;
	}

}

 

AI에 제공할 Function도 Bean으로 생성하여 등록한다.

@Configuration
class AIFunctionConfig {
	......

	@Bean
	@Description("List the todo that the user'todo has")
	Function<TodoRequest, TodoResponse> listTodos(AIDataProvider dataProvier) {
		return (request) -> {
			try {
				return new TodoResponse(dataProvier.findAllTodo(request));
			} catch (JsonProcessingException e) {
				e.printStackTrace();
				return null;
			}
		};
	}
}

......

record TodoRequest(Todo todo) { }

record TodoResponse(List<String> todos) { }

 

spring:
  ai:
    openai:
      api-key: << API_KEY >>
      chat:
        options:
          model: gpt-4o
          temperature: 0.7
          functions:
          .....
          - listTodos

 

Demo

Todo 관련 검색을 해보면 정상적으로 유사한 Todo 목록을 3개 알려준다.

 

 

그동안 AI관련해서는 파이선으로만 예제들이 많아서 Java를 주로 사용하는 프로그래머에서는 AI 관련 작업과 조금 거리가 있었지만, Spring AI를 사용하면 대부분의 AI 기능을 기존 애플리케션에 코드 변경 없이 간단한 설정과 약간의 코드만으로 쉽게 연동할 수 있다. 추가적으로 VectorStore와 ChatMemory에 대하여 외부 솔루션을 사용하여 실제 현업에서 사용할 만한 성능이 나오는지 확인하는 작업도 해봐야겠다.

 

참고