당신의 에이전트가 운영 환경에서 실패했습니다. 재현에 행운을 빕니다.
요약
LLM 에이전트 운영 중 발생하는 재현성 문제의 원인과 해결책을 다룹니다. 비트 단위 결정론과 재생 가능성의 차이를 설명하며, Temperature 0 설정만으로는 해결할 수 없는 에이전트 디버깅의 핵심 과제를 제시합니다.
핵심 포인트
- 비트 단위 결정론과 재생 가능성을 구분해야 함
- Temperature 0 설정만으로는 완벽한 재현이 불가능함
- 디버깅을 위해 실행 과정(도구 호출, 중간 상태 등)의 기록이 필수적임
- 모델의 결정론적 특성보다 실행 로그의 재구성 능력이 중요함
오전 9:04
티켓 하나가 접수됩니다. 고객이 어제 당신의 에이전트(Agent)를 실행했는데, 잘못된 도구(Tool)를 호출하여 엉뚱한 레코드를 삭제했습니다. 이제 당신의 편지함에는 피해 사실을 빨간 상자로 표시한 스크린샷이 들어와 있습니다. 사용자 ID도 있고, 타임스탬프(Timestamp)도 있습니다. 로그에서 정확한 프롬프트(Prompt)를 복사하여, 동일한 시스템 프롬프트(System Prompt)와 함께 동일한 모델(Model)에 붙여넣고 실행 버튼을 누릅니다.
완벽하게 작동합니다.
다시 실행해 봅니다. 또 잘 작동합니다. 열 번을 더 실행해 봅니다. 에이전트는 매번 모범적인 직원처럼 행동하며, 정작 문제가 되었던 그 한 번, 즉 고객의 데이터를 날려버린 그 실행은 어디에도 나타나지 않습니다. 다시 재현할 수 없다는 것은 디버깅(Debugging)을 할 수 없다는 뜻이며, 이는 다음 고객에게는 이런 일이 발생하지 않을 것이라고 약속할 수 없음을 의미합니다.
이것이 바로 재현성(Reproducibility) 문제이며, 만약 당신이 거대 언어 모델(Large Language Model, LLM)을 기반으로 구축된 무엇인가를 출시하고 있다면, 이는 이미 당신의 문제입니다. 이 글은 왜 이런 일이 발생하는지, 왜 그중 일부는 실제로 제거하고 싶지 않은 기능인지, 그리고 당신에게 꼭 필요한 단 한 가지, 즉 실행된 과정을 발생한 그대로 정확하게 다시 재생(Replay)할 수 있는 능력을 되찾기 위해 무엇을 할 수 있는지에 대해 다룹니다.
여기서 "재현 가능하다"는 것이 의미하는 바
대부분의 팀은 이 단어를 서로 다른 두 가지 의미로 사용하며 서로 엇갈린 논쟁을 벌입니다. 이 두 가지를 분리하면 전체 주제가 더 명확해집니다.
첫 번째 의미는 **비트 단위 결정론(Bitwise Determinism)**입니다. 즉, 동일한 입력이 항상 토큰(Token) 하나하나까지 동일한 출력을 생성하는 것을 말합니다. 이것은 일반적인 소프트웨어에서는 당연히 갖추고 있다고 가정하는 것이지만, LLM에서는 거의 불가능한 것입니다.
두 번째 의미는 **재생 가능성(Replayability)**입니다. 이미 발생한 실행(Run)이 주어졌을 때, 디버깅을 할 수 있을 만큼 충분히 입력값, 샘플링된 출력값(Sampled Outputs), 도구 호출(Tool Calls), 중간 상태(Intermediate State) 등 정확히 어떤 일이 일어났는지를 재구성할 수 있는 것을 의미합니다. 모델이 결정론적(Deterministic)일 필요는 없습니다. 실행 과정이 기록되어 있어야 할 뿐입니다.
함정은 실제로 두 번째가 필요한 상황에서 첫 번째를 쫓는 것입니다. 팀들은 모델을 비트 단위 결정론으로 강제하기 위해 몇 주를 허비하다가 실패하고, 시스템은 알 수 없는 것이라고 결론짓습니다. 그렇지 않습니다. 당신은 잘못된 계층(Layer)을 목표로 삼았던 것입니다.
Temperature zero가 당신을 구원하지 못할 것입니다
모두가 가장 먼저 시도하는 것은 Temperature (온도)를 0으로 설정하는 것입니다. 그 논리는 명확합니다. Temperature는 샘플링 (Sampling) 과정에서의 무작위성을 제어합니다. 이를 0으로 설정하면 모델은 매번 가장 확률이 높은 단일 다음 토큰을 선택해야 하며, 이는 탐욕적 디코딩 (Greedy decoding) 방식으로서 결정론적 (Deterministic)이어야 합니다. 하나의 입력에 대해 영원히 하나의 출력만 나오는 식이죠.
이론적으로는 그렇습니다. 하지만 실제로 Temperature 0에서 동일한 프롬프트를 두 번 실행해 보면, 조만간 출력 결과가 달라집니다. 종종 단어 하나로 시작하여 문장이 약간 다른 방향으로 흐르기 시작하고, 그 이후의 나머지 부분은 거기서부터 멀어지게 됩니다. 이 혼란을 해결해 줄 핵심적인 이유는 이 분야의 대부분의 오해를 바로잡아 주는 구분법에서 오는데, 이는 이 주제에 관한 Sara Zan의 글에서 비롯되었습니다: 샘플링 결정론 (Sampling determinism)은 시스템 결정론 (System determinism)과 동일한 것이 아닙니다.
이후부터 모든 곳에서 등장할 것이기에 용어를 빠르게 짚고 넘어가겠습니다. 모델이 토큰을 방출하기 전에, 모델은 자신의 어휘 사전(Vocabulary)에 있는 모든 후보 토큰에 대해 원시 점수(Raw score)를 생성합니다. 이 점수들을 **Logits (로짓)**이라고 부릅니다. 단일 최고 로짓을 가진 토큰을 선택하는 연산을 **Argmax (아규맥스)**라고 하며, 이는 말 그대로 "최댓값을 주는 인자"를 의미합니다. 탐욕적 디코딩 (Greedy decoding)은 매 단계에서 단순히 Argmax를 수행하는 것입니다.
따라서 Temperature 0은 *선택 규칙 (Selection rule)*을 결정론적으로 만듭니다. 항상 Argmax를 취하게 하는 것이죠. 하지만 당신이 Argmax를 취하는 대상인 로짓 (Logits) 자체가 실행할 때마다 동일하다는 것을 보장하지는 않습니다. 만약 두 후보 토큰의 로짓이 거의 비슷하다면, 마지막 몇 비트의 차이만으로도 승자가 바뀔 수 있습니다. 일단 하나의 토큰이 바뀌면, 그 이후의 모든 토큰은 서로 다른 접두사 (Prefix)로부터 생성되므로 그 차이는 복리로 증폭됩니다.
그렇다면 질문은 이것이 됩니다: 왜 동일한 입력에 대해 동일한 모델을 두 번 실행했을 때 로짓 (Logits)이 달라질 수 있는 걸까요?
원죄: 부동 소수점은 결합 법칙이 성립하지 않는다 (Floating point is not associative)
수치 코드를 깊이 들여다본 적이 없는 사람들을 놀라게 할 대목이 여기 있습니다. 실수(Real numbers)의 경우, 덧셈은 결합 법칙(Associative)이 성립합니다. (a + b) + c는 a + (b + c)와 같습니다. 하지만 부동 소수점(Floating point) 수에서는 그렇지 않습니다. 모든 중간 결과가 유한한 정밀도(Finite precision)로 반올림되기 때문입니다. Horace He와 협력자들이 작성한 Thinking Machines의 글에 나온 전형적인 예시는 다음과 같습니다:
(0.1 + 1e20) - 1e20 = 0
0.1 + (1e20 - 1e20) = 0.1
동일한 세 개의 숫자, 다른 그룹화, 다른 결과입니다. 이것은 버그가 아닙니다. 일정한 유효 숫자(Significant figures)로 거대한 값과 아주 작은 값을 모두 표현하기 위해 부동 소수점이 지불해야 하는 대가입니다.
이제 이를 확장해 봅시다. 트랜스포머(Transformer)의 순전파(Forward pass), 즉 입력값에 대해 모델을 한 번 완전히 실행하는 과정은 행렬 곱셈(Matrix multiplications), 정규화(Normalizations), 어텐션(Attention) 전반에 걸쳐 수백만 번의 덧셈, 곱셈, 리덕션(Reductions)이 일어납니다. 이러한 리덕션 중 어느 하나라도 누적되는 순서를 바꾸면 결과의 마지막 몇 비트(Bits)가 바뀝니다. 로짓(Logit)의 마지막 몇 비트를 바꾸면 어떤 토큰이 아그맥스(Argmax)가 될지가 바뀔 수 있습니다. 이것이 저수준 산술(Low level arithmetic)에서부터 완전히 다른 문장에 이르기까지의 연쇄 과정입니다.
진짜 범인은 모두가 지목하는 그 존재가 아니다
흔히 알려진 설명은 부동 소수점과 동시성(Concurrency)에서 멈춥니다. 한 줄로 요약하자면 이렇습니다: 수천 개의 GPU 스레드(Threads)가 아무도 통제할 수 없는 순서로 종료되며, 부동 소수점 덧셈은 결합 법칙이 성립하지 않기 때문에 동일한 숫자를 다른 순서로 더하면 약간 다른 합계가 나오고, 결과적으로 출력이 실행할 때마다 흔들린다는 것입니다. 이 설명은 완결성 있게 들립니다. 하지만 틀렸으며, Thinking Machines의 분석은 이를 가장 명확하게 반박합니다.
대중적인 이야기를 깨뜨리는 불편한 사실은 다음과 같습니다. 동일한 데이터에 대해 동일한 GPU에서 동일한 행렬 곱셈을 천 번 실행하면, 매번 비트 단위로 완전히 동일한(Bitwise identical) 결과를 얻게 됩니다:
A = torch.randn(2048, 2048, device='cuda', dtype=torch.bfloat16)
B = torch.randn(2048, 2048, device='cuda', dtype=torch.bfloat16)
ref = torch.mm(A, B)
...
부동 소수점 (Floating point)이 관여하고 있습니다. 대규모 병렬 처리 (Massive concurrency)가 관여하고 있습니다. 그럼에도 불구하고 결과는 완벽하게 재현 가능합니다. 따라서 병렬 처리와 부동 소수점만으로는 전체 답이 될 수 없습니다.
진정한 범인은 배치 불변성 (batch invariance), 더 정확히 말하면 그것의 부재입니다. 운영 환경의 추론 서버 (Production inference servers)는 효율성을 위해 당신의 요청만을 단독으로 실행하지 않습니다. 효율성을 위해 같은 순간에 도착한 다른 요청들과 함께 이를 배치 (batch)로 묶어 처리합니다. 당신의 출력을 계산하는 저수준 GPU 루틴인 커널 (kernels)은 정규화 (normalization), 행렬 곱셈 (matrix multiplication), 어텐션 (attention) 내부에서 리덕션 (reductions)을 수행하며, 그 결과는 실행된 _배치의 형태 (shape of the batch)_에 따라 달라집니다. 고정된 배치에 대한 순전파 (forward pass)는 결정론적 (deterministic)입니다. 하지만 배치는 고정되어 있지 않습니다. 그것은 동시 부하 (concurrent load), 동일한 밀리초 내에 서버에 접속하는 다른 사용자, 즉 당신이 제어할 수 없고 볼 수도 없는 조건들에 달려 있습니다.
따라서 당신의 프롬프트는 동일하고, 파라미터 (parameters)도 동일하지만, 변한 것은 서버 내부에서 당신과 함께 머물렀던 동료들입니다. 이것이 바로 프롬프트가 로컬 테스트에서는 매우 견고해 보이다가 운영 환경에서는 불안정해지는 (flaky) 이유이기도 합니다. 모델이 더 창의적으로 변한 것이 아닙니다. 배칭 (batching) 조건이 변한 것입니다.
Thinking Machines 팀은 이 문제의 규모와 해결책을 모두 보여주었습니다. 표준 vLLM을 실행했을 때, Qwen-3-8B에 천 개의 동일한 프롬프트를 입력하면 80개의 서로 다른 완성문 (completions)이 생성되었습니다. 배치 형태에 관계없이 동일한 결과를 생성하는 배치 불변 커널 (batch invariant kernels)을 사용하자, 동일한 천 개의 프롬프트는 정확히 단 하나의 결과만을 생성했습니다. 비용은 실재했지만 완만했습니다. 그들의 테스트 중 하나는 26초에서 42초로 늘어났습니다. 그들의 라이브러리인 batch-invariant-ops는 이후 SGLang에 채택되었습니다. 배치 불변성을 갖춰야 하는 세 가지 연산은 RMSNorm (정규화 단계), 행렬 곱셈, 그리고 어텐션입니다.
교훈: 진정한 비트 단위 재현성 (bitwise reproducibility)은 달성 가능하지만, 커널에 이르기까지 전체 추론 스택 (inference stack)을 제어해야만 가능합니다. 호스팅된 API (hosted API)를 호출하는 사람 중 이를 제어할 수 있는 사람은 거의 없습니다.
전문가 혼합 (Mixture of experts)이 또 다른 문을 엽니다
한 줄로 요약하자면: 전문가 혼합 (Mixture of Experts, MoE) 모델은 하나의 거대한 네트워크를 여러 개의 작은 전문가 서브 네트워크 (specialist subnetworks)로 나눈 것이며, 매번 전체 모델을 실행하는 대신 라우터 (router)가 각 토큰을 오직 몇 개의 전문가에게만 보내는 방식입니다. 많은 프런티어 모델 (frontier models)이 이런 방식으로 구축되었으며, 이 아키텍처는 동일한 문제의 두 번째 독립적인 원인이 됩니다. 만약 그 라우팅 (routing)이 토큰별로 독립적이라면 결정론적 (deterministic)일 것입니다. 하지만 그렇지 않으며, 그 이유는 용량 계수 (capacity factor)라고 불리는 수치 때문입니다.
각 전문가는 주어진 배치 (batch) 내에서 처리할 수 있는 토큰의 양이 정해져 있습니다. 그 상한선이 바로 용량 계수 (capacity factor)입니다. 즉, 한 전문가가 가득 차기 전까지 수용할 수 있는 토큰의 임계값입니다. 배치 내의 너무 많은 토큰이 모두 동일한 전문가를 원할 때, 한도를 초과한 토큰들은 모두 처리될 수 없습니다. 초과된 토큰들은 두 번째 선택지로 지정된 전문가에게 밀려나거나, 해당 레이어 (layer)에서 완전히 누락됩니다. 따라서 당신의 토큰이 첫 번째 선택지 전문가에게 도달할 수 있는지 여부는 동일한 배치 내의 다른 얼마나 많은 토큰이 그 전문가를 두고 경쟁하느냐에 달려 있습니다.
이는 배치 불변성 (batch invariance)과 동일한 함정이지만, 다른 모습을 하고 있을 뿐입니다. 당신의 토큰에 대한 라우팅 결정은 당신의 토큰 단독의 함수가 아닙니다. 그것은 당신의 토큰이 속한 배치 전체의 함수입니다. Vincent Schmalbach가 설명했듯이, 이는 전문가 혼합 (Mixture of Experts) 모델을 배치 수준에서는 결정론적 (deterministic)으로 만들고, 단일 시퀀스 (sequence) 수준에서는 비결정론적 (nondeterministic)으로 만듭니다. 동일한 프롬프트 (prompt)를 두 번 보내더라도, 서로 다른 이웃들과 함께 배치되면 용량 계산 결과가 달라지므로 당신의 토큰은 다르게 라우팅됩니다. 근본 원인은 같지만, 이를 전달하는 두 번째 메커니즘이 존재하는 것입니다.
360도 전체 보기: 당신의 발밑에서 움직이는 모든 것
샘플링 (Sampling)과 커널 (kernels)은 추론 레이어 (inference layer)일 뿐이며, 어제의 실행과 오늘의 실행 사이에서 당신의 발밑에서 움직인 약 8가지 요소 중 단 두 가지에 불과합니다. 실제 에이전트 (agent)에서 이들은 종종 가장 안정적인 요소들 중 하나입니다. 나머지 6가지는 다음과 같으며, 모델 자체가 완전히 고정되어 있더라도 이들 각각은 출력을 변화시킬 수 있습니다.
프롬프트(Prompt)는 고정되어 있는 경우가 드뭅니다. 날짜, 사용자의 이름, 기능 플래그(feature flag), 또는 샘플링된 퓨샷(few-shot) 예시를 보간(Interpolate)하면, "동일한" 프롬프트라도 이전과 같지 않은 프롬프트가 됩니다.
컨텍스트(Context)는 런타임(Runtime)에 조립됩니다. 검색(Retrieval)은 지속적으로 업데이트되는 인덱스에서 데이터를 가져오므로, 어제의 청크(Chunk)는 오늘의 청크와 다릅니다.
도구(Tools)는 실시간 데이터를 반환합니다. 날씨 호출, 데이터베이스 읽기, 검색 API는 매번 서로 다른 것을 반환하며, 모델은 당신이 캡처하지 못한 세계 상태(world state)를 바탕으로 추론합니다.
시간이 유입됩니다. "다음 주 화요일로 예약해 줘"라는 명령은 실행되는 시점에 따라 서로 다른 날짜로 해석됩니다.
모델 버전이 표류(Drift)합니다. 지난달에 호출했던 gpt-4o나 claude는 당신이 제어할 수 있는 버전 업데이트 없이 이번 달에 다른 가중치(Weights) 세트로 변경되었을 수 있습니다.
대화 기록(Conversation history)이 축적됩니다. 멀티 턴(Multi-turn) 에이전트에서 이전 턴들은 입력의 일부가 되므로, 만약 그중 하나라도 변했다면 이후의 모든 턴이 그 변화를 상속받습니다.
이것이 바로 대부분의 재현성(Reproducibility) 논의가 온도(Temperature) 설정에만 매몰되어 놓치고 있는 부분입니다. 샘플러(Sampler)는 8개의 조절 장치가 달린 기계의 노브(Knob) 중 하나일 뿐입니다. 실행 과정을 재현하려면 단 하나가 아니라 8개 모두를 고정해야 합니다.
잠깐: 사실 우리는 이 중 일부를 원합니다
더 나아가기 전에, 우리는 한 가지 사실에 대해 솔직해져야 합니다. 위의 모든 상황에 대한 당연한 반응은 "좋아, 그럼 전부 결정론적(Deterministic)으로 만들어서 끝내버리자"이기 때문입니다. 그러지 마십시오. 만약 스위치 하나만 눌러서 모델을 토큰 단위로 영원히 완벽하게 결정론적으로 만들 수 있다고 해도, 당신은 그 스위치를 눌러서는 안 됩니다. 당신의 재현성을 망가뜨리는 비결정론(Nondeterminism)은 바로 모델을 훌륭하게 만드는 것과 동일한 속성입니다. 이 논거는 네 부분으로 구성되어 있으며, 대부분의 팀은 첫 번째 부분만을 알고 있습니다.
품질 (Quality): 탐욕적 디코딩 (greedy decoding)은 안전하지 않으며, 결함이 있습니다. 직관적으로는 항상 가장 확률이 높은 단일 토큰을 선택하는 것이 신중하고 보수적인 선택이라고 생각하기 쉽습니다. 하지만 그렇지 않습니다. Holtzman과 동료들은 2020년 핵 샘플링 (nucleus sampling) 연구에서 탐욕적 디코딩 (greedy decoding)과 빔 서치 (beam search) 같은 최대화 기반 디코딩 (maximization based decoding)이 개방형 생성 (open ended generation)을 인간이 즉각적으로 기계가 작성한 것으로 인식할 수 있는 단조롭고, 반복적이며, 루프가 발생하는 텍스트로 몰아넣는다는 것을 보여주었습니다. 그들의 결론은 단호했습니다. 최대화 (maximization)는 개방형 텍스트 생성 (open ended text generation)을 위한 잘못된 목적 함수 (objective)라는 것입니다. 해결책은 샘플링 (sampling)을 하되, 신뢰할 수 없는 꼬리 (tail) 부분을 잘라내고 분포의 신뢰할 수 있는 머리 (head) 부분에서만 샘플링하는 것입니다. 그것이 바로 핵 샘플링 (nucleus sampling)이며, 보통 0.95 정도로 설정되는 top-p 조절 장치입니다. 이 변동성은 단순한 장식이 아닙니다. 이를 끄는 순간 산문 (prose)은 무너집니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기