为什么 AI agent 需要一条不同的路径穿过你的应用?
越来越多发往你的应用的请求来自 AI agent: 为用户获取文档的助手、阅读产品页面的研究工具、调用你的 API 的自治工作流。其中大多数都在通过为人设计的界面工作。它们下载完整的 HTML 页面,解析布局,跟随为鼠标设计的导航,并且在脚本很重的页面没有按预期渲染时重试。
这对所有人都低效。agent 会拿到嘈杂的内容并犯错。你的服务器把资源花在没人看的页面渲染上。而且这类流量很难推理,因为它看起来像一个行为异常的浏览器。如果你还不确定这类流量占多少,可以先看如何判断网站流量是否来自 AI agent。
更好的做法是给 agent 一条结构化路径: 以程序可用的格式提供内容和动作,在标准位置可被发现,并通过奖励正确标识自己的客户端的限制来管理。这个指南会说明如何做到这一点,而不必再搭建一个应用。
第 1 步: 决定 agent 应该能够做什么
先从意图开始,而不是技术。查看你已经看到 agent 流量的路由,并将它们分组:
- 读取内容: 文档、产品信息、定价页面、帮助文章、公开资料页。
- 查询数据: 搜索、目录查询、可用性、状态。
- 执行操作: 代表用户创建、更新或删除某些内容。
读取和查询是适合结构化路径的好候选项,这种路径对任何行为良好的客户端都开放。无论客户端如何识别自己,执行操作都需要认证和授权,这部分内容在保护敏感操作的指南中有说明。
第 2 步: 提供机器友好的格式
对于内容路由,提供不带页面外壳的同样信息。下面两种格式已经覆盖大多数需求:
- Markdown 用于文档和文章等叙述性内容,使用在 RFC 7763 中注册的
text/markdown媒体类型提供。它保留标题、列表、链接和代码块,而且很紧凑。 - 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 的说明),这样缓存会分别存储每种表示。很多团队也会公开一个明确的备用 URL,例如在文档路径后追加 .md,因为有些客户端觉得这比协商更容易。通过 <link rel="alternate" type="text/markdown" href="..."> 在 HTML 页面中链接它,这样就可以被发现。像这样的类型化链接遵循 Web Linking 模型。
第 3 步: agent 如何发现你的结构化路径?
agent 无法使用它找不到的东西。把入口点发布到自动化客户端已经会查看的位置:
- 站点根目录下的
/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 步: 按 actor class 路由,而不是按 user agent 字符串
一旦你有了结构化响应,就决定默认把它们给谁。使用 actor class 及其 basis,而不是临时解析 user agent 字符串。AI operator 的确会发布它们的 user agent,就像 OpenAI 和 Anthropic 为它们的 crawler 和 fetcher 所做的那样,但字符串匹配仍然只是一种声明。
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");
}下面两个原则可以保持安全:
- 显式请求始终优先。 如果客户端请求 Markdown 或 JSON,就提供它,不论 class 是什么。
- 默认值绝不会夺走任何内容。 Human 或 Unknown 访问者始终拿到完整页面。给 agent 设定结构化默认值只会改变格式,不会改变它们被允许看到的内容。
第 5 步: AI agent 应该获得什么速率限制?
速率限制是 actor 上下文最直观发挥作用的地方。不要给所有人一条限制,而是为每个 class 和 basis 的组合分配合理额度:
| Actor and basis | Read routes | Notes |
|---|---|---|
| Agent, verified | 更高限制 | Operator 可识别且可追责 |
| Agent, declared or inferred | 标准限制 | 声明无法核实 |
| Unknown | 标准限制 | 默认不惩罚 |
| Human or Human + AI | 标准限制 | 交互式使用很少接近限制 |
以可用的最稳定标识符来键定限制。对于 verified agent,那是 operator origin 和 key identifier。对于其他情况,则使用 credential 或网络级 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 头部,以及指向你的自动化访问页面的链接。构建良好的 agent 会退避并读取它。
第 6 步: 将操作保留在授权之后
针对读取的结构化路径,不是绕过你的安全模型的路径。任何会更改数据的操作仍然需要:
- 使用应用签发的凭证进行认证,
- 针对该特定操作进行授权,
- 以及该路由风险等级要求的任何额外检查。
经过验证的签名告诉你是谁在调用。它并不能告诉你该客户端是否可以代表某个用户执行操作。把这些问题分开处理。我们关于签名的 AI agent 请求能证明什么的文章解释了其中的区别。
第 7 步: 衡量效果
先以观察模式运行结构化路径,然后比较:
- 使用结构化格式的 agent 请求占比,
- 结构化前后 agent 流量的服务器成本和延迟,
- agent 客户端的错误率和重试率,
- 在你发布签名指南后,有多少自动化客户端从 declared 转为 verified。
这些数字能说明结构化路径是否在发挥作用,以及下一步应扩展到哪里。
第 8 步: 为结构化路径做版本管理并写文档
把结构化路径视为公共接口,因为 agent 会在其上构建。下面几个习惯能让它保持可靠:
- 保持 URL 稳定。 如果 agent 已经知道
/docs/getting-started.md存在,它就应该继续可用。当内容迁移时,返回一个到新位置的301,而不是一个404。 - 包含更新时间。 在 JSON 响应中加入
updatedAt字段,并在 Markdown 上加入Last-Modified头部,这样 agent 可以合理缓存,只在内容变更时刷新。 - 支持 条件请求。 对
If-None-Match和If-Modified-Since返回304 Not Modified。当 agent 可以重新验证而不是再次下载全部内容时,轮询你的内容的成本会低得多。 - 为破坏性变更做版本管理。 如果 JSON 结构必须改变,就添加新的版本路径或媒体类型,并在公开期内保留旧版本。
- 为自动化访问页面发布变更日志。 行为良好的 agent operator 会阅读它,而这也给你提供了一个宣布新结构化路由的地方。
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 });
});这些实践同样有利于你自己的前端和合作伙伴集成。稳定、可缓存且有文档的结构化路径,很快就会成为任何程序与应用协作的最简单方式。
常见陷阱
- 对 agent 和人提供不同事实。 结构化版本应包含与页面相同的信息。内容不一致会让向助手询问产品的用户感到困惑。
- 把结构化路径藏起来。 如果它没有从
/llms.txt、你的 API 文档或页面本身链接出去,agent 就会继续抓取 HTML。 - 奖励声明。 更高限制应该给 verified 客户端。复制来的 user agent 字符串不应获得更大的额度。
- 忘记缓存。 没有
Vary: Accept,缓存可能会把 Markdown 提供给浏览器。
Proof of Actor 在哪里发挥作用
Proof of Actor 会为你提供将 agent 路由到结构化内容并设置公平限制所需的 actor class 和 basis。它会检测已签名、已声明和推断出的 agent,帮助你理解它们如何使用你的路由,并把路由决策留在你的代码中。
如果你想和我们的团队一起为自己的应用设计结构化路径, 申请早期访问。对于 API 和 MCP 场景,另请参阅 APIs 和 MCP Servers,并阅读谁在调用你的 MCP server?,了解 actor 上下文如何应用于 Model Context Protocol 工具调用。
来源
- RFC 9110: HTTP Semantics: 内容协商(第 12 节)、
Vary(第 12.5.5 节)、条件请求(第 13 节)以及Retry-After(第 10.2.3 节) - RFC 7763: The text/markdown Media Type: 提供 Markdown 表示
- RFC 8288: Web Linking: 类型化的
alternate链接 - RFC 6585: Additional HTTP Status Codes, Section 4:
429 Too Many Requests - llms.txt proposal:
/llms.txt文件格式 - OpenAPI Specification: 机器可读的 API 描述
- Sitemaps protocol: 站点地图文件格式
- OpenAI: Overview of OpenAI Crawlers: OpenAI 公布的 user agent 和 IP 范围
- Anthropic: crawler and fetcher documentation: ClaudeBot、Claude-User 和 Claude-SearchBot
- Model Context Protocol specification (2026-07-28): 截至 2026 年 11 月的当前 MCP 修订版