AI要約が、2026年の記事を「2024年から変更」と伝えていた話

当ページのリンクには広告が含まれています。
この記事の結論
AI要約が、元記事に書かれていない「年」を自分で補い、これから始まる変更を2年前の出来事として伝えていました。プロンプトに公開日を渡す修正と同時に、要約を保存する前の照合も入れました。要約と元記事から同じ規則で西暦を抜き出して突き合わせ、根拠の無い年があれば作り直し、それでも駄目なら保存しない仕組みにした話です。

お疲れ様です!IT業界で働くアライグマです!

個人開発している開発関連ニュースのキュレーションサービス「DevPick」で、AIが書いた記事の要約に誤った年が混ざる問題を直しました。DevPickは、開発関連のニュースをAIが分析し、要約を付けてその人の興味に合った記事を届けるWebアプリです。今回は、なぜAIが年を間違えたのか、そしてなぜ指示の修正と一緒に出力の照合まで入れたのかを書きます。

目次

何が起きていたか

きっかけは、本番の記事詳細ページに表示されていた要約でした。2026年9月26日に公開された、Microsoft Copilotの大型アップデートを伝える記事です。その要約の重要ポイントに、次の1文が入っていました。

発表日は2024年9月25日(※文中では9月25日と記載)であり、Copilotの機能拡張が示された

元記事の概要には「9月25日」としか書かれていません。AIはそこに「2024年」を自分で書き足していました。しかも「文中では9月25日と記載」と断り書きまで付けているので、一見すると丁寧に見えてしまうのが厄介なところです。

1件だけの偶然なのかを確かめるため、手元の開発用データベース(本番と同じ公式媒体の記事を取り込んだもの)を読み取り専用で調べました。直近30日で要約済みの記事は2,383件あり、その中から次の条件にすべて当てはまるものを数えました。

  • 要約の中に「○年○月○日」の形の日付がある
  • その月日は、記事の公開日から60日以内である
  • それなのに年だけが公開年と違う
  • その年は、タイトルにも概要にも書かれていない

当てはまったのは24件でした。多くは、概要に月日だけが書かれている記事です。代表的なものを並べると、次のようになります。

公開日 元の見出し・概要 要約の記述
2026-08-31 GitHub Actionsの保持設定範囲が変更、10月1日から適用 2024年10月1日より、GitHub Actionsのデータ保持期間…
2026-09-01 GitHub Copilot、ポリシーと課金方式を9月1日より変更へ …料金体系が2024年9月1日より変更される
2026-08-28 「StrictlyVC」が9月10日にニューヨークで開催 2024年9月10日にニューヨークのウエストヴィレッジで…
2026-09-01 キャンペーンの概要に「9月1日から11月3日まで」 実施期間は2024年9月1日から11月3日まで…

「10月1日から変更」という、これから適用されるお知らせが、「2024年10月1日から変更」という2年前の出来事に書き換わっています。読んだ人は「もう終わった話か」と受け取ってしまいます。

DevPickの要約は、記事カードだけでなく、記事詳細ページ、毎日のダイジェスト配信、RSS、モバイルアプリ、Chrome拡張にもそのまま流れます。記事詳細ページは検索からの入口としても公開しているので、誤った日付が検索結果の説明文に出る可能性もありました。1か所の間違いが、届け先の数だけ広がる状態でした。

IT女子 アラ美
親切に注釈まで付けて、堂々と間違えてるのがいちばん怖いわね。

ITアライグマ
そうなんです。もっともらしい書き方なので、読む側は疑いにくいと思いました。

なぜ「2024年」になったのか

DevPickの要約は、記事のタイトル・媒体名・概要・タグをまとめてAI(LLM)に渡し、JSONで返してもらう形で作っています。LLMへの指示(システムプロンプト)には、もともと次の一文を入れていました。

- Every specific fact, number, date, or claim you write MUST come from the provided Description.
  NEVER invent or guess a fact that is not stated in the Description.

「日付を含む具体的な事実は、概要に書かれていることだけを書け。推測で作るな」という指示です。それでも年は補われていました。

原因は、LLMに記事の公開日を渡していなかったことです。当時、1記事分の入力は次のような形でした(実装を単純化した例です)。

Title: GitHub Copilot、ポリシーと課金方式を9月1日より変更へ
Source: CodeZine
Description: (概要。「9月1日より」とだけ書かれている)
Tags: GitHub, Copilot

この入力には、いつの記事なのかを示す情報がどこにもありません。人間でも、日付の書いていないチラシに「9月1日から」とだけあれば、今年の話だろうと自分の「今」を基準に読みます。LLMにとっての「今」は、学習したデータの時期です。そのため、学習データの時期に近い2024年を当てはめて文章を完成させてしまった、と考えています。

もう1つの穴は、出力をチェックする処理でした。DevPickには、LLMが返した要約を保存する前に確かめる処理がすでにありました。ただ、確かめていたのは「要約がタイトルの言い換えになっていないか」だけです。年が混ざっているかどうかは、どこでも見ていませんでした。

整理すると、「日付は概要からだけ書け」という指示はあったものの、その指示が守られたかを確かめる仕組みがなかった、ということになります。

IT女子 アラ美
日付の無いチラシを渡して「今年の話?」って聞いてたようなものね。

ITアライグマ
はい。材料が足りないところを、LLMは自分の知識で埋めてしまっていました。

公開日を渡す修正と、出力の照合を同時に入れた理由

今回の修正では、2つの対策を同じタイミングで入れました。1つ目は、入力に公開日を加えることです。公開日が取れない記事は、代わりに取り込んだ日を渡します。どちらを渡しているかが分かるように、ラベルも分けました。

Title: GitHub Copilot、ポリシーと課金方式を9月1日より変更へ
Source: CodeZine
PublicationDate: 2026-09-01T00:00:00
Description: (概要。「9月1日より」とだけ書かれている)
Tags: GitHub, Copilot

あわせて、システムプロンプトに次の指示を足しました。

- If a date in Title or Description has no year, keep it WITHOUT a year (e.g. "9月1日").
  NEVER infer a year from training data. PublicationDate is context, not evidence of an event date.

ここで意識したのは、公開日を「年を補うための材料」にはしないことです。9月1日に公開された記事が「9月1日から変更」と書いていても、それが今年の9月1日とは限りません。翌年の話かもしれませんし、過去の出来事を振り返る記事かもしれません。公開日はあくまで「この記事がいつ書かれたか」という文脈であって、「その出来事がいつ起きたか」の証拠ではありません。なので、年の無い日付は年を付けずに「9月1日より」のまま書く、という方針にしました。

ただ、この指示の追加だけに頼ることはしませんでした。そもそも今回の問題は、「推測で書くな」という指示が既にあったのに起きています。指示を1行足したからといって、LLMが必ず守るとは言えません。守られなかったときに気づける仕組みがなければ、同じことがまた静かに起きるだけです。

そこで2つ目の対策として、LLMへの指示とは別に、LLMの出力を機械的に確かめる処理を同じ修正に入れました。確かめるのは、要約をデータベースへ保存する前です。ここで弾かれた要約は保存されないので、画面にも配信にも出ません。考え方は単純です。

  1. 要約に書かれた西暦をすべて抜き出す
  2. 元記事のタイトルと概要に書かれた西暦をすべて抜き出す
  3. 要約にあって、元記事に無い年があれば「根拠の無い年」とみなす
def unsupported_summary_years(article, texts):
    """タイトル・概要に根拠の無い西暦(20XX)を返す。"""
    evidence = f"{article.title or ''} {article.description or ''}"
    return _extract_years("\n".join(texts)) - _extract_years(evidence)

要約から取り出した年の集合から、元記事から取り出した年の集合を引き算して、残ったものが根拠の無い年です。この判定は、LLMの気分にも指示の書き方にも左右されません。

以前、LLMが返したJSONの型を保存前と読み出し時の両方で確かめるようにした話を「LLMが返したJSONは、保存できても安全とは限らなかった話」に書きました。あのときは形の検証でしたが、今回は中身の検証です。どちらも「LLMの出力を、そのまま信用して使わない」という点では同じ考え方だと思っています。

IT女子 アラ美
お願いを増やすより、答え合わせの仕組みを作る方が確実ってことね。

ITアライグマ
そう考えました。指示は効きやすくするもの、検証は漏れを止めるものと分けています。

レビューで3回直した「年の数え方」

考え方は単純でしたが、「何を年として数えるか」は単純ではありませんでした。DevPickでは、PRを出すとCodexが自動でコードレビューをします。今回のPRでは、マージ前のレビューで3回続けて指摘を受け、4回目でようやく指摘なしになりました。どれも本番に出る前に直したものですが、どの指摘も判定の抜け穴を突くものでした。

1回目:公開年なら許してしまっていた

PRの最初の版では、要約の年が公開年と同じなら問題なしとしていました。問題を見つけたときの調査で「公開年と違う年」を数えていたので、その条件をそのまま実装に持ち込んだ形です。

しかし、これは自分で決めた「公開日は証拠ではない」という方針と食い違っています。2026年に公開された記事で、タイトルにも概要にも年が無いのに、LLMが公開年から推測して「2026年9月1日」と書いた場合、それは結果的に合っていても推測です。翌年の話だったなら、今度は1年ずれた情報を流すことになります。

そこで、公開年は根拠に含めず、タイトルと概要に明記された年だけを根拠として認めるように直しました。

2回目:「2024年」以外の書き方を見逃していた

次に指摘されたのは、要約側の年を「20XX年」という日本語の書き方でしか探していなかったことです。LLMが「2024-09-01」や「2024/9/1」のような日付の書き方をしたり、英語の文中で「in 2024」と書いたりすると、検証をすり抜けていました。

年の書き方は1つではありません。日本語の「年」、ハイフンやスラッシュで区切った日付、英語の文中に単独で出てくる年、の3つを拾えるようにしました。

3回目:年ではない数字を「根拠」として数えていた

3回目の指摘は、元記事側の抜き出し方でした。元記事側は数字の並びだけを見て年を拾っていたため、概要に「GPT-2024.1」のような版数や「2024億円」のような金額があると、それを「2024年の根拠あり」と数えていました。その結果、要約がイベントの年として「2024年」を補っても、根拠があるとみなして通してしまいます。

要約側で年を探す規則と、元記事側で年を探す規則が違っていたのが原因です。そこで、両方に同じ抜き出し規則を使うようにしました。最終的な規則は次のとおりです。

def _extract_years(text: str) -> set[str]:
    """根拠と要約の双方から、年として表記された西暦だけを抽出する。"""
    # 日本語の年、ISO/スラッシュ日付、英語文中の独立した年を対象とする。
    # GPT-2024.1や2024億など、版数・数量を年として扱わない。
    year_pattern = (
        r"(?<!\d)(20[0-9]{2})(?=年|[-/]\d{1,2}[-/]\d{1,2}(?!\d))"
        r"|(?<![\w.])(20[0-9]{2})(?!\w|\.\d)"
    )
    return {ja or en for ja, en in re.findall(year_pattern, text)}

テストでは、この規則が狙いどおりに動くかを表の形で確かめています。一部を抜き出すと次のようになります(左が元記事側の条件、中央が要約の文、右が根拠が無いと判定される年です)。

元記事側 要約の文 根拠の無い年
年の記載なし 適用日は2024/9/1 2024
年の記載なし Effective September 1, 2024 2024
年の記載なし(公開は2026年) 2026年9月1日より変更 2026
年の記載なし GPT-2024.1と2024億円の市場 なし
概要に「GPT-2024.1を発表」 2024年の発表 2024
概要に「Launched in 2024.」 2024年の発表 なし

3回とも、指摘を受けて初めて「たしかにそうだ」と気づくものでした。自分では「年を比べるだけ」と思っていた処理に、「何を根拠とするか」「どんな書き方を年とみなすか」「両側を同じ物差しで測っているか」という3つの判断が隠れていたことになります。

IT女子 アラ美
引き算するだけなのに、数え方でこんなに揉めるとは思わなかったわ。

ITアライグマ
私もです。比べる両側を同じ物差しで測ることが、いちばん大事でした。

作り直しても直らなければ保存しない

根拠の無い年を見つけたあとの動きも決める必要がありました。DevPickには、要約がタイトルの言い換えになっていた記事だけをLLMにもう一度頼む「作り直し」の仕組みが、もともとありました。今回はそこに、根拠の無い年を含む記事も乗せています。

作り直しを頼むときは、「前回の要約はタイトルの繰り返しか、根拠の無い年を含んでいたので却下した」と理由を伝え、年の無い日付に年を付けないよう改めて指示します。

Your previous summaries for these articles were REJECTED because they repeated the title
or included unsupported years.

Do not add a year to dates that have no year in Title or Description.

ここで気をつけたのは、作り直した結果もそのまま信用しないことです。LLMにもう一度頼んだからといって、次は正しく書いてくれる保証はありません。作り直した要約本文と要点も同じ検証にかけ、通ったものだけを差し替えます。記事のカテゴリなどの分類結果は、最初の結果をそのまま使い、差し替えるのは要約だけです。

そのうえで、データベースへ保存する直前に、すべての記事をもう一度検証します。作り直しでも根拠の無い年が残っていた場合、作り直しのリクエスト自体が失敗した場合、作り直しの結果からその記事が抜け落ちていた場合のどれでも、その記事は保存しません。

# 再試行の欠落・失敗・再度の誤生成でも誤った年を保存しない。
rejected = False
for item in parsed:
    ...
    article = articles[idx]
    if unsupported_summary_years(article, [item["summary"], *item["summary_points"]]):
        rejected = True
        logger.warning(f"Leaving article {article.id} unanalyzed: unsupported summary year")
        continue
    # ここから保存処理

保存しなかった記事は「未分析」のまま残り、定期的に動いている未分析記事の処理が、次の回でもう一度分析します。つまり、誤った年を含む要約を画面に出すくらいなら、要約がまだ無い状態で待ってもらう、という選択です。

要約が少し遅れて付くことと、誤った日付が記事詳細ページやダイジェスト配信に広がることを比べると、後者の方がずっと困ります。一度配信したメールは取り消せないからです。なので、迷ったら保存しない側に倒しました。

IT女子 アラ美
間違った要約を出すくらいなら、まだ要約が無いほうがマシって判断なのね。

ITアライグマ
はい。一度配信したものは取り消せないので、少し待ってもらう方を選びました。

すでに保存した要約を直す

ここまでの対策は、保存前に弾く仕組みなので、これから作る要約にしか効きません。この修正より前に保存された誤った要約は、そのまま残ります。あとから直すのは、この既存分だけです。そこで、既存データを点検して直すための管理コマンドを用意しました。

# 根拠の無い年を含む要約を探して、記事IDを一覧表示する
python -m app.cli check-summary-years

# 見つかった記事だけを「未分析」に戻し、定期処理で作り直してもらう
python -m app.cli check-summary-years --repair

判定には、新しい要約の検証とまったく同じ関数を使います。保存前の検証と既存データの点検で基準が違うと、「新しい要約では弾くのに、古い要約では見逃す」といったずれが起きるためです。古い形式で保存されている要約も同じように検査します。

直し方は、要約をその場で書き換えるのではなく、対象の記事を未分析の状態に戻すことにしました。未分析に戻せば、前の章の仕組みで作り直され、検証を通ったものだけが保存されます。要約を直す経路を1本にまとめておけば、修復だけ別の抜け道になることがありません。

未分析に戻すときは、要約だけでなく、分析結果、検索用の要約、カテゴリの対応、おすすめ度の計算結果のキャッシュも、1つのトランザクションでまとめて消します。要約だけ消して、古い要約から計算したカテゴリやおすすめ度が残ると、データの辻褄が合わなくなるからです。

ほかにも、点検の途中で状態が変わることへの備えを入れています。

  • 通常の分析処理と同じロックを取ってから点検を始める。分析が動いている最中なら、何もせずにエラーで終わる
  • 点検したときと要約が変わっている記事は、書き換えずに飛ばす。ほかの管理コマンドが先に直した要約まで消さないため
  • このコマンドは自動デプロイでは実行しない。デプロイ後に、まず一覧表示で対象を確かめてから --repair を付けて実行する手順にしている

本番データを変える操作は、何が変わるかを先に見てから実行したいと考えています。一覧表示と修復を同じコマンドのオプションで分けたのは、そのためです。

IT女子 アラ美
データを直すための処理がまた別のバグを生んだら、笑えないものね。

ITアライグマ
そうなんです。直す経路は通常の作り直しと同じ1本にそろえました。

まとめ

DevPickのAI要約は、元記事に月日しか書かれていない日付に、学習データの時期から推測した年を補っていました。「日付は概要からだけ書け」という指示はあったのに、記事の公開日を渡しておらず、指示が守られたかを確かめる仕組みもありませんでした。

対策は、同じ修正で2つ入れました。1つは、入力に公開日を渡し、年の無い日付には年を付けないよう指示することです。ただし公開日は文脈であって証拠ではない、という線引きは崩していません。もう1つは、保存前の照合です。要約と元記事から同じ規則で西暦を抜き出して突き合わせ、根拠の無い年があれば作り直し、それでも駄目なら保存しません。この修正より前に保存された要約だけは、同じ判定であとから見つけて作り直しに回せるようにしています。

今の私の結論は、LLMへの指示と、LLMの出力の検証は別物として両方用意する、ということです。指示は間違いを減らしますが、ゼロにはしてくれません。間違いが混ざったときに機械的に止められる検証があって、初めて安心して届けられると考えています。そして検証を作るときは、比べる両側を同じ物差しで測れているかを疑うようにしています。今回は、そこをレビューで3回も突かれました。

IT女子 アラ美
お願いと答え合わせはセットってことね。物差しの確認も忘れずに。

ITアライグマ
はい。今後もLLMの出力は、使う前に答え合わせをする方針で進めます。

運営サービス・姉妹メディア

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITアライグマのアバター ITアライグマ ITエンジニア / PM

都内で働くPM兼Webエンジニア(既婚・子持ち)です。
AIで作業時間を削って実務をラクにしつつ、市場価値を高めて「高年収・自由な働き方」を手に入れるキャリア戦略を発信しています。

目次