この記事は Decentralized Governance および Organized around Business Capabilities の観点から food-delivery-demo の registry 設計を掘り下げます。capability-based discovery は、サービスが互いを直接知らなくても協調できる構造を作り、チームの自律性とサービスの疎結合を両立させます。

service mesh が解く問いと、解かない問い

service mesh(Istio、Linkerd など)が解決するのは、L4/L7 レベルの通信管理です。たとえば「order-service から menu-service へ HTTP で呼び出すときに、TLS・リトライ・トレースをどう扱うか」という問いに答えます。

ただし、service mesh が答えない問いがあります。

「このリクエストを処理できる capability を持つサービスは、いま誰か」

service mesh は、あらかじめ定義されたルーティング設定に基づいて通信を制御します。つまり、呼び出し先のサービス名を呼び出し側が知っていることが前提です。注文サービスが「menu-service」というサービス名を静的に設定ファイルに書き、mesh がその名前に基づいて通信を管理する、という構造です。

この前提は、サービス名と役割が事前に確定しているなら問題ありません。ですが「欠品時に代替候補を出せるサービスは今どれか」「外部 ETA を出せるパートナーは今いくつ稼働中か」といった、実行時に解決すべき問いには答えにくくなります。

capability-based discovery という発想

food-delivery-demo に実装した registry-service の POST /registry/discover は、この問いに答えるための API です。

呼び出し側は、サービス名を知らなくても 業務能力の記述 をクエリとして送れます。たとえば「外部 ETA を提供するサービスは?」「欠品時の代替候補提示ができるサービスを教えて」です。ここで使うのは英語キーワードではなく、業務で使う自然言語の記述です。

レスポンスには、そのクエリに最もマッチするサービスのエンドポイント URL と capability 説明が返ります。呼び出し側はそのエンドポイントに対してリクエストを送るだけです。

このクエリ文字列は、設計意図をコードに残す手段でもあります。設定にサービス名を書く代わりに「曖昧な注文意図を具体的な商品提案に変換できるサービス」と書けば、なぜそのサービスを呼ぶのか をコードから読めます。

POST /registry/discover の実行フロー。order-service は menu-service の名前を知らず、capability クエリ経由で endpoint を取得します。

sequenceDiagram
  actor Caller as order-service
  participant Reg as registry-service
  participant RA as capability-registry-agent
  participant Target as menu-service

  Caller->>Reg: POST /registry/discover
  Note right of Caller: query: メニュー提案<br/>在庫付き提案 注文候補
  Reg->>RA: capability マッチング(LLM)
  RA-->>Reg: menu-service を選択
  Reg-->>Caller: {endpoint: ".../internal/menu/suggest", ...}
  Caller->>Target: POST /internal/menu/suggest
  Target-->>Caller: MenuSelectionDecision

registry の内側とクライアントパターン

POST /registry/discover を受け取った registry は、内部でエージェントを使って照合を行います。登録済みサービスの capability 説明と送られてきた自然言語クエリを照合し、最もマッチするサービスを選択結果として返します。registry そのものがエージェントを内包している、という構造です。

呼び出し側のサービスは、ダウンストリームごとに capability query を設定として保持します。この文字列は「曖昧な注文意図を具体的な商品提案に変換できるサービス」「欠品時の代替候補提示ができるサービス」のような自然言語の役割記述です。サービス名ではなく能力の記述を設定として持つ、という逆転がここにあります。

GET /registry/services との使い分け

POST /registry/discover と GET /registry/services は意図が異なります。

POST /registry/discover は実行時の collaborator 解決用です。「今、このリクエストを処理できるのは誰か」を問います。エージェントが自然言語クエリで呼び出す、ランタイムパスの API です。

GET /registry/services は棚卸し・閲覧用です。登録済みの全サービスを返します。customer-ui の /agents ページが呼び出し、サービスカード・system prompt・スキル一覧を表示するために使います。ランタイムの協調ではなく、人間や監視ツールが「今どんなサービスが登録されているか」を確認するための API です。

この 2 つを同じ API にしなかったのは、用途が根本的に違うためです。discover はクエリ付きで呼ばれ、エージェントが LLM で候補を絞り込みます。services は全件を返す読み取り専用 API です。同一化すると、discover 側の「LLM が選ぶ」挙動を viewer 側にも持ち込んでしまいます。

POST /registry/discover(サービス間の実行時解決)と GET /registry/services(UI 向け一覧)は意図が異なる 2 つの API です。

graph LR
  subgraph rt["実行時 collaborator 解決(POST /registry/discover)"]
    S1[order-service] -->|capabilityQuery| R[registry-service]
    S2[delivery-service] -->|capabilityQuery| R
    S3[kitchen-service] -->|capabilityQuery| R
    R -->|endpoint URL| S1
    R -->|endpoint URL| S2
    R -->|endpoint URL| S3
  end
  subgraph vw["棚卸し・閲覧(GET /registry/services)"]
    UI["customer-ui\n/agents"] -->|全件取得| R
  end

service mesh との関係

capability-based discovery は service mesh の代替ではありません。

service mesh は通信の信頼性と可観測性を担います。capability-based discovery は「誰に送るか」を決めます。food-delivery-demo でも、もし本番を想定するなら registry-service の resolve 結果で得た endpoint URL に対して service mesh 経由でリクエストを送ることは十分ありえます。

ただし、AI エージェントが協調する構造では、呼び出し先のサービス名を設計時に確定することが難しいケースがあります。delivery-service が ETA 比較のために呼ぶべき外部パートナーの数は、運用中に変わります。kitchen-service が欠品代替を委ねる先が「menu-service」という固定名でよいのか、将来別のサービスが担うかもしれない。そういう構造に対して、「設定ファイルにサービス名を書く」という前提は脆くなります。

capability-based discovery が解くのは、この 設計時に固定できない協調関係 です。mesh より上位のレイヤーで、「誰が担当するか」を業務能力の記述で解決します。

この設計のトレードオフ

正直に書いておくと、この設計にはコストがあります。

POST /registry/discover のたびに LLM が動きます。food-delivery-demo では RegistryServiceEndpointResolver 側でキャッシュしているため、起動後しばらくすると registry への問い合わせは減ります。しかし、キャッシュが切れるたびにエージェントが走ります。

また、capability の記述と query の文言が食い違うと、発見が失敗します。LLM が解釈する余地が入ると、意図していないサービスが選ばれる可能性があります。このため、capability クエリが期待するサービスを正しく返すことは回帰テストで保護する必要があります。

さらに、registry-service が単一障害点になります。food-delivery-demo では registry が落ちると resolve が止まります。本番化するなら、キャッシュ戦略と registry の冗長化が必要です。

それでもこの設計を採用したのは、変化する協調関係を宣言的に記述できる 利点を優先したためです。呼び出し側コードに「menu-service」と固定名を書く代わりに、「曖昧な注文意図を具体的な商品提案に変換できるサービス」と書く。この文字列自体が、設計意図の記述になります。

この断面を含む探索全体の評決は Conclusions にまとめています。