240716 최종 프로젝트 - Spring AI Embedding 써보기

노재원·2024년 7월 17일
0

내일배움캠프

목록 보기
83/90

Spring AI :: Spring AI Reference

2024.03.29에 Spring AI가 발표되었다. Spring AI의 특징은 발표하기론 다음과 같다:

원래는 AI Model API를 다루기 위해선 HttpClient를 직접 사용해서 API 호출을 해야했지만 추상화된 모델을 제공해줘서 Client의 변경을 손쉽게 만들 수 있고 맵핑같은 이슈 또한 손쉽게 처리할 수 있어진다.

원래 RestClient를 사용해서 개발을 시도하고 있었지만 RestClient의 요청이 제대로 되지 않아 Spring AI의 구현체를 다뤄보기로 했다.

아직 Release된 상태가 아니라서 milestone 또는 snapshot 에서 관리되는 상태라 gradle repositories 주소도 추가해야 한다.

repositories {
    mavenCentral()
    // 추가
    maven { url = uri("https://repo.spring.io/milestone") }
}

이후 종속성 관리와 의존성을 추가한다.

dependencyManagement를 통해 종속성의 버전을 관리하는데 org.springframework.ai 그룹의 BOM(Bill of materials) 파일을 읽어 종속성 버전을 결정하고,

이를 spring-ai-openai-spring-boot-starter 의존성이 버전 정보를 지니게 된다.

BOM이 없어도 다른 spring-boot-starter 처럼 버전을 찾아가는 줄 알았는데 아직 milestone이라서 버전 정보를 자동으로 찾지 못해 종속성 정보를 추가한다고 보면 된다.

그래서 최종적으로는 openai만 쓸거니까 spring-ai-openai-spring-boot-starter 에 따로 버전을 명시해줬다.


val springAiVersion = "1.0.0-M1"

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

dependencies {
    implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter")
    // 똑같다
    implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter:${springAiVersion}")
}

// application.yml
spring.ai.openai.api-key= /* auto configuration */

레퍼런스를 보고 프로젝트가 사용할 목적인 Embedding Model API인 EmbeddingModel 만 별도로 사용해봤다.

@RestController
class OpenAIController(
    private val embeddingModel: OpenAiEmbeddingModel
) {
    @PostMapping("/test/openai/embeddings")
    fun createEmbedding(input: String): String {
        return embeddingModel.call(
            EmbeddingRequest(
                listOf(input),
                OpenAiEmbeddingOptions.builder().withModel("text-embedding-3-small").build()
            )
        )
            .result
            .output
            .toString()
    }

    @PostMapping("/test/openai/embeddings2")
    fun createEmbedding2(input: String): String {
        return embeddingModel.embedForResponse(listOf(input))
            .result
            .output
            .toString()
    }
}

createEmbedding2 가 설정하는 기본적인 Embedding 모델은 ADA_002를 사용한다.
굳이 싸고 적당히 성능 좋은 text-embedding-3-small 모델을 사용 안할 이유는 없어 보여서 모델은 변경할 것 같다.
그 외에도 CHAT_MODEL3_5_TRUBO 모델인 것으로 보아 GPT-4o 모델이 출시하기 전에 설정된 것으로 보인다.

static {
        DEFAULT_CHAT_MODEL = OpenAiApi.ChatModel.GPT_3_5_TURBO.getValue();
        DEFAULT_EMBEDDING_MODEL = OpenAiApi.EmbeddingModel.TEXT_EMBEDDING_ADA_002.getValue();
        SSE_DONE_PREDICATE = "[DONE]"::equals;
    }

결과:

왜 써야 했는가?

일단 원래 작성했던 restClient 를 통한 요청이 잘 안먹혀들었다.

fun getEmbedding(input: String): String {
        val url = "https://api.openai.com/v1/embeddings"
        val requestBody = mapOf("input" to input, "model" to "text-embedding-3-small")

        return restClient.post()
            .uri(url)
            .contentType(MediaType.APPLICATION_JSON)
            .body(LinkedMultiValueMap<String, String>().apply { this.setAll(requestBody) })
            .retrieve()
            .onStatus(HttpStatusCode::isError) { _, _ ->
                throw RuntimeException("조회 실패") // 발생한다
            }
            .body<String>()
            ?: throw RuntimeException("조회 실패")
    }

이전에 OAuth2 클라이언트에서 쓰던 POST 요청 방식을 그대로 따라했는데 잘 먹혀들지 않았고 다른 언어의 레퍼런스나 예시를 봐도

사실 별 달리 특별한 요구사항이 존재하는 API는 아니라서 retrieve가 실패로 떨어지는 이유에 대해 정확히 알 수가 없었다.

예상해보기론 그나마 LinkedMultiValueMap 이 의심되지만 벌써부터 request, response의 설정에 살짝 골치가 아팠고

Client, Service등 구조적인 부분도 챙겨줘야 해서 차라리 비록 아직 마일스톤이지만 Spring-ai를 통해 커버가 된다고 판단을 내렸다.

의존성의 버전 또한 고정되어있는 상태로 M1 자체가 날아가지 않는 이상 서비스에 문제가 생길 것은 아니고

현재 기획으로는 최초 와인 데이터 변환에만 사용하므로 실제 런타임에서는 딱히 문제가 될 여지는 없다.

아니면 Spring 내부에서 다 처리하려고 하지 말고 간단하고 가장 예시가 많은 Python으로 csv를 읽어 Embedding 변환 값을 얻어내는 방법도 효과적이라 생각한다.

현재 기획 단계에선 그 쪽이 더 가볍고 성가신 부분이 없을 수 있다.

0개의 댓글