毎朝のニュース確認を「根拠に戻れる形」で自動化した
個人用に、毎朝ニュースを収集して要点を手元に残すシステムを作りました。読むのは自分だけです。公開もしていません。
作った動機は単純で、後から「なぜそう判断したのか」に戻れなかったからです。ニュースを読んだ記憶はあるのに、元の発表が何だったか、数字がいくつだったかを思い出せない。検索し直すと当時とは違う記事が出てくる。
LLM に要約させれば早いのですが、それだけだと問題が入れ替わるだけでした。要約に、元の発表に書いていないことが混ざる。 しかも自然な日本語で混ざるので、読んでいるときには気付きません。
この記事は、その対策としてどういう仕組みにしたかの話です。コードは出しません。構造と、実際に踏んだ失敗を書きます。
出発点:モデルを2つ使っても真偽は保証されない
最初に考えたのは「片方のモデルに書かせて、もう片方に事実確認させる」でした。よくある構成です。
これは不十分だと途中で気付きました。2つのモデルが、同じ誤りをもっともらしく採用しうるからです。片方が「発表には○○と書かれている」と書き、もう片方が「そのとおり」と答える。どちらも元の発表を見ていなければ、確認になっていません。
そこで方針を変えました。
真偽の判定を、LLM ではなく決定的な処理に持たせる。
LLM には「与えられた証拠の範囲で書く」ことだけをさせる。
以降の設計は全部この一点から出ています。
全体の流れ
ポイントは、LLM が2箇所にしか出てこないことです。それ以外は全部、決まった手順の処理です。
1. 証拠を先に固定する
最初に、その日に使う材料を確定させてしまいます。ここで固定したもの以外は、以降どの段階でも使えません。
やっていること。
- 公式の RSS/Atom だけを見る。 中央銀行や規制当局など、発表元そのもの
- HTTPS のみ。ドメインは事前登録した一覧と照合。 リダイレクトされた後の行き先も再検査する
- 各項目について、タイトル・URL・公開時刻・取得時刻・本文の取得できた範囲・内容のハッシュを記録する
- 締め切り時刻より後に公開されたものは、その日の対象に入れない
- 前日までに扱ったものは内容のハッシュで判定して除外する
そして一番効いているのがこれです。
新しい証拠が1件も無ければ、記事を作らずに正常終了する。
「今日は書くことがない」を異常ではなく正常な結果として扱います。これを入れておかないと、材料が薄い日に過去の内容の焼き直しや、誇張が発生します。 毎日必ず何か出力させる、という設計にしないことが安全側に効きました。
2. 生成モデルには固定証拠だけを渡す
記事の下書きは Codex CLI に作らせています。渡すのは3つだけです。
- 固定した証拠一式
- 出力の構造定義
- 書き方の規則
外部の知識で補わせません。 モデルが元から知っていることを足されると、その部分だけ検証できなくなるためです。
出力は文章ではなく構造化データにしています。記事の各主張が、次を持つ形です。
| 項目 | 内容 |
|---|---|
| 主張ID | 本文中のどの記述かを一意に指す |
| 種別 | 「事実」か「推論」か |
| 影響度 | 通常か、大きいか |
| 参照した証拠ID | どの証拠に基づくか |
推論を禁止していません。 禁止すると内容が味気なくなります。かわりに「これは推論です」とラベルを付けさせ、何を根拠にした推論かを示させています。事実と推論を分けて扱えれば、読むときに区別できます。
3. 機械で検査する(ここに LLM は使わない)
生成された下書きに対して、決まった検査を通します。この段階では一切 LLM を使いません。 通らなければ次へ進みません。
主な検査項目です。
- 存在しない証拠IDを参照していないか
- 「事実」とラベルされた主張に、証拠が付いているか
- 影響の大きい事実が、独立した2つの発表元で確認できているか
- 本文に出てくる数値が、参照している証拠の中に実際に存在するか
- 締め切りと鮮度の条件を満たしているか
- URL が許可した範囲に収まっているか
- 断定的な売買表現や、利益を保証する表現が入っていないか
- 本文の記述と、主張の一覧が対応しているか
太字にした数値の検査が、実測でいちばん効きました。モデルはそれらしい数字を作ります。 しかも文章として自然なので、読んでいて違和感がありません。証拠のテキストに同じ数値が出現するかを機械的に照合すると、これが止まります。
4. 別のモデルに、同じ証拠だけで反証させる
機械検査を通ったものだけを、別系統のモデル(Gemini 系)に渡して検証させます。
検証側には、生成側がどう考えたかを渡しません。 渡すのは固定証拠と、出来上がった主張の一覧だけです。生成側の推論過程を見せると、それに引きずられます。
主張ごとに、次のいずれかを返させます。
| 判定 | 意味 | 事実としての合否 |
|---|---|---|
| 支持できる | 証拠から確認できる | 合格 |
| 矛盾する | 証拠と食い違う | 不合格 |
| 判断できない | 証拠が足りない | 不合格 |
| 推論として妥当 | 証拠を超えないが妥当 | 事実としては不合格 |
「判断できない」を合格にしないのが要点です。 ここを通すと、検証が意味を失います。
修正の機会は1回だけにしています。指摘を構造化して生成側へ戻し、もう一度最初から検査します。2回目で駄目なら、その日はそこで止めます。何度も往復させると、内容が指摘に合わせて薄くなっていくだけでした。
5. 失敗を成功として扱わない
自動処理でいちばん怖いのは、失敗が成功に見えることです。そこで状態を細かく分けています。
- 収集した
- 下書きができた
- 機械検査を通った
- 独立検証を通った
- 保存した
- 書く材料が無かった(正常)
- 検査で止めた(異常ではないが、出力しない)
終了コードも、設定の誤り・収集不足・生成の失敗・検査不合格・検証不合格・保存失敗で分けています。「動いたけど中身が無い」と「そもそも動かなかった」を混同しないためです。
実行ごとに監査記録を残しています。使った証拠、各段階の入出力のハッシュ、呼び出した CLI の名前とバージョン、指示文の版、検査結果、失敗の理由。後から「なぜこの記事になったか」を辿れるようにしてあります。もともとの動機がこれだったので、ここは省けませんでした。
実際に踏んだ失敗
設計より、こちらのほうが役に立つかもしれません。
公式サイトが HTTP のリンクを配信していた
日本銀行の RSS は HTTPS で配信されているのに、中の項目リンクが HTTP でした。HTTPS 必須のゲートが全項目を拒否し、その日は1件も取れませんでした。
調べると、同じパスの HTTPS 版が普通に存在しました。そこで、同じドメイン・認証情報なし・ポート指定なしの3条件を全部満たす場合に限り、通信する前に HTTPS へ書き換える処理を入れました。元の HTTP URL へは一度も通信しません。書き換えた事実は記録に残します。
「公式サイトなら整合が取れている」という前提が崩れた例でした。
証明書エラーを「とりあえず無視」しない
欧州中央銀行の取得が、ローカル環境の証明書チェーンを信頼できず失敗しました。検証を切れば通ります。
切りませんでした。 信頼ストアが正しく更新されるまで、取得失敗として記録に残す扱いにしています。一度緩めると戻さないので、ここは最初に決めておく必要がありました。取得率よりも、通信相手が本物であることを優先しています。
モデルが証拠にない数を作った
証拠の件数を、記事中で「○件の発表があった」と書いてきました。証拠のIDを数えただけで、発表本文には出てこない数字です。
数値の照合検査を入れたことで止まりました。同時に「証拠本文に無い数を作らない」を書き方の規則にも足しました。
検証モデルがファイルを読もうとした
検証させようとしたら、モデルが入力ファイルを読むための権限を要求してきました。権限を広く与えれば動きますが、そのために全権限を渡すのは筋が違います。
証拠と下書きを、指示文の中に直接埋め込む形に変えました。ファイルを読ませなければ権限が要りません。副次的に、生成側と検証側へ完全に同じ入力を渡せるようになり、比較の条件が揃いました。
コマンドライン長の上限
Windows で、埋め込む内容が長すぎて起動できなくなりました。
証拠は完全な形で監査用に保存したまま、検証モデルへ渡す本文だけを決まった長さで切り詰めるようにしました。切り詰めた事実も記録します。足りなければ検証側が「判断できない」を返すので、不合格として扱われます。黙って短くしないことが重要でした。
表示用のテキストと構造化出力を取り違えていた
CLI の応答には、画面表示用の文字列と、構造定義に従って検証済みの構造化出力の両方が含まれていました。最初は前者を読んでいました。
見た目は同じでも、後者は形式が保証されています。保証されている方を使うように直しました。
分かったこと
安全側に倒すと「今日は書けません」が増える。それでいい
実際に動かすと、NO_CONTENT で終わる日がかなりあります。締め切り時刻までに新しい発表が無い、あるいは取得に失敗した日です。
最初は物足りなく感じましたが、これが正しい振る舞いです。 材料が無い日に何かを出力させたら、それは作文です。自分が後から読み返す資料としては、空の日があるほうがずっと信頼できます。
本文が取れないと内容が薄くなる
中央銀行の発表は PDF や Excel が主体のことが多く、標準的な処理だけでは本文を抽出できません。フィードの要約文しか取れない場合、記事は「こういう発表があった」で終わります。
実際、日本の消費関連統計の公表を扱った日は、指数の具体的な数値が1つも入りませんでした。 公表があったこと、どういう統計か、いつ出るか、までです。
安全性としては正しい(無い数字を書いていない)のですが、資料としては物足りない。ここは仕組みの限界がそのまま出た形です。抽出の対応は、扱うファイル形式ごとに検査方法を決めてからでないと入れられないので、保留しています。
1つの発表元だけでは照合できない
「影響の大きい事実は2つの独立した発表元で確認する」という条件を入れているため、1媒体しか取れなかった日は、重要な主張を採用できません。結果として「今回の材料では踏み込んだことは言えない」という内容になります。
これも正しい振る舞いですが、発表元を増やさないと解決しません。 仕組みではなく、収集範囲の問題です。
同じ構造を別の用途にも広げた
作っているうちに、同じ構造が別の目的に使えることに気付きました。
同一の入力を、系統の違う3つのモデルへ同時に渡すという使い方です。事実確認ではなく、解釈の違いを見るために使います。
- 同じ証拠一式と、同じ質問を3つへ渡す
- 互いの回答は見せない
- この段階では外部検索もツールも使わせない
- 返ってきた回答を、機械的に「全員一致」「多数」「1つだけの視点」「意見が割れた」に分類する
面白かったのは、意見が割れること自体が情報になることでした。全員が同じことを言う論点は、たいてい発表を読めば分かることです。割れる論点は、本質的に判断の余地がある箇所でした。自分がどこを考えるべきかが分かります。
ここでも、3つ揃わなければ「未完成」として扱い、結果を出しません。1つ失敗したから2つで代用する、をやらないようにしています。比較が目的なので、揃わない比較には意味がないためです。
まとめ
作ってみて、効いた順に並べるとこうなります。
- 材料が無い日は何も作らない。 これが一番効いた
- 数値が証拠に存在するかを機械照合する。 モデルは自然な文章で数字を作る
- 「判断できない」を合格にしない。 検証を形骸化させない
- 失敗を成功として扱わない。 状態と終了コードを分け、記録を残す
- モデルを増やすより、決定的な処理を挟む。 2つのモデルは同じ誤りを共有しうる
逆に、モデルの性能を上げることは、あまり効きませんでした。 起きた問題の大半は、必要な情報が届いていないか、届いた情報を検査していないかのどちらかでした。
自分用の道具なので、見栄えは何もありません。毎朝、根拠を追跡できる形でその日の材料が手元に残る。半年後に「なぜそう考えたか」に戻れる。 目的はそれだけで、いまのところ足りています。