AI 에이전트 — 모델이 아니라 그래프 · 실패·재시도·예산 · 실습
카드 거절에도 세 번씩 다시 보냈다
목표
노드에 RetryPolicy 를 붙이고, 다시 해도 되는 실패만 다시 하게 만든다. 같은 요청이 두 번 반영되지 않게 멱등 열쇠를 붙이고, 호출 예산을 세어 바닥나면 재시도 대신 포기하되 결과를 남긴다. 판정은 전부 횟수로 한다 — 시간이나 속도는 재지 않는다.
왜 중요한가
재시도는 한 줄로 켜진다. 그래서 켜 놓고 확인하지 않는다. 그런데 RetryPolicy 의 기본 retry_on 은 ValueError·TypeError·RuntimeError·OSError 같은 예외를 다시 하지 않는다(이 실습 환경의 langgraph 0.2.60 에서 확인한 사실입니다). 우리가 만드는 예외는 대개 그 계열을 물려받으므로, 정책을 붙여 놓고도 시도 기록에는 한 줄만 남는다. 오류도 경고도 나지 않는다.
반대로 retry_on 을 넓게 잡으면 영구 실패까지 상한까지 반복한다. 카드 거절 한 건에 시도가 세 줄 쌓이고, 그만큼이 정상 건이 쓸 몫을 먹는다. 그래서 "다시 해도 되는 실패" 를 가르는 기준이 필요하다 — 같은 요청을 그대로 다시 보냈을 때 다른 답이 나올 가능성이 있는가.
재시도가 안전하려면 조건이 하나 더 있다. 시간이 지나 끊긴 요청은 응답을 못 받았을 뿐 저쪽에서 이미 처리됐을 수 있다. 그대로 다시 보내면 두 번 청구된다. 요청마다 열쇠를 붙이고 받는 쪽이 그 열쇠로 "이미 한 건인가" 를 봐야 한다.
마지막으로 재시도는 그대로 호출 예산을 먹는다. 앞 건이 흔들리기만 해도 뒤 건은 시도조차 못 한다. 예산은 건수가 아니라 도구 몸통이 돈 횟수로 세고, 바닥났을 때는 예외를 올리는 대신 결과 기록의 한 줄로 남긴다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 열쇠·금액·실패 계획으로 돌리고, ATTEMPTS 에 쌓인 줄 수와 원장에 반영된 금액을 채점기가 따로 계산한 값과 대조한다.
단계
1. /root/work/agbudget/budget.py 에 MAX_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.py를 파이썬 모듈로 불러MAX_ATTEMPTS·ATTEMPTS·LEDGER·PLAN·reset·charge·applied_total·TransientError·PermanentError·build_plain·run_once와 뒤 단계에서 더하는 이름들을 직접 씁니다. 스크립트로 실행하지 않으므로if __name__ == "__main__"은 없어도 됩니다. reset(plan)은ATTEMPTS·LEDGER를 비우고PLAN을plan으로 바꿉니다.plan은{request_key: 남은 실패 횟수}또는{request_key: "permanent"}입니다. 채점기가 매번 다른plan을 넣습니다.charge(request_key, amount)는 부를 때마다ATTEMPTS에request_key를 한 줄 덧붙입니다.PLAN이 정수 n(n 이 0보다 큼)이면TransientError를 내고 n 을 하나 줄이며,"permanent"면PermanentError를 냅니다. 그 밖에는 원장에{"request_key", "amount", "seq"}를 한 줄 남기고duplicate를 붙여 돌려줍니다.seq는 1부터 하나씩 올라갑니다.run_once·run_default·run_retry_all·run_split은 모두{"ok", "error", "attempts", "receipt", "outcome"}를 돌려줍니다.attempts는len(ATTEMPTS)이고,error는 실패했을 때 예외 클래스 이름(예:"TransientError"), 성공이면 빈 문자열입니다. 이 함수들은reset()을 부르지 않습니다 — 초기화는 부르는 쪽이 합니다.run_budgeted(request_key, amount, budget)는 위 열쇠에 더해budget과applied를 담고,outcome이"완료"·"예산초과"·"실패"중 하나입니다.settle_batch(orders, budget)의orders는[(request_key, amount), ...]이고 돌려주는 값은{"done", "failed", "skipped", "calls", "applied", "budget"}입니다.done·failed·skipped에는request_key만 순서대로 담습니다.MAX_ATTEMPTS는 3 으로 둡니다. 총 시도 횟수이지 추가 재시도 횟수가 아닙니다.- 대기 시간은
initial_interval=0.01, backoff_factor=1.0, jitter=False로 아주 작게 두세요. 기본값(0.5초에서 두 배씩)이면 실습이 느려집니다. 채점기는 시간을 재지 않고 횟수만 봅니다. RetryPolicy의retry_on에는 예외 클래스의 튜플도,(예외) -> bool함수도 줄 수 있습니다.- 8단계 기록에 쓸 배치 상황은 이렇게 고정합니다:
orders = [("A-1", 1500), ("A-2", 2300), ("A-3", 900)],plan = {"A-1": 2},budget = 4. - 이 파드에는 인터넷이 없습니다.
pip install은 되지 않습니다. langgraph 0.2.60 이 이미 들어 있습니다(python3 -c "import langgraph"). - 공식 문서: [Types 레퍼런스](https://reference.langchain.com/python/langgraph/types/) · [Graph API overview](https://docs.langchain.com/oss/python/langgraph/graph-api) · [Use the graph API](https://docs.langchain.com/oss/python/langgraph/use-graph-api)
- 흔한 실수:
retry_on을 적지 않고 재시도가 될 것이라 믿기,max_attempts를 추가 재시도 횟수로 알기, 노드 안에서try/except로 예외를 삼켜 재시도가 아예 걸리지 않게 하기, 멱등 열쇠를 받는 쪽에서 새로 만들기, 예산을 건수로 세기.
단계 8개
- 재시도가 없으면 시도는 한 번뿐이다
- 정책을 붙였는데 왜 한 번만 하는가
- 다시 할 것을 직접 적는다
- 영구 실패를 세 번 보내지 않는다
- 두 번 와도 한 번만 반영한다
- 바닥나면 포기하되 기록을 남긴다
- 앞 건의 재시도가 뒤 건의 몫을 먹는다
- 재어 본 횟수로 기록한다