Testing LLM Responses Using Spring AI Evaluators

1. 개요

현대의 웹 애플리케이션은 챗봇 및 가상 비서와 같은 솔루션을 구축하기 위해 대규모 언어 모델(Large Language Models, LLMs)과 점점 더 통합되고 있습니다.

그러나 LLM은 강력하지만, 생성하는 답변이 환각이 발생할 수 있으며, 그 응답이 항상 적절하거나 관련성이 있거나 사실적으로 정확하지 않다는 단점이 있습니다.

LLM 응답을 평가하는 한 가지 방법은 LLM 자체를 사용하는 것입니다. 바람직하게는 서로 다른 LLM을 사용하는 것이 좋습니다.

이를 위해 Spring AI는 Evaluator 인터페이스를 정의하고 LLM 응답의 관련성과 사실적 정확성을 확인하기 위해 RelevanceEvaluatorFactCheckingEvaluator라는 두 가지 구현을 제공합니다.

이번 튜토리얼에서는 Spring AI Evaluators를 사용하여 LLM 응답을 테스트하는 방법을 살펴보겠습니다. Spring AI에서 제공하는 두 가지 기본 구현을 사용하여 Retrieval-Augmented Generation (RAG) 챗봇의 응답을 평가할 것입니다.

2. RAG 챗봇 구축

테스트할 LLM 응답을 시작하기 전에, 먼저 테스트할 챗봇을 만들어야 합니다. 본 시연을 위해 사용자 질문에 문서 세트를 기반으로 답변하는 간단한 RAG 챗봇을 구축할 것입니다.

우리는 Ollama라는 오픈 소스 도구를 사용하여 로컬에서 채팅 완료 및 임베딩 모델을 가져오고 실행할 것입니다.

2.1. 의존성

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

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

Ollama 스타터 의존성는 Ollama 서비스와 연결을 설정하는 데 도움이 됩니다.

추가적으로, 우리는 Spring AI의 마크다운 문서 리더 의존성을 가져오며, 이는 .md 파일을 벡터 저장소에 저장할 수 있는 문서로 변환하는 데 사용됩니다.

현재 버전인 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 리포지토리와 달리 기능 미리보기 버전이 게시되는 곳입니다.

여러 가지 Spring AI 스타터를 프로젝트에서 사용하고 있으므로, Spring AI Bill of Materials (BOM)pom.xml에 포함하겠습니다:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0-M5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

이 추가로 인해 두 개의 스타터 의존성에서 version 태그를 제거할 수 있습니다.

BOM은 버전 충돌 위험을 제거하고 우리의 Spring AI 의존성이 서로 호환되도록 보장합니다.

2.2. 채팅 완료 및 임베딩 모델 구성

다음으로 application.yaml 파일에서 채팅 완료 및 임베딩 모델을 구성해보겠습니다:

spring:
  ai:
    ollama:
      chat:
        options:
          model: llama3.3
      embedding:
        options:
          model: nomic-embed-text
      init:
        pull-model-strategy: when_missing

여기서 우리는 Meta가 제공하는 llama3.3 모델을 채팅 완료 모델로, Nomic AI가 제공하는 nomic-embed-text 모델을 임베딩 모델로 지정합니다. 다른 모델로 이 구현을 시도해도 됩니다.

추가적으로 pull-model-strategywhen_missing으로 설정합니다. 이는 Spring AI가 지정된 모델이 로컬에 없을 경우 이를 가져오도록 보장합니다.

유효한 모델이 구성되면, Spring AI는 자동으로 ChatModelEmbeddingModel 유형의 빈을 생성하여 우리가 채팅 완료 모델 및 임베딩 모델과 상호 작용할 수 있도록 합니다.

이들을 사용하여 챗봇에 필요한 추가 빈을 정의합시다:

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

@Bean
public ChatClient contentGenerator(ChatModel chatModel, VectorStore vectorStore) {
    return ChatClient.builder(chatModel)
      .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
      .build();
}

먼저, VectorStore 빈을 정의하고, SimpleVectorStore 구현을 사용합니다. 이 방식은 java.util.Map 클래스를 사용하여 벡터 저장소를 에뮬레이트하는 메모리 내 구현입니다.

생산 애플리케이션에서는 ChromaDB와 같은 실제 벡터 저장소를 사용하는 것을 고려할 수 있습니다.

다음으로, ChatModelVectorStore 빈을 사용하여 주요 채팅 완료 모델과 상호작용하기 위한 ChatClient 타입의 빈을 생성합니다.

우리는 벡터 저장소를 사용하여 사용자의 질문에 따라 관련 문서를 검색하고 이를 채팅 모델에 대한 컨텍스트로 제공하는 QuestionAnswerAdvisor로 이를 구성합니다.

2.3. 우리의 메모리 내 벡터 저장소 채우기

우리의 시연을 위해, 우리는 src/main/resources/documents 디렉터리에 휴가 정책에 대한 샘플 정보를 포함하는 leave-policy.md 파일을 포함하였습니다.

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

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

    // 표준 생성자

    @Override
    public void run(ApplicationArguments args) {
        List<Document> documents = new ArrayList<>();
        Resource[] resources = resourcePatternResolver.getResources("classpath:documents/*.md");
        Arrays.stream(resources).forEach(resource -> {
            MarkdownDocumentReader markdownDocumentReader = new MarkdownDocumentReader(resource, MarkdownDocumentReaderConfig.defaultConfig());
            documents.addAll(markdownDocumentReader.read());
        });
        vectorStore.add(new TokenTextSplitter().split(documents));
    }
}

run() 메서드 내에서, 먼저 주입된 ResourcePatternResolver 클래스를 사용하여 src/main/resources/documents 디렉터리에서 모든 마크다운 파일을 가져옵니다. 우리는 현재 하나의 마크다운 파일만 작업하고 있지만, 우리의 방법은 확장 가능합니다.

그런 다음, 가져온 resourcesMarkdownDocumentReader 클래스를 사용하여 Document 객체로 변환합니다.

마지막으로, TokenTextSplitter 클래스를 사용하여 더 작은 청크로 분할한 후 벡터 저장소에 documents를 추가합니다.

add() 메서드를 호출할 때, Spring AI는 자동으로 평문 콘텐츠를 벡터 표현으로 변환한 후 벡터 저장소에 저장합니다. 우리는 EmbeddingModel 빈을 사용하여 명시적으로 변환할 필요가 없습니다.

3. Ollama 설정하기 Testcontainers와 함께

로컬 개발 및 테스트를 용이하게 하기 위해, 우리는 Testcontainers를 사용하여 Ollama 서비스를 설정할 것입니다. 이의 전제 조건은 활성 Docker 인스턴스입니다.

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 모듈을 가져옵니다.

이 의존성들은 Ollama 서비스를 위한 임시 Docker 인스턴스를 시작하는 데 필요한 클래스를 제공합니다.

3.2. Testcontainers 빈 정의하기

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

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

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

OllamaContainer 빈을 생성할 때 Ollama 이미지의 최신 안정 버전을 지정합니다.

그런 다음, Ollama 서비스의 base-url을 구성하는 DynamicPropertyRegistrar 빈을 정의합니다. 이는 우리의 애플리케이션이 시작된 컨테이너에 연결할 수 있도록 합니다.

이제 테스트 클래스에 @Import(TestcontainersConfiguration.class) 주석을 추가하여 이 구성을 사용할 수 있습니다.

4. Spring AI Evaluators 사용하기

이제 RAG 챗봇을 구축하고 로컬 테스트 환경을 설정했으므로, Spring AI의 Evaluator 인터페이스의 두 가지 구현을 사용하여 챗봇이 생성하는 응답을 테스트하는 방법을 살펴보겠습니다.

4.1. 평가 모델 구성하기

테스트의 품질은 궁극적으로 사용되는 평가 모델의 품질에 달려 있습니다. 우리는 현재 업계 표준인 bespoke-minicheck 모델을 선택할 것입니다, 이는 Bespoke Labs에 의해 평가 테스트를 위해 특수하게 훈련된 오픈 소스 모델로, LLM-AggreFact 리더보드에서 상위에 위치해 있으며, 예/아니오 응답만을 생성합니다.

application.yaml 파일에 이를 구성해봅시다:

com:
  baeldung:
    evaluation:
      model: bespoke-minicheck

이제 우리의 평가 모델과 상호작용하기 위해 별도의 ChatClient 빈을 생성하겠습니다:

@Bean
public ChatClient contentEvaluator(
  OllamaApi olamaApi,
  @Value("${com.baeldung.evaluation.model}") String evaluationModel
) {
    ChatModel chatModel = OllamaChatModel.builder()
      .ollamaApi(olamaApi)
      .defaultOptions(OllamaOptions.builder()
        .model(evaluationModel)
        .build())
      .modelManagementOptions(ModelManagementOptions.builder()
        .pullModelStrategy(PullModelStrategy.WHEN_MISSING)
        .build())
      .build();
    return ChatClient.builder(chatModel)
      .build();
}

여기서, Spring AI가 생성한 OllamaApi 빈과 우리의 커스텀 평가 모델 속성을 사용하여 새로운 ChatClient 빈을 정의합니다. 이는 @Value 주석을 통해 주입합니다.

이때 유의해야 할 점은, 평가 모델에 대해 커스텀 속성을 사용하고 해당하는 ChatModel 클래스를 수동으로 생성하고 있다는 것입니다. 이는 OllamaAutoConfiguration 클래스가 spring.ai.ollama.chat.options.model 속성을 통해 한 모델만 구성할 수 있게 되어 있기 때문입니다. 우리는 이미 콘텐츠 생성 모델을 위해 이를 사용했습니다.

4.2. RelevancyEvaluator로 LLM 응답의 관련성 평가하기

Spring AI는 LLM 응답이 사용자 질문과 벡터 저장소에서 검색된 컨텍스트와 관련이 있는지 확인하기 위해 RelevancyEvaluator 구현을 제공합니다.

먼저 이를 위한 빈을 만듭니다:

@Bean
public RelevancyEvaluator relevancyEvaluator(
    @Qualifier("contentEvaluator") ChatClient chatClient) {
    return new RelevancyEvaluator(chatClient.mutate());
}

우리는 @Qualifier 주석을 사용하여 이전에 정의한 relevancyEvaluator ChatClient 빈을 주입하고 RelevancyEvaluator 클래스의 인스턴스를 생성합니다.

이제 챗봇의 응답에 대한 관련성을 테스트해봅시다:

String question = "How many days sick leave can I take?";
ChatResponse chatResponse = contentGenerator.prompt()
  .user(question)
  .call()
  .chatResponse();

String answer = chatResponse.getResult().getOutput().getContent();
List<Document> documents = chatResponse.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS);
EvaluationRequest evaluationRequest = new EvaluationRequest(question, documents, answer);

EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isTrue();

String nonRelevantAnswer = "A lion is the king of the jungle";
evaluationRequest = new EvaluationRequest(nonRelevantAnswer, documents, answer);
evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isFalse();

여기서 우리는 contentGenerator ChatClient를 사용하여 question을 호출하고, 반환된 ChatResponse에서 생성된 answer와 사용된 documents를 추출합니다.

그런 다음 question, 검색된 documents, 챗봇의 answer를 포함하는 EvaluationRequest를 생성합니다. 우리는 이를 relevancyEvaluator 빈에 전달하고 isPass() 메서드를 사용하여 answer가 관련성이 있는지 확인합니다.

그러나 사자에 대한 완전히 무관한 답변을 제공하면 평가자는 이를 비관련으로 올바르게 판별합니다.

4.3. FactCheckingEvaluator로 LLM 응답의 사실적 정확성 평가하기

유사하게, Spring AI는 LLM 응답이 검색된 컨텍스트에 대해 사실적으로 정확한지 검증하기 위해 FactCheckingEvaluator 구현을 제공합니다.

우리의 contentEvaluator ChatClient를 사용하여 FactCheckingEvaluator 빈도 생성합시다:

@Bean
public FactCheckingEvaluator factCheckingEvaluator(
    @Qualifier("contentEvaluator") ChatClient chatClient) {
    return new FactCheckingEvaluator(chatClient.mutate());
}

마지막으로, 챗봇의 응답의 사실적 정확성을 시험해봅시다:

String question = "How many days sick leave can I take?";
ChatResponse chatResponse = contentGenerator.prompt()
  .user(question)
  .call()
  .chatResponse();

String answer = chatResponse.getResult().getOutput().getContent();
List<Document> documents = chatResponse.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS);
EvaluationRequest evaluationRequest = new EvaluationRequest(question, documents, answer);

EvaluationResponse evaluationResponse = factCheckingEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isTrue();

String wrongAnswer = "You can take no leaves. Get back to work!";
evaluationRequest = new EvaluationRequest(wrongAnswer, documents, answer);
evaluationResponse = factCheckingEvaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isFalse();

앞서의 방법과 유사하게, 우리는 question, 검색된 documents, 챗봇의 answer를 포함하는 EvaluationRequest를 생성하고 이를 factCheckingEvaluator 빈에 전달합니다.

우리는 챗봇의 응답이 검색된 컨텍스트에 대해 사실적으로 정확하다고 주장합니다. 또한, 잘못된 사실성과 관련된 하드코딩된 응답으로 재시험하고, isPass() 메서드가 false를 반환하는지 확인합니다.

하드코딩된 wrongAnswerRelevancyEvaluator에 전달하면, 평가는 통과될 것이라는 점에 유의해야 합니다. 왜냐하면 응답이 사실상 잘못되었음에도 불구하고 사용자가 묻는 Sick Leave에 대한 주제와 여전히 관련성이 있기 때문입니다.

5. 결론

이번 기사에서는 Spring AI의 Evaluator 인터페이스를 사용하여 LLM 응답을 테스트하는 방법을 살펴보았습니다.

우리는 문서 집합을 기반으로 사용자 질문에 답변하는 간단한 RAG 챗봇을 구축하고 테스트를 위해 Ollama 서비스를 설정하기 위해 Testcontainers를 사용하여 로컬 테스트 환경을 만들었습니다.

그런 다음, Spring AI에서 제공하는 RelevancyEvaluatorFactCheckingEvaluator 구현을 사용하여 챗봇의 응답의 관련성과 사실적 정확성을 평가했습니다.

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

원본 출처

You may also like...

답글 남기기

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