TT Lab
はじめる
学ぶ 学習パス コース

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

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

TT Labで続きを見る

目標

ノードにRetryPolicyを付けて、やり直してよい失敗だけをやり直すようにします。同じリクエストが2回反映されないように冪等キーを付け、呼び出しバジェットを数えて、尽きたらリトライの代わりに断念しつつ、結果を残します。判定はすべて回数で行います。時間や速度は測りません。

なぜ重要なのか

リトライは1行で有効になります。そのため、有効にして確認しません。ところが、RetryPolicyのデフォルトのretry_onは、ValueError・TypeError・RuntimeError・OSErrorのような例外をやり直しません(このラボの環境のlanggraph 0.2.60で確認した事実です)。自分たちが作る例外は、たいていその系統を継承するので、ポリシーを付けても、試行の記録には1行しか残りません。エラーも警告も出ません。 逆に、retry_onを広く取ると、恒久的な失敗まで上限まで繰り返します。カード拒否1件に試行が3行たまり、その分が、正常な案件が使うはずの分を食います。そのため、「やり直してよい失敗」を分ける基準が必要です。同じリクエストをそのまま送り直したとき、別の答えが出る可能性があるか、です。 リトライが安全であるためには、条件がもう1つあります。タイムアウトで切れたリクエストは、レスポンスを受け取れなかっただけで、向こうですでに処理されているかもしれません。そのまま送り直すと、2回請求されます。リクエストごとにキーを付けて、受け取る側がそのキーで「すでに行った件か」を見る必要があります。 最後に、リトライはそのまま呼び出しバジェットを消費します。前の件が不安定になっただけで、後ろの件は試行すらできません。バジェットは件数ではなく、ツールの本体が動いた回数で数え、尽きたときは、例外を上げる代わりに、結果の記録の1行として残します。 採点ツールは、書かれた説明を信用しません。書かれたモジュールを実際に読み込んで、毎回異なるキー・金額・失敗の計画で動かし、ATTEMPTSにたまった行数と、元帳に反映された金額を、採点ツールが別に計算した値と突き合わせます。

ステップ

  1. /root/work/agbudget/budget.pyにMAX_ATTEMPTS・TransientError・PermanentError・ATTEMPTS・LEDGER・PLAN・reset()・applied_total()・charge()・State・build_plain()・run_once()を作成してください。リトライがないので、失敗すると試行は1回だけです。
  2. DEFAULT_RETRY・build_default_retry()・run_default()を追加してください。retry_onを書かないRetryPolicyをノードに付けたのに、試行がやはり1回であることを確認します。
  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で2回呼ばれても、元帳には1行しか残らず、2回目の呼び出しは、そのときのレシートをduplicateの印付きで返します。
  6. BudgetExhausted・call_with_budget()・build_budgeted()・run_budgeted()を追加してください。バジェットが残っていなければツールを呼び出さず、断念する場合も、outcomeが"예산초과"(韓国語で「予算超過」を意味する語です)の記録を残します。
  7. settle_batch(orders, budget)を追加して、複数の件が1つのバジェットを分け合うようにしてください。バジェットが尽きたあとの件は、skippedとして残します。
  8. 測った回数を記録してください(保存先: /root/work/agbudget/budget_report.json、/root/work/agbudget/budget_report.md)。

参考

リトライがなければ試行は1回だけ

/root/work/agbudget/budget.pyにMAX_ATTEMPTS・TransientError・PermanentError・ATTEMPTS・LEDGER・PLAN・reset()・applied_total()・charge()・State・build_plain()・run_once()を作成してください。build_plain()は、リトライが付いていないグラフをcompile()して返します。

chargeは、呼び出すたびに、先にATTEMPTSに1行を追加してからPLANを見ます。失敗した試行も数える必要があるからです。ノードは、失敗を飲み込まず、例外をそのまま出してください。ノードの中でtry/exceptで捕まえてしまうと、リトライがまったくかかりません。run_onceは、invokeをtry/exceptで包んで、例外を記録に変えます。ノード名と状態のキーが重なるとコンパイルが死ぬので、別々にしてください。

ポリシーを付けたのになぜ1回だけなのか

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件を動かしても、試行がやはり1回であることを確認します。

from langgraph.types import RetryPolicyを使い、add_node(..., retry=DEFAULT_RETRY)で付けます。このステップは、わざと動かないことを再現するものです。デフォルトのretry_onは、ValueErrorの系統をやり直しません。採点ツールは、ノードにポリシーが実際に付いているか(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には、例外クラスのタプルを渡します。クラス1つだけを渡すときも、カンマを忘れないでください。max_attemptsは総試行回数なので、3なら最初の1回にやり直し2回です。上限を使い切っても失敗すると、リトライしたという表示が付いていない最後の例外が、そのまま出ます。その痕跡は、ATTEMPTSを数えて、自分で残す必要があります。

恒久的な失敗を3回送らない

is_retryable(exc)を作成して、RETRY_SPLIT = RetryPolicy(retry_on=is_retryable, ...)・build_split()・run_split()を追加してください。恒久的な失敗の試行回数が、MAX_ATTEMPTSから1に減る必要があります。

retry_onには、(예외) -> bool(プレースホルダーは例外です)の関数も渡せます。分ける基準は1つです。同じリクエストをそのまま送り直したとき、別の答えが出る可能性があるか。やり直してよい種類だけを真にして、知らない例外は偽にしてください。真をデフォルトにすると、恒久的な失敗が新しい名前で来るたびに、黙って漏れ出します。採点ツールは、同じ恒久的な失敗をrun_retry_allとrun_splitでそれぞれ動かして、試行回数を比べます。

2回来ても1回だけ反映する

charge()が同じrequest_keyを2回受け取ったら、元帳に新しい行を作らず、そのときのレシートを、duplicateが真のまま返すようにしてください。元帳の行数とapplied_total()が2倍になってはいけません。

キーで元帳を先に走査して、すでにあるかを見ます。seqもそのままである必要があります。新しい番号を振ると、同じ件が2件に見えます。失敗を出す場所より後ろで探す必要があります。キーは、呼び出す側が決めた値だという点が重要です。採点ツールは、異なる2件と、そのうち1つの重複した配信を混ぜて入れ、元帳の行数・合計・seqを一緒に見ます。

尽きたら断念して記録を残す

BudgetExhausted・call_with_budget(request_key, amount, budget)・build_budgeted()・run_budgeted(request_key, amount, budget)を追加してください。len(ATTEMPTS)がbudget以上ならツールを呼び出さず、その場合はoutcomeが"예산초과"(韓国語で「予算超過」を意味する語です)の記録を返します。

バジェットは、件数ではなくツールの本体が動いた回数です。BudgetExhaustedを、is_retryableが真と見ないようにしてください。バジェットがなくて起きた失敗を、やり直しても無駄です。バジェットの検査は、chargeを呼び出す前でなければ、試行が1つも増えません。run_budgetedは、例外を外に出さず、outcomeが入った記録に変えてください。

前の件のリトライが、後ろの件の分を食う

settle_batch(orders, budget)を追加してください。複数の件が1つのバジェットを分け合い、バジェットが残っていなければ、残りの件はツールを呼び出さず、skippedに入れます。返す値は{"done", "failed", "skipped", "calls", "applied", "budget"}です。

件ごとにreset()を呼び出してはいけません。バジェットはバッチ全体で分け合うものなので、ATTEMPTSがつながっている必要があります。件を始める前にも、残りのバジェットを1回見て、なければrun_budgetedをそもそも呼び出さないでください。done・failed・skippedには、request_keyだけを順に入れます。採点ツールは、同じルールを別に計算して、リストとcallsを突き合わせます。

測った回数で記録する

/root/work/agbudget/budget_report.jsonにmax_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に## 무엇을 다시 해도 되는가 ## 상한을 어디에 두었나 ## 같은 요청이 두 번 와도 안전한 이유 ## 예산이 바닥났을 때 무엇을 남기는가の4つの節を書いてください(4つの見出しは、順に韓国語で「何をやり直してよいか」「上限をどこに置いたか」「同じリクエストが2回来ても安全な理由」「バジェットが尽きたときに何を残すか」を意味します)。

数値は手で書かず、自分のモジュールを実際に動かして得た値で埋めてください。split_transient_attemptsは、一時的な失敗2回のあとに成功したときの試行回数で、duplicate_delivery_entriesは、同じキーで2回送ったあとに元帳に残った行数です。バッチは、指示文の参考の節が定めたorders・plan・budgetのとおりに動かしてください。採点ツールは、同じものを別に計算して突き合わせます。