Spring AI로 질문에 답하는 Kotlin 앱 만들기 — 튜토리얼(Create a Kotlin app that answers questions with Spring AI)
Spring AI로 질문에 답하는 Kotlin 앱 만들기 — 튜토리얼(Create a Kotlin app that answers questions with Spring AI)
이 튜토리얼에서는 Spring AI를 통해 LLM에 연결하고, 문서를 벡터 데이터베이스에 저장한 뒤, 그 문서의 컨텍스트를 이용해 질문에 답하는 Kotlin 앱을 만드는 방법을 배워요.
이 튜토리얼에서 사용할 도구는 다음과 같아요:
- Spring Boot: 웹 애플리케이션을 구성하고 실행하는 기반.
- Spring AI: LLM과 상호작용하고 컨텍스트 기반 검색을 수행.
- IntelliJ IDEA: 프로젝트를 생성하고 애플리케이션 로직을 구현.
- Qdrant: 유사도 검색용 벡터 데이터베이스.
- Docker: Qdrant를 로컬에서 실행.
- OpenAI: LLM 제공자.
출처: Create a Kotlin app that answers questions with Spring AI — tutorial
본문
시작하기 전에(Before you start)
- 최신 버전의 IntelliJ IDEA를 다운로드해 설치하고 Ultimate 구독을 사용해요. IntelliJ IDEA를 Ultimate 구독 없이 쓰거나 다른 IDE를 쓴다면 웹 기반 프로젝트 생성기로 Spring Boot 프로젝트를 만들 수 있어요.
- OpenAI 플랫폼에서 API에 접근할 OpenAI API 키를 만들어요.
- Qdrant 벡터 데이터베이스를 로컬에서 실행하기 위해 Docker를 설치해요.
- Docker를 설치한 뒤 터미널을 열고 다음 명령을 실행해 컨테이너를 시작해요:
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
프로젝트 만들기(Create the project)
Spring Boot 웹 기반 프로젝트 생성기를 대안으로 사용해 프로젝트를 만들 수도 있어요.
Ultimate 구독으로 IntelliJ IDEA에서 새 Spring Boot 프로젝트를 만들어요:
-
IntelliJ IDEA에서 File | New | Project를 선택해요.
-
왼쪽 패널에서 New Project | Spring Boot를 선택해요.
-
New Project 창에서 다음 필드와 옵션을 지정해요:
- Name:
springAIDemo - Language: Kotlin
- Type: Gradle - Kotlin 이 옵션은 빌드 시스템과 DSL을 지정해요.
- Package name:
org.example.springaidemo - JDK: Java JDK 이 튜토리얼은 Oracle OpenJDK 버전 21.0.1을 사용해요. JDK가 설치되어 있지 않다면 드롭다운 목록에서 다운로드할 수 있어요.
- Java: 17 Java 17이 설치되어 있지 않다면 JDK 드롭다운 목록에서 다운로드할 수 있어요.
[IMAGE: Create Spring Boot project]
- Name:
-
모든 필드를 지정했는지 확인하고 Next를 클릭해요.
-
Spring Boot 필드에서 최신 안정 버전의 Spring Boot를 선택해요.
-
이 튜토리얼에 필요한 다음 의존성을 선택해요:
- Web | Spring Web
- AI | OpenAI
- SQL | Qdrant Vector Database
[IMAGE: Set up Spring Boot project]
-
Create를 클릭해 프로젝트를 생성하고 설정해요. IDE가 새 프로젝트를 생성하고 열 거예요. 프로젝트 의존성을 다운로드하고 가져오는 데 시간이 걸릴 수 있어요.
이후 Project 뷰에서 다음과 같은 구조를 볼 수 있어요:
[IMAGE: Spring Boot project view]
생성된 Gradle 프로젝트는 Maven의 표준 디렉터리 구조에 해당해요:
main/kotlin폴더 아래에 애플리케이션에 속하는 패키지와 클래스가 있어요.- 애플리케이션의 진입점은
SpringAiDemoApplication.kt파일의main()메서드예요.
프로젝트 구성 업데이트하기(Update the project configuration)
build.gradle.ktsGradle 빌드 파일을 다음과 같이 업데이트해요:
plugins {
kotlin("jvm") version "2.4.20"
kotlin("plugin.spring") version "2.4.20"
// Rest of the plugins
}
springAiVersion을2.0.0으로 설정해요:
extra["springAiVersion"] = "2.0.0"
- Sync Gradle Changes 버튼을 클릭해 Gradle 파일을 동기화해요.
src/main/resources/application.properties파일을 다음과 같이 업데이트해요:
# OpenAI
spring.ai.openai.api-key=YOUR_OPENAI_API_KEY
spring.ai.openai.chat.model=gpt-4o-mini
spring.ai.openai.embedding.model=text-embedding-ada-002
# Qdrant
spring.ai.vectorstore.qdrant.host=localhost
spring.ai.vectorstore.qdrant.port=6334
spring.ai.vectorstore.qdrant.collection-name=kotlinDocs
spring.ai.vectorstore.qdrant.initialize-schema=true
OpenAI API 키를 spring.ai.openai.api-key 속성에 설정해요.
SpringAiDemoApplication.kt파일을 실행해 Spring Boot 애플리케이션을 시작해요. 실행되면 브라우저에서 Qdrant collections 페이지를 열어 결과를 확인해요:
[IMAGE: Qdrant collections]
문서를 로드하고 검색하는 컨트롤러 만들기(Create a controller to load and search documents)
문서를 검색하고 Qdrant 컬렉션에 저장하는 Spring @RestController를 만들어요:
src/main/kotlin/org/example/springaidemo디렉터리에KotlinSTDController.kt라는 새 파일을 만들고 다음 코드를 추가해요:
package org.example.springaidemo
// Imports the required Spring and utility classes
import org.slf4j.LoggerFactory
import org.springframework.ai.document.Document
import org.springframework.ai.vectorstore.SearchRequest
import org.springframework.ai.vectorstore.VectorStore
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestTemplate
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@RestController
@RequestMapping("/kotlin")
class KotlinSTDController(
private val restTemplate: RestTemplate,
private val vectorStore: VectorStore,
) {
private val logger = LoggerFactory.getLogger(this::class.java)
@OptIn(ExperimentalUuidApi::class)
@PostMapping("/load-docs")
fun load() {
// Loads a list of documents from the Kotlin documentation
val kotlinStdTopics = listOf(
"collections-overview", "constructing-collections", "iterators", "ranges", "sequences",
"collection-operations", "collection-transformations", "collection-filtering", "collection-plus-minus",
"collection-grouping", "collection-parts", "collection-elements", "collection-ordering",
"collection-aggregate", "collection-write", "list-operations", "set-operations",
"map-operations", "read-standard-input", "opt-in-requirements", "scope-functions", "time-measurement",
)
// Base URL for the documents
val url = "https://raw.githubusercontent.com/JetBrains/kotlin-web-site/refs/heads/master/docs/topics/"
// Retrieves each document from the URL and adds it to the vector store
kotlinStdTopics.forEach { topic ->
val data = restTemplate.getForObject("$url$topic.md", String::class.java)
data?.let { it ->
val doc = Document.builder()
// Builds a document with a random UUID
.id(Uuid.random().toString())
.text(it)
.metadata("topic", topic)
.build()
vectorStore.add(listOf(doc))
logger.info("Document $topic loaded.")
} ?: logger.warn("Failed to load document for topic: $topic")
}
}
@GetMapping("docs")
fun query(
@RequestParam query: String = "operations, filtering, and transformations",
@RequestParam topK: Int = 2
): List<Document>? {
val searchRequest = SearchRequest.builder()
.query(query)
.topK(topK)
.build()
val results = vectorStore.similaritySearch(searchRequest)
logger.info("Found ${results?.size ?: 0} documents for query: '$query'")
return results
}
}
SpringAiDemoApplication.kt파일을 업데이트해RestTemplate빈(bean)을 선언해요:
package org.example.springaidemo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.web.client.RestTemplate
@SpringBootApplication
class SpringAiDemoApplication {
@Bean
fun restTemplate(): RestTemplate = RestTemplate()
}
fun main(args: Array<String>) {
runApplication<SpringAiDemoApplication>(*args)
}
- 애플리케이션을 실행해요.
- 터미널에서
/kotlin/load-docs엔드포인트로 POST 요청을 보내 문서를 로드해요:
curl -X POST http://localhost:8080/kotlin/load-docs
- 문서가 로드되면 GET 요청으로 검색할 수 있어요:
curl -X GET http://localhost:8080/kotlin/docs
[IMAGE: GET request results]
결과는 Qdrant collections 페이지에서도 확인할 수 있어요.
AI 채팅 엔드포인트 구현하기(Implement an AI chat endpoint)
문서가 로드되면 마지막 단계로, Spring AI의 RAG(Retrieval-Augmented Generation, 검색 증강 생성) 지원을 통해 Qdrant의 문서를 사용해 질문에 답하는 엔드포인트를 추가해요:
KotlinSTDController.kt파일을 열고 다음 클래스들을 import해요:
import org.springframework.ai.chat.client.ChatClient
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor
import org.springframework.ai.chat.prompt.Prompt
import org.springframework.ai.chat.prompt.PromptTemplate
import org.springframework.web.bind.annotation.RequestBody
ChatRequest데이터 클래스를 정의해요:
// Represents the request payload for chat queries
data class ChatRequest(val query: String, val topK: Int = 3)
- 컨트롤러의 생성자 파라미터에
ChatClient.Builder를 추가해요:
class KotlinSTDController(
// Provides the builder for creating the ChatClient
private val chatClientBuilder: ChatClient.Builder,
private val restTemplate: RestTemplate,
private val vectorStore: VectorStore,
)
- 컨트롤러 클래스 안에
ChatClient인스턴스를 만들어요:
// Builds the chat client with a simple logging advisor
private val chatClient = chatClientBuilder.defaultAdvisors(SimpleLoggerAdvisor()).build()
KotlinSTDController.kt파일 맨 아래에 다음 로직을 가진 새chatAsk()엔드포인트를 추가해요:
@PostMapping("/chat/ask")
fun chatAsk(@RequestBody request: ChatRequest): String? {
// Defines the prompt template with placeholders
val promptTemplate = PromptTemplate(
"""
{query}.
Please provide a concise answer based on the "Kotlin standard library" documentation.
""".trimIndent()
)
// Creates the prompt by substituting placeholders with actual values
val prompt: Prompt =
promptTemplate.create(mapOf("query" to request.query))
// Configures the retrieval advisor to augment the query with relevant documents
val retrievalAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(
SearchRequest.builder()
.similarityThreshold(0.7)
.topK(request.topK)
.build()
)
.promptTemplate(promptTemplate)
.build()
// Sends the prompt to the LLM with the retrieval advisor and retrieves the generated content
val response = chatClient.prompt(prompt)
.advisors(retrievalAdvisor)
.call()
.content()
logger.info("Chat response generated for query: '${request.query}'")
return response
}
- 애플리케이션을 실행해요.
- 터미널에서 새 엔드포인트로 POST 요청을 보내 결과를 확인해요:
curl -X POST "http://localhost:8080/kotlin/chat/ask" \
-H "Content-Type: application/json" \
-d '{"query": "What are the performance implications of using lazy sequences in Kotlin for large datasets?", "topK": 3}'
[IMAGE: OpenAI answer to chat request]
축하해요! 이제 OpenAI에 연결하고 Qdrant에 저장된 문서에서 가져온 컨텍스트로 질문에 답하는 Kotlin 앱이 완성됐어요. 다양한 질문으로 실험하거나 다른 문서를 가져와 더 많은 가능성을 탐구해 보세요.
완성된 프로젝트는 Spring AI demo GitHub 저장소에서 확인할 수 있어요.
다음 단계(What's next)
- Kotlin AI Examples에서 다른 Spring AI 예제를 탐구해요.
- Spring Boot와 Claude로 작업 관리자 애플리케이션 만들기(Create a task manager application with Spring Boot and Claude)