AIの「読んだふり」を構造的に防ぐ
手抜き防止システムの設計と実装から得た知見
0. 何が問題なのか("読んだ"の定義がない)
LLMに「この文書を読んで要点をまとめて」と頼むと、もっともらしい文章は返ってくる。だが、そこに根本的な欠陥がある。
- 本当に読んだのか?
- 冒頭だけ見て推測していないか?
- メタデータ(タイトル、見出し、ファイル名、サイズ)だけで雰囲気を作っていないか?
人間でも「読んだふり」はできる。LLMも同様に、最小の入力から"それっぽい出力"を作ることができる。問題は、その出力が「読んだ上での要約」なのか「推測の作文」なのかを、外部から区別できないことだ。
ここで必要なのは「誠実さ(プロセス)」の検証であって、「文章の上手さ(結果)」の検証ではない。
1. 方針:信頼ではなく、検証可能な証拠
1-1. LLMにLLMを採点させない
LLMに「要約の質を評価して」と頼むと、採点者も被採点者も同じ土俵に立つ。
うまい作文で迂回されるし、評価者も同種のバイアスを持つ。
だから設計判断はこれ:
採点は機械でやる(LLMを介さない)
ここでいう「機械採点」は、次のようなもの:
- ハッシュ一致(sha256)
- 文字列の完全一致
- 正規表現でのパターン一致
- 決められた位置(オフセット)から切り出した文字列一致
LLMが関与できない"硬い検証"だけを採用する。
2. コア技術:sha256による「実在証明」
2-1. sha256とは何か(あまり触れない人向け)
sha256はハッシュ関数で、任意のテキスト(バイト列)を固定長(256bit)の値に変換する。
重要な性質:
- 同じ入力 → 必ず同じハッシュ
- 入力が1文字でも違う → ほぼ確実に全く別のハッシュ
- ハッシュから元の文章を復元できない(不可逆)
つまり「この引用文が原文と完全一致しているか」を、文章比較より堅牢に検証できる。
検証は超単純で:
- 原文から引用部分を切り出す
- その文字列のsha256を計算する
- Readerが提出したsha256と一致するかを見る
一致すれば「この文字列は原文のこの部分と同一」と言える。
2-2. context_sha256の意味("周辺も読んだ"を担保)
sha256だけだと、引用箇所だけを切り出して提出すれば通る。
それでは「引用部分だけ読む」ショートカットが残る。
そこで context_sha256 を入れる。
- 引用部分の前後(例:±128文字、合計256文字)の文字列を取り
- そのsha256も一致させる
これにより「引用の周辺も同じテキストを見ている」ことが機械的に証明される。
**"切り取り作文"**がやりにくくなる。
3. 「散らばり」:冒頭だけ読む手抜きを構造的に封じる
長文で最も起きやすいショートカットはこれ:
- 文書の冒頭だけ読む
- 見出しや結論っぽい箇所だけ見る
- 全体を読んだかのようにまとめる
対策が head/middle/tail散らばり。
文書を3区間に分け、必ず各区間から引用を出させる。
- head:冒頭付近
- middle:中盤付近
- tail:終盤付近
これで「最初の1/3だけ読んで終わり」がほぼ不可能になる。
読む行為が"面で要求される"。
4. 監査質問:引用だけ読む最適化を潰す
4-1. verbatim_substring(直前N文字を答えよ)
これは「読まずに答えられない」質問を機械採点できる形に落としたもの。
例:
- quote_spanの直前20文字を答えよ
- Gateは原文から直前20文字を切り出して完全一致で採点
ポイント:
- LLMの言い換えを許さない(完全一致)
- "雰囲気"や"推測"が通らない
4-2. is_non_quote質問(非引用部の強制)
引用部分だけ精読して他を読まない最適化が起きる。
それを潰すのが is_non_quote。
- quote_spanに含まれない範囲からも質問する
- 例:「chunk_002の冒頭50文字に"深夜アニメ"は含まれるか?」
このタイプは「引用を見れば答えられる」を防ぐ。
文書の別地点への注意を強制する。
5. 実装で詰まったポイントと "設計の学び"
5-1. JSON手打ちは必ず壊れる(特殊文字・エスケープ)
なぜ壊れるのか(丁寧に)
JSONは文字列を " で囲む。
だから本文中に " があると、JSONの構文そのものが壊れる。
さらに日本語文章には以下が混ざる:
- " "(U+201C/U+201D)
- ' '(U+2018/U+2019)
人間は見た目で「ダブルクオート」と理解するが、JSONはただの文字なので、
生成時の扱いを誤ると簡単にパース不能になる。
対処:LLMにJSONを書かせない
結論はこれ:
LLMは意図(フレーズ)だけ出す。JSONはPythonが生成する。
build_extraction.py が
- エスケープ処理
- sha256/context_sha256の計算
- 位置(offset)の整合
を全部引き受ける。
LLMが触る余地を減らすほど堅牢になる。
5-2. "条件付き必須"はドキュメントではなくスキーマで強制
is_non_quote: true のとき chunk_id が必須。
ドキュメントに書いても漏れる。
ここで効くのが JSON Schema の if/then。
JSON Schemaとは(丁寧に)
JSONの構造に「型・必須・制約」を与える仕様。
コードがなくても「このJSONは正しい/間違い」を機械で判定できる。
if/thenの意味
- もし
is_non_quoteが true なら - その場合は
chunk_idなどを必須にする
これで「人間が忘れた」が機械で弾ける。
ドキュメント < スキーマ < コード
スキーマは"設計書"であり"強制力"。
5-3. 検索失敗:特殊引用符の揺れ
原文が "投資" で、検索語が "投資"(ASCII)だと一致しない。
逆も同様。見た目は同じでもコードポイントが違う。
対処:--normalize(正規化)
- 検索時に " " を " に寄せる
- ' ' を ' に寄せる
- ただし原文そのものは変えない(忠実性を守る)
ここがポイント:
検索は正規化しても良いが、証拠(sha256)は原文のまま。
6. 設計原則(落としてはいけないもの)
| 本質 | 理由 |
|---|---|
| sha256 + context_sha256 | 「読んだ」の実在証明。核 |
| head/middle/tail散らばり | 冒頭だけ読む手抜きを封じる |
| 機械採点(LLM不使用) | 迂回されにくい・再現性がある |
| is_non_quote必須 | 引用部だけ読む最適化を封じる |
| rejectで差し戻す | "通らないものは採用しない"を制度化 |
7. 削ってよかった非効率(ここが重要)
| 削ったもの | 理由 |
|---|---|
| 手動sha256計算 | 誠実性ではなく作業ミス源。自動計算でよい |
| exact_match(文境界一致) | Markdownで壊れる。目的は誠実性であって句読点ではない |
| JSON手打ち | 壊れるのが自然。生成はコードで固定する |
| 旧形式の後方互換 | "いつまでも移行できない"を生む。rejectで強制移行 |
8. 得られた知見(実務的まとめ)
8-1. 「信頼するが検証する」
LLMを信頼しないのではない。
"読んだ"という主張を検証可能にすることで、使える領域が増える。
8-2. 抜け道は必ず見つかる(だから制度で潰す)
make_quote_span.pyは便利だが、便利は抜け道も作る。
だから「便利を捨てる」ではなく、is_non_quoteのような制度で押さえる。
8-3. LLM=意図、Python=証拠
LLMにやらせるのは「どこが重要か」「何を抜きたいか」の意図。
証拠(sha/offset/JSON)はPythonが生成。
この分業が一番堅牢。
8-4. スキーマは"口約束"を排除する
「入れてください」は漏れる。
入れてないと通らないにすると漏れない。
付録:この先の拡張(任意)
- Markdown構造に基づくchunk分割(見出し/段落境界で切る)
- is_non_quote質問の自動生成(引用外領域から自動サンプリング)
- traceの自動保存と差し戻しのワークフロー固定