「AI APIを業務に組み込みたいが、公式ドキュメントのサンプルを動かした先が分からない」——受託の相談で最も多いのがこのつまずきです。チャットを1往復させるコードは10行で書けます。しかし実務で必要なのは、出力が毎回同じ形で返り、料金が予測でき、深夜のバッチが落ちない実装です。この記事では Claude API を軸に、構造化出力・プロンプトキャッシュ・エラーハンドリングまで、そのまま現場に持ち込める Python コードで解説します。
まずは最小構成——Claude API は実質10行で動く
Anthropic の公式 Python SDK を使えば、最初の呼び出しは驚くほど短く書けます。pip install anthropic のあと、APIキーは環境変数 ANTHROPIC_API_KEY に置いておけば、クライアント生成時に自動で読み込まれます。コードにキーを直書きしない、これは絶対条件です。
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY を環境変数から自動読み込み
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system="あなたは日本語の議事録要約アシスタントです。事実のみを書き、推測を混ぜないでください。",
messages=[
{"role": "user", "content": "次の議事録を3行で要約して:\n" + minutes_text},
],
)
print(resp.content[0].text)
print(resp.usage) # 入力/出力トークン数。コスト試算はここから
ここで押さえておくべき設計上のポイントが3つあります。
1つ目は system プロンプトが独立したパラメータであること。OpenAI API では messages の中に {"role": "system", ...} として混ぜますが、Claude API では別枠です。この違いは後述するプロンプトキャッシュで効いてきます。2つ目は max_tokens が必須であること。出力の上限であり、暴走したときのコスト上限でもあるので、用途に対して過大な値を入れないでください。3つ目は レスポンスがブロックの配列であること。resp.content[0].text と決め打ちすると、後でツール使用を足したときに壊れます。テキストだけ取り出すなら次のように書きます。
text = "".join(b.text for b in resp.content if b.type == "text")
OpenAI API から移植する場合、書き換えが必要なのは実質この3点だけです(resp.choices[0].message.content → 上記の形、system の位置、max_tokens の必須化)。プロバイダを抽象化する共通ラッパーを最初から作りたくなりますが、差分がこの程度なら不要です。抽象化は2社目を本気で併用すると決まってから書けば間に合います。
実務の本命は「構造化出力」——tool use で JSON を確実に受け取る
業務自動化でAIに求めるのは、たいてい文章ではなくデータです。請求書から金額を抜く、問い合わせメールを分類する、スクレイピングした商品説明から仕様を正規化する。ここで「JSONで返して」とプロンプトに書くだけの実装は、必ず事故ります。前置きの挨拶が付く、コードフェンスで囲まれる、全角の引用符が混ざる——100件に2件でも壊れれば、バッチは止まります。
解決策は tool use(関数呼び出し)です。スキーマを渡し、tool_choice でそのツールを必ず使わせることで、モデルの出力をスキーマに沿った辞書として受け取れます。
tools = [
{
"name": "extract_invoice",
"description": "請求書テキストから支払処理に必要な項目を抽出する",
"input_schema": {
"type": "object",
"properties": {
"issuer": {"type": "string", "description": "請求元の会社名"},
"total_yen": {"type": "integer", "description": "税込合計金額。数値のみ"},
"due_date": {"type": "string", "description": "支払期限。YYYY-MM-DD 形式"},
"confidence": {
"type": "string",
"enum": ["high", "low"],
"description": "読み取りに自信がなければ low",
},
},
"required": ["issuer", "total_yen", "due_date", "confidence"],
},
}
]
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "extract_invoice"}, # このツールの使用を強制
messages=[{"role": "user", "content": invoice_text}],
)
data = next(b.input for b in resp.content if b.type == "tool_use")
# => {'issuer': '株式会社◯◯', 'total_yen': 132000, 'due_date': '2026-08-31', 'confidence': 'high'}
ここで一番効いているのは、実は description です。スキーマの説明文はプロンプトの一部として読まれます。「数値のみ」「YYYY-MM-DD 形式」と書くかどうかで、"132,000円" のような表記ゆれが返る確率が変わります。プロンプト本文を推敲するより、スキーマの説明を丁寧に書くほうが精度への寄与が大きいことは多いです。
もう一つ実務的なテクニックが、上の confidence フィールドです。AIに「分からないときは分からないと言わせる」経路を作っておくと、後段で confidence == "low" のレコードだけ人間のレビューに回すフローが組めます。全件自動化を狙って精度99%を追うより、95%を自動処理して残り5%を人が見る設計のほうが、導入は圧倒的に速く、事故も起きません。
なお、受け取った辞書は Pydantic モデルで検証してから DB に入れることを勧めます。スキーマ準拠とはいえ、due_date が実在しない日付である可能性までは保証されないためです。
コストと速度を削る——プロンプトキャッシュ・ストリーミング・並列化
PoC が動いた次に必ず来るのが「毎月いくらかかるのか」という問いです。API のコストは入力トークンと出力トークンで決まり、業務システムでは入力側が支配的になりがちです。社内マニュアル、分類ルール、few-shot の例——毎回同じ長い前置きを送っているからです。
ここで使うのがプロンプトキャッシュです。変化しない部分に cache_control を付けると、その部分がサーバ側にキャッシュされ、2回目以降の同一プレフィックスは大幅に安く・速く処理されます。system が独立パラメータである Claude API の設計は、この用途にきれいに噛み合います。
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=[
{"type": "text", "text": "あなたは経理チェック担当です。"},
{
"type": "text",
"text": ACCOUNTING_MANUAL, # 数万字の社内規程
"cache_control": {"type": "ephemeral"}, # ここまでをキャッシュ
},
],
messages=[{"role": "user", "content": invoice_text}],
)
u = resp.usage
print(u.cache_creation_input_tokens, u.cache_read_input_tokens) # 効いているか必ず確認
注意点は2つ。キャッシュには最小トークン数の条件があり(モデルにより異なる。目安として1024トークン程度以上)、短いプロンプトでは効きません。またキャッシュはプレフィックス一致です。日付や連番など毎回変わる値を先頭側に入れると、以降が丸ごとキャッシュミスになります。「固定 → 可変」の順に組み立てる、これが鉄則です。実装したら cache_read_input_tokens が伸びているかを必ず目視してください。付けたつもりで効いていない、が非常によくあります。
体感速度が問題になる場面——チャットUIや社内ツールのフォーム——ではストリーミングを使います。総処理時間は変わりませんが、最初の1文字が出るまでの時間が劇的に縮み、「固まった」というクレームが消えます。
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
) as stream:
for chunk in stream.text_stream:
print(chunk, end="", flush=True)
final = stream.get_final_message() # usage や stop_reason はここで取れる
逆に、夜間バッチのように「誰も待っていない」処理でストリーミングは不要です。そちらで効くのは並列化で、API呼び出しはI/O待ちなので ThreadPoolExecutor で十分に速くなります。ただし同時実行数は控えめ(4〜8程度)から始めてください。いきなり50並列にすると次章のレート制限に激突します。件数が数千を超え、即時性が不要なら、Batch API を使うほうが安く済みます。
本番で落ちない実装——リトライ、レート制限、モデルの選び分け
AI API は外部サービスであり、失敗する前提で組みます。SDK には指数バックオフ付きのリトライが内蔵されているので、まずはこれを有効にします。
import anthropic
client = anthropic.Anthropic(max_retries=5, timeout=120.0)
def summarize(text: str) -> str | None:
try:
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": text}],
)
except anthropic.RateLimitError:
# 429。リトライ後もダメならキューに戻す
return None
except anthropic.APIStatusError as e:
# 529(過負荷)や 500 系。ログに status_code を残す
logger.warning("api error: %s", e.status_code)
return None
if resp.stop_reason == "max_tokens":
logger.warning("出力が途中で切れた。max_tokens を見直すこと")
return "".join(b.text for b in resp.content if b.type == "text")
エラーハンドリングは外部APIとの境界にだけ置く、という原則がここでも有効です。内側のパース処理まで try で包むと、スキーマ違反という本来気付くべきバグが握り潰されます。
stop_reason のチェックも忘れがちな急所です。max_tokens に到達すると、レスポンスは正常終了扱いのまま途中で切れます。JSON を組み立てさせていた場合、壊れたデータが静かに DB に入ります。
最後にモデルの選び分けです。Claude には Opus 5 / Sonnet 5 / Haiku 4.5 系があり、賢さ・速度・価格が段階的に異なります。分類やタグ付けのような定型処理に最上位モデルを常用するのはコストの無駄で、逆に契約書レビューのような判断を安価なモデルに任せれば精度で泣きます。実務での定石は、まず Sonnet クラスで精度の当たりを付け、そこから軽いモデルに落として許容できるか試すという順序です。モデルIDは定数で一箇所にまとめておけば、乗り換えは1行で済みます。
📚 この記事に関連するおすすめ書籍
さらに深く学びたい方に、参考になる書籍をご紹介します。
※ 本サイトはAmazonアソシエイト・プログラムの参加者です。紹介リンクを経由してご購入いただいた場合、手数料を受け取ることがあります。
まとめ
AI API の実装は、最初の呼び出しが簡単すぎるがゆえに、本番化のギャップで止まります。押さえるべきは4点です。第一に、tool use で構造化出力を強制し、スキーマの description を丁寧に書くこと。第二に、変化しない前置きにプロンプトキャッシュを効かせ、「固定 → 可変」の順で組むこと。第三に、stop_reason と usage を必ず見て、切れた出力とコストを監視すること。第四に、リトライとレート制限を前提に、外部との境界だけでエラーを扱うこと。そして最も重要なのは、100%の自動化を目指さず、AIが自信のない分を人に回す導線を最初から設計に入れることです。この割り切りが、動くPoCと使われ続けるシステムを分けます。
AIを活用した業務自動化を導入したい方はnashiまでお問い合わせください。https://nashi-portfolio.netlify.app