なぜ AI エージェントにはアプリ内の別の経路が必要なのですか?
アプリケーションへのリクエストのうち、AI エージェントから来るものの割合は増え続けています。たとえば、ユーザーのためにドキュメントを取得するアシスタント、製品ページを読むリサーチツール、自律的なワークフローから API を呼び出す処理です。その多くは、人間向けに設計されたインターフェースをそのまま使っています。エージェントは HTML ページ全体をダウンロードし、レイアウトを解析し、マウス操作を前提にしたナビゲーションをたどり、スクリプト中心のページが想定どおりに表示されないと再試行します。
それは誰にとっても非効率です。エージェントは雑多な内容を受け取り、誤りを起こします。サーバーは誰も見ないページのレンダリングにリソースを費やします。そして、そのトラフィックは奇妙に振る舞うブラウザーのように見えるため、把握が難しくなります。これがどれほど自分のトラフィックに含まれているかまだ不明なら、まずはWeb サイトのトラフィックが AI エージェント由来かを見分ける方法から始めてください。
よりよい方法は、エージェントに構造化された経路を与えることです。つまり、プログラム向けの形式で、標準的な場所から見つけられ、正しく自分を識別するクライアントに報いる制限を備えたコンテンツとアクションです。このガイドでは、別のアプリケーションを作らずにそれを実現する方法を示します。
ステップ 1: エージェントに何をさせるかを決める
まずは技術ではなく意図から考えます。すでにエージェントのトラフィックが見えているルートを確認し、次のように分類します。
- コンテンツを読む: ドキュメント、製品情報、価格ページ、ヘルプ記事、公開プロフィール。
- データを問い合わせる: 検索、カタログ照会、在庫、ステータス。
- アクションを実行する: ユーザーに代わって何かを作成、更新、削除する。
読み取りと問い合わせは、適切に振る舞うクライアントであれば誰でも利用できる構造化された経路の有力な候補です。アクションには、クライアントがどのように自身を識別するかに関係なく認証と認可が必要であり、機密アクションを保護するガイドで扱います。
ステップ 2: マシンにやさしい形式を提供する
コンテンツ系のルートでは、ページの外枠なしで同じ情報を提供します。多くのケースは、次の 2 つの形式でカバーできます。
- Markdown は、ドキュメントや記事のような文章コンテンツ向けです。
text/markdownの media type で提供し、RFC 7763 に登録されています。見出し、リスト、リンク、コードブロックを保持し、コンパクトです。 - JSON は、製品、プラン、ステータスのような構造化レコード向けです。
コンテンツネゴシエーション を使えば、URL は同じままで、クライアントが欲しい形式を要求できます。
import express from "express";
const app = express();
app.get("/docs/:slug", async (req, res) => {
const doc = await loadDoc(req.params.slug);
if (!doc) return res.status(404).end();
res.vary("Accept");
const preferred = req.accepts(["text/html", "text/markdown", "application/json"]);
if (preferred === "text/markdown") {
return res.type("text/markdown; charset=utf-8").send(doc.markdown);
}
if (preferred === "application/json") {
return res.json({ title: doc.title, updatedAt: doc.updatedAt, body: doc.markdown });
}
return res.render("doc", { doc });
});Vary: Accept ヘッダーを送信し(RFC 9110 の Vary 参照)、キャッシュが各表現を別々に保存するようにします。多くのチームは、ドキュメントのパスに .md を付けるような明示的な別 URL も公開します。こちらのほうがネゴシエーションより扱いやすいクライアントもいるからです。HTML ページから <link rel="alternate" type="text/markdown" href="..."> でリンクしておけば、発見できます。このような型付きリンクは Web Linking のモデルに従います。
ステップ 3: エージェントはどうやって構造化された経路を見つけるのですか?
見つけられないものは使えません。自動化クライアントがすでに探しに行く場所へ、エントリーポイントを公開します。
- サイトルートの
/llms.txt: サイトが提供する内容の短い Markdown 概要と、最も有用なページとその構造化版へのリンク。これは llms.txt proposal に従います。 - API の説明: たとえば OpenAPI ドキュメント。開発者向けドキュメントと
/llms.txtからリンクします。 - HTML ページ内の 代替リンク。Markdown または JSON 版を指します。
- サイトマップ。主要コンテンツの URL を Sitemaps protocol で定義された形式で列挙します。
最小限の llms.txt:
# Example App
> Example App helps teams plan projects. This file lists the best entry points for automated clients.
## Docs
- [Getting started](https://example.com/docs/getting-started.md): setup in five steps
- [API reference](https://example.com/openapi.json): OpenAPI description of the public API
## Policies
- [Automated access](https://example.com/docs/automated-access.md): limits and how to sign requests自動化クライアント向けのルールを説明するページを用意してください。どのルートが公開されているか、どの制限が適用されるか、署名付きリクエストでよりよい扱いを受けるためにクライアントがどのように自身を識別できるか、を記します。明確なルールは、推測を協調に変えます。
ステップ 4: User-Agent 文字列ではなく Actor class でルーティングする
構造化されたレスポンスを用意したら、デフォルトで誰にそれを返すかを決めます。User-Agent 文字列をその場しのぎで解析するのではなく、Actor class とその basis を使います。AI の運用者は User-Agent を公開しており、OpenAI と Anthropic はクローラーやフェッチャーでそれを行っていますが、文字列一致は依然として宣言にすぎません。
type ActorClass = "human" | "human_ai" | "agent" | "unknown";
type Basis = "verified" | "declared" | "inferred" | "unknown";
function preferStructured(actor: ActorClass, basis: Basis, accept: string): boolean {
if (accept.includes("text/markdown") || accept.includes("application/json")) return true;
return actor === "agent" && (basis === "verified" || basis === "declared");
}次の 2 つの原則が安全性を保ちます。
- 明示的な要求が常に優先される。 クライアントが Markdown または JSON を要求したら、クラスに関係なくそれを返します。
- デフォルトは何も奪わない。 Human または Unknown の訪問者には常に完全なページを返します。エージェントに構造化されたデフォルトを選ぶのは、表示形式を変えるだけで、見られる内容を変えるわけではありません。
ステップ 5: AI エージェントにはどのような rate limit を設定すべきですか?
rate limit は、actor context の効果が最もはっきり表れるところです。全員に同じ制限を与えるのではなく、class と basis の組み合わせごとに妥当な予算を与えます。
| Actor と basis | 読み取りルート | 注記 |
|---|---|---|
| Agent, verified | 高めの制限 | 運用主体を識別でき、責任を持てる |
| Agent, declared or inferred | 標準制限 | 主張を検証できない |
| Unknown | 標準制限 | デフォルトで罰則にしない |
| Human or Human + AI | 標準制限 | 対話的な利用が制限に達することはまれ |
制限は、利用可能な中で最も安定した識別子に紐づけます。verified なエージェントでは、運用主体の origin と key identifier です。それ以外では、credential または network-level key を使います。
function limitKey(actor: { actor: ActorClass; basis: Basis; operator?: string }, ip: string, apiKey?: string) {
if (actor.actor === "agent" && actor.basis === "verified" && actor.operator) return `op:${actor.operator}`;
if (apiKey) return `key:${apiKey}`;
return `ip:${ip}`;
}
function limitFor(actor: { actor: ActorClass; basis: Basis }): number {
if (actor.actor === "agent" && actor.basis === "verified") return 600;
return 120;
}数値はプレースホルダーです。観測したベースラインから設定してください。クライアントが制限を超えたら、429 Too Many Requests を返し、Retry-After ヘッダーと、自動化アクセス用ページへのリンクを付けます。よくできたエージェントは待機して、それを読みます。
ステップ 6: アクションは認可の後ろに置く
読み取りのための構造化された経路は、セキュリティモデルの抜け道ではありません。データを変更するアクションには、引き続き次が必要です。
- アプリケーションが発行した credential による認証、
- その特定のアクションに対する認可、
- ルートのリスク階層が求める追加チェック。
verified な署名は、どのクライアントが呼び出しているかを示します。特定のユーザーの代わりにそのクライアントが動けるかまでは示しません。これらの問いは分けて扱ってください。署名付き AI エージェント要求が証明するもの では、その違いを説明しています。
ステップ 7: 効果を測定する
まずは observation mode で構造化された経路を運用し、そのあとで比較します。
- 構造化された形式を使うエージェント要求の割合、
- 前後でのエージェントトラフィックに対するサーバーコストとレイテンシ、
- エージェントクライアントのエラー率と再試行率、
- 署名ガイダンスを公開したあとに、declared から verified に移行する自動化クライアントの数。
これらの数値は、構造化された経路が役に立っているか、次にどこへ拡張すべきかを示します。
ステップ 8: 構造化された経路をバージョン管理し、文書化する
エージェントはこれを基盤に構築するので、構造化された経路は公開インターフェースとして扱ってください。いくつかの習慣で信頼性が上がります。
- URL を安定させる。 エージェントが
/docs/getting-started.mdの存在を学習したなら、そのまま動き続けるべきです。コンテンツが移動したら、404ではなく新しい場所への301を返します。 - 更新時刻を含める。 JSON レスポンスには
updatedAtフィールドを、Markdown にはLast-Modifiedヘッダーを入れて、エージェントが適切にキャッシュし、変更があったときだけ更新できるようにします。 - 条件付きリクエスト をサポートする。
If-None-MatchとIf-Modified-Sinceには304 Not Modifiedで応答します。コンテンツをポーリングするエージェントは、毎回すべてを再ダウンロードする代わりに再検証できると、コストを大きく抑えられます。 - 破壊的変更にはバージョンを付ける。 JSON の形を変える必要があるなら、新しいバージョンパスまたは media type を追加し、公開期間のあいだは古いものも残します。
- 自動化アクセス用ページの changelog を公開する。 振る舞いのよいエージェントの運用者はそれを読みますし、新しい構造化ルートを告知する場所にもなります。
app.get("/api/plans", async (req, res) => {
const plans = await loadPlans();
const etag = `"${plans.version}"`;
res.set("ETag", etag);
res.set("Cache-Control", "public, max-age=300");
if (req.get("if-none-match") === etag) return res.status(304).end();
return res.json({ updatedAt: plans.updatedAt, plans: plans.items });
});これらの実践は、自社の frontend やパートナー統合にも役立ちます。安定し、キャッシュ可能で、文書化された構造化された経路は、どのプログラムにとってもアプリケーションと連携する最も簡単な方法になっていきます。
よくある落とし穴
- エージェントと人間に異なる事実を返す。 構造化版はページと同じ情報を含めるべきです。内容が食い違うと、アシスタントに製品について尋ねるユーザーを混乱させます。
- 構造化された経路を隠す。
/llms.txt、API docs、またはページ自体からリンクされていなければ、エージェントは HTML のスクレイピングを続けます。 - 主張に報いる。 高い制限を与えるのは verified なクライアントです。コピーされた User-Agent 文字列に、より大きな予算を与えるべきではありません。
- キャッシュを忘れる。
Vary: Acceptがなければ、キャッシュがブラウザーに Markdown を返してしまうことがあります。
Proof of Actor が適している場所
Proof of Actor は、エージェントを構造化されたコンテンツへルーティングし、公平な制限を設定するために必要な Actor class と basis を提供します。署名済み、宣言済み、推定されたエージェントを検知し、それらがルートをどう使っているかの理解を助け、ルーティングの判断はコードに残します。
ご自身のアプリケーション向けに構造化された経路を私たちのチームと設計したい場合は、早期アクセスに申し込む をご利用ください。特に API と MCP のシナリオについては、APIs と MCP Servers を参照し、誰があなたの MCP server を呼び出しているのか? で、Actor context が Model Context Protocol の tool call にどう適用されるかを確認してください。
参照資料
- RFC 9110: HTTP Semantics: content negotiation (Section 12),
Vary(Section 12.5.5), conditional requests (Section 13) andRetry-After(Section 10.2.3) - RFC 7763: The text/markdown Media Type: serving Markdown representations
- RFC 8288: Web Linking: typed
alternatelinks - RFC 6585: Additional HTTP Status Codes, Section 4:
429 Too Many Requests - llms.txt proposal: the
/llms.txtfile format - OpenAPI Specification: machine-readable API descriptions
- Sitemaps protocol: sitemap file format
- OpenAI: Overview of OpenAI Crawlers: OpenAI's published user agents and IP ranges
- Anthropic: crawler and fetcher documentation: ClaudeBot, Claude-User and Claude-SearchBot
- Model Context Protocol specification (2026-07-28): the current MCP revision as of November 2026