「PoCでも本番と同じエラー処理を入れるべき?」
「雑に書いていいって言うけど、どこまで?」
AIのPoC開発で、この判断に悩んだ話と今後は迷わないためのルールをまとめます。
結論:PoCの品質目標は「落ちない」ではなく「早く学ぶ」
保守開発(運用)の目標は、だいたいこうなっています。
- 例外を握ってでもサービス継続
- リトライ、フォールバック、監視、復旧手順
- 変更しても壊れない(堅牢性と保守性)
一方、PoCの目標はこうです。
- 仮説が正しいか最短で白黒つける
- ダメなら 早く壊れて原因が見えるほうが得
- 成果物は「コード」より 学び(意思決定できる材料)
この目的の違いが “fail-fast” の正体です。
よくある誤解:fail-fast = エラー処理ゼロ、ではない
fail-fast は「握りつぶさない」ですが、何も考えずに雑に書くことではありません。
PoCでやるべきはむしろ逆で、
- 前提(契約)を明文化する
- 前提違反が起きたら 即落ちる
- 落ちたら 原因・入力・設定が追える(観測できる)
つまり「壊れ方の設計」です。
迷ったらこれ:エラー処理の“境界”を3つに分ける
PoCで苦しいのは「全部に薄くエラー処理を塗る」ことです。
代わりに、境界で考えます。
境界A:PoCのコア(仮説検証ロジック)
- 原則:握らない
- 変な値は落とす(
assert/raise) - ここにリトライや復旧ロジックを入れない
境界B:入口(CLI / APIハンドラ / デモ実行)
- 原則:最小限だけ握る
- 目的:隠すためじゃなく「失敗を説明可能にする」
- 例:入力不足は 400、設定不足は起動失敗、など
境界C:外部I/O(LLM API、DB、ファイル、Webhook)
- 原則:今回の仮説に関係するものだけ処理する
- 例:コスト/レート制限を検証するPoCなら429対処は価値がある
機能検証PoCなら落としてOK
決定木:例外を“握っていい”のはどんなとき?
次の質問で Yes が1つでもあるなら、そこはエラー処理を“投資対象”にしてOKです。
- そのエラーは 今回の仮説に関係する?
- 握らないと 学びが得られない?(原因が特定できない、再現できない)
- 握らないと 危険?(課金暴走、機密漏えい、データ破壊)
- デモ相手に見せる必要があり、落ち方が説明不能になる?
全部Noなら、落としてOK(fail-fast)。
省略していいコード(PoCの目的に関係ないなら)
PoCで「ちゃんとしたくなる」けど、目的に関係ないなら後回しでOKなやつです。
- 複雑なリトライ(指数バックオフ、サーキットブレーカー)
- フォールバック(別モデル切替、代替経路)
- 例外クラスの階層設計、エラーコード体系の作り込み
- 本番同等の入力バリデーションの網羅
- 永続化の堅牢化(トランザクション、リカバリ設計)
- 監視/アラート/SLO/オンコール手順
- DI/抽象化しすぎた設計(早すぎる綺麗さ)
省略していい = “やらない”ではなく、今やらないです。
してはいけない(PoCでも省略しない)コード
AI PoCは、ここを省略すると「動いたけど結論が出ない」「事故る」になりがちです。
1) 再現性(最低限)
- モデル名 / 主要パラメータ(temperature等)
- プロンプト(バージョン)
- 入力データ(版数、サンプル)
2) 観測性(最低限)
- 入力、出力
- 失敗時の例外(握りつぶさない)
- 可能ならリクエストID(LLM APIのrequest_idなど)
3) “結果が無意味になる”入力の排除
- 空文字、null、フォーマット崩れ
→ ここはPoCでも弾かないと、検証結果が汚れます
4) 事故防止(AI PoCの必須)
- 機密データを外部へ送らない
- 課金暴走を防ぐ(上限、件数制限、ログで可視化)
- 破壊的操作(削除/更新)をPoCでやらない or 明示ガード
NG例:try/exceptで握りつぶして“動いたことにする”
PoCで一番まずいのは、「失敗してるのに成功っぽく見える」ことです。
def call_llm(prompt: str) -> str:
try:
return client.generate(prompt) # 失敗するかもしれない
except Exception:
return "{}" # とりあえず空JSON返しとく(←最悪)
- 失敗が成功に見える
- どの入力で壊れたか分からない
- 検証結果が汚れる(“なんかうまくいった気がする”)
OK例:PoCコアは fail-fast(前提違反は落とす)
例:LLMにJSONを返させるPoC(コアは握らない)
import json
def build_prompt(user_text: str) -> str:
assert user_text.strip(), "user_text must be non-empty"
return (
"Return JSON only.\n"
f'Input: "{user_text}"\n'
'Output schema: {"summary": string, "labels": [string]}\n'
)
def parse_llm_json(text: str) -> dict:
obj = json.loads(text) # 壊れてたら例外で落ちてOK
assert isinstance(obj.get("summary"), str), "summary must be str"
assert isinstance(obj.get("labels"), list), "labels must be list"
return obj
ポイント:
-
assertは「前提(契約)」を明文化するため - 失敗したら落とす(=原因が見える)
- PoCの学びの速度が上がる
入口だけ最小限握る(“説明可能”にする)
コアは落ちてOK。でも入口は「何が起きたか」を残すだけやる。
import traceback
def main() -> int:
try:
run_poc()
return 0
except Exception:
traceback.print_exc() # 調査に必要な情報を残す
return 1
if __name__ == "__main__":
raise SystemExit(main())
入口で握るのは「隠すため」ではなく 観測のため。
AI PoCあるある:どこまで扱うべきエラー?
-
429 / rate limit
- 仮説が「スループット/コスト」なら扱う(投資対象)
- 仮説が「機能」なら落としてOK(後回し)
-
JSON崩れ
- 仮説が「構造化抽出」なら扱う(出力検査+再試行など)
- 仮説が「UIモック」なら落としてOK
-
データ品質(空欄/欠損)
- 結果が無意味になるならPoCでも弾く(省略しない)
まとめ:PoCのエラー処理は「事故防止」と「学びの速度」
- コアは握らず落としてOK(fail-fast)
- 入口は“説明可能”にするために最小限握る
- 外部I/Oは「仮説に関係するなら」投資する
- PoCで守るべきは稼働率より 再現性・観測性・事故防止
コピペ用チェックリスト
- このPoCの仮説は1文で言える
- 成功基準 / 中止基準(timebox)がある
- コアは例外を握らない(fail-fast)
- 入口で例外が観測できる(ログ/入力/設定)
- モデル名/プロンプト/主要パラメータ/データ版数が残る
- 機密/課金/破壊的操作の事故だけは防いでいる