前提
個人開発で、AIに日次のレポート記事を自動生成させる自動化を運用しています。生成AIが事実と異なる内容を書かないよう、「その日に実際に起きた出来事」を業務ログテーブルから時間範囲で取得し、それを記事の事実の根拠(事実一覧)としてAIへ渡す設計にしていました。
ある時、生成された記事の事実一覧を確認していて、その記事自身を生成・校正・査読するためにAIへ送った指示コマンドまでもが、「その日の出来事」として一覧に混ざっていることに気づきました。この記事では、固有の内部システム名や具体的な関数名・ファイル名は出さず、原因の切り分け方と、そこから確立した実装パターンを一般化してまとめます。
何が起きていたか
事実収集のロジックは単純で、「対象日の00:00〜23:59に記録された業務ログを、種類を問わず全部集める」というものでした。疑似コードで書くとこうです。
function gatherDailyFacts(dateStr) {
const logs = queryActivityLogsByDateRange(dateStr); // 時間範囲だけで絞り込み
return logs.map(toFactEntry);
}
これ自体は「その日に何が起きたか」を正しく拾えているように見えます。しかし実際には、業務ログテーブルにはその記事を生成するパイプライン自身がAIへ送ったコマンド(「この内容で記事を保存してください」「このセクションを書き直してください」といった指示)も、他の業務ログと全く同じ形式・同じテーブルに記録されていました。
時間範囲だけでフィルタしているため、この自己言及的なログも「その日の出来事」として区別なく拾われ、事実一覧へ混入していました。つまり、AIが自分自身への生成指示を、自分の記事の「事実」として引用しかねない状態になっていたのです。幸い本文への直接漏洩は別のチェック機構で防がれていましたが、事実の根拠一覧そのものが汚染されているという点は、そのチェックの対象外でした。
なぜ場当たり的な文字列除外では不十分か
最初に思いつく対処は、「生成コマンドの文言に含まれがちな単語(『保存してください』『書き直してください』など)を固定リストにして除外する」という方法です。しかし、これには構造的な弱点があります。
- パイプラインに新しい処理(新しいAI呼び出しの種類)が追加されるたびに、除外リストを手動でメンテナンスし続けなければならない。更新を忘れると、その新しい処理のログだけが静かに漏れ続ける。
- 固定文言はプロンプトの言い回しを少し変えただけで一致しなくなる、壊れやすい一致条件になりがちです。
つまり、「除外すべき対象を後から人が思い出して追記する」設計そのものが、漏れの温床でした。
採用した設計: 除外リストを「唯一の情報源」から自動生成する
対処方針を変えて、除外リストを人手で管理せず、システム内に既に存在する唯一の情報源(single source of truth)から機械的に導出することにしました。
このパイプラインでは、AIに構造化出力をさせる際、必ず「ツールschema定義(各処理が公式に持つ名前の一覧)」を経由しており、AIへのプロンプトには構造化出力の要件上、必ず該当ツールの名前を明示的に参照する文言(「〇〇ツールで保存してください」)が含まれることが、コードの構造上保証されていました。この性質を使い、除外リストをハードコードするのではなく、ツールschemaの一覧から実行時に導出するようにしました。
// Before: 除外したい文言を、人がその都度リストへ追記する
const META_LOG_KEYWORDS = ['保存してください', '書き直してください']; // 増えるたびに手動更新が必要
function isMetaLog(command) {
return META_LOG_KEYWORDS.some(kw => command.includes(kw));
}
// After: 除外リストを、既存のツールschema定義(唯一の情報源)から自動導出する
const GENERATION_TOOL_NAMES = Object.values(articleGenerationTools)
.map(tool => tool && tool.name)
.filter(Boolean); // 新しいツールが増えても自動的に拾われる
function isGenerationMetaLog(command) {
if (!command) return false;
return GENERATION_TOOL_NAMES.some(name => command.includes(`${name}ツール`));
}
function gatherDailyFacts(dateStr) {
const logs = queryActivityLogsByDateRange(dateStr);
return logs
.filter(l => !isGenerationMetaLog(l.command)) // 自己言及ログだけを除外
.map(toFactEntry);
}
ポイントは2つです。
- 「何を除外すべきか」を、新しいコードを書くたびに人が思い出す前提にしない。ツールschemaの一覧はパイプラインの実装上すでに存在する正本であり、新しい処理を追加するとそこへ自動的に登録される。除外ロジックはそこを参照するだけにする。
- 一致条件は、コード側で構造的に保証されている性質(ツール名が必ずプロンプトに明示される)に乗せる。プロンプトの言い回しの揺れに影響されにくい。
新しい生成処理を追加しても、除外リスト側の更新は不要になりました。
テストでどう検証したか
このロジックに対して、以下のカテゴリでテストケースを用意しました(2026-09-26時点で再実行し、全20件PASSを確認済みです)。
| テストカテゴリ | 確認すること |
|---|---|
| 通常の業務ログの保持 | 自己言及ではない通常の業務ログは、引き続きfactsに残る |
| 自己言及ログの除外 | 記事生成そのものへの指示ログが、factsから除外される |
| 同日内の他ログとの区別 | 同じ日に記録された通常業務ログは、自己言及ログの除外に巻き込まれず残る |
| 除外対象の網羅性 | ツールschemaに登録されている複数の処理名それぞれについて、個別に除外が効くことを1件ずつ確認する |
| 日付境界ロジックとの独立性 | 既存の「日付境界(前日/翌日への混入防止)」ロジックと、自己言及除外ロジックが、互いに干渉せず両方機能する |
| 出来事が無い日の既存仕様への回帰 | 対象日に出来事が無い場合は引き続き空配列を返し、事実を創作しない既存仕様が壊れていない |
特に「除外対象の網羅性」を1件ずつ個別テストにしたのは、ツールschema一覧から自動導出する設計に変えた後も、「実際に全ツール分がちゃんと拾えているか」を機械的に保証するためです。自動導出にした時点で「たぶん大丈夫」ではなく、実際の一覧を1件ずつ検証することで、導出ロジック自体の回帰にも気づけるようにしています。
なぜこの種の汚染に気づきにくいか
- 時間範囲によるログ収集は、単体で見ると正しく動いているように見える。「対象日のログを全部拾う」という実装は、それ単体でテストすれば期待通りに動くため、収集元のログテーブルに複数の異質な情報(通常業務ログと自己言及ログ)が混ざっていること自体に気づきにくい。
- 本文への直接的な漏洩を防ぐチェックがあると、「もう防げている」と錯覚しやすい。実際には、事実の根拠一覧(引用元候補)そのものが汚染されているかどうかは別の検査対象であり、本文側のチェックとは独立して確認する必要がありました。
- 自己言及ログは、内容だけを見ると業務ログとして「もっともらしい」。異常な値や壊れたフォーマットではないため、パイプラインが正常に完走していても発見が遅れやすい性質があります。
まとめ
- AIに「事実」を渡して記事や回答を生成させる仕組みでは、その生成処理自身が書き込んだログを、無条件に事実の情報源へ含めてはいけない。自己参照的な汚染は、システムが正常に動いているように見えても発生しうる。
- 除外リストを人手でメンテナンスする設計は、追加のたびに漏れが発生する構造的なリスクを抱える。除外条件は、システム内に既に存在する唯一の情報源(この場合はツールschema一覧)から自動導出し、新しい処理が増えても追従できる形にする。
- 除外ロジックを自動導出に変えた後も、「実際に全対象が正しく拾えているか」は個別テストで機械的に保証する。自動化した設計自体が正しく機能しているかどうかは、別途検証が必要。
AIに事実収集や引用元の絞り込みを任せる自動化を設計している方の参考になれば幸いです。
検証条件と限界
このロジックは、実際に発生した事象(自己言及ログの混入)を踏まえて実装したものであり、2026-09-26時点でテスト20件のPASSを再確認しています。一方で、次の点は限界として明記します。
- この設計は、「各処理が構造化出力の要件上、必ず自分の名前をプロンプト内に明示する」という、このパイプライン固有の構造的な保証に依存しています。プロンプトの自由文章内に処理名が含まれる保証がないシステムでは、同じ一致条件はそのまま使えません。
- 除外リストの自動導出自体は、ツールschemaという「唯一の情報源」が実際に存在し、かつそれが処理の実体と1対1で対応している場合にのみ有効です。そのような正本が存在しない設計では、まず正本を作るところから始める必要があります。