上限を上げてもその案件は終わらない
目標
ループのあるグラフを自分で作り、再帰の上限が何を数えるかを自分の手で測り、上限に達したことと、仕事を終えたことを、別の結果として残します。最後に、終える条件に届かない入力を見分ける診断を作ります。
なぜ重要なのか
LangGraphで、ループは特別な文法ではなく、ただの後ろへ戻るエッジです。そのため、作るのは簡単で、終わらせるのが難しいです。終える条件があっても、その条件に届き得ない入力が1つ混ざると、その1件は永遠に回ります。このラボのエージェントが、まさにそうです。スコアを引く理由が2つ(禁止語・長さ)あるのに、直すノードは1つしか直しません。
再帰の上限は、こんなときの最後の防衛線ですが、それが数える単位を知らないと、診断を間違えます。上限が数えるのは、ノードの実行回数ではなくスーパーステップで、並べたノードは、いくつあっても1スーパーステップです。このラボで、自分で測って確認します。
そして、上限に達して例外が出たことと、グラフが自分で終えたことは、まったく違う出来事です。同じ欄に「失敗」と書いておくと、あとで何が問題だったのかを分けられません。上限を二重(状態のroundsとrecursion_limit)に置き、内側の上限は結果を残すようにします。
採点ツールは、書かれた説明を信用しません。書かれたモジュールを実際に読み込んで、ルール関数を任意の文で叩いてみて、グラフを任意の上限で動かして出た結果を、採点ツールが別に計算した値と突き合わせます。
ステップ
- /root/work/agroute/route.pyに
BANNED・MIN_LEN・PASS・make_draft・score_of・revise_text・State・4つのノード(compose・review・revise・ship)・after_review・build_graph()を作成してください。reviewで分岐する条件付きエッジにはパスマップを渡し、このステップでは、reviseの次がENDです。 reviseからreviewに戻るエッジを追加して、ループを作ってください。禁止語が複数ある問い合わせが、何周か回って通過する必要があります。build_unbounded()とrun_unbounded(question, limit)を追加してください。終える条件をわざと外したグラフを、与えられた上限で動かし、上限に達したら{"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지}(プレースホルダーは上限値と例外メッセージです)を返します。MAX_ROUNDS = 5とescalateノードを追加して、build_graph()がどんな入力でも例外なく終わるようにしてください。上限に達したら、outcomeはescalateです。chain_graph(n)・fan_graph(n)・min_limit(app, state)を追加してください。min_limitは、そのグラフを最後まで動かすのに必要な最小のrecursion_limitを探します。run_guarded(question, limit)を追加して、終えたものと上限に達したものを、別のstatusで残してください。diagnose(question)を追加して、この入力がルールを満たせるか、満たせないならなぜか(too_shortまたはbanned)を見分けるようにしてください。- 測った数値と判断を記録してください(保存先: /root/work/agroute/route_report.json、/root/work/agroute/route_report.md)。
参考
- 実行の契約: 採点ツールは、
/root/work/agroute/route.pyをPythonモジュールとして読み込んで、上に書いた名前を直接使います。スクリプトとして実行しません。 make_draft(question)は、question.strip() + " 라고 안내드립니다."です(この韓国語の文字列は「と案内いたします。」という意味の語句です)。score_of(text)は、100から始めて、禁止語が出るたびに30を、長さがMIN_LEN未満なら40を引き、0より下には下がりません。revise_text(text)は、禁止語を1つだけ削除します。- 定数:
BANNED = ("무조건", "절대로", "보장합니다")(韓国語で、順に「無条件に」「絶対に」「保証します」を意味する3つの語です)、MIN_LEN = 30、PASS = 70、MAX_ROUNDS = 5。 - 状態のキー:
question・draft・score・rounds・outcome・trace。traceは連結するリデューサーを使います。 - ノード名と状態のキーは、重なってはいけません。LangGraphは、両方を同じ名前空間に置くので、下書きを書くノードを
draftと呼ぶと、コンパイルのときにValueError: 'draft' is already being used as a state keyが出ます。そのため、このラボの最初のノード名はcomposeです。 after_reviewはブランチ名を返します(ship・revise・escalate)。どのブランチがどのノードへ行くかは、add_conditional_edgesの3番目の引数が決めます。chain_graph(n)・fan_graph(n)は、ノードn個のグラフを返します。ノード名は自由で、各ノードは、自分の名前を連結するキーに1つ書きます。min_limit(app, state)は、1から上げながらinvokeを試して、最初に成功する上限を返します。失敗はlanggraph.errors.GraphRecursionErrorとして来ます。run_guardedの答え: 終えたなら{"status": "done", "rounds": 정수, "outcome": "ship"|"escalate", "score": 정수}、上限に達したなら{"status": "limit", "rounds": -1, "outcome": "", "score": -1}です(プレースホルダーは整数です)。diagnoseの答え:{"fixable": 참거짓, "reason": ""|"too_short"|"banned", "best_score": 정수, "rounds_needed": 정수}です(プレースホルダーは真偽値と整数です)。best_scoreは、禁止語をこれ以上削除できなくなるまで削除したあとのスコアです。- このPodにはインターネットがありません。langgraph 0.2.60がすでに入っています。
- 公式ドキュメント: Use the graph API・GRAPH_RECURSION_LIMIT・Graph API overview
- よくある間違い: 条件関数がブランチ名ではなくノード名を返すようにして、パスマップを省略すること、終える判断をノードと条件関数の2か所に置くこと、上限を上げて症状だけをやり過ごすこと、並べたノードが上限を使うと信じることです。
ブランチ名とパスマップを分ける
/root/work/agroute/route.pyに、ルール関数3つとノード4つ、after_review、build_graph()を作成してください。reviewの条件付きエッジには、パスマップ{"ship": "ship", "revise": "revise"}を渡し、このステップでは、reviseの次がENDです。
条件関数は、状態を受け取ってブランチ名を返します。ノード名を直接返すようにすると、グラフの形を変えるたびに判断のコードを直す必要があります。score_ofがどのようにスコアを引くかが、あとのステップの材料なので、指示文のルールを正確に守ってください。
直したものをもう一度採点に送る
reviseからreviewに戻るエッジを追加して、ループを作ってください。禁止語が3つある問い合わせは、3周回ってshipに行く必要があります。
ループは特別な文法ではなく、ただの後ろへ戻るエッジです。reviseが禁止語を1回に1つだけ削除することが、周回数を決めます。周回数を数えるroundsは、reviseが増やしてください。そうすれば、あとで内側の上限をかける場所ができます。
上限に達する様子を自分で見る
build_unbounded()とrun_unbounded(question, limit)を追加してください。終える条件をわざと外したグラフを、与えられた上限で動かし、上限に達したら{"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지}(プレースホルダーは上限値と例外メッセージです)を、終わったならerrorを空の文字列にして返します。
例外はlanggraph.errors.GraphRecursionErrorです。メッセージに上限の数字がそのまま入っているので、でっち上げず、str(예외)(プレースホルダーは例外です)をそのまま入れてください。build_unbounded()は、build_graph()と別のグラフである必要があります。あとのステップでbuild_graph()に上限を入れても、このステップが有効であり続ける必要があるからです。
結果を残す内側の上限
MAX_ROUNDS = 5とescalateノードを追加して、after_reviewが上限でescalateブランチを返すようにしてください。パスマップにも、そのブランチを追加します。これで、build_graph()はどんな入力でも例外なく終わる必要があります。
外側の上限(recursion_limit)に達すると例外になり、結果が消えます。内側の上限は結果を残します。それがこのステップの要点です。判断はafter_reviewの1か所だけに置いてください。ノードの中でも終えるかどうかを見ていると、2か所が食い違う日が来ます。
上限が数えるのはスーパーステップである
chain_graph(n)(一列)・fan_graph(n)(並べる)・min_limit(app, state)を追加してください。min_limitは、そのグラフを最後まで動かすのに必要な最小のrecursion_limitを探して返します。
1から上げながらinvokeを試して、最初に成功する値を返せばよいです。2つのグラフの答えがどう違うかが、このステップの核心です。並べたノードは、いくつあっても同じスーパーステップで動きます。状態は、リデューサーが付いたキー1つで十分です。
終えたものと達したものを分ける
run_guarded(question, limit=25)を追加してください。グラフが自分で終わったなら{"status": "done", "rounds": 정수, "outcome": "ship"|"escalate", "score": 정수}、上限に達したなら{"status": "limit", "rounds": -1, "outcome": "", "score": -1}です(プレースホルダーは整数です)。
2つの出来事を同じ欄に「失敗」と書くと、あとで分けられません。例外を捕まえて別の名前を付けるだけですが、その1行が運用で大きな違いを生みます。上限を小さく指定すると、正常な入力でもlimitが出ます。採点ツールがそれを確認します。
直しても改善しない入力を見分ける
diagnose(question)を追加して、{"fixable": 참거짓, "reason": ""|"too_short"|"banned", "best_score": 정수, "rounds_needed": 정수}(プレースホルダーは真偽値と整数です)を返すようにしてください。best_scoreは、禁止語をこれ以上削除できなくなるまで削除したあとのスコアです。
グラフを動かさなくてもわかります。revise_textを、これ以上変わらなくなるまで適用してみて、そのときのスコアを測ればよいのです。そのスコアがPASSに届かなければ、この入力はいくら回しても通過できません。診断が、上限を上げるよりコストが低い理由が、ここにあります。
測った数値で報告する
/root/work/agroute/route_report.jsonにdefault_recursion_limit・chain_min_limit・fan_min_limit・clean・dirty・stuck・stuck_reasonを、/root/work/agroute/route_report.mdに## 고리를 어디에 두었나 ## 재귀 한도는 무엇을 세는가 ## 상한에 닿은 것과 마친 것 ## 고쳐도 나아지지 않는 입력の4つの節を書いてください(4つの見出しは、順に韓国語で「ループをどこに置いたか」「再帰の上限は何を数えるのか」「上限に達したものと終えたもの」「直しても改善しない入力」を意味します)。
default_recursion_limitは、configを渡さなかったときのデフォルト値です。でっち上げず、上限を上げながら測って確認してください。chain_min_limit・fan_min_limitは、ノード数を文字列のキーにしたオブジェクトです(例: {"1": ..., "2": ..., "3": ...})。clean・dirty・stuckは、それぞれrun_guardedの答えをそのまま入れます。