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

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

止まらないエージェントは、ループではなく条件が悪い

TT Labで続きを見る

一言でいうと

エージェントが止まらない事故は、たいていループを作ったからではなく、ループを終える条件に届かない入力があるために起きます。

なぜ必要なのか

回答の下書きを、ルールに合うまで書き直すエージェントを立ち上げました。数日はうまく動きました。ところがある日、1件が終わらずに回り続け、ログにはreviewとreviseが交互に数百行出力されていました。

コードを見ると、終える条件はちゃんとあります。「スコアが70点以上なら出す」です。問題は、その1件の下書きが70点に届き得ないことでした。スコアが下がった理由は2つあり(禁止語と長さ)、直すノードは禁止語しか取り除きませんでした。長さが原因で引かれた40点は、何回回してもそのままです。

ループに間違いはありませんでした。間違っていたのは、「直せばよくなる」という仮定でした。

終わらないループを描いた図です。reviewとreviseが行き来し、スコアが70以上のときだけshipに出ます。ところが、100点のうち長さで引かれた40点は、直すノードが手を付けないので、スコアは最大でも60で止まり、出す基準の70に届きません

どう動くのか

LangGraphでは、ブランチをadd_conditional_edgesで作ります。引数は3つです。

graph.add_conditional_edges("review", after_review,
                            {"ship": "ship", "revise": "revise", "escalate": "escalate"})

真ん中の関数は、次のノード名ではなくブランチ名を返します。どのブランチがどのノードへ行くかは、3番目の引数(パスマップ)が決めます。この1枚がなぜあるのか。判断するコードと配線を切り離すためです。ノード名を関数の中に書いておくと、グラフの形を変えるたびに判断のコードを直す必要があり、描画する側(get_graph())も、どこへ行けるのかがわかりません。

ループは、ただの後ろへ戻るエッジです。graph.add_edge("revise", "review")の1行で、reviseを通ったあと、またreviewに戻ります。特別な文法がないというのが、このモデルのよい点であり、怖い点でもあります。

再帰の上限は何を数えるのか: 実測した数値

終える条件がなければ、無限に回るのでしょうか。違います。LangGraphはrecursion_limitに達すると、GraphRecursionErrorを投げます。メッセージは次のとおりです。

Recursion limit of 25 reached without hitting a stop condition.
You can increase the limit by setting the `recursion_limit` config key.

数える単位は、ノードの実行回数ではなくスーパーステップ(superstep)です。1つのスーパーステップで動けるノードは複数あります。このラボのイメージのlanggraph 0.2.60で実測すると、次のとおりです。

グラフ 最後まで動かすのに必要な最小のrecursion_limit
ノード3つを一列につないだもの 4
ノード3つを並べたもの 2
(デフォルト値) 25

一列につないだほうが노드 수 + 1(プレースホルダーはノード数です)なのは、開始のチャネルが1スーパーステップを使うからで、並べたほうがノード数と無関係に2なのは、同じスーパーステップですべてが動くからです。この違いを知らないと、「ノードを増やしたら上限に引っかかった」を、的外れに診断します。並べて増やしたものは、上限を使いません。

デフォルト値の25は、十分な数ではありません。直すループが1周にスーパーステップ2つ(review + revise)を使うなら、12周もできません。

上限に達したことと、仕事を終えたことは違う

ここで、実務がよく間違えます。GraphRecursionErrorを捕まえて「失敗」とだけ書いておくと、あとで、その1件がなぜ失敗したのかがわかりません。2つがまったく違う出来事なのに、同じ欄に入ってしまうからです。

そのため、捕まえた例外は、{"status": "limit"}のように別の名前で残します。そして、上限に達した件がたまったら、上限を上げるのではなく、終える条件に届くことができるかから見ます。

二重の上限を置く

終える条件が正常なときも、上限は必要です。実務では、二重に置きます。

内側の上限がないと、外側の上限が代わりにかかり、そのときは、それまでに作った下書きも一緒に消えます。内側の上限は結果を残し、外側の上限は結果を捨てます。その違いが、運用では大きいのです。

現場での姿

1つ目: 上限を上げてやり過ごす。recursion_limitを25から200に上げれば、その1件は通ります。そして、来月に400に上げます。本当の問題は、終える条件に届かない入力があることで、それは上限と無関係です。

2つ目: 判断があちこちに散らばる。終えるかどうかを、ノードの中でも条件関数でも見ていると、2か所が食い違う日が来ます。判断は条件関数の1か所に集め、ノードは仕事だけをします。

3つ目: ブランチ名とノード名を同じにする。楽ですが、パスマップを省略するようになり、そうするとグラフを描くときに、どのブランチがあるのかが見えません。名前が同じでも、マップは書くほうがよいです。

4つ目: 並べて増やしたノードを、上限のせいにする。上の表が、その誤解を正します。並んだノードは、同じスーパーステップです。

実務で本当に大切なこと

次のラボですること

/root/work/agroute/route.pyを、1ステップずつ育てます。まず、パスマップを備えたブランチを作り、直した下書きをもう一度採点に送るループをつなぎます。次に、わざと終える条件を外したものを作ってGraphRecursionErrorを自分の目で見て、ノードを一列につないだグラフと並べたグラフの最小の上限を実測して、スーパーステップが何かを確認します。最後に、上限に達したものと終えたものを別の結果として残し、直しても改善しない入力を見分ける診断を作ります。