この記事では 5 つの設計原則を横断し、food-delivery-demo の各サービスで「原則がぶつかりやすい点」にどう対応したかを実装から整理します。Organized around Business Capabilities を守りながらエージェントを配置する具体例です。

サービス設計の出発点 ── 「エージェントを置くかどうか」

food-delivery-demo を設計するとき、最初に決めたことは「どのサービスにエージェントを置くか、置かないか」です。

エージェントを置く基準はひとつです。自然言語の入力を解釈して、業務上の判断を下す責務があるかどうか。この責務がなければ、エージェントを置いても複雑さが増すだけです。

結果として、エージェントを持つのは order-servicemenu-servicekitchen-servicedelivery-servicesupport-serviceregistry-service の 6 サービスです。payment-servicecustomer-service はエージェントを持ちません。以下でそれぞれの設計を見ていきます。

8 サービスのエージェント配置。意図解釈・業務判断の責務があるサービスのみエージェントを持ちます。

graph TB
  subgraph yes["エージェントあり(6 サービス)"]
    OS["order-service\norder-intake-agent\n意図分類・ワークフロー調整"]
    MS["menu-service\nmenu-agent\ncatalog grounding"]
    KS["kitchen-service\nkitchen-agent\n在庫・調理確認"]
    DS["delivery-service\ndelivery-agent\nETA 比較・配送推奨"]
    SS["support-service\nsupport-agent\n会話型サポート"]
    RS["registry-service\ncapability-registry-agent\ncollaborator 解決"]
  end
  subgraph no["エージェントなし(2 サービス)"]
    PS["payment-service\n決定論的コード\n課金・承認"]
    CS["customer-service\n決定論的コード\n認証・JWT"]
  end

menu-service はこのデモで最も設計の変遷が大きかったサービスです。初期は「order-service から kitchen-service への経由点」という位置づけでしたが、最終的に「order-service から handoff された正規化済み意図を live catalog に grounded な商品提案へ落とし込む単一エージェント」に落ち着きました。

単一エージェント、1 ターン完結という設計が鍵です。エージェントが 1 回の実行で構造化出力を返します。出力には「ユーザーが明示指定したアイテム」と「エージェントが追加提案するアイテム」を別フィールドとして持たせています。

2 フィールドを分けた理由があります。「明示指定されたアイテムは必ず含む」「追加提案は断ることができる」という UI 上の分岐を、型レベルで保証するためです。direct item request に対しては明示指定を最優先で grounded し、exact match がない場合は menu 側の代替として追加提案フィールドに明示します。文字列の自然言語出力だけだと、「ユーザーが言ったか、catalog grounding の結果なのか、代替提案なのか」を後から解析しなければなりません。

スキルの内容だけでなく、スキルを選ぶ条件もスキルドキュメントに書くという設計も menu-service 固有の判断です。各スキルドキュメントには業務知識の内容に加え、そのスキルをいつ使うかの条件記述がメタデータとして含まれています。サービスは起動時にこれをパースしてシステムプロンプトのスキル発動セクションを動的に組み立てます。エンジニアがコードを変更しなくても、業務担当者がスキルドキュメントを編集するだけで発動条件を変えられます。


kitchen-service ── ツール連鎖と代替品承認

kitchen-service のエージェントは「調理ラインが実際に対応できるかを確認する」責務を持ちます。注文候補を受け取り、在庫・調理キュー・代替品の 3 段階でチェックします。

3 段階のチェックにはそれぞれ役割があります。在庫確認は欠品アイテムの特定、調理スケジュール確認はキュー状態を見た ETA 計算、代替品照会は欠品時のみ実行です。

「欠品時だけ代替品クエリを送る」という条件分岐がシステムプロンプトに書いてあります。代替品の問い合わせ先のエンドポイント URL は POST /registry/discover で「欠品時の代替候補提示」という capability query で解決します。kitchen-service は menu-service の存在を直接知りません。

kitchen-service が承認するのは、自分が提供できる代替品だけというルールもシステムプロンプトで定義されています。menu-service が候補を返しても、調理ラインが対応できないものは承認しません。この「承認する側と提案する側を分ける」という構造が、2 サービスにまたがる責務分離の形です。


delivery-service ── 外部 ETA 比較とリスク評価

delivery-service は「最適な配送オプションを選ぶ」判断をエージェントに委ねます。選択肢は「自社急配(In-house Express)」と「外部パートナー(Partner Standard)」です。

外部パートナーの探索は capability クエリを使います。サービス名ではなく「外部 ETA を提供するサービス」という能力記述でクエリを送り、registry が現在 AVAILABLE なパートナーサービスを返します。パートナーが増減しても delivery-service のコードは変わりません。

続けて各パートナーに ETA・混雑度・料金を問い合わせます。このとき URL は registry が返したものをそのまま使います。エージェントが URL を自前で組み立てることはありません。また、このエンドポイントには SSRF ガードが入っており、registry に登録された URL しか呼べません。

エージェントは最終的に推奨配送オプション(自社急配または外部パートナー)とその理由を structured output として返します。「外部パートナーが全員 NOT_AVAILABLE のとき」は自社急配を推奨するという fallback もシステムプロンプトに書いてあります。

テスト用のアダプターは周期的に停止するパートナーと常時 AVAILABLE なパートナーという 2 種類の振る舞いを持ちます。この設定により、「外部パートナーが 1 件しか取れないとき」「2 件取れるとき」の比較動作を実際に確認できます。


order-service ── 4 ステップ workflow コーディネーター兼 front door

order-service はエージェントを持ち、その役割は「4 ステップのワークフローを調整すること」と「suggest ステップの自然言語 front door になること」の 2 つです。order-intake-agent がユーザー入力の一次解釈を担い、その結果を各 downstream へ渡します。

4 ステップは suggestconfirm-itemsconfirm-deliveryconfirm-payment です。各ステップで別のサービスを呼び出し、結果をユーザーに返します。

  • suggest → order-intake-agent が direct item request / recommendation request / reorder / refinement を正規化し、その handoff を menu-service に渡して商品提案を依頼
  • confirm-items → kitchen-service に在庫・調理確認を依頼
  • confirm-delivery → delivery-service に ETA・配送オプション比較を依頼
  • confirm-payment → payment-service に支払い処理を依頼

各ダウンストリームの URL 解決は RegistryServiceEndpointResolverPOST /registry/discover を使って行います。capabilityQuery には「catalog grounding」や「配送オプション比較」といった責務記述を設定し、registry から endpoint を受け取ります。

order-service のエージェントは各ステップの入力を整形・意図分類する役割を担います。ユーザーが「やっぱりデザートも追加したい」と言ったときに、それが提案の再実行なのかアイテム選択ステップでの変更なのかを型付きで分類します。menu-service はこの一次解釈をやり直さず、受け取った正規化済み意図を catalog grounding と menu-side alternatives に変換することへ集中します。

payment-service は意図的にエージェントを持ちません。支払い処理は決定論的コードで実行します。「課金という正確性が必要な処理は LLM に渡さない」という原則の体現です。


設計全体のパターン

各サービスの設計を横断して見ると、共通するパターンが浮かびます。

エージェントは解釈と判断を担い、状態変更はコードが担う。menu-agent が商品を選んでも、在庫を減らすのは kitchen-service のリポジトリです。delivery-agent が配送業者を選んでも、支払いは payment-service の決定論的コードが処理します。

サービス間の協調は capability query で記述する。kitchen-service のコードに "menu-service" という文字列はありません。"欠品時の代替候補提示" という capability の記述があるだけです。これはコードの中に設計意図を残す方法でもあります。

structured output でサービス間の契約を型として定義する。各サービスの出力は構造化された型として定義されており、フィールド名と型がサービス間の契約です。自然言語のテキストをパースして後から構造を取り出すという処理はありません。

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