あなたはAIエージェントをデプロイしました。API呼び出しは安価です。トークンコストは記録されています。スプレッドシートで費用を監視しています。
スプレッドシートには表示されない数値:幻覚的出力による下流コスト。
LLMがチャットボットで誤ったテキストを生成するのは煩わしいです。督促メールで支払い金額を捏造したり、開発ブリーフィングでチケットのステータスを作り出したり、持っていない会社の資金データを自信満々に埋めたりする、これらは別の問題です。ダメージはAPI呼び出しそのものではありません。出力がシステムを離れた後に起こることです。
ここには、実運用で現場の実際のエージェントに対して遭遇した3つのカテゴリと、それらに対処するために現在私が使っているパターンを示します。
カテゴリ 1: 構造化出力における捏造
構造化出力の幻覚は、モデルがデータを持っていないフィールドを埋めるときに発生します。nullを返す代わりに、もっともらしいものを作り出します。
Scout は、企業名を受け取り、5つのソース(ウェブサイト、Googleニュース、LinkedIn、Crunchbase、求人情報)をクロールし、Bedrock 経由で Amazon Nova Lite を呼び出して構造化されたブリーフィングを合成する販売調査エージェントです。合成プロンプトは、funding.total_raised、founded、headquarters、key_people のようなフィールドを含むJSONオブジェクトを生成します。
初期のバージョンには微妙な問題がありました。データを返すソースが2つしかない場合でも、モデルは funding.total_raised を訓練データから作成した数値で埋めてしまいました。 営業ミーティングに入ったユーザーは資金調達の数字を引用し、間違っていました。 「わからない」ではなく、自信を持って間違っていました。
最初の修正は、システムプロンプトに明示的な指示を追加することでした:
SYNTHESIS_PROMPT = """...
## Rules
- Only include information from the source data. Never fabricate.
- If a field has no data, use null or empty array.
- Confidence: 0.9+ if 4+ sources succeeded, 0.7+ if 3, 0.5+ if 2, below 0.5 if only 1.
"""
2番目の修正は、Pydantic を用いてスキーマレベルでこれを強制することでした。すべてのオプションのフィールドは Optional 型として型付けされます:
class FundingInfo(BaseModel):
total_raised: Optional[str] = None
last_round: Optional[str] = None
investors: List[str] = Field(default_factory=list)
class Briefing(BaseModel):
company_name: Optional[str] = None
summary: str
founded: Optional[str] = None
headquarters: Optional[str] = None
funding: Optional[FundingInfo] = None
confidence: float = 0.0
モデルが None が期待された値を返した場合、Pydantic はそれを受け付けます(ソースが欠落していることは分かりません)。しかし、構造的に無効な出力は拒否します。そして、confidence フィールドは、ソース数に基づいてモデルが設定するもので、データのカバレッジが薄いときに UI に警告を出すことができます。
3番目の修正は、ユーザーが行動する前に問題を露呈させるものであり、モデルが非JSONを返した場合に適切に失敗することです:
try:
text = raw_text.strip()
if text.startswith"``"):
lines = text.splitlines()
text = "
".join(lines[1:-1] if lines[-1].strip() == "``" else lines[1:]
briefing_data = json.loads(text.strip())
return Briefing(**briefing_data)
except json.JSONDecodeError as e:
logger.error(f"Failed to parse Nova response as JSON: {e}")
return Briefing(
summary=f"Data extracted but synthesis JSON parse failed: {e}",
confidence=0.0,
)
The markdown fence stripping is not an edge case. Nova Lite added `json fences on roughly 15% of responses in testing despite "no markdown fences" in the prompt. If you don't strip them, you get a JSONDecodeError every time.
カテゴリ 2: 自信を持って間違った回答
RAG(retrieval-augmented generation、検索補助生成)はこれを修正するはずです。モデルに関連データを与えると、そのデータから回答し、学習ウェイトではなくデータから回答します。実際には、RAG はこの問題を大幅に低減しますが、完全には排除しません。
DevContext は、GitHub のプルリクエスト、Google カレンダーのイベント、Slack メッセージをリアルタイムにツール呼び出しで集約する開発者ブリーフィングエージェントです。システムプロンプトは明示的です:
`plaintext
CRITICAL RULE: You MUST call tools before responding to ANY question about work, PRs,
meetings, messages, schedule, or developer context. Never generate a text response
about these topics without first calling the relevant tool(s). The tools handle their
own error states -- always invoke them.
`
問題は、GitHub、Jira、Linear、そして一般的なチームのワークフローについての強力な訓練データを持つモデルが、「私の開いているPRは何ですか」という質問に対してツールを呼ばずに答えることがある、という点です。説得力のある回答を作成するパターンを吸収しているためです。モデルが強力であればあるほどこれが起こりやすくなります。GPT-4 は私たちのテストでは Gemini Flash よりこの点で悪かったです。
対策は maxSteps とツール優先の実装を組み合わせたものです:
const result = streamText({
model: getModel(),
system: SYSTEM_PROMPT,
messages,
tools: allTools,
maxSteps: 5,
stopWhen: stepCountIs(5),
});
返却形式: {"translated": "翻訳されたHTML"}
そして各ツールは、認証情報が欠如している場合に明示的な not_connected のステータスを返します。モデルに補完させるのではなく:
`typescript
try {
token = getAccessTokenFromTokenVault();
} catch {
logAudit("github", "Token Vault Exchange", "No GitHub token -- service not connected", "error");
return {
status: "not_connected",
message: "GitHub is not connected. Visit /dashboard/permissions to connect it.",
};
}
`
ここでの設計の要点は、ツールが構造化されたエラーを返すことです。モデルはそのエラーをユーザーへ伝えます。トークン呼び出しが失敗した場合に、PR(プルリクエスト)のリストをでっち上げることはありません。
ツールを黙って失敗させたままにしておくと(空の応答を返すか、モデルが見ない例外を投げる場合)、モデルは訓練データからもっともらしい内容でその空白を埋めます。
カテゴリ 3: フォーマット幻覚
フォーマット幻覚は過小評価されています。モデルは技術的には正確な情報を返しますが、形式が誤っていたり、余分なフィールドが含まれていたり、フィールド名が変更されていたりします。パーサーが失敗します。下流のコードは古いデータを読み取ります。ユーザーには何も表示されなかったり、あるいは完全に見えるが実は部分的な結果が表示されてしまうことがあります。
Rebillはテンプレート置換を用いて催促メールを送信します。メールテンプレートはダブルブラケットのプレースホルダを使用します: {{customer_name}}、 {{amount}}、 {{product_name}}。これらは送信時に Stripe の Webhook データから埋められます:
`typescript
body: `Hi {{customer_name}},
We noticed that your recent payment of {{amount}} for {{product_name}} didn't go through.
Please update your payment method to keep your subscription active:
{{update_payment_link}}
Thanks,
{{company_name}},``
ここではテンプレートは静的です。これは催促システムには適切な設計です。しかし以前のバージョンで、AI が生成した個別のバリエーションを試してみました。モデルは時々 {customer_name}(単一のブレース)、 {{ customer_name }}(空白付き)、または [CUSTOMER_NAME](括弧)を返すことがありました。置換の正規表現はこれらを見逃し、件名欄に生のプレースホルダが表示されたままメールが送信されました。従来の幻覚の意味ではありません。形式幻覚です:誤った構造の出力が黙示的な失敗を引き起こします。
templating の AI 生成コンテンツに対する修正は、使用前の検証です:
`python
import re
REQUIRED_PLACEHOLDERS = ["{{customer_name}}", "{{amount}}", "{{update_payment_link}}"]
def validate_template(template: str) -> bool:
for placeholder in REQUIRED_PLACEHOLDERS:
if placeholder not in template:
return False
# Catch common format hallucinations: single braces, spaced braces, brackets
suspicious = re.findall(r'{[^{].?[^}]}|[.?]', template)
if suspicious:
return False
return True
`
このチェックに失敗するテンプレートをモデルが生成した場合は却下して再試行します。再試行しても失敗した場合は静的デフォルトにフォールバックします。壊れたバージョンを送信してはいけません。
実務で機能するパターン
温度 0.1、0 ではない。 ゼロ温度は、特に文脈が長い場合に、いくつかのモデルで反復ループを起こします。0.1 は、多様性を確保しつつ、事実ベースのタスクを地に置きます。
`python
inferenceConfig={
"maxTokens": 2048,
"temperature": 0.1,
}
`
明示的な null 指示。 「あるフィールドにデータがない場合は null または空の配列を使用する」という指示は、偽情報の作成を減らします。これだけでは完全にはなくせませんが、分布を動かします。null を受け入れる型付きスキーマと組み合わせてください。
Pydantic バリデーションを全出力に適用。 モデルが JSON を返すときは、直ちにあなたのスキーマを通して解析します。ダウンストリームで生の辞書キーにアクセスしないでください。 Briefing(**briefing_data) は必須フィールドが欠如している場合 ValidationError を発生させます。 それを捕捉して、ログに記録し、クラッシュさせる代わりに低下した結果を返します。
信頼度スコアリング。 Scout の confidence フィールドは、ソース数に基づいてモデルによって設定されます。4+ の成功ソースで 0.9 以上、2 つで 0.5。これは完璧ではありませんが、モデルが自分の出力を評価しているため、現実的な信号を浮かび上がらせます。信頼度が 0.5 未満の場合、UI に警告が表示されます。ユーザーは低信頼の結果を権威あるものとして扱わなくなります。
3 段階フォールバックモード。 Scout は、利用可能なものに応じて3つのモードを実行します:
`python
if app_settings.mock_mode:
# Dev mode -- no API keys needed
from backend.extractors.mock import MockWebsiteExtractor as WebsiteExtractor
from backend.synthesis.mock_briefing import mock_synthesize_briefing as synthesize_briefing
elif app_settings.nova_act_api_key:
# Full browser automation via Nova Act
from backend.extractors.website import WebsiteExtractor
from backend.synthesis.briefing import synthesize_briefing
else:
# HTTP fallback -- real data via requests + Bedrock synthesis
from backend.extractors.http_website import HttpWebsiteExtractor as WebsiteExtractor
from backend.synthesis.briefing import synthesize_briefing
`
モックモードは単に合成をスキップするだけではありません。既知の良好なデータを持つハードコードされた Briefing オブジェクトを返します。これにより、実際のモデルに触れることなく全レンダリングスタックをテストできます。
What Doesn't Work
「Just tell the model to be accurate。」 Prompt-only approaches have a ceiling. The model will comply until it doesn't have data, at which point the directive to be accurate competes with the directive to be helpful. Helpfulness often wins.
「Use a better model。」 Switching from Lite to Pro reduces fabrication rates. It doesn't eliminate them. A more capable model is also more capable of generating convincing fabrications.
「Retry on bad output。」 Retrying a malformed JSON response at temperature 0.1 will often produce the same malformed response. If the failure is structural (wrong format, missing fence stripping), retry won't fix it. Parse first, retry only when the error is stochastic (e.g., a failed tool call, not a format mismatch).
The Measurement Problem
The hard part: you often don't have ground truth. You can't compute a hallucination rate without knowing what the correct answer was.
The proxies I use:
-
json.JSONDecodeErrorandValidationErrorcatch rates. These are a lower bound on format hallucinations. - Fields left null vs. fields filled in, tracked over time. A sudden spike in filled-in
funding.total_raisedwhen source quality drops is a signal. - Confidence score distribution. If your agent's average confidence drops from 0.75 to 0.45 with no change in query mix, something changed in the model or your extraction pipeline.
- Manually auditing a 2% sample weekly. Slow, but catches things the other three miss.
None of these is a complete solution. The measurement problem is real and unsolved at the system level. The best you can do is instrument your failure modes and build audit habits early.
Structured output plus schema validation plus explicit null instructions plus confidence scoring gets you most of the way there. The combination is more robust than any single technique. And critically, it degrades gracefully: when things go wrong, the user sees low confidence or "data unavailable" rather than wrong data served with false certainty.
I build production AI systems with hallucination guardrails baked in. If your agents are generating outputs you can't fully trust, I'd like to hear about it. astraedus.dev

