왜 우리는 자체 LLM 추상화 레이어를 만들었나

D-SKET Canvas는 처음부터 멀티벤더였습니다. 사용자가 Claude를 쓰든, OpenAI를 쓰든, Gemini를 쓰든 똑같이 동작해야 했죠. 한 번의 라이브러리 교체로 끝나는 일이라 생각했는데, 6개월이 지난 지금 우리는 800줄짜리 자체 추상화 레이어를 운영하고 있습니다.

처음에는 단순했습니다. Vercel AI SDK가 있고, 각 벤더의 공식 SDK가 있으니, 인터페이스만 통일하면 되지 않을까. 실제로 첫 일주일은 그게 맞았습니다. generateText({ model, prompt }) 하나로 세 벤더 모두 호출이 됐죠.

문제 1: 응답 형식

다이어그램 생성을 위해 우리는 JSON 응답을 강제해야 합니다. “노드 5개, 엣지 4개 만들어줘” 같은 프롬프트에 정확한 JSON 스키마로 답해야 캔버스에 렌더링이 가능합니다.

그런데 세 벤더의 JSON 모드 동작이 다 달랐습니다:

  • Claudetools 파라미터로 강제 — 가장 엄격하고 안정적
  • OpenAIresponse_format: { type: 'json_object' } + 시스템 프롬프트 명시 필요
  • GeminigenerationConfig.responseMimeType: 'application/json' 사용

여기까지는 어떻게든 통합했습니다. 진짜 문제는 스트리밍에서 터졌습니다.

문제 2: 스트리밍 토큰의 의미가 다름

SSE 기반으로 토큰을 실시간으로 받아오면 사용자 경험이 훨씬 좋습니다. 다이어그램 생성도 노드 하나가 만들어질 때마다 캔버스에 즉시 표시할 수 있죠.

그런데 각 벤더의 스트림 청크가 의미하는 바가 달랐습니다.

OpenAI의 스트림 청크 하나는 보통 1~3 토큰. Claude는 의미 단위로 묶여서 한 번에 길게 와요. Gemini는 거의 한 줄씩 던집니다. 사용자에게 “타이핑하듯 보이는 효과”를 같은 속도로 만들려면 벤더별로 다른 버퍼링 로직이 필요했습니다.

— 조부건 (AI 엔진 리드)

문제 3: 에러의 모양이 다름

가장 짜증났던 부분입니다. 같은 종류의 에러(레이트 리밋, 토큰 초과, 콘텐츠 정책 위반)인데 응답 코드와 메시지 구조가 다 달랐습니다.

// OpenAI: HTTP 429
{ "error": { "type": "rate_limit_exceeded", "code": "rate_limit", ... } }

// Claude: HTTP 429
{ "type": "error", "error": { "type": "rate_limit_error", ... } }

// Gemini: HTTP 200 (!) but with error embedded
{ "promptFeedback": { "blockReason": "SAFETY" } }

Gemini가 200을 반환하면서 에러를 응답 본문에 넣는 건 정말 의외였습니다. axios interceptor에서 한 번 더 검증하는 로직을 추가해야 했죠.

그래서 우리는 자체 레이어를 만들었습니다

3주에 걸쳐 다음을 추상화했습니다:

  • 요청 어댑터 — 통일된 chat({ messages, schema, stream }) 인터페이스
  • 응답 정규화 — 모든 벤더의 응답을 같은 모양으로 (delta token, function call, finish reason)
  • 에러 분류 — 7가지 에러 타입으로 분류 (rate, token, policy, network, auth, parse, unknown)
  • 버퍼링 정책 — 사용자 화면에서 “비슷한 속도로 타이핑”하도록 자동 조절
왜 Vercel AI SDK를 그대로 안 썼냐: SDK 자체는 훌륭하지만, BYOK(고객 API 키)와 한국 사용자 환경에서의 안정성(특히 Gemini KR 리전 에러)을 다루기 위해 우리 비즈니스 로직과 더 가까운 레이어가 필요했습니다. SDK 위에 우리 어댑터가 올라가는 구조입니다.

지금 얻고 있는 것

몇 개월 운영해 보니 자체 추상화의 가장 큰 효용은 **“모델 교체가 환경변수 한 줄”**이 됐다는 거예요. 사용자가 Pro 플랜에서 Free로 다운그레이드하면 자동으로 더 저렴한 모델로 폴백. 특정 벤더에 장애가 생기면 다른 벤더로 자동 라우팅.

또 하나, BYOK 사용자의 API 키 검증 로직을 한 곳에서 관리할 수 있게 된 것. 사용자가 잘못된 키를 입력했을 때 어느 벤더든 동일한 에러 메시지로 안내합니다.

다음으로 할 일

다음 분기에는 온프레미스 LLM(Llama, Qwen 등) 어댑터를 추가합니다. 보안이 까다로운 엔터프라이즈 고객들이 사내 GPU 서버를 쓰고 싶어 하는 경우가 늘고 있어서요. 같은 어댑터 인터페이스로 통일하면 사용자에겐 차이가 보이지 않게 할 수 있습니다.

덧붙이자면, 이 추상화 레이어는 D-SKET Canvas뿐 아니라 컨설팅 프로젝트에서도 그대로 재사용합니다. SI로 만든 시스템에 AI 기능을 붙일 때 같은 코드가 들어가니, 우리가 자체 SaaS를 운영하는 의의가 이런 데서도 있습니다.

← 목록으로