高額返金が承認待ちにすら現れなかった
目標
取り消せない作業の前でグラフを止め、人が値を直すか拒否したあとで続きを動かします。止める方法を2つとも作ってみて、2つの違いを、自分で数えた数値で確認します。
なぜ重要なのか
LangGraphで、中断は保存の上に築かれています。止まるとは、「次のステップを今は実行せず、残しておいた状態から、あとで続きを行う」という意味なので、残しておく場所(チェックポインター)がなければ、続きを行うこともできません。ところが、チェックポインターなしで中断点だけを渡しても、エラーが出ません。グラフは黙って止まり、半分だけの状態を返します。このラボで、それをまず自分の目で見ます。
止まったあとに人が行うことは、承認だけではありません。金額を減らすことも、拒否することもあります。update_stateで直した値は、そのキーのリデューサーを通って入るので、上書きするキーと連結するキーで、動作が変わります。
止める方法は2つあります。interrupt_beforeは、ノードに入る前に止まり、ノードの中のinterrupt()は、呼んだその場所で止まります。後者は、再開するときにそのノードを最初からもう一度動かします。前に置いた副作用が2回起きるという意味です。このラボで、何回入ってくるかを自分で数えます。
採点ツールは、書かれた説明を信用しません。書かれたモジュールを実際に読み込んで、任意の金額と理由で動かしてみて、止まった場所と続きを動かした結果を、採点ツールが別に計算した値と突き合わせます。
ステップ
- /root/work/aghitl/approve.pyに
AUTO_LIMIT = 100000・RISKY・needs_approval(amount, reason)・State・3つのノード(intake・auto_settle・settle)・triage・build_graph()を作成してください。まだ止まりません。 build_forgotten()とrun_forgotten(amount, reason)を追加してください。チェックポインターなしでinterrupt_before=["settle"]だけを渡したグラフを動かして、何が起きるかを書きます。SAVER = MemorySaver()・build_pausing()・thread(name)・start_case(amount, reason, name)を追加して、本当に止まるようにしてください。start_caseは{"next": ["settle"], "outcome": "", "paid": -1}を返します。resume_case(name)を追加して、invoke(None, config)で続きを動かすようにしてください。resume_case(name, amount=..., decision=...)で、人が金額を直すか拒否したあとで、続きを動かすようにしてください。拒否なら、支払いはまったく行われません。confirmノードとbuild_asking()・ask_and_answer(amount, reason, answer, name)を追加してください。ノードの中でinterrupt()で止まり、Command(resume=답)(プレースホルダーは答えです)で続きを動かします。ノードに何回入ってきたかをSIDE_EFFECTSで数えます。handle(...)を追加して、1件を最後まで処理し、要求値と実行値、承認の有無を1行で残すようにしてください。- 記録を残してください(保存先: /root/work/aghitl/approve_report.json、/root/work/aghitl/approve_report.md)。
参考
- 実行の契約: 採点ツールは、
/root/work/aghitl/approve.pyをPythonモジュールとして読み込んで、上に書いた名前を直接使います。スクリプトとして実行しません。 needs_approval(amount, reason)は、金額がAUTO_LIMITを超えるか、理由がRISKYにあれば真です。RISKY = ("파손", "오배송")です(韓国語で、順に「破損」「誤配送」を意味する2つの語です)。- 状態のキー:
amount・reason・decision・paid・outcome・trace。traceは連結するリデューサーを使います。ノード名と状態のキーは、重なってはいけません。重なると、コンパイルのときにValueError: 'x' is already being used as a state keyが出ます。 - ノードが残す値:
auto_settleはoutcome="auto"、settleは、承認ならoutcome="settled"、状態のdecisionが"reject"ならoutcome="rejected"で、paidは0です。承認なら、paidはその時点のamountです。 traceに書く名前は、ノード名と同じにし(intake・auto_settle・settle・confirm)、人が直した痕跡はhumanと書きます。run_forgottenの答え:{"raised": 참거짓, "trace": [...], "outcome": 문자열, "resumable": 참거짓}(プレースホルダーは真偽値と文字列です)。自分で動かして、見たとおりに埋めてください。start_caseの答え:{"next": [...], "outcome": 문자열, "paid": 정수}(プレースホルダーは文字列と整数です)。nextは、get_state(config).nextをリストに変えたものです。resume_caseの答え:{"outcome": 문자열, "paid": 정수, "trace": [...]}(プレースホルダーは文字列と整数です)。ask_and_answerの答え:{"next": [...], "asked": [...], "entered_before": 정수, "entered_after": 정수, "outcome": 문자열, "paid": 정수, "decision": 문자열}(プレースホルダーは文字列と整数です)。askedは、get_state(config).tasksのinterruptsに入っている値です。答えが"yes"なら承認、そうでなければ拒否です。handleの答え:{"approval": "not_required"|"approved"|"rejected", "outcome": 문자열, "paid": 정수, "requested": 정수, "changed_by_human": 참거짓}(プレースホルダーは文字列、整数、真偽値です)。- 同じ
thread_idをもう一度使うと、前の記録が残っています。採点ツールは、毎回異なる名前を渡します。 update_stateで直した値は、ブランチをもう一度走らせます。その値は、最後に動いたノードが書いたものとして記録され、そのノードの条件付きエッジが再評価されます。そのため、承認待ちの案件の金額をAUTO_LIMITより下に減らすと、その案件は承認の経路を外れてauto_settleに抜けます。採点ツールは、上限より上でしか減らさないので、このラボではその再配線を扱いませんが、知っておく必要があります。- このPodにはインターネットがありません。langgraph 0.2.60がすでに入っています。
- 公式ドキュメント: Interrupts・Persistence・Typesリファレンス
- よくある間違い: チェックポインターなしで中断点だけを渡すこと、
thread_idを渡さないこと、続きを動かすときに元の入力をもう一度入れること(そうすると最初からもう一度動きます。Noneを渡してください)、interrupt()の前に取り消せないことを置くことです。
人が見るべき作業を分ける
/root/work/aghitl/approve.pyにAUTO_LIMIT = 100000・RISKY・needs_approval・State・3つのノード・triage・build_graph()を作成してください。少額で単純な理由ならauto_settle、そうでなければsettleに進みます。まだ止まりません。
基準をneeds_approvalの1か所に書いておけば、方針を変えることが、その関数を直すことになります。triageはブランチ名(auto・hold)を返し、どのノードへ行くかは、パスマップが決めます。ノード名は、状態のキーと違う名前にしてください。
チェックポインターを忘れると黙って消える
build_forgotten()とrun_forgotten(amount, reason)を追加してください。チェックポインターなしでinterrupt_before=["settle"]だけを渡したグラフを動かし、例外が出たか・どこまで進んだか・続きを動かせるかを、{"raised": ..., "trace": [...], "outcome": ..., "resumable": ...}として書いてください。
自分で動かして、見たとおりに書いてください。予想と違うはずです。その驚きが、このステップのすべてです。保存されたものがないので、invoke(None, ...)で続きを動かすこともできません。これがなぜ危険かは、ログにエラーが1つもないことにあります。
本当に止める
SAVER = MemorySaver()・build_pausing()・thread(name)・start_case(amount, reason, name)を追加してください。承認が必要な案件はsettleの前で止まり、start_caseは{"next": ["settle"], "outcome": "", "paid": -1}を返します。
3つが一緒にある必要があります。チェックポインター、thread_id、中断点です。get_state(config).nextが空でないことが、そのまま「まだ終わっていない」という意味です。少額の案件は止まらずにそのまま終わる必要があるので、中断点を置いたノードを通らないブランチがあることが重要です。
残しておいた場所から続きを動かす
resume_case(name)を追加して、invoke(None, config)で続きを動かすようにしてください。答えは{"outcome": "settled", "paid": 금액, "trace": [...]}です(プレースホルダーは金額です)。
入力の場所にNoneを渡すのが、「新しい入力はない。保存された場所から続きを実行する」という意味です。元の入力をもう一度入れると、最初からもう一度動き、そうすると足跡が2倍になるので、すぐわかります。同じthread_idを使って初めて、つながります。
承認者が金額を直すか拒否する
resume_case(name, amount=..., decision=...)が、update_stateで状態を先に直してから、続きを動かすようにしてください。decision="reject"なら、outcomeはrejectedで、paidは0です。直した痕跡は、traceにhumanとして残します。
update_stateが入れる値も、そのキーのリデューサーを通ります。amountのように上書きするキーは上書きしますが、traceのように連結するキーは積み上がります。そのため、痕跡を1行残すのにちょうどよいです。拒否も結果である必要があります。承認者が「ただ押さない」ことで拒否するようにしておくと、その案件は永遠に待ち行列に残ります。
ノードの中で止まると、そのノードは2回動く
confirmノードとbuild_asking()・ask_and_answer(amount, reason, answer, name)を追加してください。interrupt()で止まり、Command(resume=답)(プレースホルダーは答えです)で続きを動かし、SIDE_EFFECTSで、ノードに何回入ってきたかを数えます。
interrupt()が返す値が、そのまま人が出した答えです。再開すると、そのノードが最初からもう一度動くので、interrupt()の前に置いたことは2回起きます。自分で数えて確認してください。get_state(config).tasksの中のinterruptsに、止まるときに渡した値が入っています。
要求した値と出た値を一緒に残す
handle(amount, reason, name, amount_override=None, decision=None)を追加して、1件を最後まで処理し、{"approval": ..., "outcome": ..., "paid": ..., "requested": ..., "changed_by_human": ...}を返すようにしてください。
承認記録の核心は、人の名前ではなく変わった値です。要求された金額と実際に出た金額が違うなら、その事実が記録にあって初めて、あとで説明できます。承認が不要な案件は、not_requiredと書いて、止まりません。
何を見たかを記録する
/root/work/aghitl/approve_report.jsonにauto_limit・forgotten・small・large・trimmed・rejected・confirm_enteredを、/root/work/aghitl/approve_report.mdに## 어디서 멈추기로 했나 ## 체크포인터를 빠뜨리면 ## 사람이 고친 값은 어디로 들어가나 ## 노드 밖에서 멈추기와 노드 안에서 멈추기の4つの節を書いてください(4つの見出しは、順に韓国語で「どこで止めることにしたか」「チェックポインターを忘れると」「人が直した値はどこへ入るのか」「ノードの外で止まることとノードの中で止まること」を意味します)。
forgottenはrun_forgottenの答えをそのまま、small・large・trimmed・rejectedは、handleの答えからapproval・outcome・paidの3つのキーだけを入れます。confirm_enteredは{"before": 정수, "after": 정수}(プレースホルダーは整数です)で、ask_and_answerが数えた値です。でっち上げず、動かして得てください。