コンテンツにスキップ

8.4.3 API 設計とサービス化

  • LLM サービス API が最低限どんな内容を定義すべきか理解する
  • わかりやすいリクエスト・レスポンス構造を設計できるようになる
  • 冪等性、エラー返却、トレース_id、バージョン管理といったサービス化の重要概念を理解する
  • 最小限の API 処理の流れを読めるようになる

API 設計は、次の言葉に直感を持てるとかなり読みやすくなります。

用語初学者向けの意味この節での役割
APIApplication Programming Interface。あるプログラムが別のプログラムを安定して呼ぶための入口他のコードが依存するサービス入口
endpoint/api/v1/chat のような、具体的に呼び出せるアドレス機能を URL パスとして公開する場所
schemaどのフィールドを許可し、何を必須にするかを決めるルールリクエストとレスポンスの形を予測しやすくする
payloadリクエストで送るデータ本体この節では、ユーザーの質問や関連メタデータを指すことが多い
trace_id1件のリクエストを追跡するための一意な IDAPI ログ、検索ログ、モデルログ、エラーをつなげる
idempotency同じリクエストを繰り返しても、制御できない副作用が増えない性質タイムアウトやネットワーク失敗後のリトライで重要

これらは単なる用語ではありません。実際のシステムでは、フロントエンド、バックエンド、ログ、評価、デプロイが協調するための部品です。


なぜ API 設計は「ただ JSON で包むだけ」ではないのか?

Section titled “なぜ API 設計は「ただ JSON で包むだけ」ではないのか?”

よくないインターフェースはどんな形?

Section titled “よくないインターフェースはどんな形?”
bad_request = {
"msg": "返金ポリシーは何ですか"
}
bad_response = {
"text": "7 日以内なら返金可能です"
}

何が問題でしょうか?

  • msg は何を意味するのか? ユーザーメッセージ? システムメッセージ?
  • trace_id がない
  • エラー構造がない
  • バージョン情報がない
  • コンテキスト用のフィールドがない

よい API 設計は何をしているのか?

Section titled “よい API 設計は何をしているのか?”

本質的には、次のことに答えています。

  • 入力はどんな形か
  • 出力はどんな形か
  • 失敗したときどう表すか
  • 1回呼んでも10万回呼んでも安定するか

つまり、API 設計は「入口を作る」ことではなく、次を定義することです。

システムと外部世界の契約。


まずはリクエスト構造を設計する

Section titled “まずはリクエスト構造を設計する”

最小のリクエスト構造には、少なくともこれが必要

Section titled “最小のリクエスト構造には、少なくともこれが必要”
  • query
  • user_id(任意)
  • session_id(複数ターンのとき)
  • metadata(任意)

もっとわかりやすいリクエストオブジェクト

Section titled “もっとわかりやすいリクエストオブジェクト”
request = {
"query": "返金ポリシーは何ですか?",
"user_id": 1,
"session_id": "sess_001",
"metadata": {
"channel": "web"
}
}
print(request)

想定出力:

{'query': '返金ポリシーは何ですか?', 'user_id': 1, 'session_id': 'sess_001', 'metadata': {'channel': 'web'}}

これで、次のことがはっきりします。

  • 何についての問い合わせか
  • 誰から送られたのか
  • どの会話に属するのか
  • 追加のコンテキストは何か

これは「文字列を1つ渡すだけ」よりずっと良い設計です。


次にレスポンス構造を設計する

Section titled “次にレスポンス構造を設計する”

なぜレスポンスも規約化する必要があるのか?

Section titled “なぜレスポンスも規約化する必要があるのか?”

実際の呼び出し元は、人だけではありません。たとえば:

  • フロントエンド
  • 他のサービス
  • ログシステム
  • 評価システム

これらは、安定した形式で結果を受け取る必要があります。

response = {
"trace_id": "trace_001",
"answer": "コース購入後 7 日以内、かつ学習進捗が 20% 未満であれば返金申請できます。",
"sources": [
{"id": "doc_001", "section": "返金ポリシー"}
],
"usage": {
"prompt_tokens": 120,
"completion_tokens": 35
}
}
print(response)

想定出力:

{'trace_id': 'trace_001', 'answer': 'コース購入後 7 日以内、かつ学習進捗が 20% 未満であれば返金申請できます。', 'sources': [{'id': 'doc_001', 'section': '返金ポリシー'}], 'usage': {'prompt_tokens': 120, 'completion_tokens': 35}}

これらのフィールドに価値がある理由

Section titled “これらのフィールドに価値がある理由”
  • trace_id:処理の流れを追いやすくなる
  • answer:実際の業務出力
  • sources:参照元の確認や検証に使える
  • usage:コスト分析に使える

多くのシステムは成功時の返却だけを考えがち

Section titled “多くのシステムは成功時の返却だけを考えがち”

でも、実務で多いのはむしろ次のような問題です。

  • パラメータが不正
  • 上流タイムアウト
  • 権限不足
  • ナレッジベースが空
error_response = {
"trace_id": "trace_002",
"error": {
"code": "INVALID_ARGUMENT",
"message": "query は空にできません"
}
}
print(error_response)

想定出力:

{'trace_id': 'trace_002', 'error': {'code': 'INVALID_ARGUMENT', 'message': 'query は空にできません'}}

これはとても重要です。呼び出し側が次のことを明確に判断できるからです。

  • 何が起きたのか
  • エラーの種類は何か
  • リトライする価値があるか

API 契約、エラー構造、バージョン管理の図


最小で動くサービス化処理関数

Section titled “最小で動くサービス化処理関数”

純粋な Python で API handler を模擬する

Section titled “純粋な Python で API handler を模擬する”
def handle_chat(request):
trace_id = "trace_demo_001"
if "query" not in request or not request["query"].strip():
return {
"trace_id": trace_id,
"error": {
"code": "INVALID_ARGUMENT",
"message": "query は空にできません"
}
}
answer = f"システム応答:{request['query']}"
return {
"trace_id": trace_id,
"answer": answer,
"sources": [],
"usage": {"prompt_tokens": 12, "completion_tokens": 8}
}
print(handle_chat({"query": "返金ポリシーは何ですか?"}))
print(handle_chat({"query": ""}))

想定出力:

{'trace_id': 'trace_demo_001', 'answer': 'システム応答:返金ポリシーは何ですか?', 'sources': [], 'usage': {'prompt_tokens': 12, 'completion_tokens': 8}}
{'trace_id': 'trace_demo_001', 'error': {'code': 'INVALID_ARGUMENT', 'message': 'query は空にできません'}}

このコードは何を教えているのか?

Section titled “このコードは何を教えているのか?”

教えているのは、次の3点です。

  1. まずリクエストを検証する
  2. すべてのリクエストに trace_id を付ける
  3. 成功時と失敗時の返却形式を統一する

これが、サービス化設計の最も重要な基礎です。


簡単に言うと、

同じリクエストを何回呼んでも、結果が同じ、または制御可能であること。

これは次のような場面で特に重要です。

  • リトライ
  • タイムアウト後の再送信
  • ネットワークの揺らぎ

どんなインターフェースで特に意識すべきか?

Section titled “どんなインターフェースで特に意識すべきか?”

特に重要なのは、次のようなものです。

  • 問い合わせチケットの作成
  • 支払いの開始
  • 注文変更

一方、純粋な QA インターフェースは、もともと「読み取り」に近いので、冪等性は比較的扱いやすいです。


なぜバージョン管理は後回しにできないのか?

Section titled “なぜバージョン管理は後回しにできないのか?”

API は一度他のシステムに組み込まれると、自由に項目を変えにくい

Section titled “API は一度他のシステムに組み込まれると、自由に項目を変えにくい”

たとえば、今日の返却が

  • answer

だったのに、明日いきなり

  • response_text

に変えると、呼び出し側はすぐ壊れます。

api_info = {
"version": "v1",
"endpoint": "/api/v1/chat"
}
print(api_info)

想定出力:

{'version': 'v1', 'endpoint': '/api/v1/chat'}

小さなプロジェクトでも、早めにバージョン意識を持つことをおすすめします。


より実際のサービスに近い FastAPI の例

Section titled “より実際のサービスに近い FastAPI の例”

実際のバックエンドに近い書き方を見たいなら、次の最小例が参考になります。

from fastapi import FastAPI
from pydantic import BaseModel, Field
class ChatRequest(BaseModel):
query: str = Field(min_length=1)
session_id: str | None = None
app = FastAPI()
@app.post("/api/v1/chat")
def chat(payload: ChatRequest):
return {
"trace_id": "trace_demo_002",
"answer": f"システム応答:{payload.query}",
"session_id": payload.session_id,
}

このコードはシンプルですが、直接 dict を受け取るより実サービスに近い形です。ChatRequest はリクエスト schema であり、FastAPI はビジネスロジックに入る前に payload を検証します。本番では通常、認証、統一エラー、ログ、実際の trace_id 生成も追加します。


目標が「ナレッジベース駆動の SOP 文書アシスタント」なら、API の最小構成はどうなるか?

Section titled “目標が「ナレッジベース駆動の SOP 文書アシスタント」なら、API の最小構成はどうなるか?”

この種のシステムは、/chat だけでは足りないことが多いです。
少なくとも次のようなインターフェースがあるとよいです。

インターフェース役割
/sop-drafts/generateポリシー、ケース、チェックリスト根拠から構造化 SOP ドラフトを生成する
/sop-drafts/previewエクスポート前に構造化された SOP セクションを確認する
/documents/ingestPDF / Word / PPT をアップロードして解析する
/retrieval/search検索結果をデバッグする

最初に作るときは、より安定した進め方として、だいたい次の順番がよいです。

  1. まず generate だけを作る
  2. まずは構造化結果かエクスポートリンクを返す
  3. その後にデバッグ用インターフェースやバッチ処理を追加する

最小のリクエスト構造は、まずこんな形で定義できます。

generate_request = {
"topic": "返金エスカレーション SOP",
"audience": "一次サポート",
"doc_format": "word",
"case_count": 2,
"checklist_required": True,
}
print(generate_request)

想定出力:

{'topic': '返金エスカレーション SOP', 'audience': '一次サポート', 'doc_format': 'word', 'case_count': 2, 'checklist_required': True}

このオブジェクトの価値は、次の点にあります。

  • 複数ターン対話で集めた項目を、実際のサービス API のパラメータとして落とし込める

実践:SOP ドラフト API の契約を模擬する

Section titled “実践:SOP ドラフト API の契約を模擬する”

本物の FastAPI endpoint を作る前に、まず純粋な Python でリクエスト検証とレスポンス契約を書いてみます。これにより、サービス境界がはっきりします。

REQUIRED_FIELDS = ["topic", "audience", "doc_format", "case_count", "checklist_required"]
def validate_generate_request(payload):
missing = [field for field in REQUIRED_FIELDS if field not in payload or payload.get(field) is None]
if missing:
return False, {
"code": "INVALID_ARGUMENT",
"message": f"不足フィールド:{missing}"
}
if payload["doc_format"] not in {"word", "ppt"}:
return False, {
"code": "INVALID_ARGUMENT",
"message": "doc_format は word または ppt である必要があります"
}
return True, None
def handle_generate(payload):
trace_id = "trace_sop_001"
ok, error = validate_generate_request(payload)
if not ok:
return {"trace_id": trace_id, "error": error}
return {
"trace_id": trace_id,
"status": "accepted",
"sop_draft": {
"title": payload["topic"],
"audience": payload["audience"],
"format": payload["doc_format"],
"sections": ["ポリシー要約", "処理済みケース", "一次サポートチェックリスト"],
}
}
generate_request = {
"topic": "返金エスカレーション SOP",
"audience": "一次サポート",
"doc_format": "word",
"case_count": 2,
"checklist_required": True,
}
print(handle_generate(generate_request))
print(handle_generate({"topic": "返金エスカレーション SOP", "doc_format": "pdf"}))

想定出力:

{'trace_id': 'trace_sop_001', 'status': 'accepted', 'sop_draft': {'title': '返金エスカレーション SOP', 'audience': '一次サポート', 'format': 'word', 'sections': ['ポリシー要約', '処理済みケース', '一次サポートチェックリスト']}}
{'trace_id': 'trace_sop_001', 'error': {'code': 'INVALID_ARGUMENT', 'message': "不足フィールド:['audience', 'case_count', 'checklist_required']"}}

SOP ドラフト API 契約結果図

この練習が役立つのは、成功と失敗を同時に設計する必要があるからです。成功パスの返却だけでは、サービスが準備できたとは言えません。

初学者がよくつまずくポイント

Section titled “初学者がよくつまずくポイント”

最初は楽でも、後でとても苦しくなります。

エラー構造が統一されていない

Section titled “エラー構造が統一されていない”

フロントエンドや他サービスとの接続がどんどん難しくなります。

問題が起きたときに、処理の流れを追えません。

最初からインターフェースを単一の業務ロジックに固定しすぎる

Section titled “最初からインターフェースを単一の業務ロジックに固定しすぎる”

後からの拡張がかなり難しくなります。


このページを終えたら、この証拠カードを残します。

サービス契約
エンドポイント、入力スキーマ、出力スキーマ、エラースキーマ
実行シグナル
レイテンシ、スループット、ログ、ヘルスチェック、またはコンテナ状態
可観測性
request id、trace id、構造化ログ、または metric
失敗確認
タイムアウト、リトライの連鎖、ログ不足、デプロイ不一致
運用アクション
バックオフ、キュー、アラート、段階展開、またはロールバック

この節で最も大事なのは、インターフェースを動かすことそのものではなく、次を理解することです。

API 設計の本質は、入力、出力、エラー、トレース情報を安定したシステム契約にすること。

契約がはっきりしていれば、サービスは他人に長く安定して使われるようになります。


  1. handle_chat()session_id フィールドのサポートを追加してみましょう。
  2. INVALID_ARGUMENTTIMEOUTNOT_FOUND のような統一エラーコード列挙を設計してみましょう。
  3. 考えてみましょう:もしこれが「チケット作成」インターフェースなら、冪等性をどう考えますか?
  4. 自分の言葉で説明してみましょう。なぜ API 設計は本質的にシステム契約を定義することだと言えるのでしょうか?
参考実装と解説
  1. session_id は request parsing、state lookup、logs、response trace を通って流れるべきです。空値や不正形式も検証します。
  2. error enum があると client は安定してエラー処理でき、ユーザー起因のエラーとサービス起因のエラーも分けられます。
  3. idempotency key を使うと、client が timeout 後に retry してもチケットが重複作成されません。
  4. API contract は入力、出力、エラー、権限、時間的期待、互換性を定義します。