📕 전자책 전문 읽기 불확실한 AI를 제어가능하게 · 15쪽
브라우저가 PDF를 바로 표시하지 못합니다. 새 탭에서 열기
이 글은 요약해 다시 쓴 것이고, PDF가 전문입니다.

LLM을 API 뒤에 두고 서비스를 만들어본 엔지니어라면 한 번쯤 겪었을 장면이 있습니다. 어제까지 멀쩡하게 동작하던 프롬프트가 오늘은 다른 형식의 답을 내놓습니다. 코드를 하나도 바꾸지 않았는데 말입니다. 이 현상을 마주친 팀은 대개 프롬프트를 더 정교하게 다듬는 쪽으로 움직입니다. 지시문을 늘리고, 예시를 추가하고, “반드시 JSON으로만 답하라”는 문장을 몇 번이고 반복해서 넣습니다.

이 접근은 근본적으로 틀린 방향은 아니지만 충분하지 않습니다. LLM이 확률적으로 다음 토큰을 선택하는 기계라는 사실은 프롬프트 튜닝으로 사라지지 않기 때문입니다. 아무리 정교한 프롬프트도 모델 내부의 비결정성, 입력에 대한 과민한 반응, 없는 사실을 그럴듯하게 지어내는 습성을 완전히 제거하지는 못합니다. 이 글은 그 사실을 받아들인 다음에 무엇을 해야 하는지에 관한 이야기입니다. 핵심 주장은 하나입니다. 프로덕션에서 LLM을 다루는 엔지니어링의 본질은 출력을 예측하는 일이 아니라, 예측 불가능한 출력이 시스템에 들어오고 나가는 경계를 설계하는 일입니다.

LLM 출력은 통제할 수 없다, 그래도 시스템은 통제할 수 있다 개념을 형상화한 이미지 글의 핵심 개념을 형상화했습니다.

왜 같은 질문에 다른 답이 나오는가

먼저 원인을 정확히 짚고 넘어갈 필요가 있습니다. 많은 엔지니어가 LLM의 비결정성을 온도(temperature) 설정 문제로 오해합니다. 온도를 0으로 낮추면 모델이 항상 가장 확률이 높은 토큰만 선택하게 되니 결정론적으로 동작하지 않겠냐는 생각입니다. 실제로는 그렇지 않습니다. 온도를 0으로 고정해도 아키텍처 수준의 비결정성은 완전히 사라지지 않습니다.

이유는 토큰 선택이 우리가 생각하는 단어 단위가 아니라 훨씬 더 잘게 쪼개진 하위 토큰 단위에서 일어나기 때문입니다. 예를 들어 한글 문자열 하나조차 모델 내부에서는 여러 개의 바이트 쌍으로 분해될 수 있고, 그 분해 경로는 실행할 때마다 미묘하게 달라질 수 있습니다. 여기에 부동소수점 연산의 병렬 처리 방식, GPU 배치 처리 중 발생하는 계산 순서의 차이 같은 하드웨어 수준의 요인까지 겹치면, 이론상 결정론적이어야 할 설정에서도 실제로는 미세한 확률적 흔들림이 남습니다.

이 사실이 프로덕션 관점에서 중요한 이유는 소프트웨어를 안전하게 운영하는 전통적인 전제가 여기서는 성립하지 않는다는 데 있습니다. 우리가 평소 다루는 소프트웨어는 같은 입력이 들어오면 같은 출력이 나온다는 전제 위에 세워져 있습니다. 테스트를 작성하고, 캐시를 설계하고, 재현 가능한 버그 리포트를 만드는 모든 관행이 이 전제에 의존합니다. LLM은 이 전제를 깨뜨립니다. 같은 입력이 다른 출력을 낼 수 있다는 사실을 받아들이지 못하면, 시스템은 겉보기에는 멀쩡해 보여도 운영 중에 예측하지 못한 방식으로 흔들리게 됩니다.

그렇다고 이 비결정성이 언제나 해로운 것은 아닙니다. 창작 작업이나 대화형 응답에서는 오히려 다양성이 장점이 되기도 합니다. 문제는 그 다양성을 프로덕션 시스템의 다른 구성 요소가 그대로 받아들일 준비가 되어 있느냐입니다. 준비가 안 된 상태에서 비결정성을 마주하면, 장점이어야 할 특성이 장애 원인으로 뒤바뀝니다.

프롬프트 민감도라는 두 번째 함정

비결정성이 모델 내부의 문제라면, 프롬프트 민감도는 입력과 출력 사이의 관계에서 생기는 문제입니다. 프롬프트를 거의 그대로 두고 단어 하나만 바꿔도 결과가 극단적으로 달라지는 경우가 놀라울 만큼 흔합니다. 문장 하나를 추가하거나, 단어의 순서를 살짝 바꾸거나, 심지어 줄바꿈 하나를 넣고 빼는 것만으로 모델의 답변 전체가 뒤바뀌는 일이 실제로 벌어집니다.

이 현상이 위험한 이유는 두 가지입니다. 첫째, 개발 단계에서 잘 동작하던 프롬프트가 운영 환경에서 사용자가 입력하는 다양한 변형 앞에서 무너질 수 있습니다. 개발자가 테스트한 몇 가지 문구는 안정적으로 동작했지만, 실제 사용자는 그 문구를 정확히 재현하지 않습니다. 둘째, 이 민감도는 디버깅을 극도로 어렵게 만듭니다. 버그를 재현하려는 엔지니어가 원래 실패했던 입력을 아주 조금만 다르게 입력해도 문제가 사라진 것처럼 보이기 때문입니다.

프롬프트 민감도를 완전히 없애는 방법은 존재하지 않습니다. 다만 그 영향을 줄이는 실무적인 접근은 있습니다. 프롬프트를 하나의 완성된 문장이 아니라 명확하게 구분된 섹션(역할, 맥락, 지시, 출력 형식)으로 구조화하면 모델이 각 섹션의 역할을 더 안정적으로 해석하는 경향이 있습니다. 또한 프롬프트를 변경할 때마다 대표성 있는 입력 집합에 대해 회귀 테스트를 돌리는 습관이, 눈에 보이지 않는 민감도의 영향을 조기에 드러내 줍니다.

여기서 중요한 태도 전환이 하나 있습니다. 프롬프트 민감도를 “더 좋은 프롬프트를 찾으면 해결되는 문제”로 보는 대신, “언제든 발생할 수 있는 배경 조건”으로 받아들이는 것입니다. 이 전환이 없으면 팀은 끝없이 프롬프트를 미세 조정하는 데 시간을 쓰면서도 근본적인 안정성은 얻지 못합니다.

거짓말을 만들어내는 이유

세 번째 원인은 흔히 환각이라고 부르는 현상입니다. 이 표현이 오해를 부르기 쉬운 이유는, 모델이 마치 의도적으로 속이려 한다는 인상을 주기 때문입니다. 실제로 벌어지는 일은 그보다 훨씬 기계적입니다. 모델은 학습 데이터에 없는 지식을 물었을 때 모른다고 답하는 대신, 통계적으로 그럴듯한 다음 단어를 계속 이어 붙입니다. 그 결과물이 사실처럼 읽히는 문장이 되는 것뿐입니다.

이 현상은 모델이 문맥으로 제공된 정보를 제대로 활용하지 못하거나, 학습 데이터의 특정 패턴에 지나치게 의존할 때 두드러집니다. 예를 들어 문서를 참고 자료로 넘겨줬는데도 모델이 그 문서에 없는 세부 사항을 만들어내는 경우가 대표적입니다. 사용자가 의도치 않게 모델의 학습 범위를 벗어나는 질문을 던졌을 때도 같은 현상이 나타납니다.

여기서 짚어야 할 지점은, 이 세 가지 원인, 즉 비결정성과 프롬프트 민감도와 환각이 서로 독립적인 문제가 아니라는 사실입니다. 셋 다 같은 뿌리에서 나옵니다. 모델은 정답을 아는 것이 아니라 확률이 높은 다음 토큰을 고르는 것뿐이라는 사실입니다. 이 뿌리를 이해하면 세 현상 모두를 프롬프트 수준에서 개별적으로 땜질하는 대신, 시스템 설계 수준에서 함께 다뤄야 한다는 결론에 자연스럽게 도달합니다.

그래서 다음 질문은 이렇게 바뀝니다. 확률적으로 흔들리는 컴포넌트를 어떻게 신뢰할 수 있는 시스템의 일부로 만들 것인가. 답은 모델을 더 똑똑하게 만드는 데 있지 않습니다. 모델을 감싸는 경계를 얼마나 단단하게 만드느냐에 달려 있습니다.

계약을 만들면 확률이 공학이 된다

첫 번째 경계는 출력의 형태를 계약으로 고정하는 일입니다. AI가 자유 형식으로 답을 내놓으면 그 답을 다음 단계 시스템이 자동으로 처리하기 어렵습니다. 자연어 문장은 파싱하기 까다롭고, 매번 같은 구조를 갖는다는 보장도 없습니다. 다음 단계 시스템이 이 출력을 안정적으로 소비하려면, 출력의 형태에 관한 약속이 먼저 있어야 합니다. 이 약속이 없으면 모델의 표현 방식이 조금만 바뀌어도 뒤따르는 시스템 전체가 멈추거나 엉뚱하게 동작합니다.

구조화된 출력은 이 문제를 근본적으로 해결하는 접근입니다. 대부분의 LLM API는 응답 형식을 JSON으로 강제하는 기능을 제공합니다. 형식을 미리 정의해두면 모델은 그 구조에 맞춰 텍스트를 생성하고, 시스템은 그 구조를 전제로 다음 처리 로직을 설계할 수 있습니다. 다만 이 기능만으로 완전히 안심할 수는 없습니다. 모델이 문법적으로 유효한 JSON을 항상 만들어내는 것은 아니며, 구조는 맞지만 내용의 일부가 스키마와 어긋나는 경우도 흔하게 발생합니다.

더 강한 형태의 계약은 도구 호출입니다. 모델에게 미리 정의된 도구를 호출하도록 요청하면, 그 도구의 입력과 출력 스키마 자체가 계약이 됩니다. 모델은 스키마에 맞는 인자를 만들어야만 도구를 실행시킬 수 있으므로, 강제력이 JSON 모드보다 한 단계 더 셉니다. 이 방식은 특히 에이전트 시스템에서 진가를 발휘합니다. 여러 도구를 순서대로 호출하면서 작업을 완성해가는 구조에서는, 각 도구의 계약이 명확할수록 전체 실행 흐름의 예측 가능성도 함께 올라갑니다.

어떤 방식을 선택하든 마지막에는 반드시 검증 계층이 있어야 합니다. 모델이 돌려준 결과물을 검증 라이브러리로 확인하고, 계약을 어긴 출력은 곧바로 오류로 처리해야 합니다. 이 검증 계층을 생략하면 잘못된 출력이 소리 없이 시스템 안으로 흘러 들어가, 문제가 훨씬 나중에 엉뚱한 곳에서 드러나게 됩니다. 계약과 검증을 함께 두는 것이 확률적인 컴포넌트를 결정론적인 시스템 경계 안에 가두는 첫 번째 실천입니다.

재시도는 만능이 아니다: 실패를 세 갈래로 나눈다

두 번째 경계는 실패를 다루는 방식입니다. 모델 호출이 실패하면 반사적으로 재시도부터 하고 싶어지는 것이 자연스러운 반응입니다. 네트워크 오류나 일시적인 문제라면 재시도만으로 풀리는 경우가 실제로 많습니다. 하지만 모든 실패가 재시도로 고쳐지지는 않습니다. 모델이 거짓 정보를 만들어내거나 잘못된 판단을 내린 상황이라면, 같은 요청을 다시 보낸다고 그 문제의 원인이 사라지는 것은 아닙니다. 오히려 이번에는 다른 방식으로 틀린 답이 돌아올 수도 있습니다.

여기에 재시도의 함정이 있습니다. 재시도가 실제로는 근본 문제를 전혀 해결하지 못했는데도, 시스템은 마치 문제가 해결된 것처럼 계속 동작합니다. 사용자의 눈에는 정상적인 답변이 온 것처럼 보이지만, 실제로는 또 다른 유형의 잘못된 답이 도착한 것뿐입니다. 이런 상태를 방치하면 실패가 조용히 반복되면서도 겉으로는 시스템이 건강해 보이는, 가장 위험한 형태의 장애로 이어집니다.

그래서 재시도를 설계할 때는 실패를 세 갈래로 구분하는 작업이 먼저 필요합니다. 첫 번째는 일시적 실패입니다. 네트워크 오류, 모델 서버의 순간적인 과부하, 요청 빈도 제한 초과 같은 원인으로 발생하며, 시간이 지나면 자연스럽게 풀리는 경우가 많습니다. 이런 실패에는 재시도가 효과적입니다. 두 번째는 영구적 실패입니다. API 키가 잘못됐거나 모델이 해당 작업 자체를 지원하지 않을 때 일어나며, 재시도를 반복해도 결과는 언제나 동일합니다. 이런 실패에 재시도를 반복하는 것은 시간과 비용의 낭비일 뿐입니다.

세 번째가 가장 까다로운 결과 실패입니다. 모델이 응답 자체는 돌려주었지만 그 내용이 기대와 다른 경우입니다. 구조화된 출력을 요청했는데 형식이 어긋났거나, 사실이 아닌 내용이 섞였거나, 판단 자체가 잘못된 상황이 여기에 해당합니다. 이 경우 재시도하면 다른 결과가 나올 가능성은 있지만, 같은 문제가 그대로 반복될 가능성도 똑같이 존재합니다. 이 세 갈래를 구분하지 않은 채 무조건 재시도부터 하는 시스템은, 영구적 실패에 자원을 낭비하고 결과 실패는 겉으로만 해결된 것처럼 위장하게 됩니다. 재시도할 때는 지수적 백오프, 즉 대기 시간을 1초, 2초, 4초처럼 점점 늘려가는 방식을 표준으로 쓰는 것이 좋습니다. 이렇게 하면 모델 서버에 부담을 주지 않으면서도 일시적 실패가 자연스럽게 해소될 시간을 벌 수 있습니다.

측정하지 않으면 관리할 수 없다

세 번째 경계는 관측입니다. 전통적인 소프트웨어에서는 코드의 논리가 명확하고 출력이 보장되기 때문에 로그와 메트릭만으로도 시스템 상태를 충분히 파악할 수 있는 경우가 많습니다. AI 시스템은 다릅니다. 확률적 요소가 작동하는 한 출력은 언제나 보장되지 않고, 이 불안정성을 감지하고 대응하려면 시스템의 행위를 훨씬 더 세밀하게 들여다볼 수 있어야 합니다.

옵저버빌리티는 시스템의 내부 상태를 외부에서 관측되는 신호로부터 추론할 수 있는 능력을 말합니다. 전통적인 모니터링이 시스템의 정적인 상태, 예를 들어 서버가 살아 있는지, 응답 시간이 정상 범위인지를 확인하는 일에 가깝다면, 옵저버빌리티는 시스템의 동적인 행위 패턴을 읽어내는 일에 더 가깝습니다. AI 시스템에서 이 능력이 특히 중요한 이유는, 왜 실패가 발생했는지 왜 특정 출력이 돌아왔는지를 이해해야만 같은 실패가 반복되는 것을 막을 수 있기 때문입니다.

품질을 관리하려면 먼저 품질을 측정할 수 있어야 합니다. 실무적으로 유용한 첫 번째 지표는 구조 유효성 비율입니다. 전체 응답 중에서 사전에 정의한 구조에 맞게 돌아온 응답의 비율을 말합니다. JSON 모드를 사용한다면, 모델이 돌려준 응답 중에서 실제로 유효한 JSON이었던 비율을 뜻합니다. 이 비율이 급격히 떨어진다면 모델의 동작이 변했거나 프롬프트가 의도치 않게 바뀌었다는 신호로 읽을 수 있습니다.

두 번째로 유용한 지표는 의미 유효성 비율입니다. 구조적으로는 유효한 응답 중에서 내용까지 실제로 적합했던 응답의 비율입니다. 이 지표는 검증 로직으로 자동으로 측정할 수 있습니다. 예를 들어 특정 필드의 값이 허용 가능한 범위 안에 있는지 검증하고, 그 통과 비율을 지속적으로 추적하는 식입니다. 이 비율이 낮다면 구조는 맞지만 내용이 부적합한 응답이 많다는 뜻이므로, 프롬프트를 조정하거나 검증 로직 자체를 재검토할 신호가 됩니다. 이 두 지표를 시간의 흐름에 따라 계속 지켜보는 것이, 눈에 보이지 않는 시스템의 건전성을 눈에 보이는 숫자로 바꾸는 유일한 방법입니다.

결론: 예측 불가능성을 인정하고 설계하라

지금까지 살펴본 네 가지, 즉 비결정성의 근본 원인을 이해하는 것, 구조화된 출력으로 계약을 만드는 것, 실패를 구분해서 재시도 전략을 세우는 것, 그리고 품질을 지속적으로 측정하는 것은 서로 다른 문제처럼 보이지만 결국 하나의 태도로 수렴합니다. LLM의 출력을 통제하려는 시도를 포기하고, 대신 그 출력이 시스템에 들어오고 나가는 경계를 설계하는 것입니다.

이 태도 전환이 중요한 이유는, 모델이 발전할수록 비결정성이나 환각이 완전히 사라질 것이라는 기대가 현실적이지 않기 때문입니다. 모델의 성능은 계속 좋아지겠지만, 확률적으로 다음 토큰을 선택하는 근본 구조가 바뀌지 않는 한 예측 불가능성은 어떤 형태로든 남아 있을 가능성이 높습니다. 그렇다면 프로덕션 엔지니어의 일은 이 특성을 없애는 것이 아니라, 이 특성이 시스템 전체를 무너뜨리지 않도록 경계를 단단히 세우는 것이 됩니다.

실제로 팀이 해야 할 일은 어렵지 않게 요약됩니다. 출력의 형태를 계약으로 강제하고 검증 계층을 반드시 둘 것, 실패를 일시적 실패와 영구적 실패와 결과 실패로 나누어 각각 다르게 대응할 것, 구조 유효성과 의미 유효성이라는 두 지표를 꾸준히 관측할 것입니다. 이 세 가지 실천은 특정 프레임워크나 특정 모델에 종속되지 않기 때문에, 어떤 LLM을 쓰든 그대로 적용할 수 있습니다.

이 글에서 다룬 내용을 더 깊이 들여다보고 싶다면, 각 패턴을 구체적인 코드 수준까지 풀어낸 전자책 불확실한 AI를 제어가능하게를 참고하시기 바랍니다.

챕터 삽화

1장 삽화 4장 삽화

태그: ai-engineering, llm-reliability, llmops, 관찰성, production-ai, 프롬프트 엔지니어링, structured-output

카테고리:

업데이트: