Back

本番環境でAIのハルシネーションを減らす方法:実際に効くGrounding・RAG・ガードレール完全ガイド

火曜日にAI機能をリリースしました。木曜日には、ユーザーがスクリーンショットを撮っていました。チャットボットが存在しない最高裁判例を自信満々に引用しているものです。金曜日には、製品にないはずの機能をチャットボットが説明しているとサポートに47件の問い合わせが来ました。月曜日にはPMから「とりあえず免責事項をつけて…」というメッセージが。

心当たりがあるのでは。 2026年初頭の時点で、AI出力を信頼している開発者はたった29%。 2024年の40%からさらに下がっています。 AI生成コードのほぼ半数がレビュー不十分なままコードベースに入っています。 問題の本質はLLMが壊れていることではなく、壊れた時にキャッチする仕組みなしでデプロイしていることです。

ハルシネーションはパッチで直せるバグではありません。 LLMの構造そのものから来る特性です。 とはいえ、手をこまねいているしかないわけでもありません。 LLMを実用的に信頼できるレベルにするエンジニアリングが急速に発展中です。 「とりあえずRAG入れればOK」よりずっと深い世界があります。

このガイドでは、ハルシネーション対策のフルスタックを扱います。 モデルがなぜハルシネーションするのかの理解、グラウンディング、RAGパイプライン、出力ガードレール、本番環境の監視まで。 すべての手法に、すぐ使えるTypeScriptコード付きです。

LLMがハルシネーションする理由:エンジニア向けメンタルモデル

ハルシネーションを直すには、まず原因を理解する必要があります。 論文的な説明ではなく、いつ発生するかを予測するのに役立つ実戦的なメンタルモデルです。

オートコンプリートマシン

LLMは何かを「知っている」わけではありません。先行するトークンが与えられた時に、最も確率の高い次のトークンを予測しているだけです。GPT-5に「フランスの首都は?」と聞いた時、事実を検索するのではなく、学習データにおいてそのプロンプトの後に「パリ」が来る確率が最も高いから「パリ」を生成するのです。

この区別が重要なのは、ハルシネーションがいつ起きるかを説明してくれるからです:

  1. 低信頼度領域。 複数の候補がほぼ同じ確率の場合、モデルは一つを選びます。時に間違ったものを選びます。

  2. 学習データの空白。 学習データに存在しない(あるいは希薄な)情報に遭遇すると、モデルはそれっぽいテキストをでっち上げます。 「分かりません」とは言わないのがデフォルトの動作です。

  3. 指示追従の圧力。 「必ず回答してください」と指示すると、回答すべきでない場合でも回答します。有用であれという指示と正確であれという指示が衝突するのです。

  4. コンテキストウィンドウのオーバーフロー。 関連情報が長いコンテキストの中間に埋もれていると、モデルはそれを無視してパラメトリックメモリから生成することがあります。「中間で失われる(lost in the middle)」問題です。

  5. フォーマット誘発の捏造。 構造化出力(JSON、テーブルなど)を要求すると、必須フィールドを空にする代わりに値を捏造して埋めることがあります。

ハルシネーションの分類

すべてのハルシネーションが同じではありません。分類することで適切な対策を選べます:

種類説明危険度
事実の捏造本物に聞こえる事実を作る「React useStateフックはバージョン15.3で導入された」🔴 高
出典の捏造存在しない出典を引用「2024年StackOverflow調査によると…」(不正確なデータ)🔴 高
確信に満ちた外挿事実を真実の範囲を超えて拡張「PostgreSQLは100TBテーブルをネイティブサポート」🟡 中
指示ハルシネーション存在しない機能を実行したと主張「DBを検索して3件の結果を見つけました」(実際には検索していない)🔴 高
一貫性ドリフト前の発言と矛盾「Xは正しい」→後で「Xは正しくない」🟡 中
時間的混乱時期を混同学習前の事実を学習後のイベントに適用🟡 中

種類ごとに異なる対策が必要です。事実の捏造にはグラウンディング、出典の捏造には引用検証、指示ハルシネーションにはツール使用の強制が有効です。それでは防御を構築していきましょう。

レイヤー1:グラウンディング — モデルを現実に固定する

グラウンディングとは、「この資料だけを見て回答して」とモデルに釘を打つ手法です。 信頼できるソースを直接渡し、その範囲からはみ出さないように制約します。 ハルシネーション対策の基本中の基本です。

システムプロンプト設計

システムプロンプトが第一の防衛線です。 ところが、多くの開発者が気づかないうちにハルシネーションを誘発するプロンプトを書いています:

// ❌ ハルシネーションを促すシステムプロンプト const badSystemPrompt = `あなたはAcme Corpの親切なカスタマーサポートエージェントです。 すべてのお客様の質問に丁寧で詳しい回答をしてください。`; // ✅ ハルシネーションを減らすシステムプロンプト const goodSystemPrompt = `あなたはAcme Corpのカスタマーサポートエージェントです。 重要なルール: 1. 提供されたコンテキスト文書のみに基づいて回答してください。 2. コンテキストに回答がない場合は、「その情報は持ち合わせておりません。担当者におつなぎいたします。」と回答してください。 3. 製品機能、価格、ポリシーを絶対に捏造しないでください。 4. 情報を引用する際は、具体的な文書セクションを参照してください。 5. 不確実な内容がある場合は、明示的にそう伝えてください。 あなたの知識のカットオフ日以降の最新情報は持っていない可能性があります。 不明な場合はdocs.acme.comへ案内してください。`;

主要なパターンは以下の通りです:

  • 「分かりません」の明示的な許可。 モデルはデフォルトで何かを生成しようとします。 「分からなければそう言っていい」と明示的に書く必要があります。
  • 知識の境界を明示。 「渡したドキュメントがあなたの知識の全てです」とはっきり伝えます。
  • 引用の義務化。 出典の引用を義務付けると、捏造された引用は見つけやすいので自然とハルシネーションが減ります。
  • 確信にブレーキ。 「不確実な場合はそう伝えて」という文言が、モデルのキャリブレーション機構をONにします。

Few-Shotグラウンディング例

Few-Shot例は、モデルに回答を控えるべきタイミングを教えるのに驚くほど効果的です:

const groundingExamples = [ { role: "user", content: "Enterpriseプランのアップタイム保証は何パーセントですか?" }, { role: "assistant", content: "料金ドキュメント(セクション3.2)によると、Enterpriseプランには99.99%のアップタイムSLAが含まれており、その閾値を超えるダウンタイムに対して金銭的クレジットが提供されます。" }, { role: "user", content: "SAP連携はサポートしていますか?" }, { role: "assistant", content: "現在の連携ドキュメントにSAPの記載は確認できません。docs.acme.com/integrationsで最新の連携一覧をご確認いただくか、カスタム連携についてご相談いただける連携チームにおつなぎすることが可能です。" } ];

2番目の例が重要です。情報を捏造せず、それでいて有用な「分かりません」応答のをモデルに教えています。

温度とサンプリング制御

低温度はハルシネーションを完全には消せませんが、クリエイティブな捏造は減らせます:

import OpenAI from 'openai'; const openai = new OpenAI(); async function groundedCompletion( systemPrompt: string, context: string, userQuery: string ) { const response = await openai.chat.completions.create({ model: 'gpt-5', temperature: 0.1, // 事実ベースのタスクには低温度 top_p: 0.9, // やや制約されたnucleus sampling frequency_penalty: 0.3, // 反復パターンを抑制 presence_penalty: 0.0, // 多様性は強制しない(精度優先) messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: `コンテキスト:\n${context}\n\n質問: ${userQuery}` } ], }); return response.choices[0].message.content; }

重要: temperature 0は「ハルシネーションなし」を意味しません。 各ステップで確率最大のトークン1つだけを選ぶという意味です。 その「確率最大」がハルシネーションだった場合(モデルが本当に答えを知らない場合)、temperature 0は最大の確信をもって嘘をつきます。

レイヤー2:RAG — モデルに必要な情報を渡す

Retrieval-Augmented Generation(検索拡張生成)は、最も広く採用されているハルシネーション削減手法です。モデルの内蔵メモリに頼る代わりに、関連ドキュメントを検索してコンテキストに注入します。

ただ、ほとんどのRAG実装はハルシネーション削減において中途半端です。 検索品質にばかり注力し、問題のもう半分——検索されたコンテキストをモデルがどう使うか——を無視しているからです。

ハルシネーションを本当に減らすRAGパイプライン

// ステップ1:チャンキング戦略はエンベディングモデルより重要 function intelligentChunk(document: string, metadata: Record<string, any>) { const sections = document.split(/\n## /); return sections.map((section, index) => ({ content: index === 0 ? section : `## ${section}`, metadata: { ...metadata, sectionIndex: index, documentTitle: metadata.title, sectionTitle: section.split('\n')[0]?.trim() || 'Introduction', previousSection: index > 0 ? sections[index - 1].slice(-200) : null, nextSection: index < sections.length - 1 ? sections[index + 1].slice(0, 200) : null, } })); } // ステップ2:関連性スコア付き検索 async function retrieveWithScoring( query: string, vectorStore: SupabaseVectorStore, options: { topK: number; scoreThreshold: number } ) { const results = await vectorStore.similaritySearchWithScore( query, options.topK * 2 ); const relevant = results .filter(([_, score]) => score >= options.scoreThreshold) .slice(0, options.topK); if (relevant.length === 0) { return { documents: [], confidence: 'none', message: '十分に関連性のあるドキュメントが見つかりませんでした' }; } return { documents: relevant.map(([doc, score]) => ({ content: doc.pageContent, metadata: doc.metadata, relevanceScore: score })), confidence: relevant[0][1] > 0.85 ? 'high' : 'moderate', message: null }; } // ステップ3:引用強制付きコンテキスト対応生成 async function generateWithRAG( query: string, retrievalResult: Awaited<ReturnType<typeof retrieveWithScoring>> ) { if (retrievalResult.confidence === 'none') { return { answer: "ナレッジベースにこの質問に正確にお答えする情報が見つかりませんでした。質問を言い換えていただくか、専門スタッフにおつなぎしましょうか?", citations: [], confidence: 'none' }; } const contextBlock = retrievalResult.documents .map((doc, i) => `[出典 ${i + 1}: ${doc.metadata.documentTitle} > ${doc.metadata.sectionTitle}]\n${doc.content}`) .join('\n\n---\n\n'); const response = await openai.chat.completions.create({ model: 'gpt-5', temperature: 0.1, messages: [ { role: 'system', content: `あなたは正確なアシスタントです。提供された出典のみに基づいて回答してください。 ルール: - [出典 N]表記で引用してください - 出典が質問に完全に回答できない場合は、回答できる部分とできない部分を明示してください - 出典に明示されていない内容は絶対に推論しないでください - 出典間で矛盾がある場合は、両方の見解を引用付きで提示してください` }, { role: 'user', content: `出典:\n${contextBlock}\n\n質問: ${query}` } ] }); return { answer: response.choices[0].message.content, citations: retrievalResult.documents.map(d => d.metadata), confidence: retrievalResult.confidence }; }

ハルシネーションを引き起こすRAGの5つの間違い

RAGを導入してもハルシネーションが続く理由は以下の通りです:

1. 関連性閾値がない。 RAGパイプラインが常に何かを返すと、モデルは無関係なコンテキストから無理やり回答しようとします。コンテキストがないほうがまだマシで、モデルに偽の確信を与えてしまいます。

// ❌ 無関係なものも常に返す const results = await vectorStore.similaritySearch(query, 5); // ✅ 関連性閾値以上のみ返す const results = await vectorStore.similaritySearchWithScore(query, 10); const filtered = results.filter(([_, score]) => score > 0.75);

2. チャンクが小さすぎる。 200トークンの断片に分割すると文脈が失われます。モデルは「レートは4.5%」とは見えますが、それが既に更新された2023年の価格セクションだったという情報は見えません。

3. ドキュメントメタデータがない。 タイトル、日付、セクション階層がなければ、モデルはソースの権威性や最新性を判断できません。3年前のブログ記事も昨日の公式ドキュメントも同じ扱いになります。

4. コンテキストの詰め込みすぎ。 より多くのコンテキストが常に良いとは限りません。15チャンクを注入すると、モデルは関連するものの特定に苦労します。「中間で失われる」効果により、15チャンクの4〜12番目の情報が頻繁に無視されます。

5. 「分かりません」の経路がない。 パイプラインが常に回答を生成すると、コンテキストに答えがない場合にハルシネーションします。明示的な棄権経路が必要です。

ハイブリッド検索:両方の長所を活かす

ベクトル類似度検索はキーワード完全一致を見逃し、キーワード検索は意味的類似性を見逃します。ハイブリッド検索は両方を組み合わせます:

async function hybridSearch( query: string, supabase: any, options: { topK: number; vectorWeight: number } ) { const vectorResults = await supabase.rpc('match_documents', { query_embedding: await embedQuery(query), match_threshold: 0.7, match_count: options.topK * 2, }); const keywordResults = await supabase.rpc('search_documents', { search_query: query, match_count: options.topK * 2, }); // Reciprocal Rank Fusion (RRF) で結果を統合 const scores = new Map<string, number>(); const k = 60; vectorResults.data?.forEach((doc: any, rank: number) => { const id = doc.id; const score = (scores.get(id) || 0) + options.vectorWeight / (k + rank + 1); scores.set(id, score); }); keywordResults.data?.forEach((doc: any, rank: number) => { const id = doc.id; const weight = 1 - options.vectorWeight; const score = (scores.get(id) || 0) + weight / (k + rank + 1); scores.set(id, score); }); return Array.from(scores.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, options.topK); }

レイヤー3:出力ガードレール — 生成後のキャッチ

グラウンディングとRAGでハルシネーションを減らし、ガードレールで残ったものをキャッチします。セーフティネットです。

構造検証

最もシンプルかつ効果的なガードレール:出力が期待される構造に一致するか検証します。

import { z } from 'zod'; const ProductRecommendationSchema = z.object({ productName: z.string().min(1), productId: z.string().regex(/^PROD-\d{6}$/), price: z.number().positive(), inStock: z.boolean(), reasoning: z.string().min(20), }); async function validateOutput( llmOutput: string, schema: z.ZodSchema, knownProducts: Map<string, any> ) { let parsed; try { parsed = schema.parse(JSON.parse(llmOutput)); } catch (e) { return { valid: false, error: '構造検証に失敗', data: null }; } // 実データと照合 const realProduct = knownProducts.get(parsed.productId); if (!realProduct) { return { valid: false, error: `製品ID ${parsed.productId} は存在しません`, data: null }; } const issues: string[] = []; if (Math.abs(parsed.price - realProduct.price) > 0.01) { issues.push(`価格不一致: LLMは${parsed.price}と回答、実際は${realProduct.price}`); } if (parsed.inStock !== realProduct.inStock) { issues.push(`在庫状態不一致: LLMは${parsed.inStock}と回答、実際は${realProduct.inStock}`); } return issues.length > 0 ? { valid: false, error: issues.join('; '), data: parsed } : { valid: true, error: null, data: parsed }; }

LLM-as-Judge:自己検証

2回目のLLM呼び出しで最初の出力を検証する方法です。コストはかかりますが、構造検証では捕捉できない微妙なハルシネーションを検出できます:

async function selfVerify( originalQuery: string, context: string, generatedAnswer: string ): Promise<{ isGrounded: boolean; issues: string[]; confidence: number }> { const verificationPrompt = `あなたはファクトチェッカーです。AI生成の回答が提供されたコンテキストによって完全に裏付けられているか検証してください。 コンテキスト: ${context} 元の質問: ${originalQuery} 生成された回答: ${generatedAnswer} 回答を文単位で分析してください。各主張について: 1. コンテキストで直接裏付けられているか? (SUPPORTED) 2. コンテキストから合理的に推論可能か? (INFERRED) 3. コンテキストに見つからないか? (UNSUPPORTED) 4. コンテキストと矛盾するか? (CONTRADICTED) JSONで回答: { "claims": [{ "claim": "...", "status": "...", "evidence": "..." }], "overallGrounded": true/false, "confidence": 0.0-1.0, "issues": ["..."] }`; const verification = await openai.chat.completions.create({ model: 'gpt-5', temperature: 0, response_format: { type: 'json_object' }, messages: [ { role: 'system', content: '正確なファクトチェッカーとして厳密に検証してください。' }, { role: 'user', content: verificationPrompt } ] }); return JSON.parse(verification.choices[0].message.content!); }

コスト最適化: すべての応答を検証する必要はありません。選択的検証を実装しましょう:

function shouldVerify(response: string, context: any): boolean { // 高リスク応答は必ず検証 if (context.category === 'medical' || context.category === 'legal') return true; // 数値を含む応答(捏造されやすい) if (/\d+%|\$\d+|\d+ (users|customers|times)/.test(response)) return true; // 特定の出典を引用する応答 if (/according to|によると|ドキュメントには/.test(response)) return true; // 短い確認応答はスキップ if (response.length < 100) return false; // 残りは10%ランダムサンプリング return Math.random() < 0.1; }

引用検証

モデルが出典を引用したと主張する場合、その引用が実在し、主張を裏付けているかを検証します:

async function verifyCitations( answer: string, providedSources: Array<{ id: string; content: string; title: string }> ): Promise<{ verified: boolean; fabricatedCitations: string[]; unsupportedClaims: string[]; }> { const citationPattern = /\[出典 (\d+)\]/g; const citedSources = new Set<number>(); let match; while ((match = citationPattern.exec(answer)) !== null) { citedSources.add(parseInt(match[1])); } const fabricatedCitations: string[] = []; for (const sourceNum of citedSources) { if (sourceNum > providedSources.length || sourceNum < 1) { fabricatedCitations.push(`[出典 ${sourceNum}] は存在しません`); } } return { verified: fabricatedCitations.length === 0, fabricatedCitations, unsupportedClaims: [] }; }

レイヤー4:信頼度スコアリング — 「分からない」ことを知る

最も強力でありながら活用されていない手法の一つ:モデルに自身の信頼度を報告させ、そのシグナルで応答をフィルタリングします。

トークンレベルの信頼度

OpenAIのGPT-5はlogprobsをサポートしています——生成された各トークンのログ確率です。(注意:AnthropicのClaude APIは現在logprobsをサポートしていません。Claudeでは自己報告信頼度を使用してください。)低確率トークンはハルシネーションのシグナルです:

⚠️ GPT-5の注意点: logprobsreasoning_effort"none"に設定した場合のみ使用可能です。他のreasoningレベルでlogprobsを使用するとエラーになります。

async function getConfidenceScore( prompt: string, systemPrompt: string ): Promise<{ response: string; confidence: number; lowConfidenceSpans: Array<{ text: string; probability: number }>; }> { const completion = await openai.chat.completions.create({ model: 'gpt-5', temperature: 0, logprobs: true, top_logprobs: 3, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ] }); const content = completion.choices[0].message.content!; const logprobs = completion.choices[0].logprobs?.content || []; const avgLogProb = logprobs.reduce( (sum, token) => sum + token.logprob, 0 ) / logprobs.length; const confidence = Math.exp(avgLogProb); const lowConfidenceSpans: Array<{ text: string; probability: number }> = []; let currentSpan = ''; let spanMinProb = 1; for (const token of logprobs) { const prob = Math.exp(token.logprob); if (prob < 0.5) { currentSpan += token.token; spanMinProb = Math.min(spanMinProb, prob); } else if (currentSpan) { lowConfidenceSpans.push({ text: currentSpan, probability: spanMinProb }); currentSpan = ''; spanMinProb = 1; } } if (currentSpan) { lowConfidenceSpans.push({ text: currentSpan, probability: spanMinProb }); } return { response: content, confidence, lowConfidenceSpans }; }

自己報告信頼度

モデルに自身の信頼度を評価させる方法です。完璧ではありませんが、他のシグナルと組み合わせると有用です:

async function generateWithConfidence(query: string, context: string) { const response = await openai.chat.completions.create({ model: 'gpt-5', temperature: 0.1, response_format: { type: 'json_object' }, messages: [ { role: 'system', content: `提供されたコンテキストに基づいて質問に回答してください。 回答の各部分について信頼度を評価してください: - HIGH:コンテキストに直接記載されている - MEDIUM:コンテキストからの合理的な推論 - LOW:コンテキストで十分に裏付けられていない - NONE:純粋な推測 JSONで回答: { "answer": "回答", "confidence_breakdown": [ { "claim": "...", "confidence": "HIGH|MEDIUM|LOW|NONE", "source": "..." } ], "overall_confidence": "HIGH|MEDIUM|LOW|NONE", "caveats": ["重要な制限事項"] }` }, { role: 'user', content: `コンテキスト:\n${context}\n\n質問: ${query}` } ] }); const result = JSON.parse(response.choices[0].message.content!); if (result.overall_confidence === 'LOW' || result.overall_confidence === 'NONE') { return { ...result, shouldEscalate: true, userMessage: `この回答に対する信頼度は限定的です。${result.caveats?.join(' ') || 'ご質問に関連する情報が不足している可能性があります。'}` }; } return { ...result, shouldEscalate: false }; }

レイヤー5:本番環境の監視 — 測定できないものは改善できない

本番環境でハルシネーション率を追跡する方法です。

ハルシネーション率のトラッキング

interface HallucinationEvent { id: string; timestamp: Date; query: string; response: string; hallucinationType: 'factual' | 'source' | 'instruction' | 'coherence' | 'temporal'; severity: 'critical' | 'moderate' | 'minor'; detectionMethod: 'user_report' | 'guardrail' | 'self_verify' | 'citation_check'; context: { model: string; temperature: number; ragUsed: boolean; retrievalScore: number | null; tokenConfidence: number; }; } class HallucinationMonitor { private events: HallucinationEvent[] = []; async trackResponse(params: { query: string; response: string; context: string; model: string; ragScore: number | null; confidence: number; }) { const verificationResult = await selfVerify( params.query, params.context, params.response ); if (!verificationResult.isGrounded) { const event: HallucinationEvent = { id: crypto.randomUUID(), timestamp: new Date(), query: params.query, response: params.response, hallucinationType: this.classifyHallucination(verificationResult.issues), severity: this.assessSeverity(verificationResult), detectionMethod: 'self_verify', context: { model: params.model, temperature: 0.1, ragUsed: params.ragScore !== null, retrievalScore: params.ragScore, tokenConfidence: params.confidence, } }; this.events.push(event); if (event.severity === 'critical') { console.error(`🚨 重大なハルシネーション検出: ${event.hallucinationType}`); } } return verificationResult; } getMetrics(timeWindow: { start: Date; end: Date }) { const windowEvents = this.events.filter( e => e.timestamp >= timeWindow.start && e.timestamp <= timeWindow.end ); return { totalResponses: windowEvents.length, hallucinationRate: windowEvents.length / (windowEvents.length || 1), byType: this.groupBy(windowEvents, 'hallucinationType'), bySeverity: this.groupBy(windowEvents, 'severity'), }; } private classifyHallucination(issues: string[]): HallucinationEvent['hallucinationType'] { const issueText = issues.join(' ').toLowerCase(); if (issueText.includes('source') || issueText.includes('citation')) return 'source'; if (issueText.includes('contradict')) return 'coherence'; return 'factual'; } private assessSeverity(result: any): HallucinationEvent['severity'] { if (result.confidence < 0.3) return 'critical'; if (result.confidence < 0.6) return 'moderate'; return 'minor'; } private groupBy<T>(arr: T[], key: keyof T) { return arr.reduce((acc, item) => { const k = String(item[key]); acc[k] = (acc[k] || 0) + 1; return acc; }, {} as Record<string, number>); } }

ユーザーフィードバックループ

最も正直なシグナル:ユーザーにハルシネーションを報告してもらい、そのデータでパイプラインを改善します。

async function handleFeedback(feedback: { responseId: string; feedbackType: 'hallucination' | 'incorrect' | 'helpful' | 'not_helpful'; userComment?: string; correction?: string; }) { await db.insert(feedbackTable).values({ responseId: feedback.responseId, type: feedback.feedbackType, comment: feedback.userComment, correction: feedback.correction, timestamp: new Date(), }); if (feedback.feedbackType === 'hallucination') { await db.insert(knownHallucinationsTable).values({ responseId: feedback.responseId, userCorrection: feedback.correction, reviewStatus: 'pending', }); } }

全体像:多層防御パイプライン

どの手法一つでもハルシネーションを排除することはできません。本番環境でのアプローチは多層防御です:

async function safeAIResponse( query: string, userContext: { userId: string; category: string } ) { // レイヤー1:コンテキスト検索&スコアリング(RAG) const retrieval = await retrieveWithScoring(query, vectorStore, { topK: 5, scoreThreshold: 0.7, }); // レイヤー2:グラウンディングされた応答生成 const generation = await generateWithRAG(query, retrieval); // レイヤー3:信頼度スコアリング const confidence = await getConfidenceScore(query, groundedSystemPrompt); // レイヤー4:選択的検証 if (shouldVerify(generation.answer, userContext)) { const verification = await selfVerify( query, retrieval.documents.map(d => d.content).join('\n'), generation.answer ); if (!verification.isGrounded) { return { response: "正確な情報をお伝えするため確認しましたが、" + "初回回答の一部を完全に検証できませんでした。" + "確認できた内容のみお伝えします。", confidence: 'low', citations: generation.citations, verified: false, }; } } // レイヤー5:監視 await hallucinationMonitor.trackResponse({ query, response: generation.answer, context: retrieval.documents.map(d => d.content).join('\n'), model: 'gpt-5', ragScore: retrieval.documents[0]?.relevanceScore || null, confidence: confidence.confidence, }); return { response: generation.answer, confidence: generation.confidence, citations: generation.citations, verified: true, }; }

ハルシネーション削減チェックリスト

リリース前

  • システムプロンプトが「分かりません」応答を明示的に許可
  • 事実ベースのタスクにtemperature ≤ 0.3を設定
  • RAGパイプラインに関連性閾値を設定(単なるtop-Kではない)
  • チャンクにメタデータを付与(タイトル、日付、セクション)
  • 構造化出力のスキーマ検証を実装
  • 引用がある場合は引用検証を実装
  • 信頼度スコアリングを統合(logprobsまたは自己報告)
  • 信頼度の低い出力に対するフォールバック応答を準備

リリース後

  • ハルシネーション監視ダッシュボードが稼働中
  • ユーザーフィードバック機構を導入
  • 既知のハルシネーションデータセットを蓄積
  • 週次のメトリクスレビューを実施
  • プロンプト改善のA/Bテストフレームワークを整備
  • クリティカルなハルシネーション急増時のアラートを設定

まとめ

ハルシネーションはなくなりません。LLMの根本的なアーキテクチャ——確率的にもっともらしい次のトークンを予測すること——が変わらない限り、もっともらしく聞こえる不正確な文章を生成する可能性は常にあります。どれだけRLHF、ファインチューニング、巧妙なプロンプトを駆使しても、完全に排除することはできません。

ですが、だからといって信頼性の高いAI機能を構築できないわけではありません。2026年にAIプロダクトを成功させているチームは、ハルシネーションを消す魔法のプロンプトを見つけたのではありません。モデルの周りにエンジニアリングインフラを構築したのです:グラウンディング、検索、ガードレール、信頼度スコアリング、監視、そしてフィードバックループです。

重要な考え方:LLM出力をユーザー入力と同じように扱いましょう。 任意のユーザー入力を検証なしに信頼することはないはずです。LLM出力も同様に、検証なしに信頼すべきではありません。

最もリスクの大きいレイヤーから始めてください。ユーザーが捏造された事実を目にしているなら、関連性閾値付きのRAGを実装しましょう。捏造された引用を目にしているなら、引用検証を追加しましょう。ユーザーが何を目にしているか分からない場合は、まず監視を導入しましょう。

目指すべきは完璧ではありません。許容可能なエラー率をエンジニアリングし、それを検出し、計測し、時間をかけて改善していくためのインフラを整えることです。

AILLMhallucinationsRAGguardrailsproductionTypeScriptOpenAIClaudegrounding

関連ツールを見る

Pockitの無料開発者ツールを試してみましょう