티스토리 뷰

반응형

 

 

🔍 Langfuse란?

개요

Langfuse는 LLM 애플리케이션을 위한 오픈소스 observability 및 analytics 플랫폼입니다.

쉽게 말해, AI 앱의 "Firebase Analytics + Crashlytics" 같은 역할을 합니다.

핵심 기능

1. 자동 추적 (Tracing)

  • 모든 LLM API 호출 기록
  • 입력/출력 저장
  • 응답 시간 측정

2. 비용 분석 (Cost Tracking)

  • 토큰 사용량 추적
  • 실시간 비용 계산
  • 모델별 비용 분석

3. 성능 모니터링

  • 평균 응답 시간
  • 에러율 추적
  • 병목 지점 파악

4. 프롬프트 관리

  • 프롬프트 버전 관리
  • A/B 테스팅
  • 성능 비교

 

왜 필요한가?

❌ Langfuse 없이:
"오늘 API 몇 번 호출했지?"
"이번 달 비용이 얼마지?"
"어디서 에러 났지?"
→ 알 수 없음

✅ Langfuse 사용:
대시보드에서 한눈에 확인!
- 오늘 API 호출: 127번
- 이번 달 비용: $12.34
- 에러율: 2.1%

 

🎯 목표

이번 프로젝트에서는 다음을 구현했습니다:

  • Kotlin + Spring Boot로 REST API 서버 구축
  • OpenAI API를 활용한 번역 기능
  • Langfuse를 통한 LLM 호출 모니터링

최종 결과물: 모든 AI 호출을 추적하고 비용/성능을 실시간으로 확인할 수 있는 번역 API


🛠️ 기술 스택

  • 언어: Kotlin
  • 프레임워크: Spring Boot 3.4.1
  • LLM: OpenAI GPT-4o-mini
  • 모니터링: Langfuse
  • 빌드 도구: Gradle

Step 1. Spring Boot 프로젝트 생성

1-1 Spring Initializr로 프로젝트 생성

https://start.spring.io/ 에서 다음과 같이 설정:

Project: Gradle - Kotlin
Language: Kotlin
Spring Boot: 4.0.1
Java: 17

Group: com.example
Artifact: langfuse-demo

Dependencies:
- Spring Web
- Spring Boot DevTools

GENERATE 버튼을 클릭하여 프로젝트 다운로드 후 압축 해제

 

1-2 프로젝트 구조

langfuse-demo/
├── src/
│   ├── main/
│   │   ├── kotlin/
│   │   │   └── com/example/langfusedemo/
│   │   │       └── LangfuseDemoApplication.kt
│   │   └── resources/
│   │       └── application.properties
│   └── test/
├── build.gradle.kts
└── settings.gradle.kts

Step 2. Kotlin REST API 구현

2-1 의존성 추가

build.gradle.kts에 필요한 라이브러리 추가:

kotlin
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    
    // OpenAI
    implementation("com.aallam.openai:openai-client:3.6.3")
    implementation("io.ktor:ktor-client-okhttp:2.3.7")
    
    // HTTP 클라이언트 (Langfuse API 호출용)
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.google.code.gson:gson:2.10.1")
}

2-2 데이터 클래스 정의

TranslationRequest.kt

kotlin
package com.example.langfusedemo

data class TranslationRequest(
    val text: String,
    val targetLanguage: String
)

 

 

 

TranslationResponse.kt

kotlin
package com.example.langfusedemo

data class TranslationResponse(
    val originalText: String,
    val translatedText: String,
    val targetLanguage: String,
    val traceId: String? = null
)

2-3 Controller 구현

TranslationController.kt

kotlin
package com.example.langfusedemo

import org.springframework.web.bind.annotation.*
import org.springframework.beans.factory.annotation.Value
import com.aallam.openai.api.chat.ChatCompletionRequest
import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.ChatRole
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import kotlinx.coroutines.runBlocking

@RestController
@RequestMapping("/api")
class TranslationController(
    private val langfuseService: LangfuseService
) {

    @Value("\${openai.api.key}")
    private lateinit var openAiApiKey: String

    @PostMapping("/translate")
    fun translate(@RequestBody request: TranslationRequest): TranslationResponse = runBlocking {
        val openAI = OpenAI(openAiApiKey)

        val chatRequest = ChatCompletionRequest(
            model = ModelId("gpt-4o-mini"),
            messages = listOf(
                ChatMessage(
                    role = ChatRole.System,
                    content = "You are a professional translator. Translate the given text to ${request.targetLanguage}. Only return the translation, no explanations."
                ),
                ChatMessage(
                    role = ChatRole.User,
                    content = request.text
                )
            )
        )

        val completion = openAI.chatCompletion(chatRequest)
        val translatedText = completion.choices.first().message.content ?: "Translation failed"

        // Langfuse에 로깅
        val traceId = langfuseService.logTrace(
            name = "translation",
            input = mapOf(
                "text" to request.text,
                "targetLanguage" to request.targetLanguage
            ),
            output = mapOf("translation" to translatedText),
            model = "gpt-4o-mini",
            usage = mapOf(
                "promptTokens" to (completion.usage?.promptTokens ?: 0),
                "completionTokens" to (completion.usage?.completionTokens ?: 0),
                "totalTokens" to (completion.usage?.totalTokens ?: 0)
            )
        )

        TranslationResponse(
            originalText = request.text,
            translatedText = translatedText,
            targetLanguage = request.targetLanguage,
            traceId = traceId
        )
    }
}

Step 3. OpenAI 연동

3-1 API 키 설정

application.properties 파일 수정:

properties

spring.application.name=langfuse-demo

# Server
server.port=8080

# OpenAI
openai.api.key=sk-proj-YOUR_API_KEY

 

3-2 테스트

서버 실행:

bash
./gradlew bootRun

 

API 테스트:

bash

curl -X POST http://localhost:8080/api/translate \
  -H "Content-Type: application/json" \
  -d '{
    "text": "안녕하세요",
    "targetLanguage": "English"
  }'

 

응답 예시:

json

{
  "originalText": "안녕하세요",
  "translatedText": "Hello",
  "targetLanguage": "English",
  "traceId": null
}

✅ 번역 API 작동 확인!


Step 4. Langfuse 연동

4-1 Langfuse 계정 생성

  1. https://cloud.langfuse.com 접속
  2. GitHub 계정으로 로그인
  3. 새 프로젝트 생성 (예: langfuse-demo)
  4. Settings → API Keys에서 키 복사:

 

4-2 설정 파일 업데이트

application.properties에 Langfuse 설정 추가:

properties

spring.application.name=langfuse-demo

# Server
server.port=8080

# OpenAI
openai.api.key=sk-proj-YOUR_API_KEY

# Langfuse
langfuse.public.key=pk-lf-YOUR_PUBLIC_KEY
langfuse.secret.key=sk-lf-YOUR_SECRET_KEY
langfuse.base.url=https://cloud.langfuse.com

 

4-3 Langfuse Service 구현

LangfuseService.kt

kotlin
package com.example.langfusedemo

import com.google.gson.Gson
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.util.*

@Service
class LangfuseService {

    @Value("\${langfuse.public.key}")
    private lateinit var publicKey: String

    @Value("\${langfuse.secret.key}")
    private lateinit var secretKey: String

    @Value("\${langfuse.base.url}")
    private lateinit var baseUrl: String

    private val client = OkHttpClient()
    private val gson = Gson()

    fun logTrace(
        name: String,
        input: Any,
        output: Any,
        model: String,
        usage: Map<String, Int>
    ): String {
        val traceId = UUID.randomUUID().toString()
        val generationId = UUID.randomUUID().toString()
        
        // ISO 8601 형식으로 timestamp 생성
        val timestamp = java.time.Instant.now().toString()

        val batch = mapOf(
            "batch" to listOf(
                mapOf(
                    "id" to UUID.randomUUID().toString(),
                    "type" to "trace-create",
                    "timestamp" to timestamp,
                    "body" to mapOf(
                        "id" to traceId,
                        "name" to name,
                        "input" to input,
                        "output" to output
                    )
                ),
                mapOf(
                    "id" to UUID.randomUUID().toString(),
                    "type" to "generation-create",
                    "timestamp" to timestamp,
                    "body" to mapOf(
                        "id" to generationId,
                        "traceId" to traceId,
                        "name" to "openai-call",
                        "model" to model,
                        "input" to input,
                        "output" to output,
                        "usage" to mapOf(
                            "input" to usage["promptTokens"],
                            "output" to usage["completionTokens"],
                            "total" to usage["totalTokens"]
                        )
                    )
                )
            )
        )

        sendToLangfuse(batch)
        return traceId
    }

    private fun sendToLangfuse(payload: Map<String, Any>) {
        try {
            val json = gson.toJson(payload)
            val body = json.toRequestBody("application/json".toMediaType())

            val auth = Base64.getEncoder().encodeToString("$publicKey:$secretKey".toByteArray())

            val request = Request.Builder()
                .url("$baseUrl/api/public/ingestion")
                .addHeader("Authorization", "Basic $auth")
                .addHeader("Content-Type", "application/json")
                .post(body)
                .build()

            client.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                    println("❌ Langfuse error: ${response.code}")
                    println("Response: ${response.body?.string()}")
                } else {
                    println("✅ Langfuse logged successfully")
                }
            }
        } catch (e: Exception) {
            println("❌ Langfuse exception: ${e.message}")
            e.printStackTrace()
        }
    }
}

 

4-4 최종 테스트

서버 재시작 후 API 호출:

bash
curl -X POST http://localhost:8080/api/translate \
  -H "Content-Type: application/json" \
  -d '{
    "text": "디버깅 테스트",
    "targetLanguage": "English"
  }'
```

**서버 로그 확인:**
```
✅ Langfuse logged successfully

 

 

Langfuse 대시보드 확인:


 

 

🎉 결과

Langfuse 대시보드에서 확인 가능한 정보:

  1. Traces: 모든 API 호출 기록
  2. Model costs: 사용한 비용 (예: $0.000007)
  3. Token usage: 입력/출력 토큰 수
  4. Latency: 응답 시간
  5. Input/Output: 실제 요청/응답 내용


 

🔍 핵심 포인트

1. Timestamp 형식 주의

kotlin
// ❌ 잘못된 형식
val timestamp = System.currentTimeMillis() // 숫자

// ✅ 올바른 형식
val timestamp = java.time.Instant.now().toString() // ISO 8601

Langfuse API는 ISO 8601 형식의 timestamp를 요구합니다.

 

2. Batch 형식으로 전송

Langfuse Ingestion API는 배치 형식으로 여러 이벤트를 한 번에 받습니다:

  • trace-create: Trace 생성
  • generation-create: LLM 호출 기록

 

3. Basic Auth 인증

kotlin
val auth = Base64.getEncoder()
    .encodeToString("$publicKey:$secretKey".toByteArray())

Public Key와 Secret Key를 Base64로 인코딩하여 인증합니다.


 

🚀 다음 단계

이제 기본적인 모니터링 시스템이 완성되었습니다. 다음으로 할 수 있는 것들:

  1. RAG 시스템 구축: 벡터DB를 추가하여 문서 검색 기능
  2. Agent 개발: LangGraph로 복잡한 워크플로우 구현
  3. 멀티모달: 이미지, 음성 입력 처리
  4. 프론트엔드 연동: TypeScript + React로 웹 UI 추가

 

📚 참고 자료

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함