Claude Code Agent Loop Deep Dive (4): 완료된 답변부터 stop_reason의 일곱 가지 의미까지
요약
본 글은 Claude Code Agent Loop의 종료 조건에 대해 심층적으로 다룹니다. 모델 응답에 포함되는 일곱 가지 stop reason 중, 실제 루프 제어는 `stop_reason` 자체보다 메시지 콘텐츠(tool_use 블록 유무)를 기반으로 합니다. 따라서 도구 사용 여부를 판단할 때는 콘텐츠 배열을 스캔하는 것이 가장 신뢰할 수 있는 방법입니다.
핵심 포인트
- Claude Code의 핵심은 stop_reason이 아닌 응답 '콘텐츠' 검사임.
- 도구 사용(tool_use) 블록 유무가 루프 지속 여부의 주요 지표이다.
- `stop_reason`은 주로 오류 및 복구 UX에 활용된다.
- 출력 제한(`max_tokens`) 도달 시, 토큰 증가 후 재시도하거나 명시적 메시지를 추가하여 복구할 수 있다.
앞선 세 개의 글에서는 루프 반복의 메커니즘, 즉 도구 사용 가능 여부, 훅(hooks) 개입 방식, 그리고 여러 호출이 어떻게 스케줄링되는지를 설명했습니다. 이번 글은 루프의 다른 끝부분, 즉 **언제 멈추는가?**에 초점을 맞춥니다.
첫 번째 기사에서 보여준 단순화된 형태는 다음과 같습니다:
while True:
response = call_llm(messages)
if response.has_tool_use:
...
'도구 사용이 없을 때 중단'은 유용한 개요이지만, 전체 이야기는 아닙니다. 모든 모델 응답에는 stop_reason이 포함됩니다: 모델이 완료했는지, 도구를 요청했는지, 출력 제한에 걸렸는지, 거부했는지, 또는 컨텍스트 창을 초과했는지를 나타냅니다? 각 결과는 하네스(harness)에서 다른 응답을 요구할 수 있습니다.
본 게시물에서는 일곱 가지 stop reason, Claude Code가 실제로 신뢰하는 신호, 루프를 유지시키는 복구 경로, 그리고 무한 루프를 방지하는 안전장치에 대해 살펴봅니다.
완전한 stop_reason 목록
Anthropic 응답에는 다음 stop reason 중 하나가 포함될 수 있습니다:
stop_reason | 의미 |
|---|---|
end_turn | 모델이 자신의 응답을 완료했다고 간주하고 사용자에게 턴(turn)을 넘깁니다. |
| ... |
총 일곱 가지 값이 있지만, Claude Code는 이들을 대칭적으로 취급하지 않습니다.
직관에 반하는 부분: 완료 여부는 stop_reason으로 결정되지 않는다
직관적인 루프는 다음과 같을 수 있습니다:
if stop_reason == 'end_turn':
break
elif stop_reason == 'tool_use':
...
하지만 Claude Code는 그렇게 디스패치 결정을 내리지 않습니다. 대신, 어시스턴트 메시지 자체를 검사합니다:
if response.content contains a tool_use block:
execute_tools()
else:
...
왜 그럴까요? 실제로는 stop_reason === 'tool_use'가 메시지 구조의 신뢰할 수 있는 지표가 아닙니다. 응답은 end_turn을 보고하면서도 실제 도구 사용 블록을 포함할 수 있으며, 또는 실제 tool_use 블록 없이 tool_use를 보고할 수도 있습니다.
콘텐츠가 진실의 원천입니다. Claude Code는 콘텐츠 배열을 스캔합니다: tool_use 블록이 있다는 것은 계속 진행한다는 의미이며, 그러한 블록이 없다는 것은 일반적인 완료 경로를 의미합니다. stop_reason은 주로 오류 및 복구 사용자 경험(UX)을 구동하는 데 사용됩니다—예를 들어, 출력이 잘렸거나 모델이 거부했음을 설명하는 경우—단독으로 스케줄링 신호로 사용되는 것이 아닙니다.
각 stop reason 처리 방법
end_turn: 정상 완료
모델이 정상적으로 완료되면 콘텐츠에는 도구 사용(tool use) 내용이 포함되지 않습니다. 루프는 완성된 경로를 가져와 사용자에게 제어권을 반환합니다.
tool_use: 레이블 대신 콘텐츠 확인
Claude Code는 이 레이블에만 의존하지 않습니다. 응답에 도구 사용 블록이 포함되어 있으면, 이를 실행하고 일치하는 결과를 추가한 다음, 또 다른 모델 호출을 시작합니다.
max_tokens: 포기하기 전에 복구
출력 제한에 도달하는 것은 복구 경로가 있는 오류 상태로 처리됩니다:
- 먼저
max_tokens제한을 늘리고 재시도해 봅니다. - 상향 조정 후에도 출력이 계속 제한에 걸리면, 모델이 중단된 작업을 명시적으로 계속하도록
[출력 토큰 제한 도달, 계속]와 같은 사용자 메시지를 추가합니다. - 최대 세 번의 복구 시도만 허용합니다 (
MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3). - 복구가 여전히 실패하면, 문제를 사용자에게 알립니다.
외부 루프의 관점에서 볼 때, 이것은 완료가 아닙니다. 시스템이 이를 복구할 수 있다면 계속되어야 하는 미완성 작업입니다.
model_context_window_exceeded: 압축 후 재시도
컨텍스트가 너무 클 때, Claude Code는 반응형 압축(reactive-compaction) 경로를 시작합니다: 기록을 줄인 다음, 다시 시도합니다. 그 결과는 즉각적인 종료 상태라기보다는 또 다른 계속 진행 분기입니다.
refusal: 보고 후 중지
거부는 다릅니다. Claude Code는 오류 메시지를 구성합니다—종종 /model을 통해 사용자에게 다른 모델을 시도해 보라고 제안하지만—자동으로 재시도하지는 않습니다. 모델의 의도적인 거부는 동일한 요청을 다시 보내서 해결될 가능성이 낮으므로, 루프가 종료됩니다.
stop_sequence: 정상적으로 통과
Claude Code는 이 결과(outcome)를 거의 사용하지 않는데, 이는 일반적으로 사용자 지정 stop sequence를 구성하지 않기 때문입니다. 따라서 위에서 설명한 콘텐츠 검사로 넘어갑니다. 보통 도구 사용이 없으므로 턴(turn)은 완료됩니다.
pause_turn: 이 버전의 처리되지 않은 공백
본 시리즈를 위해 검토된 v2.1.220 코드에서 SDK는 pause_turn을 인식하지만, Claude Code 루프에는 이를 위한 전용 분기(branch)가 없습니다. 이는 특별한 복구 흐름이라기보다는 실제 구현상의 격차입니다.
pause_turn은 예외적으로 길거나 복잡한 턴을 위해 의도되었습니다. Claude Code의 주된 루프는 일반적으로 사용자 요청당 몇 번의 호출로만 구성되는데, 이것이 명시적인 처리가 필요하지 않았던 이유를 설명할 수 있습니다. 만약 장시간 실행되는 추론 워크플로우가 도입된다면, 이 분기에는 의도적인 정책이 필요할 것입니다.
오류는 종료 신호가 아닌 복구 트리거입니다
max_tokens와 context-window 경로는 더 광범위한 설계 원칙을 보여줍니다:
오류는 복구가 가능한 경우 항상 복구를 유발해야 합니다.
max_tokens는 토큰 제한 에스컬레이션, 명시적인 계속 프롬프트(continuation prompt), 그리고 재시도(retry)를 유발합니다.- Context 오버플로우는 압축(compaction)과 재시도를 유발합니다.
- 과부하 또는 속도 제한 조건은 모델 폴백(model fallback)과 재시도를 유발할 수 있습니다.
- 네트워크 실패는 지수 백오프(exponential-backoff) 재시도를 사용할 수 있습니다.
목표는 시스템이 복구할 수 있는 것을 고치고, 사용자에게 진정으로 복구 불가능한 실패만 노출하는 것입니다. 상태 기계(state-machine) 수준에서 볼 때, 각 복구는 단순히 현재 루프 반복에서 다른 전이(transition)일 뿐입니다.
maxTurns: 하드 서킷 브레이커
이렇게 많은 복구 경로가 있음에도 불구하고, 루프가 영원히 실행될 수 있을까요? 이론적으로는 그렇습니다:
- 모델은 진전을 이루지 않으면서 반복적으로 도구를 요청할 수 있습니다.
- 출력 제한 복구는 증가할 때마다 다시 유발될 수 있습니다.
- 압축(Compaction)은 모델이 즉시 컨텍스트를 초과하도록 역사(history)만 줄일 수 있습니다.
Claude Code에는 강력한 안전장치인 **maxTurns**가 있습니다. 이는 사용자가 시작한 루프 내에서 모델 호출 횟수를 계산합니다. 이 횟수가 설정된 한도를 초과하면 시스템이 강제로 종료됩니다.
이러한 경우:
- 메시지 기록에
[max_turns_reached]가 추가됩니다. - SDK는
{ subtype: 'error_max_turns' }결과를 반환합니다. - 사용자는 루프가 안전 한계로 인해 중단되었다는 명확한 표시를 받습니다.
이것은 에이전트 실행이 개입하는 인간의 결정 없이도 여러 번 반복될 수 있기 때문에 중요합니다. maxTurns는 감독(supervision)의 부재가 무한 루프가 되는 것을 방지합니다.
Stop 훅이 최종 거부권을 행사하다
루프가 도구 사용 블록을 보지 못하고 달리 완료될 예정이라 하더라도, 마지막 관문인 Stop hook이 남아 있습니다.
Part 2: Hooks as Programmable Intervention Points에서 다루었듯이, Stop hook은 루프가 끝나기 직전에 실행됩니다. 만약 이 훅이 decision: block을 반환하면, 루프는 종료될 수 없으며 다른 반복을 수행해야 합니다.
이는 다음과 같은 정책들을 지원합니다:
- 테스트가 실행될 때까지 완료하지 않음.
- 관련 변경 사항이 커밋되지 않은 상태에서 완료하지 않음.
따라서 진정한 완료는 세 가지 관문을 통과해야 합니다:
- 콘텐츠에
tool_use블록이 포함되어 있지 않다. - 정지 이유(stop reason)가 복구 경로를 요구하지 않는다.
- Stop hook이 완료를 차단하지 않는다.
그때서야 루프는 completed 상태가 되어 제어권을 사용자에게 반환합니다.
완전한 턴 종료 결정
종합해 보면, 분기들은 다음과 같이 요약할 수 있습니다:
After each model call:
if content contains a tool_use block:
...
따라서 일곱 가지 API 값은 다섯 가지 의미 있는 처리 패턴이 됩니다. stop_sequence는 일반적으로 콘텐츠 검사로 넘어가는 반면, pause_turn은 분석된 버전에서는 전용 루프 레벨 처리가 없습니다. 실제 결정은 콘텐츠, 예외적인 정지 이유(exceptional stop reasons), 턴 수, 그리고 훅의 조합입니다.
주요 내용
- 실질적인 완료 기준은 응답 콘텐츠에
tool_use가 포함되어 있는지 여부이며,stop_reason만으로는 디스패치(dispatch)를 판단하기에 충분하지 않습니다. - Anthropic은 일곱 가지 stop reason을 노출합니다:
end_turn,tool_use,max_tokens,stop_sequence,refusal,pause_turn, 그리고model_context_window_exceeded입니다. max_tokens와 컨텍스트 오버플로우는 즉각적인 종료 신호가 아니라 복구(recovery) 신호입니다.- 거절(refusal)은 자동 재시도 없이 루프를 종료합니다.
- 분석된 Claude Code 버전에서는
pause_turn에 대한 전용 처리가 없습니다. maxTurns는 단일 에이전트 실행 내 모델 호출의 하드 리미트입니다.- Stop hook은 명백한 완료를 거부하고(veto) 또 다른 루프 반복을 강제할 수 있습니다.
다음 글에서는 앞선 네 개의 게시물에서 다룬 승인(approval), 훅(hooks), 병렬 스케줄링, stop-reason 처리, 복구 메커니즘을 하나로 통합하여 QueryEngine의 메인 루프 상태 머신(state machine) 관점으로 보여줍니다.
참고 자료
주요 소스 위치 (v2.1.220):
src/services/api/claude.ts:message_delta분기에서의stop_reason처리.src/services/api/errors.ts:getErrorMessageIfRefusal을 통한 거절 감지.src/query.ts:queryLoop, 턴 카운팅,maxTurns, 그리고 복구 분기.src/query/stopHooks.ts: Stop-hook 차단 로직.src/QueryEngine.ts: SDK의error_max_turns결과 타입.
관련 읽을거리:
– Part 1: Tool 선언부터 사전 실행 승인까지
– Part 2: Programmable Intervention Points로서의 Hooks
– Part 3: 파일 읽기부터 병렬 스케줄링까지
– Anthropic Messages API: stop_reason
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기