1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

llms.txtは「令和のmeta keywords」なのか ― LLMO・GEOのよくある噂を、一次情報とアクセスログで確かめた

1
Posted at

AI Detective Desk_ Bots vs Reality.png

2000年代のはじめ、Web サイトを作るときに <meta name="keywords"> にキーワードを30個も50個も詰め込んでいた時代がありました。

「これを書けば検索で上位に出る」と信じられていて、実際にしばらくは効いていたようです。でも、詰め込む人が増えすぎて検索エンジンが参考にしなくなり、ずいぶん前に Google も「ランキングには使っていない」と公式に言っています。それでも、今でも古いサイトの HTML を開くと、ときどき化石のように残っているのを見かけます。

なぜこんな話から始めたかというと、Google の John Mueller が llms.txt について、まさにこの meta keywords にたとえたからです。

最近、Web 制作や SEO の相談を受けていると、LLMO・GEO・AIO(呼び方はいろいろありますが、要するに「AI の回答に自分のサイトを載せてもらうための対策」です)について聞かれることがぐっと増えました。今週も「AI 検索対策の無料診断」を始めた、という会社のプレスリリースが続けて出ていました。それだけ関心が高いということだと思います。

ただ、話を聞いていると、噂と事実がかなり混ざっています。今日は、よく聞く噂を6つ取り上げて、各社の公式ドキュメントと、実際のアクセスログの見方で1つずつ確かめていきます。

判定は3段階です。

  • 本当:公式の情報や実測で裏付けがある
  • 半分本当:条件付きで正しい、または誤解されやすい
  • ほぼ誤解:少なくとも今の時点では根拠が乏しい

噂1:「llms.txt を置けば、AI に引用されやすくなる」

判定:ほぼ誤解(ただし、別の使い道はある)

llms.txt は、サイトの概要と主要なページへのリンクを Markdown で書いて、サイトのルートに置くファイルです。「AI 向けの robots.txt」のように紹介されることもあります。

Google は2026年5月15日に、生成 AI の検索機能に向けた最適化ガイドを初めて公開しました。その中で、llms.txt は、コンテンツの「チャンク分け」や AI 向けの書き換え、特別な構造化データと並んで、生成 AI の検索機能に表示されるためには不要なものとして挙げられています。6月には表現が補足されて、Google 検索(生成 AI 機能を含む)はこのファイルを無視すること、ほかのサービスのために作るのは自由で、Google 検索での見え方に悪影響も好影響もないこと、が明記されました。

OpenAI や Anthropic も、少なくとも公開されているクローラーのドキュメントの中で、llms.txt を引用の判断材料に使うとは書いていません。SEO ツールの SE Ranking がおよそ30万ドメインを調べた分析でも、llms.txt の有無と AI に引用される頻度の間に関係は見られなかったそうです。

一方で、話はここで終わりません。Chrome の Lighthouse 13.3 には「Agentic Browsing」という実験的な監査カテゴリが加わり、その中に llms.txt があるかどうかのチェックが入りました。ドキュメントによると、AI エージェントがサイトの全体像をつかむための道案内として、という位置づけです。検索の順位や引用とは関係ない、とも明記されています。

つまり、「引用されやすくなる」は今のところ根拠がない。でも、**「AI エージェントにサイトの地図を渡す」**という意味では、今後使われる可能性がある。meta keywords と違うのはこの点で、検索のためではなく、ブラウザを操作するエージェントのための案内板として生き残る道はありそうです。

置くかどうかは、作るのに手間がかからないなら置いてもいい、くらいの温度感で十分だと思っています。少なくとも、「llms.txt を設置しました。これで AI 検索対策は万全です」という提案を見かけたら、少し疑ったほうがいいです。

噂2:「AI に読まれやすいよう、文章を短いチャンクに分けて書くべき」

判定:ほぼ誤解(でも、その理由は大事)

これも、上の Google のガイドで「不要」とされているものの1つです。Mueller は、AI のためだけに別の Markdown ページを用意することについて、かなり強い言葉で否定しています。

ただ、この噂が生まれた理由は分かります。AI 検索の裏側でやっていることは、以前の記事で書いた RAG とほとんど同じです。ページを取ってきて、細かく切って、質問に関係しそうな部分だけを LLM に渡す。だから「切られやすいように書けば有利なのでは」と考えたくなります。

でも、AI のために文章を切り刻む必要はありません。必要なのは、人間にとって読みやすい、ふつうにまともな HTML です。

  • 見出し(h2、h3)が内容をちゃんと表している
  • 表は画像ではなく <table> で書く。見出し行を <th> にする
  • 1ページに1つのテーマ。結論が前のほうにある
  • 大事な情報が JavaScript を実行しないと表示されない、ということがない

最後の1つは特に大事です。2024年の末に Vercel が公開した調査で、GPTBot や ClaudeBot などの主要な AI クローラーは JavaScript を実行していない、という結果が出ていました。React や Next.js でクライアント側だけで描画しているページは、人間には見えていても、AI クローラーには空っぽに見えている可能性があります。Next.js なら、大事なページはサーバー側で HTML を返す(SSR や SSG)ようにしておくのが安全です。

RAG の記事で「表が切られてヘッダーと泣き別れると、LLM は列を取り違える」と書きました。AI 検索でも同じことが起きます。だからこそ、表は表として、見出しは見出しとして、正しくマークアップしておく。結局のところ、それがいちばんの「AI 対策」なんだと思います。

噂3:「robots.txt で AI クローラーを全部ブロックすれば安心」

判定:半分本当(何から守りたいかによる)

ここは、最近いちばん誤解が多いところです。

AI のクローラーは、大きく分けて3種類あります。

目的 OpenAI Anthropic Perplexity
モデルの学習 GPTBot ClaudeBot ー
AI 検索の索引づくり OAI-SearchBot Claude-SearchBot PerplexityBot
ユーザーの質問に応じてその場で取りに来る ChatGPT-User Claude-User Perplexity-User

「AI に自分のコンテンツを学習されたくない」という理由で全部ブロックすると、AI 検索の結果にも出なくなります。OpenAI のドキュメントには、OAI-SearchBot をブロックしたサイトは ChatGPT の検索の回答に表示されない、と書かれています。Anthropic のヘルプページにも、Claude-SearchBot や Claude-User を止めると、検索での見え方に影響する可能性があると書かれています。

学習は断りたいけれど、検索には載せてほしい。そういう場合は、目的ごとに分けて書きます。

robots.txt
# モデルの学習には使わないでほしい
User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Google-Extended
Disallow: /

# AI 検索には載せてほしい
User-agent: OAI-SearchBot
Allow: /

User-agent: Claude-SearchBot
Allow: /

User-agent: PerplexityBot
Allow: /

Allow の行は書かなくてもデフォルトで許可されますが、意図をはっきりさせるために、あえて書いています。

細かいけれど、知っておいたほうがいい点がいくつかあります。

  • Google-Extended は、クローラーの名前ではなく「トークン」です。 Gemini のモデルの学習などに使われるかどうかを制御するもので、Google 検索の AI による概要や AI モードへの掲載には影響しません。Google 検索の AI 機能に出たくない場合は、通常の検索と同じ仕組み(Googlebot や nosnippet など)で制御することになります
  • ChatGPT-User のような「ユーザー代理」のアクセスには、robots.txt が適用されない場合があります。 OpenAI のドキュメントにも、ユーザーの操作によるものなので robots.txt のルールが当てはまらないことがある、と書かれています。人がブラウザでページを開くのに近い扱いです
  • OpenAI の場合、GPTBot と OAI-SearchBot の両方を許可していると、1回のクロール結果が両方の目的に使われることがある、とも書かれています
  • 反映には時間がかかります。 OpenAI は、robots.txt の変更が検索に反映されるまで約24時間かかるとしています

噂4:「うちのサイトにも AI クローラーは来ているはず」

判定:確かめないと分からない

これは本当かどうかではなく、確かめるべきことです。百聞は一見に如かず。サーバーのアクセスログを見れば分かります。

ただ、User-Agent は自己申告なので、誰でも「GPTBot です」と名乗れます。実際、怪しいアクセスが有名なボットの名前を借りてくるのはよくある手口です。OpenAI はクローラーごとに IP アドレスのレンジを JSON で公開しているので、名乗っている名前と送信元の IP が合っているかも照合できます。

そこで、アクセスログ(nginx や Apache の combined 形式)を読んで、次の4つを数える小さなスクリプトを書きました。

  • AI クローラーのアクセス数と、目的・ステータスコード
  • 名乗っている名前と送信元 IP の照合結果
  • robots.txt や llms.txt を取りに来たボット
  • AI サービス経由で来た人間の訪問(リファラーや utm_source=chatgpt.com から判定)
ai_bot_report.py
#!/usr/bin/env python3
"""アクセスログから、AI のクローラーと AI 経由の訪問を数える。

使い方:
  python3 ai_bot_report.py access.log [access.log.1 ...] [--ranges GPTBot=gptbot.json ...]

--ranges には、運営元が公開している IP レンジの JSON を指定できる(OpenAI の形式)。
指定したボットは、名乗っている User-Agent と送信元 IP が一致するかも確かめる。
"""
import argparse
import ipaddress
import json
import re
from collections import Counter, defaultdict
from urllib.parse import urlsplit, parse_qs

LINE = re.compile(r'^(\S+) \S+ \S+ \[[^\]]+\] "(?:\S+) (\S+)[^"]*" (\d{3}) \S+ "([^"]*)" "([^"]*)"')

# (User-Agent に含まれる名前, 運営元, 目的)
BOTS = [
    ("GPTBot", "OpenAI", "学習"),
    ("OAI-SearchBot", "OpenAI", "検索"),
    ("ChatGPT-User", "OpenAI", "ユーザー代理"),
    ("ClaudeBot", "Anthropic", "学習"),
    ("Claude-SearchBot", "Anthropic", "検索"),
    ("Claude-User", "Anthropic", "ユーザー代理"),
    ("PerplexityBot", "Perplexity", "検索"),
    ("Perplexity-User", "Perplexity", "ユーザー代理"),
]
AI_REFERRERS = {
    "chatgpt.com": "ChatGPT", "chat.openai.com": "ChatGPT",
    "perplexity.ai": "Perplexity", "www.perplexity.ai": "Perplexity",
    "claude.ai": "Claude", "gemini.google.com": "Gemini",
    "copilot.microsoft.com": "Copilot",
}


def load_ranges(specs):
    ranges = {}
    for spec in specs:
        name, path = spec.split("=", 1)
        with open(path, encoding="utf-8") as f:
            prefixes = json.load(f)["prefixes"]
        ranges[name] = [ipaddress.ip_network(p.get("ipv4Prefix") or p["ipv6Prefix"]) for p in prefixes]
    return ranges


def which_bot(ua):
    for name, operator, purpose in BOTS:
        if re.search(rf"\b{re.escape(name)}/", ua):
            return name, operator, purpose
    return None


def ai_source(path, referer):
    host = urlsplit(referer).hostname or ""
    if host in AI_REFERRERS:
        return AI_REFERRERS[host]
    utm = parse_qs(urlsplit(path).query).get("utm_source", [""])[0]
    return AI_REFERRERS.get(utm)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("logs", nargs="+")
    ap.add_argument("--ranges", nargs="*", default=[])
    args = ap.parse_args()
    ranges = load_ranges(args.ranges)

    hits, statuses, verified = Counter(), defaultdict(Counter), defaultdict(Counter)
    special, referrals = Counter(), Counter()

    for log in args.logs:
        with open(log, encoding="utf-8", errors="replace") as f:
            for line in f:
                m = LINE.match(line)
                if not m:
                    continue
                ip, path, status, referer, ua = m.groups()
                if bot := which_bot(ua):
                    name = bot[0]
                    hits[bot] += 1
                    statuses[name][status] += 1
                    if name in ranges:
                        addr = ipaddress.ip_address(ip)
                        ok = any(addr in net for net in ranges[name])
                        verified[name]["本物" if ok else "偽物の疑い"] += 1
                    if path.split("?")[0] in ("/robots.txt", "/llms.txt", "/llms-full.txt"):
                        special[(name, path.split("?")[0])] += 1
                elif source := ai_source(path, referer):
                    referrals[source] += 1

    print("## AI クローラーのアクセス")
    print(f"{'ボット':<18}{'運営元':<12}{'目的':<10}{'件数':>6}  ステータス")
    for (name, operator, purpose), n in hits.most_common():
        codes = ", ".join(f"{c}:{k}" for c, k in sorted(statuses[name].items()))
        print(f"{name:<18}{operator:<12}{purpose:<10}{n:>6}  {codes}")

    if verified:
        print("\n## 送信元 IP の照合")
        for name, result in verified.items():
            print(f"{name:<18}" + "  ".join(f"{k}: {v}" for k, v in result.items()))

    print("\n## robots.txt / llms.txt を取りに来たボット")
    for (name, path), n in sorted(special.items()) or [(("(なし)", ""), 0)]:
        print(f"{name:<18}{path:<16}{n:>6}")

    print("\n## AI サービス経由で来た人間の訪問")
    for source, n in referrals.most_common() or [("(なし)", 0)]:
        print(f"{source:<18}{n:>6}")


if __name__ == "__main__":
    main()

使い方はこうです。IP レンジの JSON は、OpenAI の公式サイトから取ってきます。

curl -sO https://openai.com/gptbot.json
curl -sO https://openai.com/searchbot.json
python3 ai_bot_report.py /var/log/nginx/access.log \
  --ranges GPTBot=gptbot.json OAI-SearchBot=searchbot.json

動作確認のために、架空のアクセスログ(IP アドレスは、公開されているレンジの中から取ったものと、ドキュメント用の予約アドレスを混ぜています)を作って流した結果がこれです。

## AI クローラーのアクセス
ボット               運営元         目的            件数  ステータス
GPTBot            OpenAI      学習             3  200:2, 404:1
OAI-SearchBot     OpenAI      検索             2  200:1, 404:1
ClaudeBot         Anthropic   学習             1  200:1
Claude-User       Anthropic   ユーザー代理         1  200:1

## 送信元 IP の照合
GPTBot            本物: 2  偽物の疑い: 1

## AI サービス経由で来た人間の訪問
ChatGPT                1
Perplexity             1

(一部省略しています)

「偽物の疑い」の1件は、GPTBot を名乗って /wp-login.php を叩きに来たアクセスとして仕込んだものです。本物の GPTBot はログイン画面を総当たりしたりしません。WordPress のサイトを長く運用していると、有名なボットの名前を借りた、こういうアクセスは本当によく見かけます。robots.txt で何を許可するか考える前に、まず「誰が本物か」を確かめておく。ここを飛ばすと、偽物の振る舞いを見て AI クローラーの対策を考える、というおかしなことになります。

ちなみに Anthropic は、IP アドレスでブロックすると robots.txt を読みに行けなくなって、オプトアウトが正しく機能しない可能性があるので、robots.txt で制御してほしい、とヘルプページで案内しています。

噂5:「構造化データを入れれば、AI に選ばれる」

判定:半分本当

Google の最適化ガイドでは、生成 AI の検索機能のために特別な構造化データは必要ない、とされています。生成 AI 機能に表示されるための条件は、ふつうの検索と同じで、インデックスされていてスニペットとして表示できることです。

ただ、構造化データが無意味になったわけではありません。リッチリザルト(レシピや FAQ、パンくずなどの表示)には今までどおり使われますし、ページの内容を機械に正確に伝える手段としての価値はそのままです。「AI 対策として」入れるのではなく、「ページの意味を正しく伝えるために」入れる。その結果として AI にも伝わる、くらいの順番で考えるのがちょうどいいと思っています。

噂6:「AI 検索で何位に出ているか、測れる」

判定:ほぼ誤解(測るべきものは別にある)

AI の回答は、同じ質問をしても毎回少しずつ変わります。最近出てきた AI 検索の診断サービスに「同じ質問を10回ずつ聞いて測る」ものがあるのも、そのためです。ふつうの検索のように「3位」という固定の順位はありません。

それよりも測るべきなのは、AI 経由で実際に人が来ているかです。上のスクリプトの最後の項目がそれで、ChatGPT の検索結果からのリンクには utm_source=chatgpt.com が付くことが多いので、アクセス解析でも拾えます。Perplexity や Claude、Gemini から来た訪問は、リファラーで判別できます。

順位という幻を追いかけるより、実際に来た人の数と、その人がどのページに着地したかを見る。そのほうが、次に何を直せばいいかがずっとはっきりします。

まとめの代わりに:機械に推測させないページ

「流行り物は廃り物」と言います。LLMO や GEO という言葉が数年後にどうなっているかは、正直分かりません。meta keywords のように、ある日まとめて意味を失う「対策」も、きっと出てくると思います。

それでも、たぶん廃らないものがあります。

Web のユーザビリティの古典に、スティーブ・クルーグの『Don't Make Me Think』(邦題『ウェブユーザビリティの法則』)という本があります。「ユーザーに考えさせるな」という意味のタイトルです。AI の時代になって、自分はこれにもう1行足したくなりました。

人間に考えさせないページは、機械にも推測させない。

見出しが中身を表していて、表が表として書かれていて、結論が先にあって、JavaScript を待たずに中身が読める。そういうページは、人間にとって読みやすく、AI にとっても読み間違えにくい。特別な裏ワザより、この地味な積み重ねのほうが、よほど長く効きます。

自分は LLMO の相談を受けると、最初にやることはいつも同じです。サーバーのアクセスログを一緒に開いて、誰が、どのページに、本当に来ているのかを見る。対策を考えるのは、それからです。

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?