開発/設計

同じAIニュースを何度も読まない。重複候補を絞るPythonツール

毎朝流れるAIニュースから、表記の近い見出しを文字n-gramとコサイン類似度で重複候補に絞る。日本語で既定設定が動かない理由、閾値の決め方、人が確認すべき限界まで解説する。

この記事でわかること

  • 本文に入る前に、まず押さえるべき結論
  • 開発や実装の判断が、ここからどう変わるか
  • 次に読むべき関連記事の入口
同じAIニュースを何度も読まない。重複候補を絞るPythonツール
目次

毎朝、AIコーディング関連の見出しを追っていると、別の記事だと思って開いたのに「昨日と同じ話だった」と気づくことがある。タイトルの助詞や語順が少し違うだけで、人の目には新着記事のように見えてしまう。

必要だったのは、ニュースの正しさを自動判定する仕組みではない。読む前に「表記が近い見出し」を重複候補としてまとめ、人が確認する本数を絞る小さなフィルターだった。

この記事では、文字n-gramのTF-IDFとコサイン類似度を使ったPythonツールを、実行結果とともに紹介する。日本語では既定設定のまま動かない理由、閾値を一律の正解にしない考え方、この方法では判定できない範囲まで隠さず説明したい。

情報が多すぎるより、重複が見抜けないことが本当の敵だった

ブログのネタ探しで毎朝RSSリーダーとXのタイムラインを開くと、AIコーディングツール関連の見出しが軽く50本以上並ぶ。最初のうちは「情報が多すぎて追いきれない」ことが問題だと思い込んでいた。だが実際に1つずつ開いて読んでみると、印象がまるで変わる。本当に新しい発表は数えるほどしかなく、残りの大半は既出ネタの言い換えか、過去の発表を引用しただけの後追い記事だった。

問題は量ではなく、同じ話が形を変えて繰り返し流れてくることの見抜きにくさにある。タイトルの言い回しが毎回微妙に違うので、パッと見ただけでは「これは前に読んだ話か、それとも新しい話か」の判断がつかない。開いて本文を読み進めて、ようやく「あ、これ先週も見た」と気づく。1本あたり数十秒でも、50本積み重なれば軽く1時間は溶けてしまう。

同じ話題が複数サイトに並んでいても、情報の正しさが証明されたことにはならない。今回のツールができるのは、似た見出しを同じ話題の候補として集めるところまでだ。真偽の確認は、公式発表や一次ソースを人がたどって別に行う必要がある。

この経験から、自分に必要なのは「読む量を減らすツール」ではなく「同じ話かどうかを先に判定するツール」だとはっきり気づいた。人間の目で1件ずつ照合していた作業を、まず機械にやらせる方向で考え始める。

キーワード一致だけでは、言い回しが違う重複を見逃す

最初に作ったのは、本当に単純なチェックだった。過去に扱ったネタのキーワードをリストにしておき、新しい見出しにそのキーワードが含まれていたら「既出の可能性あり」とフラグを立てるだけの仕組みだ。

# 最初に作った単純版(うまくいかなかった例)
used_keywords = ["新モデルA", "GitHub Copilot デフォルトモデル", "モデル刷新"]

def is_duplicate_v1(headline: str) -> bool:
    # 見出しにキーワードがそのまま含まれているかだけを見る
    return any(keyword in headline for keyword in used_keywords)

このコードは動く。でも、動くだけで実用にならなかった。「GitHub CopilotのデフォルトモデルがGPT-4から刷新」という見出しは検出できる。一方で「Copilotの中身が新しいAIに置き換わった」という言い換えは素通りしてしまう。単語がそのまま一致しないと拾えないのが最大の弱点だった。

逆のパターンも存在する。「GitHub」という単語だけでキーワードに引っかかり、まったく関係のない別の新機能の記事まで「既出」判定されてしまう誤検知も頻発した。単純な文字列一致では、助詞や語順の違いさえ吸収できない。当たり前のことなのだが、実際に自分の作業に組み込んで初めて、この限界の大きさを実感した。

ここで私がハマったのは、「キーワードを増やせば精度が上がるはず」と思い込んでいたことだ。表記や語順のパターンを1つずつ手で追加していく作業を始めたが、キリがない。リストは際限なく膨らむのに精度はさほど上がらなかった。この時点で、完全一致というアプローチそのものを見直す必要性に気づく。

文字n-gramとコサイン類似度で、表記の近さを数値化した

類似見出しを重複候補として人へ渡す4ステップ

表記や語順が少し違う見出しを数値で比較するため、TF-IDFとコサイン類似度を使うことにした。どちらも古くからある自然言語処理の基本手法で、外部APIを呼ばず手元のPCだけで試せる。

注意点は、日本語の見出しをTfidfVectorizerの既定設定にそのまま渡さないことだ。既定のword analyzerは空白で区切られていない日本語を期待どおりの単語へ分けない。そこで2〜4文字の連続した断片を特徴量にするcharacter n-gramを指定する。共通する文字列が多いほどスコアは上がるが、文章の意味を理解しているわけではない。

# TF-IDFとコサイン類似度で重複候補を探すサンプル
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# 過去に扱った見出しのリスト(実運用ではファイルやDBから読み込む)
used_headlines = [
    "GitHub Copilotのデフォルトモデルが新モデルAに刷新",
    "Cursorがタブレット端末でエージェント並走レビューに対応",
]

def find_duplicates(new_headline: str, threshold: float = 0.50):
    # 新旧の見出しをまとめてベクトル化する(TF-IDFは全体を1回で学習させる必要がある)
    all_headlines = used_headlines + [new_headline]
    # 日本語を空白区切りの単語として扱わず、2〜4文字の断片で比較する
    vectorizer = TfidfVectorizer(analyzer="char", ngram_range=(2, 4))
    vectors = vectorizer.fit_transform(all_headlines)

    # 最後の1件(新着見出し)と、それ以外(既出見出し)の類似度を計算
    new_vector = vectors[-1]
    existing_vectors = vectors[:-1]
    scores = cosine_similarity(new_vector, existing_vectors)[0]

    # 閾値を超えた既出見出しだけを重複候補として返す
    duplicates = [
        (used_headlines[i], round(float(score), 2))
        for i, score in enumerate(scores)
        if score >= threshold
    ]
    return duplicates

result = find_duplicates(
    "GitHub Copilot、デフォルトモデルを新モデルAへ刷新"
)
print(result)
# scikit-learn 1.9.0での出力:
# [('GitHub Copilotのデフォルトモデルが新モデルAに刷新', 0.61)]

この例では、読点、助詞、語順が違っても0.61となり、重複候補として拾えた。一方、「Copilotの中身が新しいAIモデルに置き換わった」まで言葉を変えると、同じ設定でもスコアは約0.20に下がる。character n-gramが拾うのは共通する文字列であり、広い意味の言い換えではない。この境界を理解せず「意味の近さを判定できる」と説明すると、ツールの能力を過大に見せてしまう。

scikit-learnというライブラリを使っているが、インストールはpip install scikit-learnの1行で済む。エンジニアでなくても、ここまでは手元の環境でそのまま再現できるはずだ。

運用し始めてから気づいた工夫も1つ書いておきたい。used_headlinesのリストは、放っておくと増え続ける一方だ。半年分のデータを毎回全部ベクトル化していると、計算量がじわじわ重くなってくる。そこで自分の環境では、直近30日分の見出しだけを比較対象として残し、それより古いものは別ファイルへ退避する運用にした。ニュースの重複判定は「最近の話題と似ているか」が知りたいことがほとんどで、半年前の記事と比較する必要はめったにない。この割り切りのおかげで、処理速度も体感の分かりやすさも両方保てている。

閾値0.50は正解ではなく、調整の出発点

全件を順に開く流れと、重複候補を先にまとめる流れの比較

このツールの役割は「重複かどうかを確定する」ことではない。スコアが高い見出しを先にまとめ、人が優先して確認する順番を作ることだ。低いスコアの記事も新着とは限らないため、最終判断は残しておく。

閾値をラベル付き事例で調整する考え方

掲載コードでは0.50を出発点にした。同じ製品名とモデル名を含み、助詞や語順だけが違う例は0.61なので候補に入る。0.90ではこの例を見逃す。一方、閾値を低くするほど、共通する一般語だけで無関係な記事まで候補に入りやすい。

0.50を万能な基準としてコピーするのも危険だ。まず「同じ話題」と人が判断した見出しと、「別の話題」と判断した見出しをそれぞれ用意する。閾値を動かし、見逃しと誤検知のどちらを減らしたいかで決める。ニュースの分野、見出しの長さ、比較対象の期間が変われば、適切な値も変わる。

さらに、比較対象を直近30日などに限定すると確認しやすい。半年以上前の見出しまで混ぜるかどうかは、「再掲載も重複として扱いたいか」という運用目的で決める。コードの数値より先に、何を候補として人へ渡すかを定義する方が重要だ。

重複候補探しは、定型文の確認にも応用できる

表記の近さを使える3つの場面

このツールを作ってから、応用範囲は記事選びだけにとどまらないと気づいた。同じ「新旧の文章を比較して似ているものを見つける」という仕組みは、いろいろな業務に転用できる。

1つ目は、問い合わせメールの重複候補を探す用途だ。同じ製品名、エラーコード、定型文が繰り返されるメールなら、表記の近いものを先にまとめやすい。ただし「動かない」「固まる」のように文字列が大きく異なる言い換えは、このままでは拾えない。意味まで比較したい場合は、埋め込みモデルや日本語の分かち書きを含む別の設計が必要になる。

2つ目は、SNS投稿の使い回しチェックだ。過去の投稿履歴と新しい文面を比べれば、語尾や絵文字だけを変えた投稿を候補として出せる。完全一致検索よりは表記ゆれに強く、投稿前の確認対象を絞る補助になる。

3つ目は、日報や議事録で繰り返された文面を探す用途だ。定例文や前回からコピーされた箇所を候補として示せば、人は差分の確認に集中しやすい。ここでも、ツールが「本当に新しい論点」を理解するわけではない点は同じだ。

MCPは増やすほど賢くなるは誤解でも書いたが、情報を増やす前に確認対象を整理する仕組みを置くと、人の判断を使う場所が明確になる。今回のツールも、自動判定器ではなく確認順を作るフィルターとして使うのが合っている。

まとめ

AIコーディング関連のニュースを毎朝追いかける中で、情報の量そのものよりも、表記の近い見出しを何度も開くことが時間泥棒だと気づいた。この記事で紹介した内容は、次のとおりだ。

  • キーワードの単純一致では、言い回しが違う重複記事を見逃す
  • character n-gramのTF-IDFなら、APIキーなしで表記の近さを数値化できる
  • 広い意味の言い換えや情報の真偽は判定できない
  • 閾値に唯一の正解はなく、ラベル付き事例と運用目的で調整する
  • 同じ仕組みは、定型メール、SNS投稿、議事録の重複候補探しにも応用できる

まずは過去の見出しを数十件だけ用意し、掲載コードの実行結果を人の判断と見比べてみてほしい。合わない例を確認すると、この方法を使える範囲と、人が確認すべき範囲の両方が見えてくる。

ゲン
Written byゲンCS × Vibe Coder

正直、一度エンジニアは諦めました。新卒で入った開発会社でバケモノみたいに優秀な人たちに囲まれて、「あ、私はこっち側じゃないな」って悟ったんです。その後はカスタマーサクセスに転向して10年。でもCursorとClaude Codeに出会って、全部変わりました。完璧なコードじゃなくていい。自分の仕事を自分で楽にするコードが書ければ、それでいいんですよ。週末はサウナで整いながら次に作るツールのこと考えてます。