Agentic OS 技術スタックを下から読む 第15回:道具の呼び出しは、分散システムへの呼び出しである
前回は、L4 の編成を「長い鎖を短く区切る」ものとして見た。
型だけでは、一手ごとの失敗は防げない
前回は、L4 の編成を「長い鎖を短く区切る」ものとして見た。
エージェントに長い仕事を一気に任せると、途中の小さな誤りが後ろへ流れ、最後には信頼性の崖として現れる。だから、探索、判断、実行、確認のように、役割を短い段に分ける。各段で何を受け取り、何を返すのかをはっきりさせる。これが、前回見た編成の基本だった。
ただし、型を作っただけでは、各段の中身までは守られない。
一つの段の中でも、エージェントは道具を呼ぶ。検索する。外部のデータを読む。記録を書き込む。予約する。通知する。そこで失敗が起きる。しかもその失敗は、単なる「答えが間違った」という形だけではない。
返ってこないことがある。遅れることがある。途中までしか分からないことがある。実行されたのに、実行されたことだけが見えないことがある。
今回は、この一手を見る。
道具の呼び出しを、手元の関数呼び出しと同じものとして扱うと、設計はすぐ甘くなる。むしろ、離れた相手に依頼する、失敗しうる呼び出しとして扱う必要がある。言い換えれば、道具の呼び出しは、分散したシステムへの呼び出しである。
この見方を腹に入れると、必要な歯止めは自然に出てくる。再試行、冪等、境界の検証、予算の上限である。
道具の呼び出しは、すぐ確実に返るとは限らない
普通のプログラムで関数を呼ぶとき、私たちはかなり強い前提に頼っている。
呼べば、すぐに処理が始まる。終われば、戻り値が返る。途中で失敗すれば、失敗として分かる。少なくとも、同じプロセスの中にある関数なら、こう考えても大きく外れない。
もちろん、実際には例外もある。重い処理もある。バグもある。それでも、手元の関数呼び出しは、呼び手と呼ばれ手が同じ空間にいる。状態も、時間も、失敗の見え方も、比較的近い。
エージェントが呼ぶ道具は、そうではない。
多くの場合、道具は外にある。ネットワークの向こうにある。別のサービスが動いている。別のデータベースがある。別の権限境界がある。そこへ入力を送り、返事を待つ。
この時点で、呼び出しの性質は変わる。
相手に届かないことがある。相手には届いたが、処理に失敗することがある。処理は成功したが、返事が戻らないことがある。返事は戻ったが、途中で切れていることがある。遅すぎて、呼び手の側ではもう不要になっていることもある。
ここで厄介なのは、呼び手から見ると、失敗の種類を完全には区別できないことである。
たとえば、応答が返ってこなかったとする。相手に届かなかったのか。相手で処理中なのか。処理は成功したが返事だけ失われたのか。呼び手には分からない。分からないまま、次の判断をしなければならない。
これが、離れた相手とやり取りするということである。
だから、道具の呼び出しを「関数を呼んだら戻り値が返る」という気分で扱ってはいけない。むしろ、「相手がいる。途中の経路がある。どちらも失敗しうる」と見る必要がある。
この見方が、L4 の一手ごとの設計を変える。
失敗するなら、やり直す。ただし、すぐ罠が出る
外部の呼び出しは、ときどき失敗する。
すべての失敗が、根本的な失敗とは限らない。少し混んでいただけかもしれない。相手側が一時的に詰まっていただけかもしれない。ネットワークの揺らぎかもしれない。そういう失敗は、少し待ってからもう一度試すと通ることが多い。
だから、再試行が要る。
一度失敗したら、すぐ諦めるのではなく、短い間隔を置いてやり直す。まだ失敗するなら、もう少し長く待つ。たとえば、数百ミリ秒、数秒、十数秒というように、間隔を少しずつ延ばしていく。これにより、一時的な混雑に同じ勢いでぶつかり続けることを避けられる。
ここまでは自然である。
しかし、再試行はただ入れればよいものではない。外部の呼び出しでは、「失敗したように見える」ことと「実際に何も起きていない」ことは同じではないからである。
ここに罠がある。
エージェントから見ると失敗に見える。だから、やり直す。ところが、相手側では一回目の処理がすでに済んでいる。すると、二回目の呼び出しが、同じ処理をもう一度起こしてしまう。
再試行は、正しく設計すれば信頼性を上げる。だが、何も考えずに入れると、失敗を増幅する。
やり直しの罠は、二重実行である
予約の道具を考える。
エージェントが、ある日時の予約を取るために道具を呼ぶ。入力には、利用者、日時、対象、人数が入っている。呼び出しは相手に届く。相手側では予約が成立する。
ところが、成功の返事だけが途中で消える。
エージェントから見ると、結果は分からない。成功したのか、失敗したのか、まだ処理中なのか判断できない。そこで、再試行する。
もし相手側が、同じ内容の呼び出しをもう一度受けて、そのまま新しい予約として扱うなら、予約は二件になる。利用者から見れば、エージェントは勝手に二重予約をしたことになる。
同じことは、予約に限らない。
支払い、発注、通知、データ作成、権限変更、外部への送信。世界に何かを起こす道具では、二重実行が問題になる。読み取りだけならまだ軽いことが多いが、書き込みや実行を伴う呼び出しでは、再試行そのものが副作用を重ねる。
ここで重要なのは、エージェントが悪意を持っていないことではない。
指示文が丁寧でも、モデルが賢くても、呼び出しの性質は変わらない。応答が失われることはある。呼び手からは結果が分からないことがある。そのとき、同じ操作をもう一度送る可能性がある。
だから、再試行を前提にするなら、二重実行を防ぐ設計も同時に必要になる。
同じ呼び出しは、一回分として扱う
この性質を、ここでは「二回受けても一回分として扱う性質」と呼ぶ。
専門用語では冪等と呼ばれることがある。言葉だけを見ると難しいが、考え方は単純である。同じ意図の呼び出しが二度来ても、結果として起きることは一度分にする、ということだ。
方法はいくつかある。
一つは、呼び出しごとに一意の札を付けることである。エージェントが予約を依頼するとき、「これはこの一回の依頼である」と分かる札を添える。相手側は、その札を記録しておく。同じ札の依頼がもう一度来たら、新しく実行しない。前に実行した結果を返す。
この場合、一回目の成功応答が失われても、再試行は安全になる。二回目の呼び出しに対して、相手側は「これはもう処理済みだ」と判断できるからである。
別の方法は、実行前に状態を確かめることである。
たとえば、同じ利用者、同じ日時、同じ対象の予約がすでに存在するかを調べる。存在するなら、新しく作らず、その予約を返す。これも、二重実行を避ける一つの形である。
どちらの方法でも、肝心なのは「呼び手が再試行するかもしれない」という前提を、道具側が受け止めることである。
再試行と冪等は、対で考える必要がある。再試行だけがあると、二重実行の危険が増える。冪等だけがあっても、失敗した呼び出しを回復する動きがなければ、前に進まない。
失敗するから、やり直す。やり直すから、同じ呼び出しを一回分に抑える。
この順番で考えると、なぜ両方が必要なのかが見える。
段の境目ごとに、データを確かめる
道具の呼び出しでもう一つ重要なのは、境目で止めることである。
L4 では、仕事を段に分ける。ある段が候補を集め、次の段が選び、さらに次の段が実行する。道具も、その途中で呼ばれる。つまり、データは段から段へ渡り、エージェントから道具へ渡り、道具からまた戻ってくる。
この境目で、データを確かめる必要がある。
必要な値はそろっているか。日付は確定しているか。宛先はあるか。数量は範囲内か。予算を超えていないか。文字列としては入っているが、意味としては空ではないか。前の段が「たぶん」と書いたものを、次の段が確定事項として扱っていないか。
こうした確認は地味である。だが、ここを省くと、壊れたデータが後ろへ流れる。
後ろの段で失敗したときには、原因が見えにくくなる。実行の段が失敗したように見えて、実は選択の段で日付が曖昧だったのかもしれない。通知の段が失敗したように見えて、実は宛先が空だったのかもしれない。外部の道具が悪いように見えて、実は渡した入力が壊れていたのかもしれない。
境目で止めれば、その場で分かる。
「この入力では実行できない」と言える。足りない情報を取りに戻れる。利用者に確認できる。少なくとも、壊れたまま世界に手を出すことは避けられる。
これは安全の話ではなく、編成の話である。
段を分けたなら、段の入口と出口を確かめる。道具を呼ぶなら、呼ぶ前の入力と、返ってきた出力を確かめる。短く区切るだけでなく、区切り目で品質を落とさないようにする。
それが、L4 の実務である。
止まらないときのために、硬い上限を置く
もう一つ、外からの歯止めが要る。上限である。
エージェントは、うまくいかないときに、同じ場所を回り続けることがある。別の候補を探す。別の言い方を試す。もう一度道具を呼ぶ。少し条件を変える。さらに別の候補を探す。
人間から見ると、もう十分に失敗している。しかし、エージェントの内部では、まだ次の一手があるように見える。結果として、時間、計算資源、外部呼び出しの回数を使い続ける。
だから、硬い上限を置く。
段数の上限。道具を呼ぶ回数の上限。使えるトークン量の上限。実行時間の上限。再試行回数の上限。候補を広げる幅の上限。どれも、曖昧な目安ではなく、実行を止める条件として置く。
上限は、エージェントを賢くするためのものではない。賢く振る舞えなかったときに、被害を有限にするためのものだ。
たとえば、検索を三回までにする。予約候補の確認を五件までにする。一つの段は二分で打ち切る。一つの依頼全体で使える量を決めておく。数字そのものは仕事によって変わる。大事なのは、決めておくことである。
上限がないと、失敗は終わらない。
うまくいかないエージェントが、同じところで延々と回り続ける。しかも、そのたびにもっともらしい次の手を出す。見た目には努力しているように見えるが、実際には費用と時間を食い潰している。
上限は、その状態を外から切る。
「ここまで試してだめなら、止める」。この単純な線があるだけで、システム全体の扱いやすさは大きく変わる。
根にあるのは、一つの見方である
ここまで見たものは、ばらばらの小技に見えるかもしれない。
失敗したら再試行する。同じ呼び出しを一回分として扱う。段の境目でデータを確かめる。時間や回数に上限を置く。どれも個別の工夫に見える。
だが、根は一つである。
道具の呼び出しを、手元の確実な関数呼び出しとして見ない。離れた相手への、失敗しうる呼び出しとして見る。
そう見れば、次の設計は自然に出てくる。
失敗するから、やり直す。やり直すから、二重実行を防ぐ。壊れたデータが渡ることがあるから、境目で確かめる。止まらなくなることがあるから、上限を置く。
これは、賢い指示文の話ではない。
もちろん、指示文は大事である。何をしてよいか、どう判断するか、どう報告するかを言葉で与える必要はある。だが、指示文だけでは、外部呼び出しの性質は変わらない。ネットワークは揺れる。相手側は遅れる。返事は失われる。途中の状態は曖昧になる。
だから、L4 では、言葉の設計だけでなく、呼び出しの設計が必要になる。
ここを押さえると、エージェントの失敗は少し別の姿で見えるようになる。モデルが賢いかどうかだけではない。道具をどう呼ばせているか。失敗したときにどう扱うか。どこで止めるか。どの境目で確かめるか。
任せられるシステムに近づくためには、この地味な規律が要る。
Agentic OS への含意
これで、L4 の編成を三つの面から見たことになる。
第13回では、間で何が起きたかを見えるようにした。複数の段やエージェントが動くなら、途中の判断、入力、出力、失敗を追える必要がある。
第14回では、長い鎖を短く区切る型を見た。信頼性の崖に対して、一つの大きな推論にすべてを背負わせるのではなく、役割を分け、段ごとに扱う。
そして今回は、一手ごとの歯止めを見た。道具の呼び出しを分散した相手への呼び出しとして扱い、再試行、冪等、境界の検証、上限を入れる。
この三つがそろうと、複数のエージェントは、ただ「賢く動くもの」から「任せられるもの」に近づく。
オペレーティングシステムは、プログラムをただ速く走らせるためだけにあるのではない。プロセスを分け、資源を割り当て、失敗が全体へ広がらないように囲う。観測し、止め、やり直せる単位を作る。
Agentic OS の L4 も、それに近い仕事をしている。
エージェントを走らせるだけでは足りない。どこで何が起きたかを見えるようにし、長い仕事を短い段に分け、外部への一手ごとに歯止めをかける。そうして初めて、知的な振る舞いは、運用できる振る舞いになる。
次に進む層では、ここまで何度も顔を出してきた、もう一つの横断的な問題を見る。
エージェントが外の世界に手を出すとき、何を信じ、何を止めるのか。L5、安全の層である。
← 一覧へ