← 一覧へ
連載 Agentic OS:技術スタックを下から読む の一部です ―― 目次を見る →

Agentic OS 技術スタックを下から読む 第15回:道具の呼び出しは、分散システムへの呼び出しである

この記事の読み方
前回は、L4 の編成を「長い鎖を短く区切る」ものとして見た。

型だけでは、一手ごとの失敗は防げない

前回は、L4 の編成を「長い鎖を短く区切る」ものとして見た。

エージェントに長い仕事を一気に任せると、途中の小さな誤りが後ろへ流れ、最後には信頼性の崖として現れる。だから、探索、判断、実行、確認のように、役割を短い段に分ける。各段で何を受け取り、何を返すのかをはっきりさせる。これが、前回見た編成の基本だった。

ただし、型を作っただけでは、各段の中身までは守られない。

一つの段の中でも、エージェントは道具を呼ぶ。検索する。外部のデータを読む。記録を書き込む。予約する。通知する。そこで失敗が起きる。しかもその失敗は、単なる「答えが間違った」という形だけではない。

返ってこないことがある。遅れることがある。途中までしか分からないことがある。実行されたのに、実行されたことだけが見えないことがある。

今回は、この一手を見る。

道具の呼び出しを、手元の関数呼び出しと同じものとして扱うと、設計はすぐ甘くなる。むしろ、離れた相手に依頼する、失敗しうる呼び出しとして扱う必要がある。言い換えれば、道具の呼び出しは、分散したシステムへの呼び出しである。

この見方を腹に入れると、必要な歯止めは自然に出てくる。再試行、冪等、境界の検証、予算の上限である。

道具の呼び出しは、すぐ確実に返るとは限らない

普通のプログラムで関数を呼ぶとき、私たちはかなり強い前提に頼っている。

呼べば、すぐに処理が始まる。終われば、戻り値が返る。途中で失敗すれば、失敗として分かる。少なくとも、同じプロセスの中にある関数なら、こう考えても大きく外れない。

もちろん、実際には例外もある。重い処理もある。バグもある。それでも、手元の関数呼び出しは、呼び手と呼ばれ手が同じ空間にいる。状態も、時間も、失敗の見え方も、比較的近い。

エージェントが呼ぶ道具は、そうではない。

多くの場合、道具は外にある。ネットワークの向こうにある。別のサービスが動いている。別のデータベースがある。別の権限境界がある。そこへ入力を送り、返事を待つ。

この時点で、呼び出しの性質は変わる。

相手に届かないことがある。相手には届いたが、処理に失敗することがある。処理は成功したが、返事が戻らないことがある。返事は戻ったが、途中で切れていることがある。遅すぎて、呼び手の側ではもう不要になっていることもある。

ここで厄介なのは、呼び手から見ると、失敗の種類を完全には区別できないことである。

たとえば、応答が返ってこなかったとする。相手に届かなかったのか。相手で処理中なのか。処理は成功したが返事だけ失われたのか。呼び手には分からない。分からないまま、次の判断をしなければならない。

これが、離れた相手とやり取りするということである。

だから、道具の呼び出しを「関数を呼んだら戻り値が返る」という気分で扱ってはいけない。むしろ、「相手がいる。途中の経路がある。どちらも失敗しうる」と見る必要がある。

この見方が、L4 の一手ごとの設計を変える。

失敗するなら、やり直す。ただし、すぐ罠が出る

外部の呼び出しは、ときどき失敗する。

すべての失敗が、根本的な失敗とは限らない。少し混んでいただけかもしれない。相手側が一時的に詰まっていただけかもしれない。ネットワークの揺らぎかもしれない。そういう失敗は、少し待ってからもう一度試すと通ることが多い。

だから、再試行が要る。

一度失敗したら、すぐ諦めるのではなく、短い間隔を置いてやり直す。まだ失敗するなら、もう少し長く待つ。たとえば、数百ミリ秒、数秒、十数秒というように、間隔を少しずつ延ばしていく。これにより、一時的な混雑に同じ勢いでぶつかり続けることを避けられる。

ここまでは自然である。

しかし、再試行はただ入れればよいものではない。外部の呼び出しでは、「失敗したように見える」ことと「実際に何も起きていない」ことは同じではないからである。

ここに罠がある。

エージェントから見ると失敗に見える。だから、やり直す。ところが、相手側では一回目の処理がすでに済んでいる。すると、二回目の呼び出しが、同じ処理をもう一度起こしてしまう。

再試行は、正しく設計すれば信頼性を上げる。だが、何も考えずに入れると、失敗を増幅する。

やり直しの罠は、二重実行である

予約の道具を考える。

エージェントが、ある日時の予約を取るために道具を呼ぶ。入力には、利用者、日時、対象、人数が入っている。呼び出しは相手に届く。相手側では予約が成立する。

ところが、成功の返事だけが途中で消える。

エージェントから見ると、結果は分からない。成功したのか、失敗したのか、まだ処理中なのか判断できない。そこで、再試行する。

もし相手側が、同じ内容の呼び出しをもう一度受けて、そのまま新しい予約として扱うなら、予約は二件になる。利用者から見れば、エージェントは勝手に二重予約をしたことになる。

同じことは、予約に限らない。

支払い、発注、通知、データ作成、権限変更、外部への送信。世界に何かを起こす道具では、二重実行が問題になる。読み取りだけならまだ軽いことが多いが、書き込みや実行を伴う呼び出しでは、再試行そのものが副作用を重ねる。

ここで重要なのは、エージェントが悪意を持っていないことではない。

指示文が丁寧でも、モデルが賢くても、呼び出しの性質は変わらない。応答が失われることはある。呼び手からは結果が分からないことがある。そのとき、同じ操作をもう一度送る可能性がある。

だから、再試行を前提にするなら、二重実行を防ぐ設計も同時に必要になる。

同じ呼び出しは、一回分として扱う

この性質を、ここでは「二回受けても一回分として扱う性質」と呼ぶ。

専門用語では冪等と呼ばれることがある。言葉だけを見ると難しいが、考え方は単純である。同じ意図の呼び出しが二度来ても、結果として起きることは一度分にする、ということだ。

方法はいくつかある。

一つは、呼び出しごとに一意の札を付けることである。エージェントが予約を依頼するとき、「これはこの一回の依頼である」と分かる札を添える。相手側は、その札を記録しておく。同じ札の依頼がもう一度来たら、新しく実行しない。前に実行した結果を返す。

この場合、一回目の成功応答が失われても、再試行は安全になる。二回目の呼び出しに対して、相手側は「これはもう処理済みだ」と判断できるからである。

別の方法は、実行前に状態を確かめることである。

たとえば、同じ利用者、同じ日時、同じ対象の予約がすでに存在するかを調べる。存在するなら、新しく作らず、その予約を返す。これも、二重実行を避ける一つの形である。

どちらの方法でも、肝心なのは「呼び手が再試行するかもしれない」という前提を、道具側が受け止めることである。

再試行と冪等は、対で考える必要がある。再試行だけがあると、二重実行の危険が増える。冪等だけがあっても、失敗した呼び出しを回復する動きがなければ、前に進まない。

失敗するから、やり直す。やり直すから、同じ呼び出しを一回分に抑える。

この順番で考えると、なぜ両方が必要なのかが見える。

段の境目ごとに、データを確かめる

道具の呼び出しでもう一つ重要なのは、境目で止めることである。

L4 では、仕事を段に分ける。ある段が候補を集め、次の段が選び、さらに次の段が実行する。道具も、その途中で呼ばれる。つまり、データは段から段へ渡り、エージェントから道具へ渡り、道具からまた戻ってくる。

この境目で、データを確かめる必要がある。

必要な値はそろっているか。日付は確定しているか。宛先はあるか。数量は範囲内か。予算を超えていないか。文字列としては入っているが、意味としては空ではないか。前の段が「たぶん」と書いたものを、次の段が確定事項として扱っていないか。

こうした確認は地味である。だが、ここを省くと、壊れたデータが後ろへ流れる。

後ろの段で失敗したときには、原因が見えにくくなる。実行の段が失敗したように見えて、実は選択の段で日付が曖昧だったのかもしれない。通知の段が失敗したように見えて、実は宛先が空だったのかもしれない。外部の道具が悪いように見えて、実は渡した入力が壊れていたのかもしれない。

境目で止めれば、その場で分かる。

「この入力では実行できない」と言える。足りない情報を取りに戻れる。利用者に確認できる。少なくとも、壊れたまま世界に手を出すことは避けられる。

これは安全の話ではなく、編成の話である。

段を分けたなら、段の入口と出口を確かめる。道具を呼ぶなら、呼ぶ前の入力と、返ってきた出力を確かめる。短く区切るだけでなく、区切り目で品質を落とさないようにする。

それが、L4 の実務である。

止まらないときのために、硬い上限を置く

もう一つ、外からの歯止めが要る。上限である。

エージェントは、うまくいかないときに、同じ場所を回り続けることがある。別の候補を探す。別の言い方を試す。もう一度道具を呼ぶ。少し条件を変える。さらに別の候補を探す。

人間から見ると、もう十分に失敗している。しかし、エージェントの内部では、まだ次の一手があるように見える。結果として、時間、計算資源、外部呼び出しの回数を使い続ける。

だから、硬い上限を置く。

段数の上限。道具を呼ぶ回数の上限。使えるトークン量の上限。実行時間の上限。再試行回数の上限。候補を広げる幅の上限。どれも、曖昧な目安ではなく、実行を止める条件として置く。

上限は、エージェントを賢くするためのものではない。賢く振る舞えなかったときに、被害を有限にするためのものだ。

たとえば、検索を三回までにする。予約候補の確認を五件までにする。一つの段は二分で打ち切る。一つの依頼全体で使える量を決めておく。数字そのものは仕事によって変わる。大事なのは、決めておくことである。

上限がないと、失敗は終わらない。

うまくいかないエージェントが、同じところで延々と回り続ける。しかも、そのたびにもっともらしい次の手を出す。見た目には努力しているように見えるが、実際には費用と時間を食い潰している。

上限は、その状態を外から切る。

「ここまで試してだめなら、止める」。この単純な線があるだけで、システム全体の扱いやすさは大きく変わる。

根にあるのは、一つの見方である

ここまで見たものは、ばらばらの小技に見えるかもしれない。

失敗したら再試行する。同じ呼び出しを一回分として扱う。段の境目でデータを確かめる。時間や回数に上限を置く。どれも個別の工夫に見える。

だが、根は一つである。

道具の呼び出しを、手元の確実な関数呼び出しとして見ない。離れた相手への、失敗しうる呼び出しとして見る。

そう見れば、次の設計は自然に出てくる。

失敗するから、やり直す。やり直すから、二重実行を防ぐ。壊れたデータが渡ることがあるから、境目で確かめる。止まらなくなることがあるから、上限を置く。

これは、賢い指示文の話ではない。

もちろん、指示文は大事である。何をしてよいか、どう判断するか、どう報告するかを言葉で与える必要はある。だが、指示文だけでは、外部呼び出しの性質は変わらない。ネットワークは揺れる。相手側は遅れる。返事は失われる。途中の状態は曖昧になる。

だから、L4 では、言葉の設計だけでなく、呼び出しの設計が必要になる。

ここを押さえると、エージェントの失敗は少し別の姿で見えるようになる。モデルが賢いかどうかだけではない。道具をどう呼ばせているか。失敗したときにどう扱うか。どこで止めるか。どの境目で確かめるか。

任せられるシステムに近づくためには、この地味な規律が要る。

Agentic OS への含意

これで、L4 の編成を三つの面から見たことになる。

第13回では、間で何が起きたかを見えるようにした。複数の段やエージェントが動くなら、途中の判断、入力、出力、失敗を追える必要がある。

第14回では、長い鎖を短く区切る型を見た。信頼性の崖に対して、一つの大きな推論にすべてを背負わせるのではなく、役割を分け、段ごとに扱う。

そして今回は、一手ごとの歯止めを見た。道具の呼び出しを分散した相手への呼び出しとして扱い、再試行、冪等、境界の検証、上限を入れる。

この三つがそろうと、複数のエージェントは、ただ「賢く動くもの」から「任せられるもの」に近づく。

オペレーティングシステムは、プログラムをただ速く走らせるためだけにあるのではない。プロセスを分け、資源を割り当て、失敗が全体へ広がらないように囲う。観測し、止め、やり直せる単位を作る。

Agentic OS の L4 も、それに近い仕事をしている。

エージェントを走らせるだけでは足りない。どこで何が起きたかを見えるようにし、長い仕事を短い段に分け、外部への一手ごとに歯止めをかける。そうして初めて、知的な振る舞いは、運用できる振る舞いになる。

次に進む層では、ここまで何度も顔を出してきた、もう一つの横断的な問題を見る。

エージェントが外の世界に手を出すとき、何を信じ、何を止めるのか。L5、安全の層である。

← 一覧へ