스프링 AI 시리즈
- [Spring AI] 준비 (기본 개념, OpenAI API Key, 크레딧 충전)
- [Spring AI] 챗봇 만들기 (Kotlin)
- [Spring AI] Vector Store와 RAG를 이용한 할루시네이션 방지
- [Spring AI] OpenAI 비용을 절감하는 방법
주의: 해당 포스팅 진행시, embedding된 데이터에 대한 질문을 OpenAI의 GPT-4 모델을 사용하여 처리할 때마다 약 0.01달러의 비용이 발생합니다.
AI 할루시네이션(Hallucination)
지난 포스팅에서 AI가 `이수재`라는 사람에 대한 정보를 보유하고 있지 않음에도 불구하고, 존재하지 않는 정보를 생성했다. 이는 할루시네이션이라고 불리며, AI가 학습한 데이터 내에서 명확한 답을 찾지 못하거나 관련 정보가 부족할 때, 추론을 통해 그럴듯한 답을 만들어내는 현상이다.
실제 프로젝트 진행 중, AI에서 할루시네이션이 발생하는 경우는 주로 AI가 정확한 정보에 접근하지 못하거나 불완전한 데이터를 기반으로 추론할 때 나타난다. 특히, 사내 기술 문서나 회사 내부에서만 사용하는 전용 자료처럼 인터넷에 존재하지 않거나 공개되지 않은 정보의 경우 이러한 문제가 자주 발생한다.
이러한 문제를 해결하는 데 도움을 줄 수 있는 것이 바로 `Vector store`, `embedding`, 그리고 `RAG (Retrieval- Augmented Generation)`이다.
`Embedding`은 문서나 데이터를 AI가 이해할 수 있는 벡터 형태로 변환하는 기술로, 이를 통해 데이터를 벡터화하여 AI가 처리할 수 있게 한다.
`Vector store`는 이렇게 벡터화된 데이터를 저장하고, 필요한 정보를 빠르게 검색할 수 있도록 해준다.
`RAG`는 이 과정에서 중요한 역할을 한다. AI가 질문을 받으면, RAG는 먼저 Vector Store에서 해당 질문과 관련된 정보를 검색하고, 검색된 데이터를 바탕으로 AI가 답변을 생성한다. 이로 인해 AI는 embedding 형태로 저장된 사내 기술 문서나 관련 정보등을 활용하여 더욱 정확하고 관련성 높은 답변을 제공할 수 있다.
결국, embedding을 통해 데이터를 벡터화하고, Vector store에서 필요한 정보를 검색하며, RAG로 이를 AI 모델이 활용할 수 있도록 하여, 할루시네이션을 방지하고 정확한 답변을 도출할 수 있게 된다.
RAG에 대해 좀 더 알아보고 싶다면, 아래 블로그를 참고하면 좋다. 내용이 굉장히 잘 정리되어있다.
이제 Spring AI 코드를 만들어보자.
Vector Store 설치
Spring AI가 지원하는 다양한 Vector Store가 존재하지만, 이번 프로젝트에서는 `Chroma DB`라는 Vector Store를 설치해보자.
docker-compose.yaml
services:
chroma:
image: ghcr.io/chroma-core/chroma:0.5.4
container_name: chroma-db
build:
context: .
dockerfile: Dockerfile
volumes:
- ./local-env/volumes/chroma-data:/chroma/chroma
command: "--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30"
environment:
- IS_PERSISTENT=TRUE
ports:
- "8000:8000"
ChromaDB Admin
ChromaDB를 위한 정식 UI 도구는 아직 존재하지 않는다. 하지만 UI 환경에서 ChromaDB를 작업할 수 있도록 도와주는 비공식 오픈소스인 ChromaDB Admin을 활용해보자.
위 리포지토리를 `git clone`한 후에, 실행시켜준다.
$ pnpm install && pnpm dev
그 후, http://localhost:3000에 접속하면 아래와 같은 화면을 확인 할 수 있다.
이제 본격적으로 코드를 작성해보자.
전체 파일 구조
build.gradle.kts
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.3.3"
id("io.spring.dependency-management") version "1.1.6"
}
group = "org.example"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
maven { url = uri("https://repo.spring.io/milestone") }
}
extra["springAiVersion"] = "1.0.0-M2"
dependencies {
implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
// 아래 라이브러리 추가
implementation("org.springframework.ai:spring-ai-chroma-store-spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
dependencyManagement {
imports {
mavenBom("org.springframework.ai:spring-ai-bom:${property("springAiVersion")}")
}
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
application.yml
spring:
application:
name: spring-ai
ai:
openai:
api-key: # 여기에 발급 받은 OpenAI API Key 값을 입력. 절대 외부(깃허브 등)에 노출 금지!!
embedding:
options:
model: text-embedding-ada-002
chat:
options:
model: gpt-3.5-turbo
temperature: 0.7
max-tokens: 200
vectorstore:
chroma:
client:
host: http://localhost
port: 8000
collection-name: vector_store
initialize-schema: true
AiChatConfig.kt (이전 포스팅에서 만들었던 파일)
@Configuration
class AiChatConfig(
val chatClient: ChatClient.Builder,
) {
@Bean
fun chatClient(): ChatClient {
return chatClient.build()
}
}
AiDocumentController.kt
@RestController
@RequestMapping("/api/v1/documents")
class AiDocumentController(
private val aiDocumentService: AiDocumentService
) {
@PostMapping("/markdown")
fun createDocumentFromMarkDown() {
return aiDocumentService.storeDocumentsFormMarkdown()
}
@PostMapping("/chat")
fun chat(@RequestBody chatAiDocumentRequest: AiDocumentChatRequest): AiDocumentChatResponse {
return aiDocumentService.chat(chatAiDocumentRequest)
}
}
AiDocumentService.kt
@Service
class AiDocumentService(
private val vectorStore: VectorStore,
private val chatClient: ChatClient,
) {
@Value("classpath:/documents/rules-of-hooks.md")
var mdResource: Resource? = null
fun storeDocumentsFormMarkdown() {
val textReader = TextReader(mdResource)
val splitDocuments = TokenTextSplitter().split(textReader.read())
vectorStore.add(splitDocuments)
}
fun chat(chatAiDocumentRequest: AiDocumentChatRequest): AiDocumentChatResponse {
val response = chatClient.prompt()
.advisors(
QuestionAnswerAdvisor(
vectorStore, SearchRequest.defaults()
)
)
.user(chatAiDocumentRequest.userInput)
.call()
.content()
return AiDocumentChatResponse(response);
}
AiDocumentChatRequest.kt
data class AiDocumentChatRequest(
@NotBlank(message = "User input must not be blank")
val userInput: String = ""
)
AiDocumentChatResponse.kt
data class AiDocumentChatResponse(val response: String)
rules-of-hooks.md
리액트 공식 문서에서 `rules-of-hooks.md` 파일을 GitHub에서 다운로드하여 프로젝트의 `resources/documents` 경로에 추가했다.
이제 서버를 실행시킨 후, markdown 문서를 chromadb에 적재시켜보자.
curl --location --request POST 'http://localhost:8080/api/v1/documents/markdown'
http://localhost:3000에 접속(chromadb-admin)하면, markdown파일의 내용들이 적재가 된 것을 확인할 수 있다.
이제 스프링부트 서버에 질문을 해보자. 앞서 적재한 `rules-of-hooks.md` 파일의 내용과 유사한 질문을 하면, 해당 내용을 바탕으로 적절한 답변을 제공할 것이다.
이제 이수재에 대해 물어보면, 이전 포스팅에서 이상한 답변이 나왔던 것과 달리, chromaDB에 저장된 내용만을 바탕으로 답변한다.
그런데...
비용이 지나치게 많이 발생하고 있다. 질문 한번에 약 0.01달러가 발생하고있다. 예를 들어, 사내 기술 문서를 위한 챗봇을 만든다면 GPT-4 모델을 사용하는 것은 오버 엔지니어링일 수 있다. 사내 문서 검색이나 관련성 높은 답변 제공에는 고사양 AI 모델이 꼭 필요하지 않을 수 있으므로, 더 경량화된 모델이나 로컬 LLM을 고려하는 것이 적합할 수 있다.
다음 포스팅에서는 비용 절감을 위한 여러 방법을 시도해보자.
REFERENCES
문서 전처리와 임베딩의 중요성: RAG 프로젝트 성공하기 - https://medium.com/@minji.sql/%EB%AC%B8%EC%84%9C-%EC%A0%84%EC%B2%98%EB%A6%AC%EC%99%80-%EC%9E%84%EB%B2%A0%EB%94%A9%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-rag-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B8%B0-97ae34e879b4
Vector Store란 - https://devocean.sk.com/blog/techBoardDetail.do?ID=164964&boardType=techBlog
RAG = Vector Search + LLM 1편 - https://blog.mnc.ai/74
스프링 AI - https://docs.spring.io/spring-ai/reference/api/embeddings.html