
「しばらくお待ちください」で終わらせない
HowTo12分で読めます
このシリーズ: 全10回
AIエラー対策とリトライの基本
はじめに
AIサービスを使ったアプリケーションを公開すると、必ずエラーに遭遇します。
「しばらくお待ちください」「エラーが発生しました」
こんなメッセージを出して終わりにしていませんか?ユーザーは何が起きたか分からず、不安になります。
本記事では、Amazon Bedrockでよく発生するエラーの対処法と、ユーザーへの適切な伝え方を解説します。
よくあるエラー3つ
1. 混雑エラー(ThrottlingException)
Amazon Bedrockには、リクエスト数の制限があります。この制限を超えると「混雑エラー」が発生します。
python
botocore.exceptions.ClientError: An error occurred (ThrottlingException)
when calling the InvokeModelWithResponseStream operation:
Too many requests, please wait before trying again.発生する状況:
- 短時間に多くのリクエストを送った
- 同時に複数のユーザーがアクセスした
- 大きなトークン数を消費した
2. タイムアウトエラー(ReadTimeoutError)
Bedrockからの応答が一定時間内に返ってこない場合に発生します。
python
botocore.exceptions.ReadTimeoutError:
Read timeout on endpoint URL:
"https://bedrock-runtime.us-west-2.amazonaws.com/..."発生する状況:
- 大きな画像を送信した
- 複雑な質問をした
- Bedrockサービスが混雑している
3. モデルタイムアウト(ModelTimeoutException)
AIモデル自体の処理が時間切れになった場合に発生します。
python
botocore.exceptions.ClientError: An error occurred (ModelTimeoutException)
when calling the InvokeModelWithResponseStream operation:
The model took too long to respond.発生する状況:
- 非常に複雑な分析を要求した
- 大量の入力データを送った
- モデルの処理能力を超えた
リトライの考え方
すぐにリトライしてはダメな理由
エラーが発生したとき、すぐにリトライしたくなります。しかし、これは逆効果です。
text
❌ 悪い例:即座にリトライ
[エラー発生] → [即リトライ] → [またエラー] → [即リトライ] → ...
サーバーがさらに混む → 全員がエラーになる混雑が原因でエラーになったのに、すぐにリトライすると混雑がさらに悪化します。
指数バックオフとは
「指数バックオフ」は、リトライの間隔を徐々に広げる戦略です。
text
✅ 良い例:指数バックオフ
[エラー発生] → 1秒待つ → [リトライ]
↓
[またエラー] → 2秒待つ → [リトライ]
↓
[またエラー] → 4秒待つ → [リトライ]
↓
[成功!]待ち時間を「1秒 → 2秒 → 4秒 → 8秒」と倍々に増やしていきます。
料理店に例えると:
- 混んでる店に入れなかった
- 5分後にまた行く → まだ混んでる
- 10分後にまた行く → まだ混んでる
- 20分後にまた行く → 入れた!
少し待てば、他のお客さんが帰って空きができます。
ジッターを加える理由
「ジッター」は、待ち時間にランダムな幅を持たせることです。
text
❌ ジッターなし:
ユーザーA: 1秒待ち → 2秒待ち → 4秒待ち
ユーザーB: 1秒待ち → 2秒待ち → 4秒待ち
ユーザーC: 1秒待ち → 2秒待ち → 4秒待ち
→ 全員が同じタイミングでリトライ → また混む
✅ ジッターあり:
ユーザーA: 0.8秒待ち → 2.3秒待ち → 3.5秒待ち
ユーザーB: 1.2秒待ち → 1.7秒待ち → 4.8秒待ち
ユーザーC: 0.9秒待ち → 2.5秒待ち → 3.9秒待ち
→ リトライが分散 → サーバー負荷が平準化実装してみよう
Pythonでのリトライ処理
python
# backend/services/bedrock_invoker.py
import time
import random
import logging
from botocore.exceptions import ClientError
logger = logging.getLogger(__name__)
# 設定値
MAX_RETRIES = 3 # 最大リトライ回数
INITIAL_DELAY = 1.0 # 初期待ち時間(秒)
MAX_DELAY = 30.0 # 最大待ち時間(秒)
JITTER_FACTOR = 0.5 # ジッターの幅(±50%)
def calculate_backoff(attempt: int) -> float:
"""
ジッター付き指数バックオフを計算
Args:
attempt: 試行回数(0から始まる)
Returns:
待機時間(秒)
"""
# 指数バックオフ: 1秒 → 2秒 → 4秒 → 8秒...
delay = INITIAL_DELAY * (2 ** attempt)
# 最大値でカット
delay = min(delay, MAX_DELAY)
# ジッターを追加(±50%のランダム幅)
jitter = delay * JITTER_FACTOR * (2 * random.random() - 1)
delay = max(0, delay + jitter)
return delay
def invoke_with_retry(request_body: dict) -> dict:
"""
リトライ付きでBedrockを呼び出す
"""
last_error = None
for attempt in range(MAX_RETRIES):
try:
logger.info(f"Bedrock呼び出し: 試行 {attempt + 1}/{MAX_RETRIES}")
response = bedrock.invoke_model(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
body=json.dumps(request_body)
)
return json.loads(response['body'].read())
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', '')
last_error = e
# リトライ可能なエラーかチェック
if error_code in ['ThrottlingException', 'ModelTimeoutException']:
if attempt < MAX_RETRIES - 1:
delay = calculate_backoff(attempt)
logger.warning(f"{error_code}: {delay:.1f}秒待機してリトライ")
time.sleep(delay)
continue
# リトライ不可能なエラーは即座に終了
raise
except Exception as e:
error_str = str(e)
last_error = e
# タイムアウト系のエラー
if 'ReadTimeoutError' in error_str or 'timed out' in error_str.lower():
if attempt < MAX_RETRIES - 1:
delay = calculate_backoff(attempt)
logger.warning(f"タイムアウト: {delay:.1f}秒待機してリトライ")
time.sleep(delay)
continue
raise
# 全リトライ失敗
raise last_error設定値の決め方
| 設定 | 推奨値 | 理由 |
|---|---|---|
| MAX_RETRIES | 3 | 3回で解決しないなら別の問題 |
| INITIAL_DELAY | 1秒 | 短すぎると効果薄、長すぎるとUX悪化 |
| MAX_DELAY | 30秒 | これ以上待たせるのはユーザー体験が悪い |
| JITTER_FACTOR | 0.5 | ±50%で十分な分散効果 |
ユーザーへの伝え方
技術エラーをそのまま出さない
text
❌ 悪い例:
"ThrottlingException: Too many requests"
ユーザーの反応:「何それ?壊れた?」text
✅ 良い例:
"現在アクセスが集中しています。
しばらく待ってから再度お試しください。"
ユーザーの反応:「混んでるのね、後で試そう」エラーメッセージの設計
良いエラーメッセージには3つの要素があります。
- 何が起きたか(状況の説明)
- どうすればいいか(次のアクション)
- いつ試せばいいか(時間の目安)
python
# backend/services/error_messages.py
ERROR_MESSAGES = {
'ThrottlingException': {
'title': 'アクセス集中',
'message': '現在アクセスが集中しています。',
'action': '1〜2分待ってから再度お試しください。',
},
'ModelTimeoutException': {
'title': '処理タイムアウト',
'message': '分析に時間がかかりすぎました。',
'action': '画像サイズを小さくするか、質問を短くしてお試しください。',
},
'ReadTimeoutError': {
'title': '応答なし',
'message': 'サーバーからの応答がありませんでした。',
'action': 'しばらく待ってから再度お試しください。',
},
'ValidationException': {
'title': '入力エラー',
'message': 'リクエストの形式に問題があります。',
'action': '画像形式(JPEG, PNG)を確認してください。',
},
}
def get_user_message(error: Exception) -> dict:
"""
エラーをユーザー向けメッセージに変換
"""
error_str = str(error)
for error_type, messages in ERROR_MESSAGES.items():
if error_type in error_str:
return messages
# 不明なエラー
logger.error(f"予期せぬエラー: {error_str}")
return {
'title': 'エラー',
'message': '予期せぬエラーが発生しました。',
'action': '問題が続く場合は管理者にお問い合わせください。',
}フロントエンドでの表示
typescript
// frontend/src/components/ErrorMessage.tsx
type ErrorInfo = {
title: string;
message: string;
action: string;
};
function ErrorMessage({ error }: { error: ErrorInfo }) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h3 className="text-red-800 font-bold">
{error.title}
</h3>
<p className="text-red-700 mt-1">
{error.message}
</p>
<p className="text-red-600 mt-2 text-sm">
{error.action}
</p>
</div>
);
}画像処理が失敗したとき
全部失敗にしない
画像の変換やダウンロードが失敗しても、テキストだけで回答を続けられる場合があります。
python
def process_request(
prompt: str,
image_url: str | None = None
) -> Generator[dict, None, None]:
"""
画像エラー時もグレースフルに続行
"""
image_contents = []
if image_url:
try:
# 画像の取得と変換
image_base64 = fetch_and_convert_image(image_url)
image_contents.append({
'type': 'image',
'source': {
'type': 'base64',
'media_type': 'image/png',
'data': image_base64,
},
})
except Exception as e:
logger.warning(f"画像処理失敗: {e}")
# 警告を返すが、処理は続行
yield {
'type': 'warning',
'message': '画像の読み込みに失敗しました。テキストのみで回答します。',
}
# 画像なしでも処理を続行
for chunk in invoke_bedrock(prompt, image_contents):
yield chunkフロントエンドでの警告表示
typescript
// frontend/src/components/ChatInterface.tsx
function handleStreamChunk(chunk: StreamChunk) {
if (chunk.type === 'warning') {
// 警告を表示(黄色い背景など)
showWarning(chunk.message);
} else if (chunk.type === 'text') {
// テキストを追加
appendText(chunk.content);
}
}まとめ
エラー対策の3原則
-
すぐにリトライしない
- 指数バックオフで待ち時間を増やす
- ジッターでタイミングを分散
-
技術用語を見せない
- ユーザーが理解できる言葉で伝える
- 次のアクションを明示
-
完全に止めない
- 一部が失敗しても続行できないか検討
- 警告を出しながら処理継続
チェックリスト
- ThrottlingExceptionのリトライ処理
- ModelTimeoutExceptionのリトライ処理
- ReadTimeoutErrorのリトライ処理
- ジッター付き指数バックオフの実装
- ユーザー向けエラーメッセージの設計
- 画像エラー時のフォールバック処理
- エラーログの出力