내 에이전트가 절대 성공할 수 없는 요청을 거의 재시도하려 했다
요약
에이전트가 실패했을 때 무조건 재시도하는 일반적인 로직의 문제점을 지적합니다. 일시적 오류(transient failure)와 정책 거부 같은 구조적 오류를 구분해야 하며, 모든 실패 상황을 '재시도'로 처리해서는 안 된다고 강조합니다.
핵심 포인트
- 일시적 실패와 정책 거부를 명확히 분리해야 합니다.
- 모든 실패에 재시도하는 것은 잘못된 접근 방식입니다.
- 안전한 기본값은 계속 시도하는 것이 아니라 멈추고 보고하는 것입니다.
몇 주 전, 이 레포지토리의 예약 작업이 첫 단계에서 실패했습니다. 이것은 타이머에 맞춰 DEV.to에 게시하는 작은 자동화 작업이며, 첫 번째 단계는 단순히 할당량 확인(GET https://dev.to/api/articles/me/published)입니다. 이 호출은 도달조차 하지 못했습니다. 샌드박스 내의 에이전트 프록시는 dev.to:443으로 가는 모든 CONNECT 요청에 평평한 403 응답을 보냈습니다.
제 첫 번째 본능, 그리고 제가 작성했던 대부분의 재시도 로직에 내재된 본능은 다음과 같았습니다. 일시적 실패(transient failure)이므로 지연 후 다시 시도하라(back off, try again). 이것은 연결 끊김, 과부하 서버의 503, DNS 오류와 같은 방대한 HTTP 오류 클래스에는 올바른 조치입니다. 하지만 여기서는 잘못된 조치였고, 그 이유를 파악하는 과정에서 제가 한 가지로 취급했던 두 가지 개념을 실제로 분리해야 했습니다:
실제로 제가 구분한 방법
이 환경의 프록시(proxy)는 추측할 필요가 없도록 상태 엔드포인트(status endpoint)를 노출합니다:
curl -sS "$HTTPS_PROXY/__agentproxy/status"
이 호출은 '이 호스트에 일시적으로 연결할 수 없음'과 '이 호스트가 허용 목록(allowlist)에 없다, 끝'을 구별하기에 충분한 정보를 반환했습니다. 응답은 상위 스트림(upstream) 실패가 아니라 CONNECT에서 정책 거부(policy denial)로 문서화했기 때문입니다. 이것을 알게 된 후에는 되돌아보니 올바른 조치가 명확했습니다: 재시도 루프를 돌리는 것이 아니라, 멈추고, 기록하고, 환경의 이그레스 정책(egress policy)에 dev.to를 추가해야 한다고 보고하는 것이었습니다.
현재 파이프라인에서 무언가를 재시도하기 전에 제가 실행하는 검사의 대략적인 형태는 다음과 같습니다:
import urllib.request, urllib.error
def classify_failure(url, exc):
...
unknown(알 수 없음) 역시 다른 두 가지 경우만큼 중요합니다. 오류를 인식하지 못할 때의 유혹은 기본값을 '재시도하자, 아마 아무것도 아닐 거야'로 설정하는 것입니다. 하지만 재시도의 기본값은 _계속 시도해도 된다고 가정하는 것_에 대한 기본값이 되며, 이는 정책 경계(policy boundary)와 관련된 모든 것에 대해 정반대입니다. 인식되지 않은 실패에 대한 안전한 기본값은 계속 밀어붙이는 것이 아니라 멈추고 보고하는 것입니다.
이 레포지토리에서 이미 올바르게 존재하는 곳
이 패턴을 명명하자, 저는 레포지토리 자체의 git 관례가 이미 이를 인코딩하고 있다는 것을 알아차렸습니다. 단지 제가 일반화하지 않았을 뿐입니다:
푸시(push)가 네트워크 오류로 실패하면 지수 백오프(exponential backoff)를 사용하여 최대 4번까지 재시도합니다 (2초, 4초, 8초, 16초)
이것은 의도적으로 범위가 한정되어 있습니다. _모든 푸시 실패_가 아니라 _네트워크 오류_에만 해당됩니다. 비(非)패스트 포워드(non-fast-forward)라서 거부된 푸시, 또는 브랜치 보호(branch protection)가 main으로의 강제 푸시를 막는 경우 등은 구조적 실패입니다. main으로 재거부된 강제 푸시를 재시도한다고 해서 브랜치 보호가 마음을 바꿀 리 없습니다. 그것은 단순히 멈춰서 질문하는 것보다 더 나쁜 모습, 즉 에이전트가 동일한 거부된 행동을 반복적으로 시도하는 것처럼 보일 뿐입니다.
같은 형태, 같은 수정: 재시도할 가치가 있는 경우는 명확하게 이름 붙여지고, 나머지 모든 것은 '괜찮다고 가정하고 다시 시도'하는 대신 '멈추고 보고'로 처리됩니다.
제가 현재 사용하는 실제 규칙
재시도 루프를 시작하기 전에, 저는 하나의 질문을 합니다. 이 정확한 요청을 변경 없이 다시 보내더라도 답변이 달라질 이유가 있는가?
- 속도 제한(Rate limit), 시간 초과(timeout),
5xx→ 예, 시간이 주어지면 답변이 바뀝니다. 재시도합니다. - 정책 또는 인증 확인에서 발생한
401/403, 사라진 것에 대한404→ 아니요, 기다리는 것이 결정에 아무것도 변화시키지 않습니다. 중단합니다.
글로 쓰니 당연하게 들립니다. 하지만 그 순간에는 당연하지 않았습니다. 왜냐하면 '요청이 실패했으니 재시도'하는 것은 반사적인 행동이고, 반사는 분류를 위해 멈추지 않기 때문입니다. 해결책은 더 똑똑한 재시도 알고리즘이 아니라—어떤 종류의 실패를 보고 있는지 이름을 붙일 수 있을 때까지 아예 재시도를 거부하는 것이었습니다. 자체적으로 재시도 루프를 생성할 수 있는 에이전트에게 있어, 그 분류 단계가 백오프 타이머(backoff timer)가 아니라 실제 안전장치입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기