コンテンツにスキップ

8.3.2 大モデル API 呼び出し実践

  • 最小の LLM API 呼び出しが、どんな要素で成り立つかを理解する
  • リクエスト、レスポンス、パラメータ、エラー処理がそれぞれ何を担うかを知る
  • 最小だけれど実用的な API クライアントのラップを書けるようになる
  • 「API を呼べること」と「アプリを作れること」の間に、まだ大きな差があると分かる

一、なぜ API 呼び出しが LLM アプリ開発の第一歩なのか?

Section titled “一、なぜ API 呼び出しが LLM アプリ開発の第一歩なのか?”

モデルが本当にシステムに入ってくる入口だから

Section titled “モデルが本当にシステムに入ってくる入口だから”

前に学んだ概念は、どれだけ強力でも、最終的にアプリの中では次の 1 つに落ち着きます。

  • リクエストを送る
  • 結果を受け取る
  • その後で処理する

だから API 呼び出しは「基礎の雑務」ではなく、次のものです。

大モデルの能力が製品に入るためのインターフェース層。

多くの人が気にするのは次です。

  • 返答を取れるかどうか

でも、実際のプロジェクトでは次のほうが重要です。

  • 返答は安定しているか
  • エラーはどう処理するか
  • token コストをどう抑えるか
  • 複数ターンのコンテキストをどう組み立てるか

だから、この節の重点は「HTTP リクエストを 1 回送れること」ではなく、「1 回のモデル呼び出しを中心にアプリコードをどう設計するか」です。


二、最小のチャットリクエストには何が含まれるのか?

Section titled “二、最小のチャットリクエストには何が含まれるのか?”

最も基本的な要素は、通常次の通りです。

  • モデル名
  • メッセージ一覧
  • 温度などのパラメータ
  • 返却内容

まずは次のように捉えるとよいです。

タスク説明、コンテキスト、制御パラメータをまとめてモデルに渡す。

LLM API 呼び出しのメンタルモデル

request = {
"model": "demo-chat-model",
"messages": [
{"role": "system", "content": "あなたはコースアシスタントです。"},
{"role": "user", "content": "返金ポリシーは何ですか?"}
],
"temperature": 0.2
}
print(request)

期待される出力:

Terminal window
{'model': 'demo-chat-model', 'messages': [{'role': 'system', 'content': 'あなたはコースアシスタントです。'}, {'role': 'user', 'content': '返金ポリシーは何ですか?'}], 'temperature': 0.2}

チャットモデルは通常、1 つの文字列だけを見るのではなく、次のものを見ます。

  • system の指示
  • user の質問
  • assistant の過去の返答

こうすることで、複数ターンの会話背景をよりよく理解できます。


三、まずは「オフライン mock クライアント」を作る

Section titled “三、まずは「オフライン mock クライアント」を作る”

コードをそのまま実行できるように、最初は本物のネットワークリクエストを使わず、最小のシミュレーション版 client を書きます。

class MockLLMClient:
def chat(self, messages, model="demo-chat-model", temperature=0.2):
user_message = messages[-1]["content"]
if "返金" in user_message:
reply = "講座購入後 7 日以内かつ学習進捗が 20% 未満であれば、返金申請できます。"
elif "証明書" in user_message:
reply = "すべての必修項目を完了し、修了テストに合格すると、修了証明書を取得できます。"
else:
reply = "これはシミュレーションの返答です。"
return {
"model": model,
"content": reply,
"usage": {
"prompt_tokens": 42,
"completion_tokens": 18
}
}
client = MockLLMClient()
response = client.chat([
{"role": "system", "content": "あなたはコースアシスタントです。"},
{"role": "user", "content": "返金ポリシーは何ですか?"}
])
print(response)

期待される出力:

Terminal window
{'model': 'demo-chat-model', 'content': '講座購入後 7 日以内かつ学習進捗が 20% 未満であれば、返金申請できます。', 'usage': {'prompt_tokens': 42, 'completion_tokens': 18}}

それは、次のことを先に理解できるからです。

  • 入力構造がどうなっているか
  • 出力構造がどうなっているか
  • 自分のビジネスロジックをどこに置くべきか

こうすれば、最初からネット接続、認証、SDK の細かい仕様に気を取られずにすみます。


四、「呼べる」から「使える」へ進む

Section titled “四、「呼べる」から「使える」へ進む”

なぜ業務コードのあちこちに直接 API 呼び出しを書いてはいけないのか?

Section titled “なぜ業務コードのあちこちに直接 API 呼び出しを書いてはいけないのか?”

もし毎回あちこちで次のように書くとします。

client.chat(...)

すると、長く使ううちに次の問題が出ます。

  • パラメータが統一されない
  • system prompt が散らばる
  • エラー処理がバラバラになる
  • 後でモデルや provider を切り替えにくい

プロジェクトコードらしいラップ

Section titled “プロジェクトコードらしいラップ”
class CourseAssistant:
def __init__(self, llm_client):
self.llm = llm_client
self.system_prompt = "あなたはコースアシスタントです。回答は正確で簡潔にしてください。"
def ask(self, user_query):
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_query}
]
return self.llm.chat(messages)
assistant = CourseAssistant(MockLLMClient())
print(assistant.ask("証明書はどうやって取得しますか?"))

期待される出力:

Terminal window
{'model': 'demo-chat-model', 'content': 'すべての必修項目を完了し、修了テストに合格すると、修了証明書を取得できます。', 'usage': {'prompt_tokens': 42, 'completion_tokens': 18}}

このラップから何を学ぶのか?

Section titled “このラップから何を学ぶのか?”

ここで学ぶのは次です。

モデル呼び出しは、より安定したアプリ層のインターフェースの後ろに隠すべきであること。

この考え方はとても重要です。というのも、後から次のものをどんどん足すことになるからです。

  • 業務用 prompt
  • tool calling
  • logging
  • retry

五、なぜレスポンスの解析も同じくらい重要なのか?

Section titled “五、なぜレスポンスの解析も同じくらい重要なのか?”

モデルの出力を受け取っても、通常はそこで終わりではありません。
その後、さらに次の処理をすることが多いです。

  • ユーザーに表示する
  • データベースに保存する
  • ワークフローに流す
  • 後処理する

だから、まずはレスポンスを分解して見る習慣をつけるとよいです。

response = assistant.ask("返金ポリシーは何ですか?")
print("reply =", response["content"])
print("usage =", response["usage"])

期待される出力:

Terminal window
reply = 講座購入後 7 日以内かつ学習進捗が 20% 未満であれば、返金申請できます。
usage = {'prompt_tokens': 42, 'completion_tokens': 18}

一見すると単純ですが、これは次のことを教えてくれます。

モデルの返却値は「テキスト」だけではなく、価値のあるメタ情報も含んでいる。


六、一番重要な工程上の問題:エラー処理

Section titled “六、一番重要な工程上の問題:エラー処理”

実際の呼び出しでよくあるのは、「いつも成功する」ことではなく、次のようなケースです。

  • タイムアウト
  • レート制限
  • ネットワーク異常
  • サーバー側エラー
class UnstableMockLLMClient:
def __init__(self):
self.fail_once = True
def chat(self, messages, model="demo-chat-model", temperature=0.2):
if self.fail_once:
self.fail_once = False
raise RuntimeError("temporary_api_error")
return {
"model": model,
"content": "再試行後に正常に返答できました。",
"usage": {"prompt_tokens": 20, "completion_tokens": 6}
}
def safe_chat(client, messages):
try:
return client.chat(messages)
except Exception as e:
return {"error": str(e)}
client = UnstableMockLLMClient()
messages = [{"role": "user", "content": "こんにちは"}]
print(safe_chat(client, messages))
print(safe_chat(client, messages))

期待される出力:

Terminal window
{'error': 'temporary_api_error'}
{'model': 'demo-chat-model', 'content': '再試行後に正常に返答できました。', 'usage': {'prompt_tokens': 20, 'completion_tokens': 6}}

なぜこの層を丁寧に作る必要があるのか?

Section titled “なぜこの層を丁寧に作る必要があるのか?”

モデル呼び出しがシステムの中間に入ると、エラーは単に「ユーザーに返事が来ない」だけでは済みません。次のような影響があります。

  • その後のワークフローが全部止まる
  • ログや集計がずれる
  • ユーザー体験が急に悪くなる

def retry_chat(client, messages, retries=2):
last_error = None
for _ in range(retries + 1):
try:
return client.chat(messages)
except Exception as e:
last_error = str(e)
return {"error": last_error}
client = UnstableMockLLMClient()
print(retry_chat(client, [{"role": "user", "content": "こんにちは"}]))

期待される出力:

Terminal window
{'model': 'demo-chat-model', 'content': '再試行後に正常に返答できました。', 'usage': {'prompt_tokens': 20, 'completion_tokens': 6}}

この例が教えているのは次です。

API 呼び出しがシステムに入ったら、retry は付加機能ではなく、基本能力になる。


八、本番プロジェクトでは何をさらに追加するのか?

Section titled “八、本番プロジェクトでは何をさらに追加するのか?”

mock から本物の API に進むとき、普通は次も追加します。

  • 認証
  • モデル切り替え
  • token コストの集計
  • ログと トレース
  • timeout
  • provider 互換層

つまり、本番の LLM API 層は、次の 2 つの顔を持つことが多いです。

  • モデルの入口

そして

  • 実行時の中間層

content が取れれば十分」と思ってしまう

Section titled “「content が取れれば十分」と思ってしまう”

実際には、usage、エラー構造、trace 情報もとても大切です。

業務コードのあちこちに client.chat(...) が散らばる

Section titled “業務コードのあちこちに client.chat(...) が散らばる”

こうなると、後で保守するのがとても大変になります。

本番で問題が起きたとき、すぐに表面化しやすくなります。


大規模言語モデル API(LLM API)の最小エンジニアリング規範

Section titled “大規模言語モデル API(LLM API)の最小エンジニアリング規範”

API 呼び出しを本番プロジェクトに組み込むときは、次の表で自分のラップが十分安定しているか確認できます。

チェック項目最低要件重要な理由
設定管理API key、model、base_url を業務関数に直書きしない環境切り替えと秘密情報保護がしやすい
統一入口すべてのモデル呼び出しを同じ client か service 経由にするlogging、retry、レート制限、コスト集計を追加しやすい
timeout 設定各リクエストに timeout を入れる1 回のリクエストで全体が止まるのを防ぐ
retry 方針一時的エラーだけを最大回数付きで再試行する無限 retry とコスト暴走を防ぐ
エラー構造失敗時は統一した error オブジェクトを返す上位ロジックが安定して失敗処理できる
usage 記録token、モデル名、所要時間を記録する後でコストと性能を分析できる
生出力の保存raw output か重要な トレース を保存する問題発生時にモデルの返答を追跡できる

この表のポイントは、API 層を「安定したインターフェース」にすることです。コードのあちこちに散らばった複数回のモデル呼び出しにしてはいけません。後で学ぶ RAG、構造化出力、Function Calling、Agent はすべてこの層に依存します。

LLM API の堅牢なクライアント閉ループ図

より本番に近いレスポンス構造

Section titled “より本番に近いレスポンス構造”

最初から、モデル呼び出しは統一された構造を返すようにしておくのがおすすめです。文字列だけ返したり、辞書だったり、例外を投げたりが混ざらないようにしましょう。

import time
def llm_response(ok, content=None, usage=None, error=None, raw=None, latency_ms=0):
return {
"ok": ok,
"content": content,
"usage": usage or {},
"error": error,
"raw": raw,
"latency_ms": latency_ms,
}
def robust_chat(client, messages):
start = time.time()
try:
raw = client.chat(messages)
latency_ms = int((time.time() - start) * 1000)
return llm_response(
ok=True,
content=raw.get("content"),
usage=raw.get("usage"),
raw=raw,
latency_ms=latency_ms,
)
except Exception as e:
latency_ms = int((time.time() - start) * 1000)
return llm_response(ok=False, error=str(e), latency_ms=latency_ms)
print(robust_chat(MockLLMClient(), [{"role": "user", "content": "返金ポリシーは何ですか?"}]))

出力例:

Terminal window
{'ok': True, 'content': '講座購入後 7 日以内かつ学習進捗が 20% 未満であれば、返金申請できます。', 'usage': {'prompt_tokens': 42, 'completion_tokens': 18}, 'error': None, 'raw': {'model': 'demo-chat-model', 'content': '講座購入後 7 日以内かつ学習進捗が 20% 未満であれば、返金申請できます。', 'usage': {'prompt_tokens': 42, 'completion_tokens': 18}}, 'latency_ms': 0}

この mock 例では実際のネットワーク呼び出しがないため、latency_ms0 になることがあります。本物の API では、まず見るべき信号の 1 つになります。

このラップがあると、上位の業務コードは次の判断をしやすくなります。呼び出しは成功したか、内容はどこか、token はどれくらい使ったか、失敗理由は何か、リクエストにどれくらい時間がかかったか、です。

API 呼び出しログには何を記録すべきか

Section titled “API 呼び出しログには何を記録すべきか”

LLM アプリで問題が起きたとき、ログがないと、たいてい推測するしかありません。少なくとも次の項目は記録するのがおすすめです。

項目用途
request_idreq_0011 回の呼び出しの文脈をつなぐ
modelデモ用チャットモデルモデルごとの違いを比べる
prompt_versioncourse_assistant_v1どの版の prompt で問題が起きたか追う
input_preview返金ポリシーは何ですかユーザー入力をすばやく特定する
output_preview講座購入後 7 日以内...モデル出力をすばやく確認する
prompt_tokens42コスト分析
completion_tokens18コスト分析
latency_ms850性能分析
errortimeout失敗原因の特定

注意: ログには敏感情報をそのまま保存しないでください。実際のプロジェクトでは、ユーザーの個人情報、キー、社内資料はマスキングするか、権限管理を行うべきです。


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

要求
入力、状態、tools/context、期待される出力の契約
検証済み出力
パーサー/スキーマ、または業務ルール確認の結果
追跡記録
モデル呼び出し、ツール/関数呼び出し、文書解析、または対話状態
失敗確認
フォーマット不正、必須フィールド不足、古い状態、または誤ったツール
次の行動
prompt、schema、state、API、または parsing の改善

この節で最も大事なのは、「1 回モデルを呼べること」そのものではありません。理解すべきなのは次です。

大モデル API 呼び出しの本当のエンジニアリング価値は、モデル能力を繰り返し使えて、保守しやすく、拡張できるシステムインターフェースにすることにある。

この視点ができると、次に LangChain、対話システム、Agent のツール層を学ぶときも、ずっと自然に理解できるようになります。


  1. MockLLMClient を拡張して、「学習順序」の質問にも答えられるようにしてみましょう。
  2. CourseAssistant に統一されたエラー返却構造を追加してください。
  3. 考えてみましょう: なぜ本番プロジェクトでは、業務コードのあちこちで messages を直接組み立てるべきではないのでしょうか?
  4. 自分の言葉で説明してみましょう: なぜ「API を呼べること」と「LLM アプリを作れること」の間には、まだ 1 層のシステム設計が必要なのでしょうか?
プロジェクト参考とレビュー観点
  1. 「学習順序」用の分岐または fixture を追加し、一般論ではなく順序付きのコース手順を返すことを確認します。
  2. {status: "error", code: "MODEL_ERROR", message: "...", trace_id: "..."} のような構造にすると、UI とログが失敗を一貫して扱えます。
  3. messages の組み立てを集中させると、system prompt、安全ルール、履歴の切り詰め、schema をそろえられます。
  4. LLM アプリには契約、状態、検索/tool、検証、可観測性、失敗処理が必要です。API 呼び出しは通信層にすぎません。