LabHub
배우기 러닝패스 코스

AIエージェント — モデルではなくグラフ

カード拒否にも三回ずつ送り直していた

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

노드에 RetryPolicy 를 붙이고, 다시 해도 되는 실패만 다시 하게 만든다. 같은 요청이 두 번 반영되지 않게 멱등 열쇠를 붙이고, 호출 예산을 세어 바닥나면 재시도 대신 포기하되 결과를 남긴다. 판정은 전부 횟수로 한다 — 시간이나 속도는 재지 않는다.

왜 중요한가

재시도는 한 줄로 켜진다. 그래서 켜 놓고 확인하지 않는다. 그런데 RetryPolicy 의 기본 retry_onValueError·TypeError·RuntimeError·OSError 같은 예외를 다시 하지 않는다(이 실습 환경의 langgraph 0.2.60 에서 확인한 사실입니다). 우리가 만드는 예외는 대개 그 계열을 물려받으므로, 정책을 붙여 놓고도 시도 기록에는 한 줄만 남는다. 오류도 경고도 나지 않는다. 반대로 retry_on 을 넓게 잡으면 영구 실패까지 상한까지 반복한다. 카드 거절 한 건에 시도가 세 줄 쌓이고, 그만큼이 정상 건이 쓸 몫을 먹는다. 그래서 "다시 해도 되는 실패" 를 가르는 기준이 필요하다 — 같은 요청을 그대로 다시 보냈을 때 다른 답이 나올 가능성이 있는가. 재시도가 안전하려면 조건이 하나 더 있다. 시간이 지나 끊긴 요청은 응답을 못 받았을 뿐 저쪽에서 이미 처리됐을 수 있다. 그대로 다시 보내면 두 번 청구된다. 요청마다 열쇠를 붙이고 받는 쪽이 그 열쇠로 "이미 한 건인가" 를 봐야 한다. 마지막으로 재시도는 그대로 호출 예산을 먹는다. 앞 건이 흔들리기만 해도 뒤 건은 시도조차 못 한다. 예산은 건수가 아니라 도구 몸통이 돈 횟수로 세고, 바닥났을 때는 예외를 올리는 대신 결과 기록의 한 줄로 남긴다. 채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 열쇠·금액·실패 계획으로 돌리고, ATTEMPTS 에 쌓인 줄 수와 원장에 반영된 금액을 채점기가 따로 계산한 값과 대조한다.

단계

  1. /root/work/agbudget/budget.pyMAX_ATTEMPTS·TransientError·PermanentError·ATTEMPTS·LEDGER·PLAN·reset()·applied_total()·charge()·State·build_plain()·run_once() 를 만드세요. 재시도가 없으므로 실패하면 시도는 한 번뿐입니다.
  2. DEFAULT_RETRY·build_default_retry()·run_default() 를 더하세요. retry_on 을 적지 않은 RetryPolicy 를 노드에 붙였는데도 시도가 여전히 한 번인 것을 확인합니다.
  3. RETRY_ALL·build_retry_all()·run_retry_all() 을 더하세요. retry_on=(ValueError,) 를 명시하면 일시 실패가 되살아나고, 상한을 넘기면 마지막 예외가 그대로 올라옵니다.
  4. is_retryable(exc)·RETRY_SPLIT·build_split()·run_split() 을 더해 영구 실패를 다시 하지 않게 하세요. 영구 실패의 시도 횟수가 MAX_ATTEMPTS 에서 1 로 줄어야 합니다.
  5. charge() 에 멱등 열쇠를 넣으세요. 같은 request_key 로 두 번 불려도 원장에는 한 줄만 남고, 두 번째 호출은 그때 그 영수증을 duplicate 표시와 함께 돌려줍니다.
  6. BudgetExhausted·call_with_budget()·build_budgeted()·run_budgeted() 를 더하세요. 예산이 남지 않으면 도구를 부르지 않고, 포기하더라도 outcome"예산초과" 인 기록을 남깁니다.
  7. settle_batch(orders, budget) 를 더해 여러 건이 예산 하나를 나눠 쓰게 하세요. 예산이 바닥난 뒤의 건은 skipped 로 남깁니다.
  8. /root/work/agbudget/budget_report.json/root/work/agbudget/budget_report.md 에 재어 본 횟수를 기록하세요.

참고

재시도가 없으면 시도는 한 번뿐이다

/root/work/agbudget/budget.pyMAX_ATTEMPTS·TransientError·PermanentError·ATTEMPTS·LEDGER·PLAN·reset()·applied_total()·charge()·State·build_plain()·run_once() 를 만드세요. build_plain() 은 재시도가 붙지 않은 그래프를 compile() 해 돌려줍니다.

charge 는 부를 때마다 ATTEMPTS 에 한 줄을 먼저 덧붙인 뒤 PLAN 을 봅니다 — 실패한 시도도 세야 하기 때문입니다. 노드는 실패를 삼키지 말고 예외를 그대로 내보내세요. 노드 안에서 try/except 로 잡아 버리면 재시도가 아예 걸리지 않습니다. run_onceinvoketry/except 로 감싸 예외를 기록으로 바꿉니다. 노드 이름과 상태 열쇠가 겹치면 컴파일이 죽으니 서로 다르게 두세요.

정책을 붙였는데 왜 한 번만 하는가

DEFAULT_RETRY = RetryPolicy(max_attempts=MAX_ATTEMPTS, initial_interval=0.01, backoff_factor=1.0, jitter=False)build_default_retry()·run_default() 를 더하세요. retry_on적지 않습니다. 일시 실패 한 건을 돌려도 시도가 여전히 1회인 것을 확인합니다.

from langgraph.types import RetryPolicy 를 쓰고 add_node(..., retry=DEFAULT_RETRY) 로 붙입니다. 이 단계는 일부러 안 되는 것을 재현하는 것입니다 — 기본 retry_onValueError 계열을 다시 하지 않습니다. 채점기는 노드에 정책이 실제로 붙었는지(compile() 한 그래프의 nodes["settle"].retry_policy)와 시도 횟수를 함께 봅니다. 그러니 build_plain() 을 그대로 돌려주면 안 됩니다.

다시 할 것을 직접 적는다

RETRY_ALL = RetryPolicy(retry_on=(ValueError,), max_attempts=MAX_ATTEMPTS, ...)build_retry_all()·run_retry_all() 을 더하세요. 일시 실패 n회 뒤에 성공하면 시도는 n+1회이고, MAX_ATTEMPTS 를 넘기면 마지막 예외가 그대로 올라옵니다.

retry_on 에는 예외 클래스의 튜플을 줍니다 — 클래스 하나만 줄 때도 쉼표를 빠뜨리지 마세요. max_attempts 는 총 시도 횟수라서 3 이면 처음 한 번에 다시 두 번입니다. 상한을 다 쓰고도 실패하면 재시도했다는 표시가 붙지 않은 마지막 예외가 그대로 나옵니다. 그 흔적은 ATTEMPTS 를 세어 직접 남겨야 합니다.

영구 실패를 세 번 보내지 않는다

is_retryable(exc) 를 만들고 RETRY_SPLIT = RetryPolicy(retry_on=is_retryable, ...)·build_split()·run_split() 을 더하세요. 영구 실패의 시도 횟수가 MAX_ATTEMPTS 에서 1 로 줄어야 합니다.

retry_on 에는 (예외) -> bool 함수도 줄 수 있습니다. 가르는 기준은 하나입니다 — 같은 요청을 그대로 다시 보내면 다른 답이 나올 가능성이 있는가. 다시 해도 되는 종류만 참으로 두고 모르는 예외는 거짓으로 두세요. 참을 기본값으로 삼으면 영구 실패가 새 이름으로 올 때마다 조용히 새어 나갑니다. 채점기는 같은 영구 실패를 run_retry_allrun_split 으로 각각 돌려 시도 횟수를 견줍니다.

두 번 와도 한 번만 반영한다

charge() 가 같은 request_key 를 두 번 받으면 원장에 새 줄을 만들지 않고 그때 그 영수증duplicate 가 참인 채로 돌려주게 하세요. 원장 줄 수와 applied_total() 이 두 배가 되면 안 됩니다.

열쇠로 원장을 먼저 훑어 이미 있는지 봅니다. seq 도 그대로여야 합니다 — 새 번호를 매기면 같은 건이 두 건으로 보입니다. 실패를 내는 자리보다 에서 찾아야 합니다. 열쇠는 부르는 쪽이 정한 값이라는 점이 중요합니다. 채점기는 서로 다른 두 건과 그중 하나의 중복 전달을 섞어 넣고, 원장 줄 수·합계·seq 를 함께 봅니다.

바닥나면 포기하되 기록을 남긴다

BudgetExhausted·call_with_budget(request_key, amount, budget)·build_budgeted()·run_budgeted(request_key, amount, budget) 를 더하세요. len(ATTEMPTS)budget 이상이면 도구를 부르지 않고, 그 경우 outcome"예산초과" 인 기록을 돌려줍니다.

예산은 건수가 아니라 도구 몸통이 돈 횟수입니다. BudgetExhaustedis_retryable 이 참으로 보지 않게 두세요 — 예산이 없어서 난 실패를 다시 해 봐야 소용없습니다. 예산 검사는 charge 를 부르기 이어야 시도가 한 칸도 더 늘지 않습니다. run_budgeted 는 예외를 밖으로 내보내지 말고 outcome 이 담긴 기록으로 바꾸세요.

앞 건의 재시도가 뒤 건의 몫을 먹는다

settle_batch(orders, budget) 를 더하세요. 여러 건이 예산 하나를 나눠 쓰고, 예산이 남지 않으면 남은 건은 도구를 부르지 않고 skipped 에 담습니다. 돌려주는 값은 {"done", "failed", "skipped", "calls", "applied", "budget"} 입니다.

건마다 reset() 을 부르면 안 됩니다 — 예산은 배치 전체가 나눠 쓰는 것이라 ATTEMPTS 가 이어져야 합니다. 건을 시작하기 전에도 남은 예산을 한 번 보고, 없으면 run_budgeted 를 아예 부르지 마세요. done·failed·skipped 에는 request_key 만 순서대로 담습니다. 채점기는 같은 규칙을 따로 계산해 목록과 calls 를 대조합니다.

재어 본 횟수로 기록한다

/root/work/agbudget/budget_report.jsonmax_attempts·no_retry_attempts·default_policy_attempts·retry_all_permanent_attempts·split_permanent_attempts·split_transient_attempts·duplicate_delivery_entries·batch_budget·batch_calls·batch_done·batch_skipped·batch_applied 를, /root/work/agbudget/budget_report.md## 무엇을 다시 해도 되는가 ## 상한을 어디에 두었나 ## 같은 요청이 두 번 와도 안전한 이유 ## 예산이 바닥났을 때 무엇을 남기는가 네 절로 쓰세요.

숫자는 손으로 적지 말고 여러분의 모듈을 실제로 돌려 얻은 값으로 채우세요. split_transient_attempts 는 일시 실패 2회 뒤 성공했을 때의 시도 횟수이고, duplicate_delivery_entries 는 같은 열쇠로 두 번 보낸 뒤 원장에 남은 줄 수입니다. 배치는 지시문 참고 절이 정한 orders·plan·budget 그대로 돌리세요. 채점기는 같은 것을 따로 계산해 대조합니다.