AI を業務やプロダクトに組み込もうとすると、わりと早い段階で同じ壁にぶつかる。「出力が安定しない」だ。
同じプロンプトを投げても、返ってくる JSON のキーが微妙に違う。昨日は動いていた抽出処理が、今日は説明文が混ざって壊れる。プロンプトを書く人間の側も、毎回同じ精度では書けない。AI の出力も、人間の入力も、本質的にゆらぐ。
それなのに、プロダクトや業務システムは「一定の出力」を要求する。API のレスポンスは決まったスキーマであってほしいし、バッチ処理は毎回同じ結果を返してほしい。このギャップ — ゆらぐ部品で、ゆらがない出力を作る — をどう埋めるかが、AI を「賢く使う」より先に効いてくる。
結論から言うと、効くのは賢い AI でも上手いプロンプトでもない。ゆらぎを吸収して一定の出力に収束させる「仕組み」、つまり昔から地味にやってきたシステム連携・信頼性設計のパターンだ。この記事では、そのパターンを7つ整理する。
なぜ「賢い AI」だけでは安定しないのか
まず前提を揃えておく。LLM の出力は確率的にサンプリングされるので、temperature=0 にしても完全な決定性は保証されない(同じ入力でも実行系・バージョンで揺れる)。プロンプトを足せば足すほど、指示が衝突して別の壊れ方をすることもある。
人間側も同じだ。プロンプトを書くエンジニアの体調・知識・その日の集中力で、入力の質はばらつく。「いいプロンプトを書けば安定する」は、属人性をプロンプトに移しただけで、ゆらぎの発生源が消えたわけではない。
だから戦略を変える。部品(AI・人間)を安定させようとするのをやめて、ゆらぐ部品を前提に、出口で一定にする仕組みを作る。これは新しい発想ではなく、不安定なネットワーク・不安定なハードウェアの上で安定したシステムを作ってきた、ごく普通の信頼性設計の応用だ。
パターン1: スキーマ強制 — 出力の「形」を先に決める
一番効くのがこれ。自由なテキストで返させると無限にゆらぐので、出力スキーマを先に定義して、それに合致するまで弾く。
from pydantic import BaseModel, ValidationError
class ExtractResult(BaseModel):
title: str
amount: int
currency: str
def parse_llm_output(raw: str) -> ExtractResult:
# LLM の生出力を pydantic で検証。形が違えば例外
return ExtractResult.model_validate_json(raw)
OpenAI / Anthropic の Structured Output(JSON スキーマを渡してその形でしか返させない機能)や、関数呼び出し(tool use)も思想は同じだ。「自由に書いていいよ」をやめて、「この型で返せ」に変える。ゆらぎの自由度を、スキーマの分だけ削る。
パターン2: 検証ゲート — 通す前に必ず一度止める
スキーマが合っていても、中身が妥当とは限らない。amount: -5 のような「形は正しいが意味が壊れている」出力を止めるために、検証ゲートを1枚挟む。
def validate(result: ExtractResult) -> ExtractResult:
if result.amount < 0:
raise ValueError(f"amount が負: {result.amount}")
if result.currency not in {"JPY", "USD", "EUR"}:
raise ValueError(f"未知の通貨: {result.currency}")
return result
ポイントは「AI の出力を一次情報として信用しない」こと。生成 AI は流暢に間違えるので、流暢さに引っ張られて検証を省くと、壊れた出力がそのまま下流に流れる。ゲートは多少うっとうしくても、必ず置く。
パターン3: フォールバック — 失敗を「想定内」にする
検証で弾いたあと、どうするか。失敗時の代替経路を最初から用意しておく。
def extract_with_fallback(text: str) -> ExtractResult:
try:
return validate(parse_llm_output(call_llm(text)))
except (ValidationError, ValueError):
# 1回だけ「形式を厳密に」と念押しして再試行
return validate(parse_llm_output(call_llm(text, strict=True)))
# それでもダメなら呼び出し元で握る(後述の人間ゲートへ)
フォールバックが無いと、ゆらぎが 1 回出ただけで処理全体が落ちる。代替経路があれば、ゆらぎは「想定内の分岐」になる。重要なのは、フォールバックの失敗を握りつぶさないこと。最終的に失敗したら、それは下流に「失敗した」と伝える(後述)。
パターン4: 冪等性 — 何度実行しても同じ結果にする
AI を組み込んだ処理はリトライが増える。リトライのたびに副作用が二重に走ると、出力どころか状態がゆらぐ。同じ入力なら何度実行しても同じ結果になる(冪等)ように設計する。
def upsert(record_id: str, result: ExtractResult):
# INSERT ではなく UPSERT。再実行しても重複しない
db.execute(
"INSERT INTO results (id, data) VALUES (?, ?) "
"ON CONFLICT(id) DO UPDATE SET data = excluded.data",
(record_id, result.model_dump_json()),
)
冪等にしておくと、リトライ・フォールバック・並列実行が安全になる。「もう一回流していい」という安心感が、不安定な部品を扱う土台になる。
パターン5: リトライは「回数上限つき」で
フォールバックと近いが、リトライは無限ループの危険があるので分けて書く。指数バックオフ + 上限回数で、諦めどころを必ず決める。
import time
def retry(fn, max_attempts=3):
for attempt in range(max_attempts):
try:
return fn()
except TransientError:
if attempt == max_attempts - 1:
raise # 上限到達。握りつぶさず上げる
time.sleep(2 ** attempt)
「成功するまで回す」は一見やさしいが、ゆらぎが構造的な場合(プロンプトが根本的に悪い等)は永遠に終わらない。上限を切って、超えたら人間に渡す。
パターン6: 人間ゲート — 不可逆な操作の前で止める
自動化を進めるほど、「AI の出力で不可逆な操作(公開・送金・削除)が走る」リスクが上がる。不可逆操作の直前に、人間の承認を1枚挟む。
def publish(article, auto_checks_passed: bool, human_approved: bool):
if not auto_checks_passed:
raise GateError("自動チェック未通過")
if not human_approved:
# 機械チェックが通っても、不可逆操作は人間承認を要求
raise GateError("人間レビュー未完了 — 公開保留")
do_publish(article)
機械チェックと人間チェックは役割が違う。機械は「形式・閾値・集合一致」を高速に弾き、人間は「これは本当に出していいのか」という文脈判断をする。両方を直列に置くと、ゆらぎの取りこぼしが減る。
パターン7: パイプライン分割 — ゆらぎを1工程に閉じ込める
最後に全体構造の話。生成・検証・整形・保存を1つの巨大な関数に詰めると、どこでゆらいだか分からなくなる。工程を分割して、各工程の入出力を固定する。
[生成] --raw text--> [スキーマ強制] --typed--> [検証] --valid--> [整形] --> [冪等保存]
↑ ゆらぎはこの2工程に閉じ込める
こうすると、ゆらぎが発生しうるのは「生成」と「スキーマ強制」の境界だけになる。それより下流は型が保証された世界なので、安定して書ける。デバッグ時も「どの工程で壊れたか」が一意に分かる。
AI 時代だからこそ、連携技術が効いてくる
ここまで挙げた7つ — スキーマ強制・検証ゲート・フォールバック・冪等性・リトライ上限・人間ゲート・パイプライン分割 — は、どれも AI 専用の新技術ではない。不安定なネットワークや外部 API を相手に、システムを安定させるために昔からやってきた、地味な連携・信頼性設計のパターンだ。
AI の登場で「賢い部品」は手に入った。けれど賢い部品は、同時にゆらぐ部品でもある。ゆらぐ部品を一定の出力に収束させる仕事は、AI が賢くなっても消えない。むしろ、部品が強力になったぶん、その出力を受け止める「仕組み」の設計がボトルネックに移ってきている。
プロンプトを磨くのも大事だが、その手前で「ゆらいでも一定に収束する仕組み」を1枚噛ませる。AI を業務に組み込んで出力の不安定さに悩んでいるなら、まずこの7パターンのどれが欠けているかを見てみるといい。多くの場合、足りないのは賢さではなく、ゆらぎを受け止める枠組みのほうだ。