ENGINEERING NOTE · STATIC SITE
AI記事の「確認済み」をどう管理するか静的サイトに根拠レコードと変更検知を実装した
記述・検証日 · 対象: 技術ブログ、AIコンテンツ、社内ナレッジを扱うエンジニア
説明対象: 0b9ad2b115fdcb83291cf7f3f928ff7edba1a708 / PR #71。記事制作開始時のmainも同じSHA。以後のmainとは切り離して読むため、コードへのリンクを固定した。
出典リンクが付いた記事でも、その資料がどの文を、どの条件で支えるのかは分からない。さらに本文を書き換えると、以前の「確認済み」だけが残ることがある。VisionHubでは、主張・出典・条件・照合記録を結び付け、古い記録をそのまま使う編集ミスを検出する仕組みを静的サイトへ加えた。
この仕組みが調べるのは、登録した記録の形式と対応関係だ。出典を読んで真偽を自動判定するものではない。本稿では、小さなPythonサンプルで「変更を検出できた」と「内容が正しい」を分けて観察する。読み終えたら、自分のシステムで何を記録し、どこで検査するかを判断できることを目指す。
1. 出典だけでは足りなかった
具体例は、Crusoeの資金調達を扱った記事の訂正だ。保存済みHTMLでは見出しに「300億ドル評価」とある一方、本文に企業価値「30億」と書かれていた。訂正後は、企業価値約300億米ドル、調達額約30億米ドル、別件の契約額約130億米ドルを分け、TechCrunchがBloomberg報道として伝えた内容だと明記した。これは訂正差分と現在の訂正定義から確認できる経緯であり、誤りを生んだ原因まで断定するものではない。
HTMLへの訂正反映の差分 / 固定した訂正定義 / 表示の実例
同じ記事中の数字でも、測っている指標が違えば別の主張になる。「公式仕様で可能」「ベンダーが測定した」「運営者が試した」も同じ強さの説明ではない。読者に必要なのはリンクの本数ではなく、主張に対応する資料の箇所と、その資料で分からない範囲だ。そこで記事全体へ一つの合格印を付けず、必要な文に安定したIDを与えた。
2. 二つの生成経路を、一つの表示契約につなぐ
静的ページは埋め込みJSONと本文要素を照合して根拠表示の区画だけを更新し、ニュースは既存の訂正・整形・解析・出力経路で同じ根拠レコードを引き継ぐ。
静的ページには、本文のdata-claim-id、表示先のdata-evidence-for、レコードを持つscript#claim-evidence-dataを置く。ヘルパーは本文の可視テキストとstatementを比較し、各IDに本文と表示先が一つずつあるか検査する。ページ全体の生成器ではなく、手で保守するHTMLの一部分を共通表示へ合わせる道具である。
ニュースでは、URLに対応する既存の訂正定義に根拠を置いた。整形時は日次テキストの専用行へJSONを保存し、解析時に復元する。Top15とDaily Newsはそれぞれの出力経路で共通レンダラーを呼び、派生JSONにも記録を残す。ホーム用JSONは同じレコードと日付固定の詳細リンクを引き継ぐ。統一DBや全記事共通の新しい生成基盤を導入したわけではない。
手で保守する静的HTML
- 本文ID + 埋め込み根拠JSON
render_static_pageで本文との対応を検査render_evidenceで指定区画を更新
既存のニュース生成
- 訂正レコード → 日次テキスト → 解析 → Top15のHTML・JSON
- 記事レコード → Daily NewsのHTML・JSON
- Top15側のJSON → ホーム用JSONへの引継ぎ
両経路は検査・表示関数を共有する。公開前検査は登録対象のHTML・JSONを照合する。矢印はデータの受け渡しを示し、出典の真偽判定は含まない。
3. 主張と根拠を別の軸で記録する
statementは照合した文、basisは根拠の種類、statusは資料との対応状態を表す。例えばベンダー公称値が資料に書かれていると確認できても、第三者実測に変わるわけではない。既存のFactやClaimという情報区分も残し、それだけで信頼性を保証しないよう役割を分けた。
source_refsには資料IDだけでなく、該当する節と、その箇所が支える内容を書く。conditionsは対象OSや評価条件、uncertaintyは未確認範囲だ。as_ofは話題の対象時点、出典の公開日はsources側、実際に資料を照合した日はverification側に置く。三つの日付とサイトの生成日は、同じ時計ではない。
架空の説明用データ · 完全な入力はサンプルに同梱
{
"version": 1,
"sources": [
{
"id": "fictional-vendor-doc",
"publisher": "架空ベンダー",
"title": "架空の固定条件レポート",
"url": "https://example.com/fictional-report",
"published_at": "2026-09-03"
}
],
"claims": [
{
"id": "fictional-rate",
"label": "架空の公表値",
"statement": "架空ベンダーは固定条件で達成率72.6%と公表した。",
"evidence_label": "Claim",
"basis": "vendor_claim",
"status": "matched",
"source_refs": [
{
"source_id": "fictional-vendor-doc",
"section": "結果",
"supports": "架空ベンダーが達成率を公表したという主張"
}
],
"conditions": [
"架空の固定ベンチマーク",
"対象は説明用データのみ"
],
"as_of": "2026-09-03",
"uncertainty": "独立した再測定は未実施",
"verification": {
"checked_at": "2026-09-05",
"method": "ai_document_review",
"fingerprint": "bdcd18d508044eb8cbbd0149fa9c575f46befc71e7c6d4b40f32c7a115b537ea",
"note": "架空資料の表と記録をAIが照合した設定。人による確認記録はありません。"
}
}
]
}
上の数値・団体・資料は説明用の架空データ。example.comへのアクセスは行わない。実行用フィクスチャでは省略なしのfingerprintを保持する。構造の仕様はSCHEMA.mdを参照。
照合済み、一部裏付け、不一致を記録するには、参照資料と有効な日付・方法・fingerprintが必要になる。一方、出典のない未確認レコードも表現できる。空欄を今日の日付で埋めるより、「何をまだ記録できていないか」を見える状態に保つことを選んだ。
4. fingerprintが結び付ける範囲を限定する
claim_fingerprintは主張・条件・参照資料を、article_fingerprintはtitle・tldr・summaryをハッシュ化する。照合状態やverification内の日付・方法は、前者のハッシュ対象に含まれない。
主張側はstatement、evidence_label、basis、conditions、as_of、uncertainty、measurementを対象にし、参照ごとのsource_refs全体と、参照先のsourceレコード全体を加える。JSONのキーを並べ替えて一定の形式へ直し、UTF-8のバイト列からSHA-256を計算する。statement中の連続した空白は正規化するが、条件の配列などをすべて意味的に正規化する処理ではない。
| 関数 | 含むもの | 含まないもの |
|---|
claim_fingerprint | 主張、情報区分、根拠の種類、条件、対象時点、未確認範囲、実測情報、参照と参照先レコード | claimのid・label・status、verification全体、未参照のsource、出典Webページの本文 |
article_fingerprint | 空白を正規化したtitle・tldr・summary | URL、ランキング点数、記事のその他のフィールド、HTML全体 |
記事側のfingerprintは、ニュースの見出しや要約だけが変わっても以前の記録を使い回さないためのものだ。subjectを渡して検査する場合は、明示した根拠情報にsubject_fingerprintが必要になる。静的HTMLでは、この代わりに本文要素とstatementの一致をヘルパーが調べる。ページ内の全文章を自動で探して監査する機能ではない。
通常のビルドは保存済みの値と再計算した値を比較する。そこで保存値まで更新してしまうと、本文変更と再照合を区別できなくなる。このため生成と照合記録の作成を分けている。ただし日付・方法・statusはハッシュ対象外であり、有効な値へ手で変更しても主張のハッシュでは検出できない。ニュースの派生データは正本との比較でも検査するが、静的ページの確認者の履歴まで保護する仕組みではない。
5. 検査で止めることと、表示を保つこと
validate_bundleはエラー一覧を返し、require_valid_evidenceは不整合で例外を出す。render_evidenceは無効なbundleの照合状態を未確認として表示し、根拠情報のない旧データには表示を加えない。
検査では不明な分類、参照先の欠落、実在しない日付、本文と保存済みハッシュの不一致などを扱う。表示側は、bundleのどこかにエラーがあると、そのbundleの主張を未確認に下げ、警告を付ける。壊れたデータに強い合格表示を残さないためだ。これは入力を書き換えて修復する処理でも、正常に公開できたという意味でもない。
静的ヘルパーは本文と記録の不一致があれば書き出さず、表示区画だけが古い場合は通常の検査でエラーにする。レビュー後の--writeで区画を更新すると整合する。公開前スクリプトは登録したページとCrusoe記事の派生物を検査する。説明対象の版では静的3ページが対象で、この記事を追加した版では記事を1ページ追加した。未登録の記事を合格扱いで集計しない。
6. 全面移行しない設計の利点と負担
既存記事のclaim_evidenceは任意項目にした。Noneや空オブジェクトなら検査はエラーを返さず、根拠欄も作らない。古い記事へ架空の照合日を補う必要がなく、段階的に導入できる。その代わり、根拠のない記事がサイト上に残る。「表示なし」と「確認済み」を取り違えない編集方針が必要だ。
後から方式を比較すると、すべてをCMSへ移す案は承認履歴を一元化しやすいが、既存の生成経路を作り替える範囲が広い。各HTMLに自由文だけを追記する案は導入が軽い一方、派生出力や変更前の記録との対応を機械で検査しづらい。ここでの比較は本稿での考察であり、過去にこれらの案を正式検討して却下したという記録ではない。
採用した形にも保守の負担はある。本文とJSONを対応させて編集し、出典を読み直したら対象と方法を記録し、派生出力を再生成する必要がある。HTTP 200を確認しても資料の中身を読んだことにはならない。小さい共通関数へ検査と表示を集めることで、形式の修正を各ページで別々に実装する負担を抑えたが、意味を確かめる作業は残る。
7. 手元で正常・拒否・限界を再現する
再現用のスクリプトは、既存のclaim_evidenceと静的HTMLヘルパーを呼ぶだけの小さな利用例だ。各ケースは初期フィクスチャの複製から始め、本文や条件を変更するケースでは保存済みfingerprintを維持する。不正入力が拒否されたら、そのケースは成功である。値を再計算してエラーを消す操作は行わない。
対象環境: Windows(OS build 26200)、PowerShell 7.6.5、Python 3.11.15、Git 2.55.0.windows.2。サンプルはPython標準ライブラリのみ。Nodeやpytest、APIキー、有料APIは不要。
まずREADMEの準備手順に従って、固定SHAのコードとこの記事のサンプルを同じ作業ディレクトリへ置く。
python -B -S examples/claim-evidence/reproduce.py
取得時のGit通信と、検証の実行は別。サンプル実行時は出典URLへアクセスせず、外部パッケージの導入もない。Linux/macOSでの実行は未確認。
2026-09-05に固定SHAの新規クリーンチェックアウトで実行。-Sで外部パッケージを読み込まず、12/12 PASS・終了コード0を確認した。12件には版の確認1件を含む。意図した不正入力の拒否は、期待する挙動として成功に数える。
| 入力・ケース | 期待と実際の結果 |
| 01 コードの版 | HEADとコア2ファイルのLF正規化ハッシュが一致。 |
| 02 正常な架空記録 | エラーなし。ベンダー公称値・資料との照合済みを表示。 |
| 03・04 主張/条件を変更 | 保存済み値を変えずに検査し、fingerprint不一致で拒否。 |
| 05・06 参照ID欠落/出典タイトル変更 | missing referenced source/fingerprint不一致を検出。 |
| 07 根拠のない旧データ | Noneと空オブジェクトの表示は空。合格印を生成しない。 |
| 08 区分を維持 | 公称値とAI照合を、第三者測定や人の照合へ自動変換しない。 |
| 09 確認日・方法だけを手で変更 | 有効な値なら検査を通る。確認者情報を認証しないという限界を確認。 |
| 10 同じ入力の再生成 | HTML・照合日・出典が不変。既存の静的チェッカーも成功。 |
| 11 不自然な架空主張 | 「月面の全利用者が一秒で宇宙船を組み立てた」でも形式上は成功。真偽判定はしない。 |
| 12 記事要約/HTML本文の変更 | subject fingerprint/本文要素との不一致で拒否。 |
今回の実行結果(末尾)
PASS 12 subject_and_body_changes_rejected
RESULT 12/12 checks passed; expected rejections were observed.
同じ入力から二度レンダリングして、HTMLと照合日と根拠が変わらないことも比較する。これは日付を固定して新しい照合を装う処理ではなく、入力の保存値を保持する確認だ。HTMLは一時ディレクトリに作成し、静的検査を終えると削除する。本番記事のデータは変更しない。作成物と後片付けの詳細もREADMEに記載した。
8. 検査を通っても残る限界
この検査は出典Webページの本文を取得・保存・比較せず、入力した主張の真偽も判定しない。fingerprintは電子署名や第三者認証ではなく、編集権限を持つ人の再計算を防がない。
架空の不自然な数値でも、構造と保存済みハッシュを整えれば検査は通る。したがって、この仕組みは自動ファクトチェックの完成形ではない。資料の該当箇所が本当に主張を支えるか、条件が本文に十分反映されているかは別途確認が必要だ。参照先が同じURLのまま書き換わっても、保存したsourceレコードが変わらなければ検出できない。
記録された確認日は、現在も情報が有効である保証ではない。未確認は虚偽を意味せず、資料との照合済みは全環境での実証を意味しない。対象外の記事まで品質を保証するものでもない。照合状態の件数を正答率に読み替えたり、不一致の少なさを誤情報の少なさとみなしたりする根拠はない。誤情報やレビュー時間をどれだけ削減したかも、本稿では測定していない。
9. 別のシステムへ持ち込むなら
技術ブログへの応用なら、価格や動作条件など更新で意味が変わる文を選び、対応する資料の版と照合対象を記録するとよい。仕様管理なら、要件IDに対応する契約や決定記録を結び付ける設計が考えられる。ただし「誰が承認できるか」と「どの変更を再確認するか」は、そのシステムの運用に合わせて決める必要がある。
RAGの回答へ応用する場合も、検索で見つかった文書と、回答中の各主張を結び付けるところから始まる。文書の版、取得した箇所、回答生成後の編集、検索できなかった範囲を別々に管理する必要があるだろう。これは設計案であり、この実装でRAG連携を動作確認したわけではない。保存期間、更新検知、承認履歴をどう扱うかは追加の検討事項になる。
他の場面に持ち込めるのは、ハッシュを付ければ安心という考え方ではない。照合した対象を具体的に固定し、変更時の機械的な検査と、資料を読んで判断する作業を分けることだ。導入先でも、検査が成功した理由と、成功しても分からないことを同じ画面や手順で伝えたい。
コード・検証・制作について
PR #71に記載された関連94テストPASSは、その公開候補での過去の結果。本稿のサンプル実行件数とは別であり、合算しない。heroIdentityの旧期待値と新規ブランチpush時のゼロSHA処理は既知の別課題で、本記事では変更していない。
実装・資料照合・記事の文章化と再現確認にはAIを使用している。運営者が依頼範囲や公開可否を判断したことと、全コードを手作業で書いたこと、すべての主張を人が確認したことは同じではない。本稿は後者を作業実績として主張しない。
再利用条件: 対象SHAのリポジトリで、全体に適用するLICENSE/COPYINGとsetup.pyのライセンス指定は確認できなかった。公開されていることだけを理由に、任意の再配布ライセンスを宣言しない。導入・転載時は適用条件を確認する。