LLM: 우리가 놓치고 있었던 것들
“지금 Temperature 값을 몇으로 쓰고 계신가요?” 누군가 이렇게 물어보면 뭐라고 답하시나요? “기본값요”, “0.7이요”, 아니면 “글쎄요, 그게 중요한가요?” 보통 이 세 가지 답변 중 하나일 겁니다. 그리고 왜 그 값을 쓰는지 정당화하려 하면 금세 말문이 막히곤 하죠.
우리는 지금 LLM을 딱 이 정도로 사용하고 있습니다. 매일 API를 호출하고, 프롬프트를 messages에 담아 보내고, 응답을 받아오죠. 하지만 “Temperature는 실제로 어떤 역할을 하지?”, “Top-P는 Temperature랑 뭐가 다른 거지?”, “Prompt Caching은 켜기만 하면 알아서 작동하나?”, “모델을 더 좋은 걸로 바꾸면 환각(hallucination) 현상이 사라질까?” 같은 질문이 나오면 답변이 모호해집니다.
이름들은 다 들어봤고, 대충 어떻게 써야 하는지도 압니다. 하지만 딱 거기까지예요. 운영 환경에서 도저히 설명할 수 없는 문제가 터지면, 우리는 늘 프롬프트나 모델 탓만 합니다. 그 주변에서 무슨 일이 일어나는지는 생각하지 않죠. 이번 포스팅에서는 바로 그 ‘주변 것들’에 대해 이야기해 보려 합니다. 라벨도 안 읽고 무작정 돌려보던 다이얼들이 환각, 컨텍스트, 그리고 generate_text() 호출 이후의 과정에 실제로 어떤 영향을 미치는지 낱낱이 파헤쳐 보겠습니다.
Temperature
Temperature는 LLM API를 다루는 개발자가 가장 먼저 접하는 파라미터입니다. “낮으면 정확하고, 높으면 창의적이다"라는 일반적인 이해가 충분해 보이지만, 실제로는 그렇지 않습니다.
Temperature가 실제로 하는 일
LLM이 다음 토큰을 고를 때, 모델은 어휘 사전의 모든 후보에 점수(logit) \(z_i\)를 부여하고, 이를 softmax를 통해 확률 분포로 바꿉니다. 이때 Temperature \(T\)는 softmax 이전의 logit을 나누는 제수로 들어갑니다.
$$ P(x_i) = \frac{\exp(z_i / T)}{\sum_{j} \exp(z_j / T)} $$이 단순한 나눗셈이 전체 분포를 완전히 뒤바꿔 놓습니다. \(T < 1\)일 때는 모든 logit이 커지면서 1등과 2등 토큰 사이의 격차가 벌어집니다. 즉, 모델이 “가장 자신 있어 하는” 토큰이 압도적으로 선택되는 것이죠. 반대로 \(T > 1\)이면 logit들이 서로 좁혀지면서 격차가 줄어들고, 2등이나 3등 후보가 선택될 가능성이 생깁니다. \(T \to 0\)으로 가면 softmax는 argmax와 같아져서 모델은 항상 가장 높은 점수의 토큰만 고르게 되는데, 이를 Greedy Decoding이라고 부릅니다. 반대로 \(T \to \infty\)로 가면 모든 토큰이 거의 동일한 확률을 갖게 됩니다.
시험 문제에 비유하자면, 95점짜리 정답과 80점짜리 답이 있을 때 낮은 Temperature는 무조건 95점을 고르지만, 높은 Temperature는 가끔 “80점도 나쁘지 않네"라고 생각하며 80점을 선택하는 셈입니다.
“Temperature 0 = 결정론적 출력"이라는 오해
Temperature가 0일 때 항상 최상위 토큰만 고른다면 같은 질문에 항상 같은 답이 나와야 할 것 같죠? 이론적으로는 맞지만, 실제 API 환경에서는 그렇지 않습니다.
Atil et al. (2025)은 GPT-4o, Llama-3-70B 등 5개 모델의 Temperature를 0으로 고정하고 같은 질문을 10번씩 반복해 보았습니다. 결과는 동일한 설정임에도 정확도가 실행마다 최대 15%까지 변했습니다.
이 문제는 모델 자체가 아니라 서빙 인프라에서 발생합니다. 같은 모델을 최적화 없이 로컬 GPU에서 돌리면 완벽하게 결정론적인 결과가 나옵니다. 하지만 API 뒤에서는 여러 사용자의 요청이 묶여서 처리(Continuous Batching 등)되는데, 이 과정에서 발생하는 아주 미세한 부동 소수점 연산 차이가 출력의 차이로 이어지게 됩니다.
흔히들 “부동 소수점의 비결합성(non-associativity) + GPU 병렬 처리 때문"이라고 말하는데, 이건 절반만 맞는 말입니다. 같은 GPU에서 같은 데이터를 가지고 똑같은 행렬 곱셈을 반복하면 동일한 결과가 나오기 때문이죠. 연산 순서와 병렬 처리가 관여하는 것은 맞지만, 그것만으로 비결합성이 무조건 생기는 것은 아닙니다.
진짜 원인은 배치 불변성(batch invariance)이 없기 때문입니다. LLM 추론 서버는 여러 요청을 배치로 묶어서 처리하는데, 배치 크기가 바뀌면 내부 연산의 축소(reduction) 순서가 바뀝니다. 부동 소수점 덧셈은 순서에 따라 결과가 미세하게 달라지는데(비결합성), 서버 부하는 계속 변하므로 똑같은 요청이라도 매번 다른 배치 상황에 놓이게 되고 결과값도 달라지는 것입니다.
이런 현상이 발생하는 구체적인 지점들은 다음과 같습니다:
- 행렬 곱셈 (MatMul): 작은 배치에서는 GPU가 코어를 효율적으로 쓰려고 연산 차원을 쪼개는 Split-K 전략을 씁니다. 이 쪼개는 방식이 배치 크기에 따라 바뀝니다.
- Attention: 디코딩 단계에서 쿼리 길이가 짧으면 병렬 처리를 위해 KV 차원을 쪼갭니다(FlashDecoding). 이 쪼개는 단위가 배치에 따라 달라집니다. 여기에 Chunked Prefill이나 Prefix Caching까지 얹히면 연산 순서는 더 꼬이게 됩니다.
- RMSNorm: 배치가 너무 작으면 행 하나가 여러 코어에 분산되는데, 이 분산 방식 역시 배치 크기에 의존합니다.
- MoE 라우팅: Mixtral이나 GPT-4 같은 Mixture-of-Experts 모델에서는 라우터가 토큰을 각 전문가에게 배정합니다. 이때 서버 부하 분산을 위해 배치 내 다른 토큰들의 라우팅 결과에 따라 내 토큰이 다른 전문가에게 밀려날 수 있습니다. 즉, 옆 사람의 요청 때문에 내 결과가 바뀌는 셈입니다.
CPU나 TPU 서빙 환경에서도 이유만 다를 뿐(예: TPU는 XLA 컴파일 방식에 의존) 비슷한 비결합성이 나타납니다. 핵심은 다른 사용자의 요청이 내 요청의 출력에 영향을 미친다는 점입니다.
문제는 이 영향이 복합적으로 커진다는 것입니다. LLM의 출력을 문자열 매칭으로 검증하는 것은 근본적으로 위태롭습니다. 멀티 단계 파이프라인으로 LLM을 체이닝하면 비결합성은 곱절로 늘어납니다. 각 단계가 95%의 안정성을 가진 분류기 4개를 연결하면 전체 안정성은 0.95의 4승, 즉 약 81%로 떨어집니다. 따라서 Temperature=0이어도 출력값은 언제든 변할 수 있다는 전제하에 시스템을 설계해야 합니다.
실험해 보니 짧고 단순한 프롬프트는 꽤 일관적인 답을 내놓았지만, RAG 결과물이 가득 담긴 길고 복잡한 실전용 프롬프트는 실행할 때마다 결과가 눈에 띄게 달랐습니다.
성능에 미치는 영향은 생각보다 미묘합니다
“정확도를 위해서는 낮은 Temperature, 창의성을 위해서는 높은 Temperature"라는 말은 직관적이지만, 실증적 연구 결과는 더 복잡합니다.
Renze & Guven (2024)은 10개 도메인의 1,000개 객관식 문제를 가지고 GPT-3.5와 GPT-4의 Temperature를 0.0에서 1.0까지 바꾸며 테스트했습니다. 결론은 0.0~1.0 구간 내에서는 통계적으로 유의미한 성능 차이가 없었다는 것입니다. 이는 모델, 프롬프트 기법, 도메인과 상관없이 동일했습니다. 다만 1.2를 넘어가면서 성능이 눈에 띄게 떨어졌고, 1.4가 되면 거의 무작위 수준이 되었습니다.
Li et al. (2025)은 더 깊이 들어가 0에서 2 사이의 Temperature로 12개 오픈소스 모델을 테스트했는데, 작업 유형에 따라 양상이 달랐습니다.
- 번역/요약: Temperature에 가장 민감합니다. 정답이 명확한 작업들이라 값이 높을수록 품질이 떨어집니다.
- 추론/ICL(In-Context Learning): 1.3 근처에서 성능이 약간 향상되었습니다. 약간의 무작위성이 탐색(exploration)에 도움을 주는 것 같습니다.
- 창의적 작업: 중대형 모델에서는 1.3이 최적이었습니다. 하지만 소형 모델은 오히려 더 낮은 온도에서 더 창의적인 결과가 나왔습니다.
- 지시 이행(Instruction Following): 1.0까지는 안정적이다가 그 이후 급격히 나빠졌습니다. 모델이 클수록 이 절벽 구간이 더 뒤에 위치했습니다.
모든 작업에서 모델이 클수록 Temperature 변화에 더 강건(robust)했습니다. 큰 모델은 넓은 범위에서 성능이 안정적이지만, 작은 모델은 매우 민감했습니다. 소형 모델을 쓴다면 Temperature 튜닝이 훨씬 중요해집니다.
왜 그럴까요? 모델들은 기본적으로 Temperature 1을 기준으로 훈련되기 때문입니다. 훈련 목표 자체가 1일 때의 분포를 최적화하도록 설계되었거든요. 잘 훈련된 고성능 모델은 이미 기본 상태에서 보정이 잘 되어 있지만, 덜 똑똑한 모델은 분포가 노이즈가 많고 온도 변화에 더 취약합니다. 그리고 최근 더 강력한 모델들이 나오면서 이러한 민감도 차이는 점점 줄어들고 있습니다.
이를 어떻게 활용할까?
- 0.0~1.0 구간을 너무 고민하지 마세요. 그 안에서의 성능 차이는 통계적으로 무의미합니다.
- 1.0 이상은 웬만하면 피하세요. 지시 이행이 중요한 에이전트 시스템에서는 특히 더 그렇습니다.
- 번역/요약은 낮게, 추론/창의적 작업은 조금 높게 잡으세요. 단, 모델 크기에 따라 조절이 필요합니다. JSON 같은 구조화된 출력이 필요하다면 0으로 설정해도 충분합니다.
- 에이전트라면 0.5~0.8을 추천합니다. 에이전트는 도구 호출을 위해 정확한 JSON 스키마를 뱉어야 하는데, 너무 높은 온도는 포맷을 깨뜨립니다. 반대로 0으로 고정하면 에이전트가 결정론적인 실패 루프에 갇혀 빠져나오지 못할 수 있습니다. 중간 범위를 유지하면 포맷은 지키면서도, 첫 시도가 꼬였을 때 탈출할 수 있는 어느 정도의 엔트로피를 확보할 수 있습니다.
- Temperature=0은 결정론적이지 않습니다. API 환경에서는 출력의 변동성을 항상 염두에 두고 설계하세요. 예외는 없습니다.
Top-K와 Top-P: 후보군 걸러내기
Temperature가 점수 분포의 ‘모양’을 바꾼다면, Top-K와 Top-P는 후보의 ‘범위’를 결정합니다. 이 둘은 모두 터무니없이 낮은 확률의 토큰이 선택되는 것을 방지한다는 동일한 문제를 해결합니다.
Top-K
가장 직관적인 방식입니다. 확률이 높은 상위 K개의 토큰만 남기고 나머지는 버리는 것이죠. 공식적으로 \(V_K\)를 확률이 가장 높은 \(K\)개의 토큰 집합이라고 할 때, 샘플링은 재정규화된 분포에서 수행됩니다.
$$ P'(x_i) = \begin{cases} \dfrac{P(x_i)}{\sum_{j \in V_K} P(x_j)} & \text{if } x_i \in V_K \\ 0 & \text{otherwise} \end{cases} $$Top-K=50이라면 전체 어휘 약 50,000개 중에서 상위 50개만 후보로 남고, 나머지는 샘플링 전에 모두 마스킹됩니다.
문제는 “50"이라는 숫자가 모든 상황에 적합하지 않다는 점입니다. “프랑스의 수도는” 다음에는 사실상 “파리” 하나면 충분한데, 50개의 후보를 두면 불필요한 노이즈만 끼어들 뿐이죠. 반면 “나는 지금 너무” 다음에는 행복한, 피곤한, 혼란스러운, 고마운 등 자연스러운 문맥이 수십 가지가 나올 수 있는데, 이때 K=10으로 제한하면 적절한 답변 선택지를 잘라버릴 수도 있습니다. 모델의 확신 수준과 관계없이 항상 고정된 크기의 후보군만 보게 되는 셈이죠.
Top-P
Holtzman et al. (2020)에서 제안한 Top-P(Nucleus Sampling)는 이 문제를 해결합니다. 고정된 개수 대신, 누적 확률이 \(p\)를 넘을 때까지 토큰을 모읍니다. 토큰을 \(P(x_1) \ge P(x_2) \ge \dots\) 순으로 정렬했을 때, \(V_p\)는 다음을 만족하는 가장 작은 접두사 집합입니다.
$$ V_p = \operatorname*{arg\,min}_{k}\; \sum_{i=1}^{k} P(x_i) \ge p $$그리고 \(V_p\) 내에서 다시 정규화하여 샘플링합니다. Top-P=0.9라면 전체 확률 질량의 상위 90%를 차지하는 토큰들만 후보군이 됩니다.
모델이 확신을 가질 때(예: “파리"가 95%) 후보군은 1~2개로 줄어들고, 확신이 없을 때(“행복한”, “피곤한”, “혼란스러운” 등이 각각 ~15%)는 수십 개로 늘어납니다. 상황에 따라 후보군 크기가 자동으로 조절되는 것이죠.
실제로는 얼마나 중요할까요?
대부분의 경우, 별로 중요하지 않습니다.
Temperature는 분포 자체를 변형(더 뾰족하게 혹은 더 평평하게)시킵니다. 반면 Top-K와 Top-P는 분포는 그대로 둔 채 꼬리 부분만 잘라냅니다. 처리 순서가 중요한데, Temperature가 먼저 분포를 재구성하고, 그 뒤에 Top-K나 Top-P가 후보를 걸러냅니다. 전체 파이프라인은 사실상 다음과 같습니다.
$$ z_i \;\xrightarrow{\;/T\;}\; z_i/T \;\xrightarrow{\text{softmax}}\; P(x_i) \;\xrightarrow{\text{Top-K / Top-P}}\; P'(x_i) \;\xrightarrow{\text{sample}}\; x_t $$둘 다 극단적인 값으로 설정하면 서로 효과를 상쇄할 수 있습니다. 예를 들어, 매우 낮은 \(T\)는 이미 거의 모든 확률을 한 토큰에 몰아주기 때문에, 여기에 공격적인 Top-P를 적용하는 건 중복일 뿐입니다. OpenAI가 Temperature와 Top-P 중 하나만 조정하고 둘 다 건드리지 말라고 권장하는 이유가 바로 이 때문입니다.
Li et al. (2025)가 벤치마크 테스트에서 Top-P(0.8, 0.9, 1.0)와 Top-K(2, 5, 10)를 다양하게 조합해 본 결과, 대부분의 작업에서 성능 변화는 미미했습니다. 성능은 Top-P/Top-K 설정과 무관하게 Temperature 설정에 따라 결정되었죠. Atil et al. (2025) 또한 Top-P가 결정론적 결과에 영향을 주지 않는다는 것을 확인했습니다.
단, 창의적인 텍스트 생성(스토리 작성, 브레인스토밍 등)에서는 예외적으로 Top-P가 의미 있는 차이를 만들었습니다. 그 외의 경우에는 기본값을 유지하고 Temperature만 튜닝하세요. Top-K의 고정된 창보다 Top-P의 유연한 후보군 크기 조절이 더 범용적이기 때문에, 대부분의 API가 Top-P를 기본 샘플링 전략으로 채택하고 있습니다.
프롬프트 캐싱(Prompt Caching)
LLM API 비용은 입력 토큰 수에 비례합니다. 문제는 시스템 프롬프트, 도구 정의(tool definitions), 퓨샷(few-shot) 예제, 공유 RAG 문서 등 매번 동일한 내용이 요청마다 반복된다는 점입니다. 100K 토큰에 달하는 컨텍스트를 매번 처음부터 다시 처리하는 건 그야말로 자원 낭비죠.
프롬프트 캐싱은 반복되는 접두사(prefix)를 서버 측에 저장하고, 후속 요청에서 계산된 결과를 재사용합니다. 이건 단순히 비용 절감을 넘어, 캐시된 접두사는 실제 계산 과정을 건너뛰기 때문에 지연 시간(latency)을 최대 80%까지 줄여줍니다. 비용 절감 효과는 벤더마다 다른데, Anthropic은 캐시 읽기 비용을 기본 입력 가격의 10%로 책정하므로 사실상 90%의 비용이 절감되는 셈입니다.
벤더별 구현 방식의 차이
같은 개념이라도 벤더마다 구현 방식이 꽤 다릅니다.
- OpenAI — 완전 자동입니다. 별도의 코드를 짤 필요가 없죠. 동일한 접두사를 가진 요청은 자동으로 캐시 히트(cache hit)가 발생하며, 추가 비용도 없습니다. 단점은 무엇이 캐시될지 직접 제어할 수 없다는 점입니다. 시스템이 알아서 판단하며, 접두사가 완전히 일치해야만 합니다.
- Anthropic (Claude) — 명시적(explicit) 캐싱과 자동 캐싱을 모두 제공합니다. 명시적 캐싱은
cache_control필드를 사용해 최대 4개의 독립적인 캐시 지점을 지정할 수 있습니다. 자동 캐싱은 마지막 콘텐츠 블록까지 캐시하는데, 대화가 길어지는 상황에서 편리합니다. 가격 정책도 명확합니다. 캐시 읽기는 기본 입력의 10%지만, 캐시 쓰기(write)는 기본 입력의 1.25배(5분 TTL 기준)가 듭니다. 첫 요청에서 약간의 프리미엄을 지불하고, 이후 재사용할 때마다 90%를 아끼는 구조입니다. - Google (Gemini) — 암시적(implicit) 및 명시적 캐싱을 모두 지원합니다. 암시적 캐싱은 Gemini 2.5 이상에서 자동으로 활성화되며 캐시 히트 시 비용이 알아서 절감됩니다. 명시적 캐싱은 설정 가능한 TTL(기본 1시간)을 가진 캐시 객체를 생성하게 해줍니다. 다른 벤더와 달리 Gemini는 캐시를 API 리소스로 취급하기 때문에 캐시 객체를 직접 생성, 조회, 삭제할 수 있으며 비용 절감도 보장됩니다.
공통점은 모두 **접두사 기반(prefix-based)**이라는 것입니다. 프롬프트의 시작 부분이 정확히 일치해야 합니다. 앞부분이 다르고 뒷부분이 똑같으면 캐시 히트는 일어나지 않습니다.
캐싱을 고려한 프롬프트 설계
정적 콘텐츠와 동적 콘텐츠를 분리하세요. 정적 콘텐츠를 앞에 두고, 동적 콘텐츠를 뒤로 배치하는 것이 핵심입니다.
일반적인 구조는 다음과 같습니다:
- 시스템 프롬프트 (거의 안 바뀜) → 캐시 대상
- 도구 정의 (배포 단위로 바뀜) → 캐시 대상
- 공통 컨텍스트 / 퓨샷 예제 (가끔 바뀜) → 캐시 대상
- 대화 기록 (턴마다 바뀜) → 부분적으로 캐시 가능
- 현재 사용자 질문 (매번 바뀜) → 캐시 불가
이 구조를 따르면 13번 항목은 매번 캐시 히트가 발생하고, 45번만 새로 처리하면 됩니다. 멀티턴 대화의 경우 이 효과는 누적됩니다. 대화가 진행될수록 캐시된 접두사가 한 번씩 늘어나기 때문에, 20번째 턴쯤 가면 대화 전체를 다시 처리하는 것과 마지막 턴만 처리하는 것 사이의 차이는 엄청나게 벌어집니다.
해시 기반의 캐시 무효화 제어
접두사 기반 캐싱을 다룰 때 아주 유용한 팁이 하나 있습니다. 시스템 프롬프트 맨 앞에 [prompt-v:a3f2c1]와 같이 프롬프트 내용의 해시값을 넣어두는 것입니다.
접두사 매칭은 시작부터 문자를 하나하나 비교합니다. 프롬프트 깊숙한 곳에서 오타를 수정하거나 문장 하나를 추가하는 등 사소한 변경을 가했을 때, 해시가 없다면 이전과 똑같은 접두사로 인식되어 낡은 캐시를 가져올 수 있습니다. 하지만 앞에 해시를 두면, 콘텐츠가 조금만 바뀌어도 해시값이 달라져 첫 글자부터 미스매치가 발생하므로 낡은 캐시를 가져오는 일을 방지할 수 있습니다. 내용이 동일하면 해시도 같으니 캐시 히트가 보장되죠.
Gemini처럼 ID를 통해 명시적 캐시 객체를 직접 관리하는 벤더에서는 이 방법이 필요 없습니다. 그냥 캐시를 생성하고 ID로 참조하면 되니까요.
캐시 무효화의 함정
캐싱은 접두사 기반이기 때문에, 앞부분을 조금만 수정해도 그 뒤에 있는 모든 캐시가 무효화됩니다.
Anthropic의 경우 무효화 계층은 도구 정의 → 시스템 프롬프트 → 메시지 순입니다. 도구 정의 하나만 바꿔도 시스템 프롬프트와 메시지 캐시는 모두 날아갑니다. 시스템 프롬프트를 수정하면 메시지 캐시가 날아가죠. 메시지 내용만 수정할 때만 도구와 시스템 캐시가 유지됩니다.
생각지도 못한 복병도 있습니다. 내부적으로 웹 검색이나 인용 기능을 켜고 끄는 것만으로도 시스템 프롬프트가 수정되어 메시지 캐시가 무효화될 수 있습니다. 운영 환경에서 “캐시 히트율이 갑자기 떨어졌는데 이유를 모르겠다"면, 대개 이런 경우가 범인인 경우가 많습니다.
최소 토큰 제한도 기억해야 합니다. Anthropic의 경우 Sonnet/Opus 모델은 최소 1,024 토큰, Haiku는 2,048 토큰 이상이어야 캐싱이 활성화됩니다. 너무 짧은 프롬프트는 아예 캐싱되지 않으니 주의하세요.
환각(Hallucination): AI의 문제인가, 우리의 문제인가?
Kalai et al. (2025)은 왜 환각이 수학적으로 피할 수 없는 현상인지 놀라울 정도로 명확하게 분석했습니다.
LLM은 왜 환각을 일으키는가?
환각은 사전 학습 과정에서 통계적으로 필연적입니다. 해당 논문은 이 문제를 이진 분류(binary classification) 문제로 축소합니다. “이 결과물이 올바른가?“라고 판단하는 것은 올바른 결과물을 생성하는 것보다 쉬운 문제죠. 하지만 특정 영역, 즉 학습 데이터에 단 한 번만 등장하는 사실(예: 특정 인물의 생일)에 대해서는 학습할 패턴 자체가 존재하지 않기에 판단조차 불가능합니다. 분류에 실패하면 생성은 더 크게 실패하게 되는데, 수학적으로는 생성 오류율 ≥ 2 × 분류 오류율이라는 식이 성립합니다.
저자는 자신의 이름을 예로 들어 이를 증명했습니다. “Adam Tauman Kalai의 생일이 언제인가?“라고 묻자, DeepSeek-V3는 세 번의 시도에서 “03-07”, “15-06”, “01-01"이라는 각기 다른(모두 틀린) 답을 내놓았습니다. 심지어 확실하지 않을 때는 답변하지 말라는 명시적인 지시도 무시했죠. 논문 제목을 물었을 때도 ChatGPT, DeepSeek, Llama는 제각기 다른 대학과 연도, 제목을 당당하게 지어냈습니다.
학습 데이터에 단 한 번만 등장하는 사실의 비율은 환각 발생률의 최저치가 됩니다. 생일 정보의 20%가 데이터셋에 딱 한 번만 나왔다면, 모델은 구조적으로 생일 관련 질문에서 최소 20%의 환각을 피할 수 없는 셈입니다.
사후 학습(Post-training)은 이 문제를 오히려 심화시킵니다. 대부분의 LLM 벤치마크는 “정답이면 1점, 오답이면 0점, ‘모름’이라고 하면 0점” 식으로 점수를 매깁니다. 시험을 치르는 학생을 상상해 보세요. 수업 시간에 거의 다루지 않은 주제가 시험에 나왔습니다. 학생은 “모른다"고 쓰면 0점이니까 일단 그럴싸한 답을 지어내겠죠. 벤치마크 점수를 잘 받도록 최적화된 LLM은 자연스럽게 “확신이 없어도 추측한다"는 전략을 학습합니다. 인간은 시험 밖의 세상에서 모르는 것을 솔직히 인정하는 법을 배우지만, LLM은 영원히 시험 모드에 갇혀 있는 꼴입니다. 논문에서는 이를 “평가 시 불확실성을 페널티로 간주하는 전염병"이라고 부릅니다.
즉, 최신 모델이 환각을 덜 일으키는 이유는 단순히 “더 많은 데이터를 봤기 때문"만은 아닙니다. 사전 학습 데이터의 품질이 좋아졌고(단발성 사실이 줄어듦), 평가 방식도 진화했기(모름을 페널티 주지 않는 벤치마크의 증가) 때문입니다. 여전히 환각을 줄이는 가장 쉬운 방법은 최신 모델을 사용하는 것입니다.
프롬프트 수준에서 가장 효과적인 대응책은 이 분석 결과를 그대로 따르는 것입니다. 모델에게 불확실할 때는 “모른다"고 말하도록 명시적으로 지시하고(벤치마크로 학습된 추측 행동을 상쇄), 검증 가능한 근거를 요구하며, RAG(검색 증강 생성)를 활용해 드물게 등장하는 사실들을 파라메트릭 지식에 의존하지 않고 컨텍스트에 직접 주입하는 것이죠.
AI의 탓이 아닌 부분
통계적 분석은 모델이 왜 허구를 지어내는지 설명해 주지만, 모델보다는 우리 인간에게 더 큰 원인이 있는 두 번째 실패 사례가 존재합니다.
Lucy Osler (2025)는 “Hallucinating with AI"에서 분산 인지 이론을 바탕으로, AI 챗봇이 단순한 도구가 아니라 우리의 신념을 함께 구성해 나가는 대화 파트너로 기능한다고 주장합니다. AI 챗봇은 사용자의 생각을 끊임없이 긍정하고, 구체화하며, 다듬어주는 ‘아첨꾼’으로 설계되었습니다. 사용자가 반쯤 맞는 가정을 제시하면 AI는 이를 기정사실로 받아들이고 그 위에 논리를 쌓아 올립니다. 사용자는 AI의 정교한 답변을 보고 자신의 원래 가정에 더욱 확신을 갖게 됩니다. AI는 사용자를 긍정하고, 사용자는 그 긍정을 증거로 삼고, AI는 이를 바탕으로 더 발전된 논리를 펼치는 피드백 루프가 형성됩니다. 여기서 “환각"을 일으키는 주체는 AI도, 사용자도 아닌, 바로 그 상호작용 자체입니다.
논문에서 다룬 Jaswant Singh Chail 사례는 극단적인 예시입니다. 그는 ‘Sarai’라는 Replika AI 챗봇과 수주간 대화하며 자신이 ‘시스 암살자(Sith assassin)‘라는 망상에 빠졌습니다. Sarai는 그의 계획에 “매우 현명하네요”, “당신은 아주 잘 훈련받았군요"라며 화답했죠. 인간 친구라면 걱정스러운 반응을 보였겠지만, AI는 마찰 없는 승인을 제공했습니다.
저 역시 이와 유사한 패턴을 가끔 발견합니다. 분명 AI가 명백하게 틀린 경우도 있지만, “정말 AI가 틀린 걸까, 아니면 내가 질문을 잘못한 걸까?” 싶은 순간도 존재하거든요.

“우유 한 통 사 와. 만약 아보카도가 있으면 여섯 개 사 와.” 남편은 우유 여섯 통을 사 왔습니다. 문법적으로는 두 해석 모두 타당합니다. 모호함은 실행이 아니라 요청 그 자체에 있었죠. 상당수의 환각이 이런 구조를 가지고 있습니다. 사용자의 프롬프트가 다중 해석의 여지를 품고 있는데, AI가 모호함을 알리지 않고 멋대로 하나를 선택해 버리는 경우입니다.
사람들은 AI와 대화할 때 특히 단답형으로 말하는 경향이 있습니다. 질문이 짧아질수록 확률 분포는 넓어집니다. 즉, 가능한 해석이 많아져서 의도와 다른 결과에 도달할 확률이 높아지는 것이죠. 해결책은 시스템이 반문하도록 설계하는 것입니다: “사용자의 요청이 모호하면 바로 답하지 말고, 먼저 명확하게 질문할 것.” 우유와 아보카도 요청에 대해 잘 설계된 AI라면 “아보카도를 여섯 개 사 올까요, 아니면 우유를 여섯 통으로 늘릴까요?“라고 되물었을 겁니다. 질문 한 번으로 환각을 미연에 방지할 수 있습니다.
컨텍스트 윈도우: 유한한 작업 기억 공간
지금까지 다룬 파라미터들이 출력 품질과 비용을 결정했다면, 컨텍스트 윈도우는 그 모든 것의 물리적인 상한선입니다.
컨텍스트 윈도우는 모델이 생성 과정에서 참조할 수 있는 텍스트의 총량을 의미합니다. 시스템 프롬프트, 도구 정의, 대화 기록, 현재 질의, 그리고 모델의 응답까지 모두 이 안에 포함되죠. 대부분의 Claude 모델은 200K 토큰을 제공하며, 최근 Sonnet 계열은 1M 토큰까지 지원합니다. 수치상으로는 넉넉해 보이지만, 진짜 문제는 크기가 아니라 모델이 그 안의 내용을 얼마나 효율적으로 활용하느냐에 있습니다.
무조건 크다고 좋은 건 아닙니다
컨텍스트 윈도우가 넓다고 해서 무엇이든 다 집어넣으라는 뜻은 아닙니다. 관련 없는 콘텐츠가 늘어날수록 모델의 집중력은 떨어지거든요. 이를 흔히 “중간 지점 망각(lost in the middle)” 현상이라고 합니다. 컨텍스트의 앞부분이나 끝부분에 있는 정보보다 중간에 끼어 있는 정보를 덜 효과적으로 참조하는 것이죠.
이는 도구를 많이 사용하는 시스템에서 아키텍처상 직접적인 문제로 이어집니다. 제가 테스트해 본 결과, 일반적인 다중 서버 MCP 설정(GitHub, Slack, Sentry, Grafana, Splunk)에서는 도구 정의만으로 약 55K 토큰이 소비되었습니다. 모델이 본격적인 작업을 시작하기도 전에 컨텍스트의 상당 부분을 잡아먹는 셈입니다. 또한, 사용 가능한 도구가 30~50개를 넘어가면 모델이 올바른 도구를 선택하는 능력이 급격히 떨어집니다. 도구 정의가 컨텍스트의 ‘중간’에 위치하기 때문에, 가장 주의력이 약한 곳에 배치되는 꼴이니까요.
이 문제의 해결책은 ‘도구 검색(tool retrieval)‘입니다. RAG와 같은 원리를 도구에 적용하는 것이죠. 모든 도구를 미리 로드하는 대신, 도구 카탈로그에서 필요한 것만 그때그때 검색해 불러오는 방식입니다. Anthropic은 이를 서버 사이드 기능으로 제공하고 있는데, 덕분에 수천 개의 도구 사이에서도 선택 정확도를 유지하면서 도구 정의 토큰을 85% 이상 절감할 수 있습니다. 다른 벤더를 사용 중이라면 직접 구현할 수도 있습니다. 사용자의 요청을 분석해 관련 도구만 필터링한 뒤 컨텍스트에 주입하는 라우팅 레이어를 만드는 방식이죠.
컨텍스트 관리 전략
대화가 길어지거나 에이전트가 반복적으로 도구를 호출하면 컨텍스트는 금방 꽉 찹니다. 단순히 윈도우를 키우는 게 아니라, 능동적인 관리가 필요합니다.
- 서버 사이드 압축: 오래된 대화 내용을 요약해 토큰을 확보하세요. 50번 넘게 주고받은 대화를 핵심 결정 사항과 사실 위주로 요약하면, 모델이 실제로 필요로 하는 정보는 유지하면서 80K 토큰을 5K 토큰 수준으로 줄일 수 있습니다.
- 도구 결과 정리(Pruning): 모델이 도구의 결과(파일 내용, 검색 결과 등)를 처리하고 나면, 원본 데이터는 더 이상 필요 없는 경우가 많습니다. 오래된 도구 결과를 자동으로 지우기만 해도 상당한 공간이 생깁니다. 코드 검색 결과 하나가 10K 토큰이 넘기도 하는데, 대화가 세 턴만 지나도 아무 쓸모가 없게 되니까요.
- 사고 과정(Thinking block) 관리: 긴 사고 과정 역시 토큰을 소비합니다. 다행히 API는 이전 턴의 사고 블록을 자동으로 제거해 실제 콘텐츠를 위한 공간을 확보해 줍니다.
- 위치 선정의 우선순위: “중간 지점 망각” 효과를 고려해 가장 중요한 정보는 컨텍스트의 맨 앞이나 맨 뒤에 배치하세요. RAG 결과를 주입할 때도 관련성이 가장 높은 청크를 무작위가 아닌, 가장 먼저 등장하도록 배치하는 것이 좋습니다.
컨텍스트에 무엇을 넣는 것보다 빼는 것이 훨씬 어렵습니다. 관련 없는 콘텐츠는 비용과 지연 시간을 늘릴 뿐만 아니라 모델의 집중력을 해칩니다. 올바른 접근 방식은 “최대한 많이 집어넣는 것"이 아니라, “딱 필요한 만큼만 남기는 것"입니다.