스프링에서 멀티 모델 연동과 프롬프트 엔지니어링 정리하기

궁금하면 500원·2025년 9월 7일
0

AI 미생지능

목록 보기
68/68

스프링 AI를 활용한 멀티 모델 연동과 프롬프트 엔지니어링

대규모 언어 모델을 활용한 애플리케이션 개발이 보편화되면서, 여러 AI 모델을 유연하게 활용하고 성능을 최적화하는 기술이 중요해지고 있습니다.
이번 포스팅에서는 Spring AI 라이브러리를 사용해 다양한 공급사의 모델을 통합하고,
효과적인 프롬프트 엔지니어링을 통해 AI 모델의 잠재력을 최대한 끌어내는 방법을 자세히 알아보겠습니다.

1. 스프링 AI 의존성 설정

스프링 AI를 사용하기 위해서는 프로젝트의 build.gradle 또는 pom.xml에 필요한 의존성을 추가해야 합니다.
스프링 AI는 다양한 모델 공급사를 통합적으로 지원하며, spring-ai-bom을 통해 버전 관리를 용이하게 해줍니다.

build.gradle 설정 예시

ext {
    set('springAiVersion', "1.0.0")
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}

과거에는 각 모델 공급사별로 spring-ai-starter 모듈이 존재했지만, 최근에는 spring-ai-starter-model로 통합되어 더 깔끔한 의존성 관리가 가능합니다.
특정 공급사(openai, anthropic 등)의 모듈을 추가하면 해당 모델에 대한 자동 설정이 활성화됩니다.


2. 여러 AI 모델 공급사 연동하기

실무에서는 여러 AI 모델 공급사를 동시에 활용하는 경우가 많습니다.
예를 들어, 비용 효율적인 작업을 위해 오픈소스 모델을 사용하고, 복잡한 태스크에는 고성능 상용 모델을 사용하는 식이죠.
이 경우 스프링 AI의 자동 설정을 비활성화하고 수동으로 ChatClient 빈을 구성하는 것이 효과적입니다.

application.yml 파일에서 자동 설정을 제외하여 불필요한 빈 생성을 막습니다.

spring:
  autoconfigure:
    exclude:
      - org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration
      - org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration
      - org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration
      # ... 필요한 만큼 추가

그리고 @Configuration 클래스에서 각 모델 공급사에 대한 ChatClient 빈을 직접 정의합니다.
이렇게 하면 원하는 모델에 대한 설정만 명시적으로 관리할 수 있습니다.

MyAiConfig.java 클래스 예시:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyAiConfig {

    @Bean
    public ChatClient openAiChatClient() {
        return OpenAiChatClient.builder()
                .baseUrl("https://api.openai.com/v1")
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .build();
    }

    @Bean
    public ChatClient lmStudioChatClient() {
        return OpenAiChatClient.builder()
                .baseUrl("http://localhost:1234/v1")
                .apiKey("dummy") // LM Studio는 보통 키가 불필요
                .build();
    }

    @Bean
    public ChatClient anthropicChatClient() {
        return AnthropicChatClient.builder()
                .apiKey(System.getenv("ANTHROPIC_API_KEY"))
                .build();
    }
}

3. AI 모델의 작동 원리 디코더 모델과 할루시네이션

AI 모델은 단순히 검색 결과를 제공하거나 미리 정해진 답을 알려주는 것이 아닙니다.
이들은 디코더 모델로, 주어진 입력에 이어질 확률이 가장 높은 다음 토큰을 예측하여 문장을 "만들어내는" 역할을 합니다.

이 과정에서 할루시네이션, 즉 사실과 다른 내용을 지어내는 현상이 발생합니다.
할루시네이션은 모델의 오류라기보다는, 디코더 모델의 본질적인 특성입니다.
예를 들어, "피보나치 순열의 일부를 JS의 배열로 만들어줘"라는 프롬프트에 대해 모델은 "피보나치", "순열"과 같은 토큰들의 시맨틱으로부터 확률적으로 가장 높은 토큰을 생성합니다.
하지만 숫자의 정확한 나열과 같은 수치 해석은 토큰 임베딩과 무관하므로, [3, 10, 17, ...]과 같이 논리적으로 틀린 결과를 생성할 수 있습니다.

이전의 Completion 모델(text-davinci-003 등)은 주어진 텍스트에 이어지는 내용을 생성하는 데 초점을 맞췄습니다.
반면, 최근의 ChatCompletion 모델system, user, assistant와 같은 역할을 인식하고, 대화의 맥락을 이해하여 출력 토큰을 생성하도록 학습되었습니다.


4. 프롬프트 엔지니어링 핵심 기법

ChatCompletion 모델의 성능을 극대화하기 위해서는 프롬프트 엔지니어링이 필수적입니다.
프롬프트는 단순히 질문을 던지는 것을 넘어, 모델의 동작 방식을 제어하는 중요한 도구입니다.

4-1. Options 모델의 답변 다양성 제어

AI 모델은 다음에 선택될 확률을 모든 토큰에 적용한 logit 값을 가집니다.
이 로짓을 기반으로 후보 토큰을 선정하고, 그 중에서 최종 토큰을 확률적으로 선택합니다.
이 과정에서 topK, topP, temperature와 같은 옵션을 사용해 답변의 다양성을 조절할 수 있습니다.

  • topK: 로짓을 상위 K개로 잘라내어 후보 토큰의 개수를 제한합니다.
  • topP: 로짓의 누적 확률이 P가 될 때까지 후보 토큰을 선택하여, 일정 유사도 내의 토큰들만 고려합니다.
  • temperature: 소프트맥스 함수의 강도를 조절하여 확률 분포의 경직도를 바꿉니다.
    • 0.1: 거의 정답 하나만 고르려 하기 때문에 매우 반복적이고 보수적인 답변을 생성합니다.
    • 1.0: 확률 분포를 그대로 사용하여 자연스럽고 다양한 답변을 생성합니다.
    • 2.0: 확률 분포가 매우 평탄해져 엉뚱하고 랜덤한 답변이 나올 가능성이 높습니다.

maxToken은 답변의 최대 길이를 의미하며, 짧게 답하는 옵션이 아니라 단지 답변이 잘리는 한계일 뿐입니다.

4-2. Messages 대화의 순서와 맥락 구성

Messagessystem, user, assistant의 조합된 리스트입니다.
이 메시지 리스트의 순서에 따라 모델은 대화의 맥락을 다르게 인식합니다.

많은 유료 모델은 Prefix Cache를 지원합니다. 이는 일정 크기 이상의 토큰을 전송할 경우, 이전에 질의했던 메시지가 앞에서부터 동일한 위치까지 캐싱되어 토큰 비용을 절약하는 기능입니다. 비용을 절감하려면 멀티턴 대화에서 메시지 순서를 함부로 바꾸지 않고, 최대한 Prefix Cache가 자주 히트되도록 구성하는 것이 좋습니다.

4-3. System Message 모델의 페르소나 및 지시 강화

대부분의 모델은 최초의 system 메시지를 특별하게 인식하도록 학습되었습니다.
따라서 system 메시지의 순서를 함부로 바꾸면 지시의 효과가 약해질 수 있습니다.
system 메시지 이후 멀티턴이 이어지면 메시지가 길어지면서 지시가 약화되는 경향이 있습니다.

이럴 때 일부 모델은 대화의 마지막에 system 메시지를 한 번 더 삽입하여 지시를 강화할 수 있습니다.
이때 내용은 최초의 지시를 상기시키는 수준으로 간결하게 작성하는 것이 좋습니다.
만약 모델이 마지막 system 메시지를 지원하지 않는다면, 마지막 user 메시지에서 리마인드하는 방법을 사용할 수 있습니다.

ChatCompletion 모델 대부분이 system 메시지를 포함하여 학습했기 때문에, 적절한 system 메시지를 사용하면 성능이 극적으로 향상됩니다.
systemuser 메시지 중 어디에 내용을 배분하냐에 따라 결과가 달라지므로, 여러 조합을 시도해 최적의 방식을 찾는 것이 중요합니다.

4-4. Zero, One, Few Shot 예시를 통한 바인딩

프롬프트에 예시를 포함시키는 방식으로 모델의 행동을 제어할 수 있습니다.

  • Zero-Shot: 예시를 전혀 주지 않고 지시만 내립니다.
  • One-Shot: 하나의 예시를 제공합니다.
  • Few-Shot: 여러 개의 예시를 제공합니다.

지시와 그 지시를 이행했을 때의 답변에 대한 예시를 system 메시지에 포함시키면 모델이 보다 구체적인 지시를 이행하게 됩니다.
하지만 이 경우 강력한 바인딩이 발생하여, 예시와 유사한 질의가 들어오면 무조건 예시대로 답변하려 할 수 있습니다.

Zero-Shot만으로 충분한 품질을 확보할 수 있다면 가장 이상적입니다.
그러나 특정 포맷이나 스타일을 강제해야 할 때는 Few-Shot 기법을 활용하여 모델의 행동을 명확히 제어할 수 있습니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글