레슨 4: 폭주와 폴백: 데드 루프, 공회전, 예산 소진
학습 목표:
- 폭주하는 루프의 전형적인 네 가지 형태 — 에러의 누적 증폭, 컨텍스트 비대화와 부패, 데드 루프와 공회전, 예산 소진 — 을 알아보고, 각각 루프의 어떤 성질이 원인인지 말하기
- 각 형태에 대응하는 폴백 게이트를 붙이기: 체크포인트와 조기 정지, 컨텍스트 거버넌스, 최대 턴 수 상한과 무진행 감지, 예산 상한 — 그리고 각 게이트가 실제로 무엇을 잡아내는지 설명하기
- "제자리에서 헛도는" 루프에 어떤 하드 게이트가 필요한지를, 경계 없는 루프를 모델 교체나 프롬프트 재작성으로 구하려 들지 말고 판단하기
전제: 레슨 2와 3을 읽었고, stop_reason으로 구동되며 최대 턴 수 상한이 달린 루프 골격이 손에 있다 | 이전: 레슨 3 << | 다음: 레슨 5 >>
도는 루프는 폭주도 한다
앞의 두 레슨에서 여러분의 루프는 일어섰습니다. stop_reason을 while 조건으로 삼았고, 그다음 레슨 3의 명시적 정지 조건을 붙여 모델이 마무리하기를 거부할 때도 눌러 앉힐 수 있게 되었습니다. 여기까지 오면 제법 안정적인 루프처럼 들립니다. 하지만 "눌러 앉힐 수 있다"는 것은 마지막 퓨즈일 뿐이고, 그것이 잡아내는 병은 딱 하나 — 멈추지 않는 루프 — 입니다. 루프가 잘못되는 방식은 그것 말고도 더 있으며, 대부분의 경우 극적인 일은 아무것도 일어나지 않습니다. 프로세스가 멈춰 있는 것도 아니고, CPU가 치솟는 것도 아닙니다. 루프는 그저 조용히, 한 바퀴 또 한 바퀴, 일을 틀린 방식으로 해 나갈 뿐입니다.
이 레슨에서는 "폭주하는 루프"라는 막연한 표현을 보는 순간 이름 붙일 수 있는 네 가지 형태로 쪼갭니다. 에러의 누적 증폭, 컨텍스트 비대화와 부패, 데드 루프와 공회전, 예산 소진입니다. 넷 다 뿌리는 같은 곳으로 이어집니다. 에이전트는 다음 한 수를 스스로 결정하는 시스템이고, 그 자율성이야말로 유용한 이유이자 궤도를 벗어나는 이유이기 때문입니다. Anthropic은 이를 직설적으로 이렇게 씁니다. "The autonomous nature of agents means higher costs, and the potential for compounding errors."1 (에이전트의 자율적 성질은 더 높은 비용과 에러가 누적 증폭할 가능성을 뜻한다.) 그리고 "The LLM will potentially operate for many turns, and you must have some level of trust in its decision-making."1 (LLM은 여러 턴에 걸쳐 동작할 수 있으므로, 그 의사결정을 어느 정도는 신뢰해야 한다.) 신뢰는 방임과 같지 않습니다. 이 레슨이 다루는 것은 신뢰 너머에 여러분이 루프에 마련해 주어야 할 것 — 폭주하는 방식마다 하나씩 붙이는 폴백 게이트입니다.
시작하기 전에 하나 짚고 넘어가겠습니다. 그러지 않으면 이후를 잘못 읽게 됩니다. 이 넷은 확률 낮은 사고가 아닙니다. 루프라는 구조에 미리 심어진 경향입니다. 루프를 내버려 두면 기본값으로 이 방향으로 미끄러집니다. 그래서 폴백은 사고가 난 뒤에 붙이는 패치가 아니라, 처음부터 용접해 두는 난간입니다. 하나씩 보겠습니다.
폭주 1: 에러의 누적 증폭 — 한 번의 잘못된 수가 이후의 모든 수를 오염시킨다
가장 교활한 것부터 시작하겠습니다. 이것은 에러를 한 번도 던지지 않기 때문입니다. 레슨 2의 온콜 봇을 떠올려 보세요. 서비스를 재시작하고, 로그를 읽고, 보고한다. 이 세 단계는 사슬로 엮여 있습니다. 두 번째 단계는 첫 번째 단계의 결과에 의존하고, 세 번째 단계는 두 번째 단계에 의존합니다. 이제 첫 번째 단계가 조용히 잘못됐다고 해 봅시다. 모델이 서비스 이름을 api-staging으로 읽고, 프리프로덕션 환경 쪽을 재시작해 버립니다. 도구는 "재시작 성공"이라고 반환하고, 모델은 그것을 액면 그대로 받아들입니다. 이어서 로그를 읽고, 새 에러가 없는 것을 보고는, 신이 나서 "재시작 완료, 로그는 깨끗합니다"라고 보고합니다. 도구 호출은 전부 성공했습니다. 예외는 하나도 올라오지 않았습니다. 그런데도 두 번째 단계부터는 통째로 잘못된 토대 위에 쌓여 올라갔고, 한 단계씩 더 벌어졌습니다.
그것이 에러의 누적 증폭입니다. 에이전트가 자율적으로 돈다는 것은 실수가 루프를 돌며 쌓이고 증폭된다는 뜻입니다1. 이것은 레슨 2의 "버그 있는 코드가 무한 루프를 일으키는" 경우와는 다른 종류의 동물입니다. 그쪽은 한눈에 짚어낼 수 있는 기계적 결함입니다. 에러의 누적 증폭은 의사결정 층위에서 일어나며, 개별 단계 하나하나는 완벽히 합리적으로 보이는데도 실수가 사슬을 따라 복리로 불어납니다. 루프가 많은 턴을 돌수록, 사슬이 길어질수록, 초반의 작은 밀림이 끝에 가서 얼마나 멀리 데려갔을지 예측하기가 더 어려워집니다.
폴백은 두 조각이 함께 맞물려 작동합니다. 첫째, 체크포인트입니다. 사슬에서 "여기가 틀리면 이후 전부가 헛수고다" 싶은 고리에서 멈추고, 루프가 중간 상태를 검증받도록 드러내게 합니다. Anthropic은 이런 종류의 멈춤을 직접 서술합니다. "Agents can then pause for human feedback at checkpoints or when encountering blockers."1 (그러면 에이전트는 체크포인트에서, 또는 막힌 지점을 만났을 때 사람의 피드백을 위해 멈출 수 있다.) 그 멈춤 지점을 어떻게 설계하는지는 레슨 5에서 다룹니다. 둘째, 조기 정지입니다. 이미 밀리기 시작한 루프가 예산을 전부 태우게 두는 대신, 무언가 이상해 보이는 순간 멈춥니다. 그것이 바로 레슨 3의 명시적 정지 조건이 하는 일이고, "it's also common to include stopping conditions (such as a maximum number of iterations) to maintain control."1 (제어를 유지하기 위해 정지 조건(예: 최대 반복 횟수)을 넣는 것도 흔하다.)라는 일반적 조언과도 통합니다. 핵심 발상은 이것입니다. 에러는 턴 수에 따라 쌓이므로, 긴 사슬을 검증 없이 돌게 두지 말라는 것입니다.
폭주 2: 컨텍스트 비대화와 부패 — 히스토리가 매 턴 무거워진다
레슨 2는 빚을 하나 심어 두었습니다. 루프를 한 바퀴 돌 때마다 그 messages.push 줄이 히스토리에 메시지 두 개를 더 쑤셔 넣습니다. 추가만 하고 결코 빼지 않습니다. 그때 우리는 "나중에 정산하자"고 했습니다. 지금이 그 나중입니다.
먼저 "비대화"부터. "An agent running in a loop generates more and more data that could be relevant for the next turn of inference,"2 (루프에서 도는 에이전트는 다음 추론 턴에 관련될 수 있는 데이터를 점점 더 많이 만들어 낸다.) 그리고 매 턴 그 데이터는 다음 요청으로 손대지 않은 채 실려 갑니다. 세 턴이나 다섯 턴 도는 작업이라면 아무도 신경 쓰지 않습니다. 하지만 루프가 수십 턴을 필요로 하게 되면 히스토리가 눈덩이처럼 불어납니다. 요청 하나하나가 점점 더 긴 컨텍스트를 끌고 다니게 되어, 요청은 느려지고 비싸집니다. 그것이 눈에 보이는 즉각적 비용입니다.
"부패"는 더 고약한 쪽입니다. 청구서만이 아니라 품질을 손상시키기 때문입니다. "LLMs have an 'attention budget' that they draw on when parsing large volumes of context," (LLM에는 많은 양의 컨텍스트를 파싱할 때 끌어다 쓰는 '어텐션 예산'이 있고,) 그리고 "Every new token introduced depletes this budget by some amount."2 (새로 들어오는 토큰마다 이 예산을 얼마간 소모시킨다.) 그 결과가 "as the number of tokens in the context window increases, the model's ability to accurately recall information from that context decreases."2 (컨텍스트 윈도우의 토큰 수가 늘수록, 모델이 그 컨텍스트에서 정보를 정확히 떠올리는 능력이 떨어진다.)입니다. 이것이 사람들이 흔히 컨텍스트 부패라고 부르는 것입니다. 히스토리에서 삭제된 것은 아무것도 없지만, 핵심 사실이 노이즈에 잠겨 버려 모델은 그것을 보면서도 붙잡지 못합니다. 루프가 오래 돌수록, 두 번째 턴에서 나온 그 하나의 결정적 지시나 제약에 대한 장악력이 느슨해지고, 행동이 밀리기 쉬워집니다.
여기서 규모 감각을 바로잡아야 합니다. 그러지 않으면 엉뚱한 것을 막게 됩니다. 이 저하는 길이에 따라 완만하게 내려가는 성능 곡선이지, 어떤 임계를 넘으면 떨어지는 절벽이 아닙니다. "These factors create a performance gradient rather than a hard cliff."2 (이 요인들은 단단한 절벽이 아니라 성능의 기울기를 만든다.) "컨텍스트가 일단 너무 커지면 전부 망한다"로 읽지 마세요. 물이 차오르고 배가 가라앉는 것에 더 가깝습니다. 어느 수위에서도 여전히 항해할 수 있고, 다만 점점 힘겨워질 뿐입니다. 그 사실을 받아들이는 것이 올바른 자세로 이끕니다. "context, therefore, must be treated as a finite resource with diminishing marginal returns,"2 (따라서 컨텍스트는 한계 수익이 체감하는 유한한 자원으로 다뤄야 한다.) 그리고 히스토리에 더하는 조각마다 "이게 아직도 그만한 값어치가 있나?"에 답할 수 있어야 합니다.
폴백은 컨텍스트 거버넌스입니다. 루프가 만들어 내는 히스토리에 무작정 push하는 대신 능동적으로 관리하는 것입니다. 구체적 기법 — 오래된 턴을 압축하고, 초기 결과를 요약하고, 더는 쓸모없는 중간 산출물을 버리는 것 — 은 이 시리즈의 코스 5 "에이전트 메모리와 상태"가 통째로 다루는 주제이므로 여기서 다시 열지 않겠습니다. 루프의 관점에서 가져가야 할 것은 이것입니다. 컨텍스트를 다스리는 것은 루프의 턴당 비용에 브레이크를 거는 방법이고, 그래서 오래 도는 루프가 갈수록 멍청해지지 않게 합니다.
폭주 3: 데드 루프와 공회전 — 아무 데도 못 가면서 멈추지도 않는다
앞의 두 실패 모드에서 루프는 적어도 앞으로는 나아갑니다. 삐뚤빼뚤하거나 굼뜨긴 해도 말입니다. 이번 것은 더 노골적입니다. 루프가 앞으로 나아가지 않으면서, 멈추지도 않습니다. 두 얼굴을 하고 있습니다.
하나는 데드 루프입니다. 레슨 2의 레벨 2 연습에서 이미 만났습니다. 루프 본문 끝에서 response를 다시 대입하는 것을 잊어, stop_reason이 옛 값에 얼어붙은 채로 있고, while 조건이 영영 참이라, 프로세스가 멈춰 서고, 같은 도구가 몇 번이고 호출됩니다. 이것은 코드의 순수한 기계적 결함입니다. 호스트 코드가 틀린 것이고, 모델의 결정과는 아무 상관이 없으며, 빠진 재요청 줄을 더하면 고쳐집니다.
다른 얼굴은 더 교활하고 공회전(라이브락이라고도 합니다)이라 불립니다. 코드는 완전히 옳고, 매 턴 정당하게 요청을 보내고, 도구를 돌리고, 새로운 stop_reason을 읽습니다 — 그런데도 새롭게 이루어지는 것이 하나도 없습니다. 전형적인 형태는 이렇습니다. 모델이 같은 검색 도구를 몇 번이고 호출하고, 매번 거의 똑같은 빈 결과를 받고, 접근을 바꾸지 않고, 다음 턴에 같은 것을 검색합니다. stop_reason으로 판단하면 이 루프는 완벽히 건강해 보입니다. 늘 tool_use이고, 늘 정상적으로 돌아갑니다. 작업으로 판단하면, 제자리걸음입니다. 턴과 토큰을 원을 그리며 태우면서 아무것도 진척시키지 못합니다.
데드 루프는 코드를 고치면 고쳐집니다. 공회전은 아닙니다. 코드는 멀쩡하고, 고쳐서 빠져나올 버그가 없습니다. 공회전에는 하드 게이트 두 개가 필요합니다.
- 최대 턴 수 상한(레슨 3의 게이트): 루프에 반복 횟수의 절대 천장을 주고, 턴이 아무리 "건강해" 보여도 천장에서 멈춥니다. 이것이 마지막 퓨즈이며, 루프가 제자리에서 아무리 세게 헛돌아도 N턴을 넘겨 돌 수는 없도록 보장합니다.
- 무진행 감지: 공회전을 위해 특별히 만든 게이트입니다. 발상은 호스트가 실제로 새로운 일이 일어나고 있는지 지켜보게 하는 것입니다. 최근 몇 번의 도구 호출의 이름과 결과를 기록해 두고, N턴 연속으로 같은 도구를 쓰면서 거의 똑같은 출력을 들고 오면, 그것을 "무진행"이라 부르고 루프를 빠져나옵니다. 이것은 최대 턴 수 상한보다 일찍 뭅니다. 50턴을 다 태울 필요 없이, 세 번째 턴부터 시작되는 반복을 잡을 수 있습니다.
두 게이트의 분업은 이렇습니다. 무진행 감지는 헛도는 것을 일찍 알아채는 일을 맡고, 최대 턴 수 상한은 아무것도 알아채지 못했을 때조차 절대 천장을 지키는 일을 맡습니다. 어느 쪽도 모델이 제정신을 차리기를 기다리지 않습니다. 공회전이 존재하는 이유 자체가 모델이 제정신을 차리지 않는다는 것이므로, 경계는 루프 바깥의 호스트 코드가 그어야 합니다.
폭주 4: 예산 소진 — 더 많은 턴 곱하기 더 큰 컨텍스트, 그리고 청구서가 폭주한다
마지막 것은 가장 이해하기 쉽고 가장 아픕니다.
앞의 세 실패 모드는 결국 언젠가 돈으로 나타나는 경향이 있습니다. 에이전트의 자율성은 더 높은 비용을 뜻하고1, 모델은 여러 턴을 연달아 돌 수 있으며1, 그 매 턴의 컨텍스트는 계속 자랍니다 — 턴 수 곱하기 턴당 토큰, 둘 다 오르므로, 청구서는 곱셈으로 올라갑니다. 공회전에 빠진 에이전트는 여러분이 안 보는 사이 밤새 상당한 API 지출을 태우고도 아무것도 끝내지 못할 수 있습니다.
여기서의 위험은 돈만이 아닙니다. 많은 턴을 돈다는 것은 모델의 의사결정을 어느 정도는 신뢰해야 한다는 뜻이고1 — 그 신뢰를 천장 없이 내주고 싶지는 않을 것입니다.
폴백: 예산 상한. 루프에 명시적인 예산 천장을 주되, 두 방식 중 하나로 잽니다.
- 턴으로. 가장 단순한 형태이고, 이것이 바로 레슨 3의 최대 턴 수 상한입니다 — 그 상한도 예산입니다.
- 토큰으로(또는 돈으로). 실제 비용에 더 가깝습니다. 누적 소비 토큰(또는 추정 지출)이 천장에 닿는 순간 즉시 멈춥니다.
요점은 멈추는 것만이 아니라 정직하게 보고하는 것입니다. 천장에서 멈추고 분명히 말합니다. 예산이 다해서 멈췄고, 작업은 끝나지 않았으며, 여기까지 왔다고. 최악의 결과는 예산을 전부 태우고 나서 완결된 결과를 낸 척하는 것입니다 — 그러면 실패 모드 1의 에러 문제로 곧장 되돌아갑니다. 정직하게 멈추는 것이야말로 사람이 뒤를 어떻게 이어받을지 알게 하는 길입니다.
네 게이트를 한자리에
네 실패 모드를 되돌아보면 기원을 공유한다는 것을 알아챌 것입니다. 루프에 경계가 없고, 모델은 그 경계를 알아채지 못하거나 스스로를 그것에 붙들어 두지 못한다.
- 에러의 누적 증폭 → 체크포인트와 조기 정지, 실수가 아직 작을 때 끊어낸다
- 컨텍스트 비대화와 부패 → 컨텍스트 거버넌스, 루프가 매 턴 지고 가는 짐을 덜어낸다
- 데드 루프 → 코드를 고친다(빠진 재대입을 더한다); 공회전 → 무진행 감지와 최대 턴 수 상한, 모델을 대신해 종료를 선언한다
- 예산 소진 → 예산 상한과 정직한 보고, 비용에 천장을 씌운다
이 네 게이트는 있어도 그만인 장식이 아닙니다. "자율"이 "제어 불능"을 뜻하지 않게 하는 조건입니다. 폴백 없는 루프는 잘 돌 때는 예뻐 보이지만, 한 턴이 어긋나는 순간 스스로를 끌어당길 메커니즘이 없어 — 작은 문제를 큰 사고로 굴려 갈 뿐입니다.
반대 극단도 조심하세요. 이 메커니즘들 자체가 복잡성이고, "you should consider adding complexity only when it demonstrably improves outcomes."1 (복잡성은 그것이 결과를 눈에 띄게 개선함이 증명될 때에만 더하는 것을 고려해야 한다.) 장난감 루프마다 게이트 한 무더기를 볼트로 죄지 마세요. 시험은 늘 같습니다. 이 루프가 폭주하면 얼마나 비싼가? 비용이 클수록 게이트 세트는 더 완전해야 합니다.
게이트가 들어가고 나면 다음 질문이 저절로 떠오릅니다. 게이트가 루프를 멈추거나, 루프가 스스로 판단할 수 없는 갈림길에 닿았을 때, 사람은 어떻게 끼어들어 중단하고, 방향을 바로잡고, 넘겨받을까요? 그것이 레슨 5입니다.
정리
- 루프는 스스로 종료를 선언하지 않는다.
while은 제 조건만 알 뿐이고, 모델은 그저 요청을 하나 더 쏘아 댈 것이다. 경계는 하네스가 붙들어야 한다.
- 에러의 누적 증폭 — 이번 턴 모델 입력의 대부분은 지난 턴 자신의 출력이라, 한 번의 잘못된 수가 이후 모든 턴에 실려 증폭된다. 여기에 여러 턴을 돌 수 있다는 사실이 겹치면, 자율성은 더 높은 비용과 에러의 누적 증폭 가능성을 함께 데려온다1. 게이트는 체크포인트와 조기 정지다.
- 컨텍스트 비대화와 부패 — 루프 안 데이터는 오직 쌓이기만 하고2, 새 토큰마다 어텐션 예산을 소모하며2, 토큰이 쌓일수록 회상이 나빠지고, 컨텍스트는 한계 수익이 체감하는 유한 자원이다2. 저하는 절벽이 아니라 기울기다. 게이트는 컨텍스트 거버넌스("에이전트 메모리와 상태"의 히스토리 관리 도구함)다.
- 데드 루프와 공회전은 서로 다른 것이다. 데드 루프는 호스트 코드의 버그(가령
response 재대입을 잊음)이고 코드를 고치면 낫는다. 공회전은 코드가 옳은데 모델이 성공 못 할 같은 도구를 반복 호출하며 아무 데도 못 가는 것이고, 무진행 감지(N턴 새 결과가 없으면 멈춤)에 최대 턴 수 상한을 퓨즈로 겹쳐 다룬다. 무진행 감지를 데드 루프에 겨누지 말고, 코드 수정이 공회전을 낫게 하리라 기대하지도 마라.
- 예산 소진 — 턴 수와 턴당 컨텍스트가 곱해져 비용을 밀어 올린다1. 게이트는 토큰이나 턴으로 세는 예산 상한이며, 천장에서 멈추고 진척을 정직하게 보고한다.
- 네 게이트는 기원 하나를 공유한다. 루프에 경계가 없고 모델은 스스로를 경계에 붙들어 두지 못한다. 이들은 장식이 아니라 자율성이 폭주로 미끄러지는 것을 막는 것이다 — 다만 결과를 눈에 띄게 개선함이 증명될 때에만 더하라1.
>> 레슨 5: 개입과 조향: 중단, 방향 전환, 휴먼 인 더 루프