본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 22. 17:44

당신을 비난하는 API를 만들었습니다. 모든 응답은 AI로 생성됩니다.

요약

실시간 LLM을 활용하여 유머러스한 응답을 생성하는 오픈 소스 REST API 'Snark'를 소개합니다. 정적인 데이터 대신 다양한 페르소나를 가진 AI가 매번 새로운 응답을 생성하며, 안정적인 서비스를 위해 다중 모델 폴백 시스템을 구축했습니다.

핵심 포인트

  • LLM을 활용해 매번 새로운 응답을 생성하는 동적 API 구현
  • Groq, Gemini, Claude를 활용한 다중 모델 폴백(Fallback) 전략 적용
  • Django, PostgreSQL, Redis, Docker Compose 기반의 기술 스택
  • 속도 제한 및 에러 대응을 위한 오케스트레이터 설계

요약 (TL;DR) 저는 AI가 생성한 유머를 제공하는 오픈 소스 REST API인 Snark를 만들었습니다. 독설(Roasts), 잔인할 정도로 솔직한 커밋 메시지(commit messages), ELI5(어린아이에게 설명하듯 쉽게 설명하기), 기업 전문 용어(corporate jargon) 등 약 25개의 엔드포인트(endpoints)를 제공합니다. 모든 응답은 실시간 LLM 호출이므로, 같은 문장을 두 번 접하는 일은 거의 없습니다. 내부적으로 어떻게 작동하는지 소개합니다.

Snark returning live AI-generated responses from real curl calls

제작 이유

대부분의 농담 API는 정적인 리스트에서 무작위로 한 줄을 뽑아줍니다. 열 번 정도 요청할 때는 괜찮지만, 그러다 보면 가진 것을 전부 다 보게 됩니다.

저는 그 반대를 원했습니다. 모든 응답이 언어 모델(language model)에 의해 특정 페르소나(persona)의 목소리로 새롭게 생성되고, 동일한 엔드포인트가 당신에게 똑같은 농담을 거의 반복하지 않는 API 말입니다. 솔직히 말하면, 제공자 폴백(provider fallback)과 캐싱(caching)을 제대로 배우기 위한 구실로 시작했지만, 예상보다 더 많이 사용하게 되었습니다.

실행 중인 서비스에서 가져온 실제 응답 몇 가지는 다음과 같습니다:

$ curl -s localhost:8100/v1/wit/commit-message/ | jq -r .response
fix: finally found the typo

...

기술 스택

특별한 것은 없습니다:

  • API를 위한 Django 및 Django REST Framework
  • 페르소나 및 응답 로그를 위한 PostgreSQL
  • 캐싱 및 IP당 속도 제한(rate limiting)을 위한 Redis
  • 기본 모델 제공자(model provider)로서 무료 티어의 Groq를 사용하며, Gemini 및 Claude를 선택적 폴백(fallbacks)으로 사용
  • Docker Compose를 사용하여, 단 한 번의 docker compose up으로 외부 프로비저닝(provision) 없이 전체 시스템을 실행 가능

제가 가장 신경 쓴 부분은 제공자 폴백(Provider fallback)입니다

단일 모델 제공자에만 의존하면 결국 문제가 생기기 마련입니다. 속도 제한(Rate limits), 콘텐츠 필터(content filters), 가끔 발생하는 500 에러 등이 그렇습니다. 그래서 Snark는 특정 제공자 하나를 단독으로 신뢰하지 않습니다.

모든 엔드포인트는 하나의 오케스트레이터(orchestrator)를 통해 실행됩니다. 기본 제공자를 시도하고, 실패할 경우 체인을 따라 내려갑니다:

def _generate_with_fallback(system_prompt, user_prompt, temperature, max_tokens):
    primary = ProviderRegistry.get()
    try:
...

제가 가장 만족스럽게 생각하는 부분은 콘텐츠 필터(content-filter) 분기입니다. 모델이 응답을 거부할 때, 보통은 즉시 중단하고 다음 제공자(provider)로 넘어가는 것이 첫 번째 본능이지만, 많은 경우 모델에게는 그저 더 차분한 프롬프트(prompt)가 필요할 뿐입니다. 그래서 무언가를 전환하기 전에, 온도를 낮추고 "안전하게 유지하세요"라는 문구를 추가하여 동일한 제공자에게 다시 요청합니다. 이것만으로도 놀라울 정도로 많은 요청을 구제할 수 있으며, 두 번째 제공자의 왕복 비용을 지불하는 것보다 저렴합니다.

또 다른 좋은 부수 효과는 제공자가 단순히 generate() 메서드를 가진 클래스라는 점입니다. Claude를 추가했을 때, 새로운 파일 하나만 만들면 되었습니다. 레지스트리(registry)가 순서를 관리하고 "이미 실패한 제공자는 재시도하지 않는다"는 로직을 처리합니다.

반복을 방지하는 방법

모든 응답이 기술적으로는 새로운 API 호출임에도 불구하고, 생성기가 계속해서 같은 말을 반복한다면 고장 난 것처럼 느껴집니다. 저는 이를 코드보다는 프롬프트에서 해결합니다.

각 호출 직전에, Snark는 데이터베이스에서 해당 페르소나(persona)의 마지막 10개 응답을 가져와 시스템 프롬프트에 "이것들을 다시 하지 마세요"라는 목록으로 집어넣습니다:

recent = (
    ResponseLog.objects.filter(persona=persona)
    .order_by("-created_at")
...

이는 비용이 저렴하고, 모델이 호출 사이에 무언가를 기억할 필요가 없으며, 실제로 다양성을 유지하는 데 효과적입니다.

모든 것을 동일하게 만들지 않으면서 캐싱하기

이 부분은 약간 모순적입니다. 캐싱(caching)은 비용과 지연 시간(latency)을 절약해주지만, 이 서비스의 핵심은 응답이 고유하다는 점입니다. 너무 과하게 캐싱하면 제가 피하려고 했던 정적인 농담 목록을 만드는 꼴이 됩니다.

제가 찾은 절충안은 요청의 정확한 형태를 기준으로 캐싱하되, 단 몇 분 동안만 유지하는 것입니다. 키(key)는 slug : user_input : mood의 SHA-256 해시값이며, 5분 후에 만료됩니다:

def _response_cache_key(slug, user_input, mood):
    raw = f"{slug}:{user_input}:{mood or ''}"
    digest = hashlib.sha256(raw.encode()).hexdigest()[:16]

따라서 만약 두 사람이 같은 분(minute) 내에 /roast/dave/?mood=spicy를 호출한다면, 그들은 하나의 결과를 공유하게 되며 저는 단 한 번의 호출 비용만 지불하면 됩니다. 서로 다른 입력값이거나, 혹은 동일한 입력이라도 몇 분이 지난 후라면 여전히 새로운 결과를 얻게 됩니다. 각 응답은 당신이 어떤 응답을 받았는지 알려줍니다:

{ "response": "...", "persona": "The Honest Committer", "cached": false }

하드코딩된 프롬프트 대신 페르소나 (Personas)

모든 엔드포인트(endpoint)는 데이터베이스에 저장된 페르소나(persona)에 매핑됩니다. 이름, 시스템 프롬프트 (system prompt), 톤 (tone), 몇 가지 규칙, 그리고 고유한 temperaturemax_tokens를 가집니다. "The Honest Committer"는 커밋 메시지를 작성하고, "The Feedback Villain"은 코드 리뷰 코멘트를 처리합니다. 엔드포인트를 추가하는 것은 보통 코드를 작성하는 것이 아니라 단순히 행(row)을 추가하는 작업입니다.

게다가 톤을 덮어쓰는 선택적 ?mood= 파라미터(sarcastic, deadpan, unhinged, wholesome 등)가 있어, 하나의 페르소나가 동일한 내용을 15가지의 서로 다른 방식으로 말할 수 있습니다.

몇 가지 배운 점들

저를 힘들게 했던 점은 제 테스트가 모델 SDK를 모킹 (mock)한다는 것이었습니다. 이는 빠르고 결정론적인 (deterministic) 테스트를 위한 올바른 선택이지만, 동시에 테스트 스위트가 통과(green)되었다고 해서 제공업체의 실제 API가 여전히 당신의 코드와 일치하는지에 대해서는 아무것도 알려주지 않는다는 것을 의미합니다. 의존성 (dependencies)을 업데이트하기 시작했을 때, 저는 체크표시를 믿는 대신 SDK를 구조적으로 확인해야 했습니다.

Postgres와 Redis를 초기부터 compose 파일에 묶어둔 것도 가치 있는 일이었습니다. docker compose up 한 번으로 모든 것이 실행되자, "이걸 어떻게 실행하나요?"라는 질문들이 사라졌습니다.

그리고 받아들이는 데 가장 오래 걸린 사실은 농담을 만드는 것이 가장 쉬운 부분이었다는 점입니다. 폴백 (fallback), 재시도 (retries), 반복 방지 (anti-repetition), 캐싱 (caching) 등이 실제로 이 프로젝트를 데모가 아닌 진짜 서비스처럼 느껴지게 만드는 요소들입니다.

체험해보기

이 프로젝트는 AGPL-3.0 라이선스 하에 오픈 소스로 공개되어 있으며 무료입니다. 엔드포인트는 인증(auth)이나 키(key)가 필요하지 않습니다. 자신만의 인스턴스를 실행하려면 무료 Groq 키만 있으면 됩니다:

git clone https://github.com/PramodTKodag/snark.git
cd snark && cp .env.example .env   # 무료 Groq 키를 추가하세요
docker compose --profile dev up
...

저장소(Repo)는 여기 있습니다: https://github.com/PramodTKodag/snark

이 프로젝트가 재미있으셨다면, 저장소에 스타(star)를 눌러주시는 것이 진심으로 큰 도움이 됩니다. 그리고 만약 여러분이라면 폴백(fallback)을 다르게 구현했을 것 같다면, 의견을 듣고 싶습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0