
AI 기능이 엔드포인트를 망가뜨려서는 안 됩니다
요약
API에 LLM 기능을 통합할 때 발생할 수 있는 비결정론적 특성과 가동 시간 종속 문제를 해결하기 위한 설계 전략을 다룹니다. 핵심 로직과 AI 설명을 분리하여 시스템의 안정성을 확보하는 '바닥과 천장(Floor and Ceiling)' 아키텍처를 제안합니다.
핵심 포인트
- LLM은 비결정론적이므로 핵심 비즈니스 로직(판결)에 직접 관여해서는 안 됨
- AI 모델의 장애가 전체 API 엔드포인트의 장애로 이어지지 않도록 설계해야 함
- 환각(Hallucination) 현상이 금전적 결정에 미치는 위험성을 경계해야 함
- 안정적인 로직(Floor)과 부가적인 AI 기능(Ceiling)을 계층적으로 분리할 것
다시 오신 것을 환영합니다. 이것은 에러 처리 (error handling)와 유효성 검사 (validation)에 이어, 저의 구매 결정 API (Purchase Decision API)에 관한 세 번째 글입니다. 이번 주에 이 API에 AI 기능이 추가되었으며, 이 글은 가장 중요했던 설계 결정에 관한 것입니다.
지금 모든 사람이 자신의 API에 LLM (Large Language Models)을 추가하고 있습니다. 하지만 LLM이 다운되었을 때 어떤 일이 발생하는지에 대해서는 거의 아무도 이야기하지 않습니다.
저는 방금 구매 결정 API를 구축했습니다. 당신이 무엇을 사고 싶은지 말하면, API는 100점 만점의 점수와 절약 계획과 함께 구매(BUY), 대기(WAIT), 또는 건너뛰기(SKIP)를 알려줍니다. 그 위에 친근하고 인간적인 언어로 AI가 작성한 설명이 덧붙여집니다.
제가 내린 가장 중요한 설계 결정은 이것이었습니다: AI는 판결(verdict)에 관여할 수 없다.
당연해 보이는 설계, 그리고 제가 그것을 사용하지 않은 이유
유혹적인 아키텍처 (architecture)는 간단합니다. 구매 세부 정보를 LLM에 보내고 그것이 결정하게 하는 것입니다. 단 한 번의 API 호출, 작성할 점수 로직 없음, 그리고 응답은 이미 자연어로 돌아옵니다.
저는 세 가지 이유로 그렇게 하지 않았습니다.
첫째, LLM은 결정론적 (deterministic)이지 않습니다. 동일한 구매에 대해 날짜마다 다른 판결이 내려질 수 있으며, 아무런 이유 없이 마음을 바꾸는 결정 API는 결정 API라고 할 수 없습니다.
둘째, 저의 가동 시간 (uptime)을 타인의 가동 시간에 종속시킵니다. 모델 제공업체에 장애가 발생하면, 저의 엔드포인트 (endpoint)에도 장애가 발생합니다.
셋째, 그리고 결정적인 이유입니다: 누군가의 돈에 대한 환각 (hallucination)된 판결은 판결이 아예 없는 것보다 더 나쁩니다.
바닥과 천장
그래서 저는 응답을 두 개의 레이어 (layer)로 나누었습니다:
CEILING LLM 설명 (있으면 좋지만, 실패할 수 있음)
FLOOR 점수, 판결, 계획 (순수 Java, 절대 실패하지 않음)
바닥(floor)은 엔드포인트가 보장하는 모든 것입니다. 저의 스코어링 엔진(scoring engine)은 결정론적(deterministic)입니다. 가처분 소득, 가격 비율, 구매 유형, 사용 빈도를 입력받아 0에서 100 사이의 점수를 생성합니다. 순수 Java로 작성되었으며, 해당 경로 어디에도 AI는 없습니다. 매번 동일한 입력에 대해 동일한 점수를 산출합니다.
천장(ceiling)은 응답을 더 좋게 만들지만, 실패해도 괜찮은 모든 것입니다. AI 설명(AI explanation)은 바로 여기에, 오직 여기에만 존재합니다.
모델에게 정답을 알려주세요
LLM이 호출될 시점에는 이미 판결이 내려진 상태입니다. 모델에게 무엇인가를 결정하라고 요청하지 않습니다. 모델에게 정답을 알려주고, 단지 그것을 설명하도록 요청할 뿐입니다:
String prompt =
"사용자가 '" + itemName + "'를 ₹" + price + "에 구매하려고 합니다. " +
"사용자의 월간 가처분 소득은 ₹" + disposableIncome + "입니다. " +
...
이 마지막 줄은 보기보다 훨씬 중요합니다. 이 줄이 없다면, 모델은 단순히 입력값을 문장으로 다시 읊어댈 뿐이며, 당신은 숫자를 약간 더 긴 숫자로 바꾸는 데 API 호출 비용을 낭비하게 됩니다.
이 구조가 무엇을 얻어내는지 주목하십시오. 설령 모델이 환각(hallucination)을 일으키더라도, 최악의 경우 발생하는 일은 올바른 판결에 대해 서투르게 표현된 설명을 내놓는 것뿐입니다. 잘못된 AI 응답의 폭발 반경(blast radius)은 산문(prose)이지, 돈이 아닙니다.
폴백 (The fallback)
이 계층들은 장애 처리(failure handling) 단계에서 두 번째로 만납니다. LLM 호출은 래핑(wrapped)되어 있어, 속도 제한(rate limit), 네트워크 오류, 또는 잘못된 형식의 응답(malformed response) 등 어떤 실패가 발생하더라도 동일한 숫자들로 구성된 템플릿으로 강등(degrade)됩니다:
try {
return generateExplanation(prompt);
} catch (Exception e) {
...
다음은 동일한 요청에서 생성된 두 가지 출력을 나란히 비교한 것입니다.
AI 버전:
"사용자의 월간 소득과 지출을 고려할 때, 해당 Sony 헤드폰에 투자하는 것은 현재 최선의 선택이 아닐 수 있어 보입니다..."
폴백 버전:
"귀하의 재정 상태를 기준으로 할 때, 30,000루피의 Sony 헤드폰을 구매하는 것은 100점 만점에 45점을 기록하여 '대기(WAIT)' 판정을 받았습니다. 이는 귀하의 월 가처분 소득인 27,000루피를 기준으로 측정되었습니다."
더 나쁜 문장입니다. 정보는 동일합니다. 사용자는 온기를 잃을 뿐, 정답을 잃지는 않습니다. 이것이 바로 우아한 성능 저하 (graceful degradation)가 실제로 의미하는 바입니다. 즉, 경험은 나빠질 수 있어도, 계약(약속된 결과 제공)은 깨지지 않는 것입니다.
그리고 맞습니다, catch (Exception e)는 보통 코드 스멜 (code smell)입니다. 하지만 여기서는 그것이 핵심입니다. 이 catch 문은 선택적 레이어 (optional layer)의 경계에 위치하며, AI 레이어에서 발생한 예외 중 사용자에게 도달해서는 안 되는 것은 없습니다.
로깅 (logging)을 존중하게 만든 버그
이 작업의 첫 번째 버전은 제가 솔직하게 밝히고 싶은 방식으로 실패했습니다. 모든 응답이 기계적인 설명과 함께 돌아왔고, 저는 그 이유를 전혀 알 수 없었습니다. 오류도, 충돌도 없었으며, 응답 내용 중 무엇도 문제를 암시하지 않았습니다.
폴백 (fallback)이 너무 잘 작동하고 있었습니다. 폴백이 스스로의 트리거를 숨기고 있었던 것입니다.
로깅을 추가하고 나서야 원인이 문자 하나가 누락되었다는 사실을 알게 되었습니다. 저는 `
이 광범위한 catch(예외 포착)는 오직 이 하나의 경계에서만 적용됩니다. 스코어링 엔진 (scoring engine) 내부에서는 제가 에러 핸들링 아티클에서 작성했던 방식처럼, 실제 핸들링이 포함된 실제 예외(real exceptions)를 여전히 사용하고 싶습니다. 모든 것을 catch하는 것은 선택적 레이어 (optional layer)를 위한 의도적인 선택이지, 일반적인 스타일이 아닙니다.
폴백 (fallback) 텍스트는 진심으로 더 좋지 않습니다. 사용자들은 따뜻한 조언과 템플릿 사이의 차이를 알아차립니다. 제가 아직 답하지 못한 열린 질문이 하나 있습니다. 응답이 클라이언트가 읽을 수 있는 필드를 통해 성능 저하 모드 (degraded mode)임을 명시적으로 인정해야 할까요, 아니면 이에 대해 침묵해야 할까요?
저는 여전히 이 분야를 배우고 있습니다. 이 설계는 단순히 제 자신의 버그들과 부딪히며 살아남은 결과물입니다.
요약 (Recap)
엔드포인트는 AI 기능이 존재하기 전에도 작동했으며, AI가 실패하더라도 계속 작동해야 합니다. 따라서 응답은 두 부분으로 나뉩니다. 점수(score), 판결(verdict), 계획(plan)을 항상 반환하는 순수 Java 기반의 결정론적 하한선 (deterministic floor), 그리고 모델에 접근할 수 있을 때는 판결을 설명하고 접근할 수 없을 때는 템플릿으로 폴백하는 선택적 AI 상한선 (optional AI ceiling)입니다. 모델에는 정답을 묻는 대신 정답을 알려주므로, 환각 (hallucination)이 발생하더라도 문구(wording)만 망가뜨릴 뿐입니다. 그리고 폴백은 모든 실패를 로깅합니다. 왜냐하면 한 번의 침묵하는 폴백이 며칠 동안 문자 하나짜리 버그를 제 눈으로부터 숨겼기 때문입니다.
AI가 사용자가 읽어야 할 양을 줄이게 하십시오. 하지만 AI가 당신의 시스템이 보장하는 바를 결정하게 해서는 절대 안 됩니다.
만약 여러분이 이것을 구축한다면, 어떤 것을 선택하시겠습니까? 명시적인 필드로 성능 저하 모드임을 공개적으로 인정하는 응답인가요, 아니면 사용자가 전혀 알지 못하는 폴백인가요? 양측의 논거를 모두 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
