Using Hugging Face Models With Spring AI and Ollama

1. 개요

인공지능은 웹 애플리케이션 구축 방식을 변화시키고 있습니다. Hugging Face는 방대한 오픈소스 및 사전 훈련된 LLMs를 제공하는 인기 플랫폼입니다.

우리는 Ollama라는 오픈소스 도구를 사용하여 로컬 머신에서 LLM을 실행할 수 있습니다. 이 도구는 Hugging Face의 GGUF 형식 모델을 지원합니다.

이 튜토리얼에서는 Hugging Face 모델을 Spring AI 및 Ollama와 함께 사용하는 방법을 탐구할 것입니다. 우리는 채팅 완료 모델을 사용하여 간단한 챗봇을 만들고 임베딩 모델을 사용하여 의미 검색을 구현할 것입니다.

2. 의존성

프로젝트의 pom.xml 파일에 필요한 의존성을 추가해보겠습니다:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
    <version>1.0.0-M5</version>
</dependency>

Ollama 스타터 의존성는 Ollama 서비스와의 연결을 설정하는 데 도움을 줍니다. 우리는 이를 이용하여 채팅 완료 및 임베딩 모델을 가져오고 실행할 것입니다.

현재 버전인 1.0.0-M5는 이정표 릴리스이므로, pom.xml에 Spring Milestones 저장소를 추가해야 합니다:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

이 저장소는 이정표 버전이 게시되는 곳으로, 표준 Maven Central 저장소와는 다릅니다.

3. Testcontainers로 Ollama 설정하기

로컬 개발 및 테스트를 용이하게 하기 위해, 우리는 Testcontainers를 사용하여 Ollama 서비스를 설정할 것입니다.

3.1. 테스트 의존성

먼저, pom.xml에 필요한 테스트 의존성을 추가하겠습니다:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>ollama</artifactId>
    <scope>test</scope>
</dependency>

우리는 Spring Boot에 대한 Spring AI Testcontainers 의존성과 Testcontainers의 Ollama 모듈을 가져옵니다.

3.2. Testcontainers 빈 정의하기

다음으로, Testcontainers 빈을 정의하는 @TestConfiguration 클래스를 생성합니다:

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
    @Bean
    public OllamaContainer ollamaContainer() {
        return new OllamaContainer("ollama/ollama:0.5.4");
    }

    @Bean
    public DynamicPropertyRegistrar dynamicPropertyRegistrar(OllamaContainer ollamaContainer) {
        return registry -> {
            registry.add("spring.ai.ollama.base-url", ollamaContainer::getEndpoint);
        };
    }
}

우리는 OllamaContainer 빈을 생성할 때 올바른 안정적인 Ollama 이미지 버전을 지정합니다.

그리고 Ollama 서비스의 base-url을 구성하기 위해 DynamicPropertyRegistrar 빈을 정의합니다. 이를 통해 애플리케이션이 시작된 Ollama 컨테이너에 연결할 수 있습니다.

3.3. 개발 중 Testcontainers 사용하기

Testcontainers는 주로 통합 테스트에 사용되지만, 로컬 개발 중에도 사용할 수 있습니다.

이를 위해 src/test/java 디렉터리에 별도의 메인 클래스를 생성합니다:

public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.from(Application::main)
          .with(TestcontainersConfiguration.class)
          .run(args);
    }
}

우리는 TestApplication 클래스를 생성하고 그 안의 main() 메서드에서 TestcontainersConfiguration 클래스를 사용하여 메인 Application 클래스를 시작합니다.

이 설정은 Spring Boot 애플리케이션을 실행하고 그것이 Testcontainers를 통해 시작된 Ollama 서비스에 연결하는 데 도움이 됩니다.

4. 채팅 완료 모델 사용하기

이제 로컬 Ollama 컨테이너를 설정했으므로, 채팅 완료 모델을 사용하여 간단한 챗봇을 만들어 보겠습니다.

4.1. 채팅 모델 및 챗봇 빈 구성하기

application.yaml 파일에서 채팅 완료 모델을 구성합니다:

spring:
  ai:
    ollama:
      init:
        pull-model-strategy: when_missing
      chat:
        options:
          model: hf.co/microsoft/Phi-3-mini-4k-instruct-gguf

Hugging Face 모델을 구성하기 위해 hf.co/{username}/{repository} 형식을 사용합니다. 여기에서는 Microsoft가 제공한 Phi-3-mini-4k-instruct 모델의 GGUF 버전을 지정합니다.

이 모델을 구현하는 데 활용할 의무는 없지만, 코드베이스를 로컬에 설정하고 더 많은 채팅 완료 모델로 실험해보는 것을 권장합니다.

pull-model-strategywhen_missing으로 설정하면, Spring AI가 지정된 모델을 로컬에서 사용할 수 없을 경우 자동으로 가져옵니다.

유효한 모델을 구성하면, Spring AI는 자동으로 ChatModel 타입의 빈을 생성하여 채팅 완료 모델과 상호작용할 수 있게 합니다.

이를 사용하여 챗봇에 필요할 추가 빈을 정의합니다:

@Configuration
class ChatbotConfiguration {
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }

    @Bean
    public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
        return ChatClient
          .builder(chatModel)
          .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
          .build();
    }
}

먼저, ChatMemory 빈을 정의하고 InMemoryChatMemory 구현을 사용합니다. 이 구현은 메모리에 채팅 이력을 저장하여 대화 컨텍스트를 유지합니다.

이후 ChatMemoryChatModel 빈을 사용하여 채팅 완료 모델과 상호작용하는 주요 진입점인 ChatClient 타입의 빈을 생성합니다.

4.2. 챗봇 구현하기

구성이 완료된 후, 이제 ChatbotService 클래스를 만들어봅시다. 이전에 정의한 ChatClient 빈을 주입하여 모델과 상호작용할 것입니다.

먼저, 채팅 요청 및 응답을 나타내는 두 가지 간단한 레코드를 정의합니다:

record ChatRequest(@Nullable UUID chatId, String question) {}

record ChatResponse(UUID chatId, String answer) {}

ChatRequest는 사용자의 질문 및 진행 중인 대화를 식별하기 위한 선택적 chatId를 포함합니다.

비슷하게, ChatResponsechatId와 챗봇의 답변을 포함합니다.

이제 의도한 기능을 구현해보겠습니다:

public ChatResponse chat(ChatRequest chatRequest) {
    UUID chatId = Optional
      .ofNullable(chatRequest.chatId())
      .orElse(UUID.randomUUID());
    String answer = chatClient
      .prompt()
      .user(chatRequest.question())
      .advisors(advisorSpec ->
          advisorSpec
            .param("chat_memory_conversation_id", chatId))
      .call()
      .content();
    return new ChatResponse(chatId, answer);
}

요청이 chatId를 포함하지 않는 경우 새 chatId를 생성합니다. 이렇게 하면 사용자가 새 대화를 시작하거나 기존 대화를 계속할 수 있습니다.

사용자의 질문chatClient 빈에 전달하고, chatmemoryconversation_id 매개변수를 해결된 chatId로 설정하여 대화 이력을 유지합니다.

마지막으로, 챗봇의 답변과 함께 chatId를 반환합니다.

4.3. 챗봇과 상호작용하기

이제 서비스 계층을 구현했으므로, 이를 바탕으로 REST API를 공개해보겠습니다:

@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
    ChatResponse chatResponse = chatbotService.chat(chatRequest);
    return ResponseEntity.ok(chatResponse);
}

위의 API 엔드포 int를 사용하여 챗봇과 상호작용할 것입니다.

HTTPie CLI를 사용하여 새 대화를 시작해보겠습니다:

http POST :8080/chat question="Who wanted to kill Harry Potter?"

챗봇에 간단한 질문을 보내고, 그에 대한 응답을 확인해보겠습니다:

{
    "chatId": "7b8a36c7-2126-4b80-ac8b-f9eedebff28a",
    "answer": "Lord Voldemort, also known as Tom Riddle, wanted to kill Harry Potter because of a prophecy that foretold a boy born at the end of July would have the power to defeat him."
}

응답에는 고유한 chatId와 챗봇의 답변이 포함되어 있습니다.

이 대화를 계속하여 위의 응답에서 chatId를 사용하여 후속 질문을 보내보겠습니다:

http POST :8080/chat chatId="7b8a36c7-2126-4b80-ac8b-f9eedebff28a" question="Who should he have gone after instead?"

챗봇이 대화의 맥락을 유지하고 적절한 응답을 제공할 수 있는지 확인해보겠습니다:

{
    "chatId": "7b8a36c7-2126-4b80-ac8b-f9eedebff28a",
    "answer": "Based on the prophecy's criteria, Voldemort could have targeted Neville Longbottom instead, as he was also born at the end of July to parents who had defied Voldemort three times."
}

우리가 볼 수 있듯이, 챗봇은 이전 메시지에서 논의한 예언을 참조하여 대화의 흐름을 유지하고 있습니다.

챗봇의 chatId는 동일하게 유지되며, 후속 답변이 동일한 대화의 연속임을 나타냅니다.

5. 임베딩 모델 사용하기

채팅 완료 모델에서 넘어와서, 이제 임베딩 모델을 사용하여 소규모 인용구 데이터세트에서 의미 검색을 구현할 것입니다.

우리는 외부 API에서 인용구를 가져오고, 이를 인메모리 벡터 저장소에 저장한 뒤 의미 검색을 수행할 것입니다.

5.1. 외부 API에서 인용구 레코드 가져오기

우리의 시연을 위해, QuoteSlate API를 사용하여 인용구를 가져올 것입니다.

이를 위해 QuoteFetcher 유틸리티 클래스를 생성합니다:

class QuoteFetcher {
    private static final String BASE_URL = "https://quoteslate.vercel.app";
    private static final String API_PATH = "/api/quotes/random";
    private static final int DEFAULT_COUNT = 50;

    public static List<Quote> fetch() {
        return RestClient
          .create(BASE_URL)
          .get()
          .uri(uriBuilder ->
              uriBuilder
                .path(API_PATH)
                .queryParam("count", DEFAULT_COUNT)
                .build())
          .retrieve()
          .body(new ParameterizedTypeReference<>() {});
    }
}

record Quote(String quote, String author) {}

RestClient를 사용하여 QuoteSlate API를 기본 카운트인 50으로 호출하고, API 응답을 Quote 레코드 목록으로 역직렬화하기 위해 ParameterizedTypeReference를 사용합니다.

5.2. 인메모리 벡터 저장소 구성 및 채우기

이제, application.yaml에서 임베딩 모델을 구성해보겠습니다:

spring:
  ai:
    ollama:
      embedding:
        options:
          model: hf.co/nomic-ai/nomic-embed-text-v1.5-GGUF

우리는 nomic-embed-text-v1.5 모델의 GGUF 버전을 사용합니다. 필요하다면 다른 임베딩 모델로 이 구현을 시도해 보세요.

유효한 모델을 지정한 후, Spring AI는 자동으로 EmbeddingModel 타입의 빈을 생성합니다.

이를 사용하여 벡터 저장소 빈을 생성합니다:

@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
    return SimpleVectorStore
      .builder(embeddingModel)
      .build();
}

우리의 시연을 위해 SimpleVectorStore 클래스의 빈을 생성합니다. 이는 java.util.Map 클래스를 사용하여 벡터 저장소를 에뮬레이트하는 인메모리 구현입니다.

이제, 애플리케이션 시작 시 인용구로 벡터 저장소를 채우기 위해 ApplicationRunner 인터페이스를 구현하는 VectorStoreInitializer 클래스를 생성합니다:

@Component
class VectorStoreInitializer implements ApplicationRunner {
    private final VectorStore vectorStore;

    // 표준 생성자

    @Override
    public void run(ApplicationArguments args) {
        List<Document> documents = QuoteFetcher
          .fetch()
          .stream()
          .map(quote -> {
              Map<String, Object> metadata = Map.of("author", quote.author());
              return new Document(quote.quote(), metadata);
          })
          .toList();
        vectorStore.add(documents);
    }
}

VectorStoreInitializer에서는 VectorStore 인스턴스를 자동 주입합니다.

run() 메서드 안에서 우리는 QuoteFetcher 유틸리티 클래스를 사용하여 Quote 레코드 목록을 가져옵니다. 그런 다음 각 quoteDocument로 매핑하고 author 필드를 메타데이터로 구성합니다.

마지막으로 모든 documents를 벡터 저장소에 저장합니다. 우리가 add() 메서드를 호출할 때, Spring AI는 자동으로 우리의 일반 텍스트 내용을 벡터 표현으로 변환한 후 이를 벡터 저장소에 저장합니다. 우리는 EmbeddingModel 빈을 사용해 명시적으로 변환할 필요가 없습니다.

5.3. 의미 검색 테스트하기

벡터 저장소가 채워지면, 의미 검색 기능을 검증해 보겠습니다:

private static final int MAX_RESULTS = 3;

@ParameterizedTest
@ValueSource(strings = {"Motivation", "Happiness"})
void whenSearchingQuotesByTheme_thenRelevantQuotesReturned(String theme) {
    SearchRequest searchRequest = SearchRequest
      .builder()
      .query(theme)
      .topK(MAX_RESULTS)
      .build();
    List<Document> documents = vectorStore.similaritySearch(searchRequest);

    assertThat(documents)
      .hasSizeBetween(1, MAX_RESULTS)
      .allSatisfy(document -> {
          String title = String.valueOf(document.getMetadata().get("author"));
          assertThat(title)
            .isNotBlank();
      });
}

여기서는 공통 인용구 주제를 테스트 메서드에 전달하기 위해 @ValueSource를 사용합니다. 그런 다음 주제를 쿼리로, MAX_RESULTS를 원하는 결과 수로 지정한 SearchRequest 객체를 생성합니다.

그 다음, searchRequestvectorStore 빈의 similaritySearch() 메서드를 호출합니다. VectorStoreadd() 메서드와 유사하게, Spring AI는 우리의 쿼리를 벡터 표현으로 변환하여 벡터 저장소를 쿼리합니다.

반환된 문서에는 주제에 의미적으로 관련된 인용구가 포함되어 있으며, 정확한 키워드를 포함하지 않더라도 관련성이 있을 수 있습니다.

6. 결론

이 기사에서는 Hugging Face 모델을 Spring AI와 함께 사용하는 방법을 탐구했습니다.

Testcontainers를 사용하여 Ollama 서비스를 설정하고 로컬 테스트 환경을 만들었습니다.

먼저, 채팅 완료 모델을 사용하여 간단한 챗봇을 구축했습니다. 그리고 임베딩 모델을 사용하여 의미 검색을 구현했습니다.

언제나 그렇듯이, 이 기사에서 사용된 모든 코드 예제는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다