LLM(大規模言語モデル、大量のテキストで学習した生成AI)を使ったアプリを設計しているエンジニアに向けた内容です。特に、生成AIが出す文章の正確さを担保する仕組みをどう作るか悩んでいる方の参考になれば幸いです。
ゲームのセーブファイルを題材に、AIレポーターがゴシップ記事を書く個人プロジェクト「The Estian Tattler」があります。RimWorld(入植地経営シミュレーションゲーム)のセーブデータから、住民の結婚や喧嘩などの出来事をClaude(Anthropic製のLLM)が記事化する仕組みです。
ここで注目したいのは記事の面白さではなく、AIの出力を検証する層の置き方です。631件の記録のうち、実際に印刷された記事が引用したのは136件にとどまります。裏を返せば、残り495件はAIが書こうとしても却下された、あるいはそもそも扱われなかった可能性があるということです。
何が起きるか: LLMは「もっともらしい嘘」を高い確率で混ぜてくる
LLMを使って何かを自動生成させるシステムを作ると、ほぼ必ず直面する問題があります。生成された文章が、事実に基づいているように見えて実は根拠がない、という現象です。これは一般に「ハルシネーション(幻覚、AIが存在しない情報をもっともらしく生成すること)」と呼ばれます。
このプロジェクトの場合、AIレポーターは「誰と誰が結婚した」「39回訪れた」といった具体的な人物名や数値を含む文章を書きます。ここで人物名や回数が少しでも間違っていれば、記事としての信頼性はゼロになります。ゴシップ紙が題材なので実害は小さく見えますが、これが医療記録の要約や、監査ログの説明文生成だったらどうでしょうか。
業務システムでLLMに「ログを要約して」「顧客の行動を説明して」と指示すると、同じ構造の問題が起きます。存在しない顧客IDを参照したり、実際のログにない数値を書いてしまったりする挙動です。これはモデルの性能の問題ではなく、生成の仕組み上避けられない性質です。
なぜ起きるか: 検証をモデル自身に任せているから
原因を段階的に分解すると、次のようになります。
まず、LLMは「次にどんな単語が来ると自然か」を確率的に予測して文章を作ります。事実データベースを検索して裏取りしているわけではありません。プロンプト(AIへの指示文)に「事実だけを書いて」と書いても、それは努力目標を伝えているだけで、システムとしての保証にはなりません。
次に、多くの実装では「もう一度AIに確認させる」という形で検証を行いがちです。しかしこれは、同じ確率的な生成の仕組みで生成物をチェックしているだけなので、根本的な解決になりません。モデルAが書いた嘘を、モデルBが正しく見抜ける保証はどこにもないからです。
このプロジェクトの設計はここが違います。ファクトチェッカーは「plain TypeScript」、つまりモデルではなく普通のコードとして実装されています。ルールは明確です。コロニスト(住民)の名前を含む文なら、その名前が引用元の記録に実在するか。数字を含む文なら、その数字が引用元のテキストに文字通り含まれているか。この判定は自然言語理解ではなく、文字列の一致検証で機械的に行えます。
さらに、記事の文はすべて「claim(裏付けが必要な主張)」か「aside(地の文、人物名や数字を含んではいけない)」のどちらかに分類されます。この分類自体もスキーマ(データ構造の定義)レベルで強制されており、AIが自由に混ぜて書くことを許していません。
自分のプロジェクトが該当するか確認する方法
自分の開発しているシステムがこの落とし穴に該当するか、次の観点で確認してみてください。
- LLMの出力に固有名詞・数値・日付など「検証可能な事実」が含まれているか
- その出力が最終的にユーザーや監査ログに表示され、誤りが業務上のリスクになるか
- 現在の検証手段が「もう一度LLMに聞く」「人間が全件目視する」のどちらかに偏っていないか
- 出力の根拠となった元データ(DB、ログ、ドキュメント)へのIDやリンクを保持しているか
該当するプロジェクトのコードを見る際は、LLM呼び出し部分の直後に「後処理」がどう実装されているか確認してください。プロンプトの中に「事実に基づいて」「嘘をつかないで」という文言しかない場合、それは検証ではなく依頼です。文字列一致・型チェック・スキーマバリデーションのような、モデルを介さないコードが後段にあるかどうかが分かれ目になります。
対策の手順: 生成と検証を別レイヤーに分離する
対策の骨格は、このプロジェクトの構成をそのまま応用できます。
手順1: 出力を「検証可能な単位」に分解する
文章まるごとを検証しようとせず、文単位・フィールド単位に分解します。今回の例では「1文=1主張」という粒度でPortable Text(構造化されたリッチテキスト形式)のアノテーションとして保存しています。検証したい要素(人物名・数値・日付)を、生成物の構造の中で個別に取り出せる形にすることが出発点です。
手順2: 検証ロジックをモデルの外に書く
名前の実在チェックや数値の一致チェックは、正規表現や文字列比較、DBクエリで実装します。ここにLLMを使わないのがポイントです。判定ロジックが単純な文字列処理で書けるなら、コストも実行時間もモデル呼び出しより圧倒的に軽く、再現性も保証されます。
手順3: 失敗時の差し戻しルートを用意する
このプロジェクトでは、チェックに落ちた原稿はレポーター(AI)に差し戻され、3回失敗すると「spike」(記事の没)扱いになります。無限リトライにせず、上限を設けて人間の判断に委ねる設計です。
# 検証ロジックの実装イメージ (擬似コード)
# LLMの出力から claim を抽出し、引用元レコードと突合する
for claim in extract_claims(article_text):
cited_records = fetch_records(claim.reference_ids)
if not all(name in cited_records.text for name in claim.names):
return reject(claim, reason="name not found in cited records")
if not all(num in cited_records.text for num in claim.numbers):
return reject(claim, reason="number not found in cited records")手順4: 人間の承認ステップを最後に残す
自動チェックを通過した原稿も、即公開せず「editor's desk」で人間が確認しています。自動検証は「明らかな嘘を弾く」フィルターであって、公開判断そのものを代替する設計にはしていません。ここは可用性や運用コストとのトレードオフになる部分なので、業務システムであれば承認フローの粒度を要件に応じて調整する判断が必要です。
手順5: 引用元へのトレーサビリティを残す
Studio上の「Receipts view」は、各文とその根拠レコードを並べて表示する機能です。生成物と根拠データの対応関係をUIやログに残しておくと、監査や障害調査の際に「なぜこの出力になったのか」を後から追跡できます。技術的負債という観点では、根拠を残さない生成パイプラインは後から検証コストが跳ね上がる典型例です。
まとめ
LLMを使ったシステムで事実の正確さが求められるなら、検証をプロンプトの中の「お願い」で終わらせず、モデルの外側に独立したチェック層を持つ設計が実務的な選択肢になります。
確認すべきは、自分のプロジェクトのLLM呼び出し直後に、モデルを介さないコードで書かれた検証ロジックが存在するかどうかです。
存在しない場合は、まず生成物を検証可能な単位(文・フィールド)に分解するところから始めると、後からチェッカーを追加しやすくなります。
差し戻し回数の上限と、最終的な人間の承認ステップも合わせて設計しておくと、自動化と信頼性のバランスを取りやすくなります。