
최근 GPT, Claude, Gemini 등 수많은 거대 언어 모델(LLM)이 쉴 새 없이 등장하고 있습니다. 마치 국가마다 다른 규격의 콘센트가 난무하는 세상처럼, OpenAI, Azure, AWS Bedrock, Google 등 여러 제공사(provider)의 API를 동시에 다뤄야 하는 복잡한 상황에 놓였습니다. 프로덕트별로 다른 성능과 속도의 요구사항에 맞춰 모델을 선택해야 했기 때문입니다. 여기에 제공사마다 발급받은 자격 증명(credential) 정보를 모두 별도로 안전하게 관리해야 하는 부담까지 더해집니다.
이렇게 여러 플랫폼이 뒤섞인 환경은 개발 속도를 저해하고 코드의 복잡성을 높이는 주된 원인이 됩니다. 자연스럽게 다음과 같은 질문이 나올 수밖에 없었습니다. "어떤 LLM이든 상관없이 같은 인터페이스로 호출할 수는 없을까?", "흩어져 있는 API 키를 한 곳에서 효율적으로 관리할 방법은 없을까?"
우아한형제들 AI플랫폼에서는 이러한 문제들을 해결하고 효율적인 LLMOps 환경을 구축했습니다. (자세한 내용은 지난 기술블로그 "LLMOps로 확장하는 AI플랫폼 2.0"에서 확인하실 수 있습니다.)
이 글에서는 그중에서도 LiteLLM과 Langfuse라는 강력한 오픈소스를 기반으로 GenAI SDK를 구축한 경험과 그 핵심 기능을 소개하고자 합니다.

1. 하나의 인터페이스로 모든 LLM 호출하기
저희가 만든 SDK의 첫 번째 목표는 명확했습니다. 바로 ‘단일 인터페이스’를 제공하는 것이었죠. 어떤 제공사의 모델을 사용하든, 사용자는 언제나 같은 메서드로 모델을 호출할 수 있어야 했습니다.
이 문제를 해결하기 위해 LiteLLM이라는 오픈소스 라이브러리를 핵심 엔진으로 채택했습니다. LiteLLM은 100개 이상의 LLM 제공사 API를 하나의 표준 포맷으로 호출할 수 있게 해주는, 말 그대로 ‘만능 번역기’ 같은 도구입니다.
LiteLLM의 강력함은 코드를 비교해 보면 명확히 드러납니다. 먼저 기존의 방식을 살펴보겠습니다.
-
AWS Bedrock (기존 방식)
import boto3 import json client = boto3.client( service_name="bedrock-runtime", aws_access_key_id="AWS_ACCESS_KEY_ID", aws_secret_access_key="AWS_SECRET_ACCESS_KEY", region_name="AWS_REGION_NAME", ) response = client.invoke_model( modelId="bedrock/anthropic.claude-3-sonnet-20240229-v1:0", body=json.dumps({"messages": [{"role": "user", "content": "Hello!"}]}), ) response_body = json.loads(response.get("body").read()) print(response_body["content"][0]["text"]) -
Google Gemini (기존 방식)
from google import genai client = genai.Client(api_key="API_KEY") response = client.models.generate_content( model="gemini-2.5-flash", contents="Hello!", ) print(response.text)
보시다시피 클라이언트 초기화 방식, 모델 호출 메서드, 요청과 응답의 구조까지 아주 다릅니다.
여러 제공사를 동시에 사용하는 경우 코드가 복잡해지고 유지보수하기 어려워집니다.
이제 LiteLLM을 사용했을 때 코드가 어떻게 변하는지 보겠습니다.
-
AWS Bedrock (LiteLLM 사용)
import litellm import os os.environ["AWS_ACCESS_KEY_ID"] = "AWS_ACCESS_KEY" os.environ["AWS_SECRET_ACCESS_KEY"] = "AWS_SECRET_ACCESS_KEY" os.environ["AWS_REGION_NAME"] = "AWS_REGION_NAME" response = litellm.completion( model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0", messages=[{"role": "user", "content": "Hello!"}], ) print(response.choices[0].message.content) -
Google Gemini (LiteLLM 사용)
import litellm import os os.environ["GEMINI_API_KEY"] = "GEMINI_API_KEY" response = litellm.completion( model="gemini/gemini-2.5-flash", messages=[{"role": "user", "content": "Hello!"}], ) print(response.choices[0].message.content)
litellm.completion이라는 단일 함수를 통해 모든 LLM을 같은 방식으로 호출할 수 있게 되었습니다. model 파라미터에 제공사와 모델명만 바꿔주면 되죠. GenAI SDK는 바로 이 LiteLLM의 강력한 통합 기능을 기반으로 설계되었습니다.
하지만 여기서 한 단계 더 나아가, 아래 코드의 model="azure/gpt-4o-mini"와 같이 제공사조차 생략할 수 있게 개선했습니다. SDK 내부에서 모델 이름의 패턴(‘gpt’, ‘gemini’, ‘claude’ 등)을 분석하여 자동으로 적절한 제공사를 찾아 LiteLLM에 전달합니다. 덕분에 사용자는 모델 이름만 지정하면 됩니다.
response = client.completion( model="gpt-4o-mini", # 'azure' 제공사 없이 모델명만 사용 messages=[{"role": "user", "content": "Hello!"}], )2. 흩어진 API 키를 한곳에서 관리하기
호출 방식은 통일했지만, 여전히 풀어야 할 큰 숙제가 남아있었습니다. 바로 API 키 관리입니다. 기존에는 사용자가 모든 키를 각자의 로컬 환경의 .env 파일에 저장하거나, 배포 환경 변수에 일일이 등록해야 했습니다. 새로운 LLM 제공사가 추가될 때마다 이 번거로움은 배가 되었습니다.
GenAI SDK는 이 문제를 중앙화된 키 관리 방식으로 해결했습니다. 모든 키와 연결 정보를 한곳에서 통합 관리하는 것이죠. 그리고 사용자는 개별 Langfuse 프로젝트 키 정보만 가지고 호출할 수 있게 구성했습니다.

사용자는 위 그림과 같이 Langfuse 웹 UI에서 사용할 LLM의 자격 증명 정보를 등록합니다. 이 정보는 Langfuse의 LLM Connections 기능으로 관리되며, 등록 시 실제 LLM 연결이 정상 동작하는지 키의 유효성 검증까지도 수행됩니다.
등록된 자격 증명 정보는 Langfuse 데이터베이스에 안전하게 암호화되어 저장됩니다. 여기서 한 가지 문제점이 있었습니다. 키를 등록하고 검증하는 기능은 제공하지만, 이를 조회할 수 있는 공식적인 API는 제공하지 않았습니다.
이 문제를 해결하기 위해, GenAI SDK의 GenAIClient는 초기화될 때 Langfuse 데이터베이스에 직접 접근했습니다. SDK 내부에 AESGCM 복호화 로직을 직접 구현하여 이 키를 런타임에 안전하게 복호화합니다. 그리고 이 정보를 LiteLLM의 Router 객체에 동적으로 등록합니다.
from litellm import Router # Langfuse DB에서 가져온 키로 Router 설정 router = Router( model_list=[ { "model_name": "azure/*", "litellm_params": { "model": "azure/*", "api_key": get_secret("azure_test").get("api_key"), }, }, { "model_name": "bedrock/*", "litellm_params": { "model": "bedrock/*", "aws_access_key_id": get_secret("bedrock_test").get("aws_access_key_id"), "aws_secret_access_key": get_secret("bedrock_test").get("aws_secret_access_key"), }, }, ], )이러한 구조 덕분에 사용자 경험이 향상되었습니다. 이제는 모든 키를 .env 파일로 관리하거나 환경 변수를 신경 쓸 필요 없이, Langfuse 키 하나로 SDK 클라이언트를 초기화하기만 하면 됩니다.
client = GenAIClient( public_key="PUBLIC_KEY", secret_key="SECRET_KEY", llm_connection_names=["azure_test", "bedrock_test"], )3. 모든 LLM 호출을 자동으로 기록하기
이제 모델 호출과 키 관리가 간소화되었습니다. 다음 단계는 이 모든 과정을 추적하고 분석하는 것입니다. LLM 호출 시간이 얼마나 걸리고, 어떤 응답을 주고 있는지 등을 확인할 때 활용합니다. LiteLLM은 Langfuse와 쉽게 연동할 수 있는 기능을 제공합니다.
GenAIClient를 초기화할 때 단 몇 줄의 코드만 추가해, LiteLLM의 모든 호출 기록이 Langfuse로 전송되도록 설정합니다.
import os import litellm # Langfuse 연결 정보 설정 os.environ["LANGFUSE_PUBLIC_KEY"] = "PUBLIC_KEY" os.environ["LANGFUSE_SECRET_KEY"] = "SECRET_KEY" os.environ["LANGFUSE_HOST"] = "LANGFUSE_HOST" # LiteLLM의 콜백을 Langfuse로 지정 litellm.callbacks = ["langfuse"]이 간단한 설정만으로 모든 요청과 응답, 사용된 토큰 수, 발생 비용, 응답 지연 시간 등의 정보가 Langfuse 대시보드에 자동으로 기록됩니다.

사용자는 이제는 로그를 확인하거나 결과를 디버깅하기 위해 복잡한 과정을 거칠 필요가 없습니다. 대시보드에서 어떤 프롬프트가 어떤 결과를 만들었는지 한눈에 비교하고 분석할 수 있게 된 것이죠. 이는 LLM 기반 기능의 개발 및 실험 속도를 올려줍니다.
4. 프롬프트를 코드처럼 버전 관리하기
훌륭한 AI 서비스를 만들기 위해서는 프롬프트를 체계적으로 관리하는 것이 필수적입니다. 우리가 개발한 SDK는 Langfuse의 프롬프트 관리 기능을 손쉽게 사용할 수 있도록 래핑하여 제공합니다.

사용자는 Langfuse UI에서 프롬프트를 작성, 테스트하고 버전을 관리할 수 있으며, 코드에서는 프롬프트 이름을 호출하는 것만으로 해당 내용을 가져올 수 있습니다.
prompt = client.get_prompt("test_prompt") # prompt: "Hello!"물론 변수를 활용한 동적 프롬프트 템플릿도 지원합니다. 아래는 language와 text라는 변수를 사용하는 프롬프트 예시입니다.

코드에서 variables 인자에 값을 전달하면, 프롬프트의 변수가 동적으로 치환됩니다.
prompt = client.get_prompt( "variable_template", variables={"language": "Korean", "text": "Hello!"}, ) # prompt: "Translate the following text into Korean: Hello!"이제 프롬프트를 수정하기 위해 코드를 다시 배포할 필요가 없습니다. Langfuse에서 캐싱 기능을 제공하기 때문에 프롬프트를 수정하고 버전을 올리기만 하면 즉시 서비스에 반영됩니다.
5. 오류를 fallback으로 유연하게 대처하기
안정적인 서비스를 위해서는 LLM API 호출 실패와 같은 예외 상황에 잘 대처해야 합니다. 저희 프로젝트 중 일부도 올해 Azure 장애(미국 동부 지역의 서비스 가용성 문제)로 인한 영향을 받았기 때문에 그 중요성을 알게 되었습니다.
코드에 예외 처리를 위해 try-except 구문을 반복적으로 추가하는 것은 가독성을 해치고 유지 보수를 어렵게 만듭니다. GenAI SDK는 litellm.Router가 제공하는 강력한 fallback 및 재시도 로직을 활용하여 이 문제를 해결합니다. SDK 클라이언트를 초기화할 때 간단한 설정만으로 정교한 에러 핸들링 전략을 구현할 수 있습니다.
예를 들어 gemini-2.5-pro 모델 호출이 실패할 경우, failover로 자동으로 gemini-2.5-flash 모델로 재시도하도록 설정할 수 있습니다.
client = GenAIClient( router_args={ "fallbacks": [ {"gemini-2.5-pro": ["gemini-2.5-flash"]}, ], }, )더 나아가, 특정 리전의 API에 문제가 생겼을 때 다른 리전으로 요청을 보내는 리전 기반 fallback도 가능합니다.
client = GenAIClient( llm_connection_names=["gemini_seoul", "gemini_usa"], router_args={ "fallbacks": [ # 'gemini_seoul' 실패 시 'gemini_usa'로 재시도 {"gemini_seoul/gemini-2.5-pro": ["gemini_usa/gemini-2.5-pro"]}, ], }, ) response = client.completion( model="gemini_seoul/gemini-2.5-pro", messages=[{"role": "user", "content": "Hello!"}], )이처럼 복잡한 에러 처리 로직을 코드 밖으로 분리함으로써, 사용자는 비즈니스 로직에 더욱 집중할 수 있습니다.
지금까지 LiteLLM과 Langfuse라는 두 강력한 오픈소스를 기반으로, 복잡한 LLM 연동 문제를 해결한 GenAI SDK 개발기를 소개해 드렸습니다.
GenAI SDK를 통해 우아한형제들의 사용자들은 이제 제공사별 API 명세나 키 관리에 시간을 쏟을 필요가 없어졌습니다. 어떤 모델이든 client.completion이라는 단일 인터페이스로 호출하고, 모든 요청과 프롬프트는 Langfuse에 자동으로 기록되어 실험과 디버깅 속도가 향상되었습니다.
우아한형제들 AI플랫폼의 목표는 "모든 구성원을 AI 서비스 개발자로 만드는 것"입니다. GenAI SDK는 복잡한 LLM 인프라를 추상화하여, 오직 창의적인 비즈니스 로직과 프롬프트 엔지니어링에만 집중할 수 있도록 돕는 중요한 첫걸음입니다.
앞으로도 GenAI 기술을 더 쉽고, 더 안정적으로 사용할 수 있도록 플랫폼을 발전시켜 나가는 저희의 여정을 지켜봐 주시길 바랍니다.











English (US) ·