※この日本語記事は英語記事を元にClaudeを用いて翻訳したものをマネージャーがレビュー修正したものになります。

本番環境でのLLM開発は、どの会社でも似たような道をたどります。最初に動くプロトタイプは、多くの場合外部モデルのAPIを呼び出すだけの薄いラッパーです。しかし、本当の意味でのシステムが姿を現すのは、最初の本番サービスを作るときです。非同期実行、モデルのガードレール、ユーザー体験、オブザーバビリティ、そして「これらの関心事を誰が受け持つのか」という問題です。本番化の段階になって、こうした課題が一気に押し寄せてきます。

ビズリーチも、自社プロダクトの開発で同じ結論にたどり着きました。「ビズリーチ」は、求職者が登録したレジュメをもとに、企業のリクルーターやヘッドハンターが直接スカウトを送ることができる転職プラットフォームです。2023年7月には、GPTを活用したレジュメ自動作成機能を公開しました。当時の評価の結果、このツールで作成したレジュメではスカウト受信数が平均で40%増加(※)しています。これはChatGPTの公開から間もなく取り組んだ、私たちにとって最初の本番LLMのPoCでした。複数のステークホルダーが安全性・プロダクト価値・運用リスクを評価するため、プロジェクトは慎重に進められました。そこから生まれたのが、社内で「SAR(Sender-Agent-Receiver)アーキテクチャ」と呼んでいる仕組みです。その後、企業側でも求人自動作成機能という形で、同じようなプロダクト要望が出てきました。

ただ、ここでの本当の学びは「LLM機能には価値がある」ことではありませんでした。一つのサービスが成功すれば、他でも同じような機能を求めるようになります。こうした状況で必要なのは、単純にコードをサービスごとにコピーすることではなく、プロダクトのLLM機能を素早く・安全に・使いやすく持続的に展開するためのプラットフォームを構築することでした。

本記事のテーマは、本番運用で鍛えられた単一のLLMサービスから、共有のLLMプラットフォームへの移行です。何を標準化し、なぜ最初のプラットフォームをあえて「ひとつのリクエスト・ひとつの生成ライフサイクル」に絞り込んだのか。その過程をご紹介します。

2023年の最初のPoCから2025年のLLMプラットフォームまでのタイムライン。曲線が後半で急になり、プラットフォームが取り組みの出発点ではなく、デリバリー速度を高めたことを示している。
共有のランタイム機能がプラットフォームになった時点で、ビズリーチのLLMの歩みは加速しました。

1. 最初のサービス:本番品質を満たすAPIラッパーの実装

2023年、多くの開発者がLLMアプリケーション開発で同じ「最初のパターン」を学んでいました。OpenAIのAPIをラップし、プロンプトを足し、エンドポイントを公開して、試しながら改善していく。このやり方はプロトタイピングには有効ですが、本番運用にはこれだけでは足りません。

ビズリーチの最初のLLMサービスも、単なる同期ラッパーでは済みませんでした。LLMの呼び出しは、通常のWebリクエストに期待される時間と比べると、はるかに長くかかる処理です。出力の性質も、従来の多くのML APIのレスポンスとは異なります。長文テキストなら、最終結果まで沈黙を保つより、生成できた分から順に見せてユーザーをつなぎとめるほうが、体験として優れています。当時は生成AIのレッドチーミングもまだ発展途上で、プロンプトインジェクションやジェイルブレイクといった手法による悪用が現実的な懸念となっていました。入力と出力には、ビズリーチのプロダクトに期待される品質を保つためのガードレールが欠かせません。クライアント側のAPIも、生成が進行中なのか、完了したのか、タイムアウトしたのか、それともガードレールや外部障害で打ち切られたのか、こうした状態を見分けられなければなりません。

最初のサービスでは、システム設計そのものが課題でした。SARは、プラットフォーム化以前から採用しているアーキテクチャです。Sender-Agent-Receiverの略で、ざっくり言えば、LLM特有のライフサイクルをまとわせたweb-queue-workerパターンです。それぞれの役割が独立したステージになっているため、時間のかかるモデル呼び出しの間も同期接続を開いたまま保持し続ける必要がありません。

  1. Sender はリクエストを受け付けて検証し、処理をキューに渡すと、すぐに request_id を返します。
  2. Agent はその処理を非同期に取り出して生成を実行し、できた分から順に出力を書き込みます。
  3. Receiver はクライアントからのポーリングに応じて、利用可能になったチャンクから生成結果を返します。

この設計によって、「APIをラップしただけ」というメンタルモデルは、本番に耐える形へと鍛えられました。公開APIはクライアントから見ればシンプルなまま。その裏側で、リクエストの受付・生成の実行・結果の取得を切り分けています。3つのコンポーネントは運用上のシグナルに応じて独立にスケールでき、キャパシティプランニングもデバッグも楽になりました。

プロトタイプのクライアント対LLMラッパーが、Sender・Queue・Agent・LLM・Receiverからなる本番ランタイムへと拡張され、Request・Status・Dataというライフサイクルの成果物を伴っている。
SARは、生成のライフサイクルを背後で明示的に扱うことで、クライアントのインターフェースをシンプルに保ちました。

この設計は、本番環境できちんと機能しました。強調しておきたいのは、後のプラットフォームは行き詰まったシステムを救済するために生まれたわけではない、ということです。むしろ、最初のサービスがプロダクトとしての価値をしっかり証明し、「どのランタイムの関心事が作り直すにはコストが高すぎるか」をはっきり教えてくれました。だからこそ、それを全社に一般化する形で生まれたのです。

本番運用では、ほころびも見えてきました。すぐに表面化した問題もあります。管理されたテストでは安全に見えたタイムアウト値が実ワークロードでは調整を要したり、並行ワーカーのチューニングが必要になったり、モデレーションで実入力でしか出ない偽陽性があったりしました。監視とアラートでTTD(time-to-detect、検知までの時間)を、ログの改善でTTM(time-to-mitigate、緩和までの時間)を縮められました。

もっと見えにくい問題もありました。後のプラットフォーム開発でAPMトレースを調べたとき、データとステータスの書き込み順序がACIDの一貫性(Consistency)に反していることが分かりました。単一のプロダクトに閉じた実装では目立ちませんでしたが、クライアントへのチャンク提供を遅らせる原因になっていました。

こうした経験を通じて、私たちの見方は変わりました。外から見ると、最初のLLM機能は、当時LLMのリスクやプロンプトエンジニアリングへの注目もあり、主にプロンプトの問題に見えていました。けれど次第に、これはプロダクト全体と障害モードをどう設計するかという問題なのだと分かってきました。プロンプトの品質はもちろん大事です。ただ、本番での信頼を左右するのは、モデルが遅い、出力が途中で切れる、フォーマットを外す、モデレーションで止まる、あるいはプロダクトチームの手が届かない上流の挙動に影響される、といったときに何が起きるかです。

2. サービスのプラットフォーム化:ランタイムリスクのShift down

最初のサービスが唯一のLLM機能のままだったなら、専用実装のままでも問題なかったかもしれません。プロダクトに密着した実装でしたし、運用チームもその経緯を理解していました。

2つ目のサービスは、この問いを変えました。一見すると、これは単純な再利用の問題に見えます。実績のあるSARアーキテクチャをコピーし、初回ローンチで苦労して得た本番向けの修正を引き継ぎ、エンドポイントごとにプロダクト固有のプロンプトとスキーマを足していけばいい。そう考えてしまいがちです。

しかし、この思い込みこそが落とし穴です。

うまくいったシステムアーキテクチャは再利用できそうに見えますが、その価値の多くは水面下にあります。目に見える部分、つまりエンドポイント、スキーマ、プロンプト、サービスコードをコピーするのは簡単です。本当に効いているのは、もっと地味な部分です。リクエストのライフサイクル、ステータスのセマンティクス、チャンキング戦略、ガードレールのパス、オブザーバビリティ、デプロイ上の制約。最初のサービスを本番で信頼できるものにしたのは、こうした地道な積み重ねでした。これを共有のアーキテクチャに落とし込まず、構造だけコピーすれば、新しいサービスはあるべき挙動から少しずつずれていってしまいます。プロダクトチームは、自分たちのドメインの意図に責任を持つべきですし、それは当然のことです。しかし、複数ドメインのサービスが同じ実行モデルに依存し始めた瞬間、その構造を守ることはプラットフォームの責務になります。

現実的な選択肢は3つありました。

選択肢 何を守れるか 何をリスクにさらすか
フォーク版コードベース 各チームがスタック全体の複製を所有し、プロダクト要件に合わせて自由に進化させていける SARの実装とバグ修正が各チームで重複し、本番挙動が時間とともに乖離していく
共有ライブラリ/SDK 共通コードを中央でバージョン管理しつつ、各サービスがリクエストライフサイクルを自前で構成できる コードは共有できても、ライフサイクルの組み立てが各サービス任せのため、運用上のセマンティクスがずれていく
共有プラットフォーム リクエストライフサイクルと運用モデルを共通化し、一貫した挙動と高い信頼性を確保できる 共有基盤への依存と結合が強まり、各サービス側の柔軟性が低下する

私たちは共有プラットフォームを選び、ランタイムをプラットフォーム化しました。フォーク版コードベースでは、各サービスが似た機能を別々の時期に、そのたびに少しずつ違う形で実装していきます。これでは実行モデルの一貫性を保つのが難しく、積み重なる乖離が将来の障害点を増やします。また、サービスごとに負荷も使われ方も異なるため、不具合はどれか1つのサービスで先に見つかります。共有ランタイムがあれば、その修正は、同じ問題が他のサービスで起きる前に行き渡ります。プラットフォームがなければ、修正は見つかったサービスに閉じ、チームの都合などから他のサービスに同じ修正が入るとは限りません。こうした修正や機能は、コードの上ではヘルパー関数のようなかたまりに見えます。しかしその実体は、実行モデルの挙動を保証する「運用上の取り決め」です。Instacartも、同社のMapleプラットフォームで同様の課題に触れています。複数チームがLLM処理を必要とし始めると、運用対応が分断されたままでは立ち行かなくなる、と。私たちのワークロードは違いましたが、構図には見覚えがありました。

Googleのプラットフォームエンジニアリングに関する記事では、繰り返し現れる責務を下層のプラットフォームへ移し、開発者の運用負荷を減らすことを「シフトダウン(shift down)」と呼んでいます。この言葉は私たちの状況によく合っていて、次の一歩で何をするかという問いを考えるのに役立ちました。「どの本番運用上の責務が、すべてのプロダクトの内側に残すには重すぎるのか?」という問いです。

これに答えるには、「品質特性」を洗い出し、プロダクトの意図とランタイムのリスクを切り分ける必要がありました。

一方、プロダクトの意図は、エンドポイント、スキーマ、プロンプト、出力への期待、ユーザー体験の側にあります。

プロダクトの意図(ルーティング、スキーマ、プロンプト、UX)が設定境界の上に位置し、その下の共有ランタイムがキューイング、ステータス、チャンキング、ガードレール、オブザーバビリティ、スケーリングを所有している。
繰り返し現れるランタイムのリスクは共有の実行モデルへと下りていき、プロダクトの意図はその上にとどまります。

プラットフォーム化とは、「共通コードを共有リポジトリにまとめる」ことではありません。目指したのは、「プロダクト側で繰り返し現れるランタイムリスクを、共有プラットフォームへ移す」ことでした。新たにプラットフォームを利用する各サービスが、自分のペースで同じ本番運用の教訓を学び直すのではなく、共有プラットフォームを通じて最初からその恩恵を受けられるようにすることです。

それをきれいに実現するには、SARを振り返り、それぞれの要素が本番で何をしていたのかを理解する必要がありました。プラットフォームの実行モデルは、このSARをそのまま昇格させたものになるからです。

3. ハーネスとしてのSAR

SARはweb-queue-workerパターンと説明するのが手っ取り早いのですが、それは出発点にすぎません。本番での価値は、長時間かかるLLM呼び出しを、制御されたリクエストのライフサイクルとして振る舞わせたことにあります。クライアントに脆い同期接続を保持させることなく、検証し、キューに入れ、実行し、モデレーションをかけ、永続化し、取得し、観測し、終了させる、という一連の流れです。

2026年現在、業界で使われるようになった言葉で振り返ると、SARは私たちなりの初期の「ハーネス(harness)」でした。ハーネスとは、LLMが行動し、観測し、記憶し、回復し、ガードレールの内側にとどまれるようにする、モデルを取り巻くインフラのことです。このSARというハーネスのおかげで、プロダクト機能はレイテンシ・状態・部分出力・障害モード・ガードレールを一貫して扱えるようになりました。

中央のむき出しのLLM呼び出しを取り巻く円環状のハーネス(検証、キュー投入、実行、モデレーション、永続化、取得、観測)が、部分的なチャンク、最終出力、あるいは終端状態を生み出している。
SARは、むき出しのLLM呼び出しを、制御された生成のライフサイクルへと変えました。モデルを取り巻くハーネスです。

クライアントから見たインターフェースはシンプルなままです。生成リクエストを送り、request_id を受け取り、ライフサイクルが終わるまでチャンクをポーリングする。その裏側の実行モデルには、プラットフォームの責務として切り出した機能がいくつもありました。

ライフサイクルの非同期化:Sender・Agent・Receiver

1つ目の機能は、非同期ライフサイクルの分離です。同期APIラッパーでは、リクエストの受付・モデルの実行・レスポンスの送出が、ひとつの脆いパスに同居します。SARは、ライフサイクルをSender・Agent・Receiverに分割することで、リクエストのパスと生成のパスを意図的に切り離しました。

ライフサイクルの機能 プラットフォームの要素 プロダクトの要素
リクエストの受付 APIルーティング、検証、エラーの整形 エンドポイント定義とOpenAPIスキーマ
リクエストの対応付け request_id の生成、リクエスト/ステータスの永続化 リクエストの識別フィールド
生成のルーティング キューメッセージのスキーマ、ワーカーの実行、生成モードのルーティング プロンプトロジック、生成パラメータ、出力モード
結果の取得 ステータス参照、チャンク参照、ステータスのマッピング ユーザー向けレスポンスのセマンティクス

最大の利点は、各コンポーネントをそれぞれ異なるシグナルに応じてスケールできる点です。SenderとReceiverは レイテンシ律速(latency-bound) で、リクエストレート(RPM)とp99レイテンシに応じてオートスケールします。一方でAgentのプールは スループット律速(throughput-bound) で、キューの深さと実行中の生成並行度に応じてオートスケールします。LLMの生成は、Webリクエストの受付より桁違いに時間がかかります。もし両者をひとつのサービスに束ねていたら、ワーカーのキャパシティを増やすためだけにWebサーバを過剰に積む羽目になっていたはずです。この「インピーダンスのミスマッチ」を吸収するのがキューです。バーストを受け止め、ワーカーが見る到着分布をならし、バックプレッシャーを明示的に扱えるようにします。その結果、キャパシティプランニングは正確で無駄のないものになりました。

クライアントは、キューのタイミングもワーカーの状態も、内部の永続化の詳細も知る必要がありません。知るべきは、チャンクを消費するのか、ポーリングを続けるのか、それとも止めるのか、それだけです。この切り分けこそが、プラットフォーム化の問いへの答えでした。プロダクトチームは、生成が「どのように」システムを流れるかを意識することなく、「何を」生成するかだけに集中できます。

データの永続化:Request・Status・Data

2つ目の機能は、リクエスト・ステータス・生成出力を、キーバリューストア上のそれぞれ別テーブルで管理することです。この分離によって、各SARコンポーネントは、ライフサイクルで生まれる成果物を扱うための、安定した最小限の領域を持てました。

テーブル 主キー 生成者(Producers) 利用者(Consumers) 目的
Request request_id Sender 運用者/アーカイブ処理 デバッグのために元のAPIコンテキストを保存する
Status request_id Sender、Agent Agent、Receiver ライフサイクルの状態と最終更新時刻を追跡する
Data request_id + chunk_key Agent Receiver 生成されたチャンクを保存する
Request・Status・Dataの3つのストアに対して、Sender・Agent・Receiverがそれぞれ書き込み・更新・読み取りを行い、3つのデバッグ用シグナルのルールが添えられている。
リクエスト、ステータス、データを分離したことで、障害の所在を特定しやすくなりました。

これは障害の切り分けにも効きました。各成果物には責任を持つステージが明確に定まっているため、レコードの欠落や停滞はシステム全体ではなく、担当コンポーネントの不具合を示します。上の図のデバッグ用シグナルは、症状を生成元のステージまでたどれるようにしたものです。

プラットフォームが担当するのは永続化モデルです。何をいつ書き込み、どのSARサービスが読み取るかを管理します。プロダクトが担当するのは、リクエストのスキーマとレスポンスのフォーマットです。データの形はプロダクトが、そのデータをライフサイクルでどう動かすかはプラットフォームが担当します。リクエストのパスの外では、アーカイブ用のジョブがライフサイクルのレコードをまとめてオブジェクトストレージへ書き出します。これにより、運用中のキーバリューストアは、進行中のリクエスト取得とデバッグに集中できました。

有限状態機械(FSM)としてのステータス

3つ目の機能は、「status」という決定論的な有限状態機械(FSM)で実行を制御することです。密に連携する非同期システムでは、あいまいな中間状態が高くつきます。Sender・Agent・Receiverは別々のサービスですが、ライフサイクルについては共通の語彙が必要です。

非終端状態と終端状態をもつステータスの有限状態機械と、その横に、生成ステータスとチャンクの利用可否をクライアントの挙動へと対応づける解釈の表が並んでいる。
小さな決定論的な有限状態機械が、Sender・Agent・Receiverをライフサイクルの上でそろえました。非終端状態はWIPだけです。

WIP だけが非終端の生成状態です。FINISHED と終端のエラー状態は、ライフサイクルを終わらせます。Receiverはそのうえで、実行状態とチャンクの利用可否を合わせて解釈します。クライアントが知りたいのは「生成が終わったか」だけではなく、「いま要求しているチャンクをどう扱えばいいか」だからです。

生成ステータス チャンクの利用可否 クライアントステータス クライアントの挙動
WIP 要求したチャンクがまだ準備できていない NOT_YET 待って再試行する(ライフサイクルは健全)
WIP 要求したチャンクが存在する SUCCESS チャンクを消費し、次のキーをポーリングする
WIP 生成が停滞し、想定時間内に更新がない TIMEOUT 通常の進行としての再試行を止める
FINISHED 最終チャンクが利用可能 FINISHED チャンクを消費して止める
FINISHED 最終チャンクは書き込み済みだが、要求したチャンクが存在しない NOT_GENERATED 止めるか、欠落した出力に対処する
... TIMEOUTERRORINVALID_REQUEST ... 通常の進行としての再試行を止める

ここでの学びはシンプルです。非同期のLLMサービスには、あいまいな中間状態ではなく、決定論的なライフサイクルの言葉が要ります。プラットフォームが受け持つのは、決定論的なステータスの挙動、すなわち状態遷移・終端条件・Receiverの解釈です。これにより、遅い出力・欠けたチャンク・モデレーションによるブロック・タイムアウトを、クライアントが勝手に解釈することを防げます。プロダクトが受け持つのは、クライアントとして、プラットフォーム標準のステータスに対してどう振る舞うべきかの判断です。

テキストとJSONのチャンク単位取得

4つ目の機能は、チャンク単位の段階的取得です。生成タスクごとに、違う出力フォーマットを返す必要が出てきました。レジュメ生成では、ユーザーが少しずつ読める長文テキストを生みます。一方、後の生成タスクでは、プロダクトのUIが各セクションに流し込める構造化JSONを返す必要がありました。

モード 取得の単位 なぜ重要だったか
text 文字ベースのチャンク 長文の書き出しにおける体感レイテンシを改善する
json トップレベルのキーバリューのチャンク 構造化されたUIのフィールドを段階的に表示できる
textモードは文字ベースのチャンクを、jsonモードはトップレベルのキーを、ひとつの共有ライフサイクルの下でストリーミングする。いずれも、最初のチャンクまでの時間が生成全体の時間よりずっと早く訪れる。
ひとつのライフサイクル、2つの取得単位。段階的な取得が体感レイテンシを改善します。

テキストでは、最終結果を待たせると、実際より遅い機能に感じられてしまいます。レスポンスをチャンク化してストリーミングすれば、ReceiverはAgentが書いたそばから部分出力を返せます。これで 最初のチャンクまでの時間(time-to-first-chunk) を生成全体の時間から切り離せました。進捗が見えることは、UX上とても重要です。

JSONでは、同じライフサイクルを使いつつ、トップレベルのキーバリューを取得単位として扱いました。こうして、構造化生成でも同じポーリングモデルで扱えるようになりましたが、トレードオフもあります。ストリーミングは体感レイテンシを改善する一方、部分出力を返す前にプラットフォームがレスポンスのスキーマ全体をどこまで厳密に検証できるかには制約が出ます。それでもこのトレードオフは受け入れました。本番でも仮説どおり、多くのプロダクトのワークフローは、完全に検証された単一レスポンスを待つより、段階的に届くほうがより多くのメリットを得られました。

チャンキングのロジック、チャンクごとのステータス更新、textjson モードに対するReceiverの解釈はプラットフォームが、出力モードの選択、期待するレスポンス形式、UIの挙動はプロダクトが担います。これにより、異なるプロダクトが同じライフサイクルを使いながら、それぞれのユーザーが期待する形で出力を受け取れるようになりました。

実行パス上のガードレール

5つ目の機能は、実行パス上に配置されたモデレーションのガードレールです。コンテンツのモデレーションをクライアント側の任意処理にせず、より厳格なシステムプロンプトに支えられたSenderとAgentの挙動の一部としました。

入力側では、生成処理をキューに投入する前に、どのリクエストフィールドをモデレーションの対象とするかをプロダクトチームが決定します。これで、安全でない、あるいはポリシー違反のリクエストをモデルに届く前にブロックできます。悪意ある利用が発生した場合も、運用者が早期に気づきやすくなります。

出力側では、Agentが生成したチャンクをデータストアに書き込む前にチェックします。当初は、ストリーミングされるテキストに対するルールベースのパターンマッチングが中心でしたが、プラットフォームの成熟に伴い、ガードレールは多段階のチェックへと拡張されました。具体的には、クラウドプロバイダのコンテンツフィルタ、モデルの拒否応答検知、ブロックリスト検知です。

リクエストが、キューの前段にある入力ゲートと、ストレージの前段にある出力ゲートを通過する。いずれのゲートもライフサイクルを INVALID_REQUEST で終了させ、承認された出力だけが取得に到達する。
モデレーションは組み込みのランタイムステップです。承認された入力だけがモデルに届き、承認された出力だけが取得に到達します。

設定可能なモデレーションの仕組み、失敗ステータス、強制のパスはプラットフォームが、モデレーション対象のフィールド、システムプロンプトの設計、モデレーション用のリソースはプロダクトが受け持ちます。これで、ドメイン固有のモデレーションリソースはプロダクトが制御しつつ、ガードレールの挙動はサービス間で揃えられました。

SARは単なる1サービスの構造ではなく、生成を取り巻くハーネスでした。そのことに気づいたことで、プラットフォーム化への道が開けました。プラットフォームの関心事とプロダクトの意図を別々の言葉で呼び分けられるようになったことで、ようやく両者を切り離せるようになりました。

4. Configuration as Code:公開する設定を最小限に絞る

共有ランタイムの役割が定まると、次の問いは「プラットフォームはどこまで公開するか」です。「シフトダウン」はリスクを共有ランタイムへ移す考え方ですが、その過程で、本来プラットフォーム側へ移すべき部品まで、プロダクト側から触れられる場所に残してしまいがちです。もちろん、利用側に公開できるものはいくらでもあります。しかし、本当に利用側が制御すべきものはどこまででしょうか。プラットフォームチームにとって、この境界の引き方こそが、本物のゴールデンパスを作るのか、それとも個別に作り込まれた技術的負債を抱えるのかの分かれ目になります。

統合のときにプロダクトチームが内部のステートマシンにうっかり触れられたり、重要なリトライの仕組みを上書きできたりすれば、インシデント対応は手探りになり、リスクを下層に移したことにはなりません。必要なのは、もっと厳格な道です。プロダクトチームに見せるのは、意図を宣言するための最小限の範囲だけ。その意図をどう実行するかはプラットフォームが引き受ける。そういう道です。

宣言的なインフラのプラットフォームが強いのは、関心の分離を徹底することで、この境界を守れるからです。Kubernetesは、現代インフラの土台となる考え方として「意図の記録」を確立しました。ユーザーがYAMLマニフェストで望ましい状態を宣言し、コントロールプレーンがそれに合わせ続ける、という考え方です。John Lewisのエンジニアリングチームは、開発者が書くもののうち本当にアプリ固有なのはごく一部だと気づき、Kubernetesの利用範囲をカスタムの Microservice リソースへとさらに絞り込みました。Airbnbは安全面の教訓を加えています。可変な部分を公開するなら、ランタイムに届く前に厳格に検証しない限り危ない、と。

私たちの規模は、こうした大規模インフラの事例よりずっと小さいものでしたが、教訓はよく当てはまりました。狙いは、何でも設定可能にすることではありません。プロダクトに公開する範囲を本当に必要な最小限に絞り、リスクの高い運用上の仕組みは設定境界の向こうにロックする。これが狙いでした。

そのためコンフィグパック(config pack)には、プロダクトごとに変わるものだけを宣言させました。エンドポイントのルーティング、スキーマ定義、生成モード、モデレーション対象フィールド、プロンプトのエントリポイント、合意済みのSLAチューニング値です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
endpoints:
  - name: "example-generation"
    route: "/example-generation"
    mode: "text"
    request_schema: "/config/schemas/openapi.yaml"
    prompt_script: "/config/prompts/generator.py"
    moderation_fields:
      - "user_input"

generation:
  chunk_update_timeout: 20
  chunk_size: 100

moderation:
  input_bl_path: "/config/data/input_blocklist.txt"
  output_bl_path: "/config/data/output_blocklist.txt"

むしろ大事なのは、コンフィグパックに「含めなかったもの」です。ステータスをどう初期化するか、チャンクをいつ返して安全か、出力モデレーションが生成をどう止めるか、停滞した生成がどうクライアントへのレスポンスになるか。これらは決めさせず、プラットフォームのランタイムのなかで不変に保ちました。

プロダクトごとに変わるものだけを宣言するコンフィグパックが設定境界の上にあり、バージョン管理されたプラットフォームのランタイムがその下で不変に保たれる。サイドバーでは、プラットフォームイメージとコンフィグパックからプロダクトのサービスイメージが組み立てられる。
コンフィグパックこそが、プロダクトとプラットフォームをつなぐインターフェースです。プロダクトは「何が変わるか」を宣言し、プラットフォームは「どう動くか」を保証します。

例外:プロンプト構築

プロダクト固有のコードをあえて許した唯一の場所が、プロンプトの構築です。プロンプトはドメインの意図や複雑な入力データと深く結びついていて、フラットなYAMLに収めるには繊細すぎます。とはいえプラットフォームの境界を壊さないよう、小さく厳格なプラグインインターフェースの内側に閉じ込めました。

1
2
3
4
5
class PromptGenerationPlugin(Protocol):
    prompt_version: str

    def create_prompts(self, input_data: dict) -> list[Prompt]:
        ...

このインターフェースは、高度なプロンプトエンジニアリングに必要な柔軟性をプロダクトチームに与えつつ、プラットフォームはドメイン固有のロジックをいっさい知らないままにします。

デプロイの仕組み

デプロイの仕組みが、この境界をアーキテクチャ上のものから運用上のものへと変えます。プロダクトのサービスは、プラットフォームイメージを拡張し、自分のコンフィグパックを決められた場所にコピーします。

1
2
3
FROM llm-platform/<sender|agent|receiver>:1.x
COPY ./config_pack /config
ENV CONFIG_PATH=/config/config.yml

プラットフォームイメージがランタイムを作ります。サービスイメージは、プラットフォームのバージョンを固定し、その上にプロダクトの設定を重ねます。設定は起動時にプラットフォームに照らして検証され、取り決めに反していれば、開発やステージングの段階で即座に失敗します(fail fast)。

Configuration as Codeだけで、LLMサービスが信頼できるものになるわけではありません。本当に効いたのは、SARが障害モードを吸収できるところまで、プロダクトに公開する範囲を絞り込んだことです。宣言が間違っていれば、設定境界で捕まえる。運用上の障害はプラットフォームが引き受ける。このオーナーシップの境界が、次のセクションで扱うチーム構成を決めました。

5. プラットフォームのオーナーシップとチームトポロジー

コンウェイの法則は、システムは結局、それを作る組織のコミュニケーション構造を映し出す、と説きます。ランタイムの実行とプロダクトの意図のあいだに厳格な境界を引いたことで、私たちはこの法則を逆手に取り、各チームが完全に独立して動けるシステムを作りました。コンフィグパックが引いた技術的な境界は、システムを守るだけでなく、チーム間のやり取りそのものを安定させたのです。

チームトポロジーの語彙を借りれば、この宣言的なやり方は責務をはっきり分けました。プロダクトチームは ストリームアラインド(stream-aligned) なオーナーとして、ドメインの価値・プロンプトの品質・ユーザー体験に集中します。プラットフォームチームは、 イネイブリング(enabling)コンプリケイテッド・サブシステム(complicated-subsystem) の役割を担いました。前者は各プロダクトチームの導入と自律を支援する役割、後者は専門性の高い基盤部分を専任で引き受ける役割です。具体的には、非同期キューの管理、レイテンシの最適化、SARコンポーネントのスケーリング、ガードレールのインフラの維持です。運用負荷は重くなりましたが、その仕事はようやく、はっきり区別でき、予測でき、ビジネスロジックからきれいに切り離されたものになりました。

意図・プロンプト・UXを所有するストリームアラインドなプロダクトチーム(Resume Gen、Job-Posting Gen、Feature N)を、ライフサイクル・ガードレール・オブザーバビリティを担うひとつのプラットフォームチームが、バージョン管理された取り決めを挟んで支えている。
ひとつのプラットフォームチームが、バージョン管理された取り決めの背後で、多数のストリームアラインドなプロダクトチームを支えます。

この分離は、リリースサイクルも自然に切り離してくれました。共有モノレポのなかで、プラットフォームチームは標準のCI/CDパイプラインを通じて、自分たちのペースでバージョン付きのベースイメージを配布します。プロダクトチームは自分のサービスディレクトリだけを見ていればよく、プロンプトを直したくなったら、固定したプラットフォームイメージの上にコンフィグパックを重ねてデプロイします。プラットフォームの定期リリースを待つことなく、1日に何度でもプロンプトを改善して出せますし、プラットフォームのエンジニアがビジネスロジックの門番になることもなくなりました。

実際に何かが壊れたときも、アーキテクチャが明快なぶん、インシデント対応が手探りになることはありません。中央のトリアージチャンネルにアラートが来た瞬間、それがリクエストのスキーマ検証エラーのようなプロダクト側の問題なのか、上流モデルのレイテンシのようなプラットフォーム側の問題なのか、すぐに切り分けられます。

LLM機能が「何をすべきか」と、システムが「それをどう実行するか」のあいだに明確な境界を引いたことで、スケールしていくための運用上の余裕が生まれました。その結果、プラットフォームの人員を線形に増やさなくても、ビズリーチ全体で増え続ける多様なAIプロダクト機能を支えられるようになりました。

6. 意図的に絞ったスコープと、次の一歩

ここで、この最初のプラットフォームがあえてやらなかったことも正直にお話ししておきます。最初の形が支えたのは、ただ一つの取り決めでした。一つの生成リクエストが、一つのライフサイクルを通り、終わるまでチャンク単位で取得される。それだけです。多段のオーケストレーションも、ツール利用も、会話の記憶も、エージェント的なループもありません。2026年の今となっては、保守的に映るかもしれません。

ですが、その制約こそが狙いでした。本記事で挙げた仕組み、すなわちステータスのステートマシン、チャンク単位の取得、モデレーションのパス、設定境界が信頼できたのは、対象とするライフサイクルが、完全に仕様化できるほど小さかったからです。プラットフォームは、現在の取り決めを「退屈」にできて、はじめて成長する資格を得ます。退屈とはつまり、運用が決定論的で、デバッグが予測可能だということです。そこに至る前に一般化していたら、答えではなく、未解決の問いのほうをスケールさせていたでしょう。

振り返ると、最初のLLM機能より前にプラットフォームを作るべきだ、とは言いません。けれど、その最初のサービスは「いつか2つ目が来るかもしれない」と思って設計しておくべきだ、とは言います。最初の成功はプロダクトの価値を証明します。2つ目は、そのアーキテクチャが組織をスケールさせられるかを試します。ひとつの作り込まれたAPIラッパーから始まった道のりは、ビズリーチのなかに新しい当たり前を残して、ひと区切りつきました。新しい生成機能を提案するチームは、もうシステムのセマンティクスを一から探り直しません。エンドポイント、プロンプト、スキーマといったプロダクトの意図に、まっすぐ取りかかれます。

次に来るのは、この境界を捨てるのではなく、試練にかけることになります。実は、それはすでに始まっています。より豊かな多段階の実行を支えるために、アーキテクチャは進化しました。現在、外側のSARが非同期のライフサイクルを管理し、その内側で、SARのAgentサービスが決定論的なLLMワークフローを実行します。さらにその先にあるエージェント的なワークロードは、ガードレールに求められる範囲をはるかに広げ、「1リクエスト=1ライフサイクル」という前提にも揺さぶりをかけます。それでも、ここで得た核心的な教訓は変わりません。こうした機能は、サービスごとにコピーした小手先のコードとしてではなく、状態に名前がつき、障害が起きた際に誰が対応するかも決まっている、明示的な実行モデルとして用意する必要があります。

左側に一つの共有SARライフサイクル(リクエスト、ライフサイクル、結果)、右側に枝分かれする多段のグラフ実行があり、その境目に最初のプラットフォームの限界を示す線が引かれている。
最初のプラットフォームは、一つのリクエスト、一つのライフサイクル、一つの結果を標準化しました。次に控えるのは、多段のグラフ型ワークフローです。

単一生成の呼び出しから、グラフベースのLLMワークフローへ。その移行に興味があれば、Part 2を見逃さないよう、ぜひVisional Engineering Blogをチェックしてください。そして、こうした分散システムやLLMOpsの課題に挑みたい方を、私たちは採用中です。ぜひ一緒に、人材採用の未来を作っていきましょう。


※:この「スカウト」とはビズリーチにおける「プラチナスカウト」を指します。プラチナスカウトとは、企業もしくはヘッドハンターが送信できる、送信可能な回数が限られたスカウトです。

Kunal Jain
Kunal Jain

AI Platform Lead at BizReach's AI Platform Group. I keep our LLMs reliable, observable, and safely employed, so AI can advance careers instead of ending them.