アプリケーションがtoolsを呼び始めた瞬間、モデルの出力は読むためのテキストであることをやめ、行動への指示になる。そしてこのシリーズがここまで積み上げてきた保証は、すべてtokenについてのものだった。フレームワークはそのループを書きやすくしてくれるが、同じ動きの中でスロットリングを隠し、ゲートウェイのルーティングを迂回し、計測された1リクエストを誰も計画していなかった11リクエストに変えることができる。第7回はアプリケーションにretrievalを与えた。本稿はその上に載るものの話であり、とりわけ、プラットフォームがすでにやっているという理由でフレームワークがやるべきではない仕事の話だ。

フレームワークをゲートウェイへ向け、そこで止める

仕組み自体は自明で、だからこそ規律を明示しなければならない。orchestrationフレームワークはOpenAI Chat Completionsフォーマットを話し、ゲートウェイはまさにそれを公開しているので、統合はbase URLとクレデンシャル1つで済む。

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    base_url="https://apim-genai-prod.azure-api.net/llm/v1",
    api_key=gateway_token,        # Entra ID token, not a model key
    model="chat-default",         # gateway alias, never a deployment name
    max_retries=0,                # the gateway owns retry and failover
    timeout=60,
)

max_retries=0が肝心の行で、そして人が真っ先に消す行でもある。フレームワークはクライアント側のretry、指数バックオフ、しばしばプロバイダーfallbackまで同梱して出荷される。モデルプロバイダーと直接話すアプリケーションにはどれも妥当な既定値だが、すでにルーティング、ロードバランシング、circuit breakingを行うゲートウェイの前では、どれも積極的に有害だ。

クライアント側のretryが第3回のcircuit breakerに何をするか考えてみるといい。breakerがトリップして503を返すのは、まさに呼び出し側が故障中のバックエンドを叩き続けるのをやめさせるためだ。503をバックオフ付きで5回retryするよう設定されたフレームワークは、その信号を5倍の負荷に変換し、しかもバックエンドが最も受け止められないタイミングで届ける。429にも同じことが言える。token制限は需要を整形するために存在し、それを突き破ってretryするクライアントは整形されていない。retryの置き場所は1つで、その場所はすでに存在する。

正直な但し書きも添えておく。接続レベルの失敗に対するクライアント側での1回のretryは問題ない。このルールが対象にするのはゲートウェイ自身の返答をretryすることであって、ネットワークの不調ではない。

フレームワークがなお担うべきもの

provider abstraction、ルーティング、retry、fallbackを職務記述から取り除いた後、フレームワークに残るのは、手で書くのが本当に面倒で、かつプラットフォームの関心事を含まない部分だ。型付き入力のプロンプトテンプレート、関数シグネチャからのtoolスキーマ生成、構造化された型へのアウトプットのパース、そしてtool callを後続メッセージへ変えるループの帳簿付け。これは実際にかなりの量の仕事で、依存を取る理由として妥当だ。

なってはいけないのは、モデル選択が住む場所になることだ。上のコードのエイリアスはゲートウェイのエイリアスで、第2回のモデルカタログが解決する。フレームワーク自身のモデルレジストリ、独自のfallbackチェーン、独自のプロバイダーリスト。それらはすべて2つ目のカタログであり、モデルが引退する日に1つ目と食い違う。

toolsは第2のサーフェスであり、最初の扉を共有できる

agentのtoolsはモデルと同じ扱いに値する。統治された単一のエントリポイント、テナントごとのアクセス、監査証跡。API Managementは、すでに管理しているREST APIをリモートMCP serverとして公開し、選択したオペレーションをMCPクライアントが呼ぶtoolsとして発行できる。Developer、Basic、Standard、Premiumの各tierとそのv2バリアントで利用可能だ。そのMCP serverをproductに関連づければ、toolアクセスはモデルアクセスと同じproductとsubscriptionで管理される。これこそが要点だ。identityは1つ、quotaの話は1つ、取り消す場所も1つ。

今日どこまで行けるかは、2つの制限が決める。マネージドREST APIから公開されたserverについて、API ManagementがサポートするのはMCP serverのtoolsのみで、MCPのresourcesとpromptsは対象外だ。そしてMCP serverの機能はworkspacesではサポートされない。これは、第5回がworkspacesを指し示した、まさにその隔離されたチームに効いてくる。workspaceゲートウェイとMCP toolの発行の両方を必要とするチームはどちらかを選ばなければならず、それはアーキテクチャ図を描いている段階で学んでおいた方がいい。

プロンプト経路上のcontent safety

agent loopは入力サーフェスを広げる。取得したドキュメント、toolの出力、ユーザーのテキストがすべてプロンプトに行き着く。llm-content-safetyポリシーは、モデルが見る前にコンテンツをAzure AI Content Safetyへルーティングする。

<llm-content-safety backend-id="content-safety-backend" shield-prompt="true">
  <categories output-type="EightSeverityLevels">
    <category name="Hate" threshold="4" />
    <category name="Violence" threshold="4" />
  </categories>
</llm-content-safety>

3つの属性は、コピーではなく注意深い読解が要る。thresholdは上限ではなく許容度として動くthreshold="4"ならフィルターはseverity 0から3を許可し、4から7をブロックする。つまり数字を上げると許容度が上がり、ブロックは減る。第6回のキャッシュのスコアthresholdと同じ反転で、同じやり方で人を引っかける。

shield-prompt="true"は敵対的なユーザー攻撃のチェックを有効にする属性で、既定はfalseだ。取得したドキュメントをコンテキストに連結するagentにとって、この既定値は間違っている。現実的な脅威はドキュメント経由で届くprompt injectionであって、ユーザーがチャットボックスに攻撃を打ち込むことではない。

enforce-on-completionsも既定はfalseで、ポリシーがinboundにある場合にチェックをモデルのレスポンスへ拡張する。そしてwindow-sizeは、Content Safetyの制限である10,000文字が既定で、設定できるのはレスポンスに対してのみだ。リクエストには常に既定のウィンドウが使われる。したがって非常に長い取得コンテキストは、自分では制御できない同じウィンドウ処理で評価される。これは、フィルターがそれに合わせてスケールすると信じるのではなく、retrieval結果を有界に保つべきだという論拠になる。

ループは第5回のすべての数字に何をするか

4回のtool callを引き起こす1つのユーザー質問は、1リクエストではない。5回のモデル呼び出しであり、それぞれが成長していくトランスクリプトを再送する。プラットフォームへの帰結は3つ。

  • 毎分quotaはユーザー量の話ではなくなる。ユーザー量に平均ループ深度を掛けたものになり、ループ深度はアプリケーションのプロンプトの性質で、プラットフォームレビューなしに変わる。
  • 暴走したループは負荷と区別がつかない。ゲートウェイに見えるのはquota内の整形式のリクエストだけだ。イテレーションの上限はアプリケーション自身が設けるしかなく、プラットフォームはそれを前提にするのではなく要件として公表すべきだ。
  • チャージバックには、リクエスト単位ではなくユーザーインタラクション単位のcorrelation IDが要る。さもなければチーム別の数字は正しくて役に立たない。そのIDはログレコードに属し、第5回が高カーディナリティのデータをそこへ置いたのは、まさにこの理由からだ。

ゲートウェイでのリクエストあたりtoken上限は、有用なバックストップであって代替ではない。1回の巨大な呼び出しの被害には蓋をするが、200回の小さな呼び出しをするループには何もしない。

注視すべき失敗モード

  • circuit breakerを打ち負かすフレームワークのretry。既定はオンだ。意図的にオフにし、理由を文書化すること。さもなければプラットフォームの保護機構が負荷増幅器になる。
  • フレームワーク内部の2つ目のモデルカタログ。引退の日にゲートウェイのカタログと食い違い、アプリケーションは誰も知らなかったデプロイメント名にピン留めされる。
  • 上限として読まれたcontent safetyのthreshold。高いほど許容的だ。誤検知を調整しようとするチームは、厳しくしたつもりで実質的にフィルターを無効化できてしまう。
  • 既定のまま放置されたshield-promptオフだ。取得したドキュメントを取り込むagentでは、それは開けっ放しのinjection経路だ。
  • 有界でないループ。「6イテレーションで止まれ」を表現するゲートウェイポリシーは存在しない。できるのはアプリケーションだけで、だからそれはコントロールではなく契約だ。
  • MCPとworkspacesの併用。今日は一緒にはサポートされない。チームに両方を約束した後で発見するのは、アーキテクチャのロールバックだ。

第9回が受け継ぐもの

orchestrationを備えたアプリケーション、同じゲートウェイ経由で発行されたtools、そしてリクエスト経路上のcontent safety。そのすべてがポリシーXML、エイリアスのマッピング、product定義の中で設定されており、現在は他のあらゆる変更と同じやり方で本番に到達している。まだ検証していないのはその部分だ。次回はAzure DevOps、そしてモデルバージョンの変更をレビューコメントではなく評価でゲートするとはどういうことかを扱う。

次に読む

これを大規模に運用する際のインフラおよびプラットフォーム側の話はercan.cloudのフィールドノートに、ハブはercanermis.comにある。

参考資料