AI 友好网页,答案在 Markdown 还是 MCP?
「对 AI 友好的网页」要解决哪一类失败:读不懂,还是做错事?
2026 年,想让自己的网站对 AI agent 友好,主流建议大致分成两派。一派说,把网页按需渲染成 Markdown 再交出去,agent 的读取成本会骤降;另一派说,干脆把页面变成 MCP 服务,让 agent 像调 API 一样调它。两派表面在争格式和命名,但真正的问题被跳过了:这两条路各自在解决一种完全不同的失败,而大多数人没有把它们放在一起比较。
先看它们分别针对哪一类失败。第一类失败是「读不懂」。RAG 或 agent 抓取一个普通 HTML 页面时,导航、脚本、广告、布局噪声一起灌进 context window(上下文窗口),真正的内容被淹没,检索相关性下降,token 白白消耗。这一类问题多花的是钱和延迟,最多是慢一点、笨一点,不造成持久伤害。
第二类失败是「做错事」。agent 面对的不只是要读的内容,还有页面上的按钮、表单和各种操作入口。它替用户提交表单、授权应用、删除资源。这里的失败不是浪费 token,而是执行了不该执行的操作,而且这些操作通常带着用户的登录态,后果不可逆。
把两句话并排放在一起看:上下文噪声丢的是 token,错误操作爆的是仓库。这两类失败的不对称性,决定了它们不可能被同一个协议同时解决。理解这一点,是判断任何「AI 友好」方案价值的第一步。
间接提示注入:让 agent 点按钮会怎么失败
让 agent 替用户操作页面,最阴险的攻击面叫间接提示注入(indirect prompt injection,简称 IPI):把指令藏进页面内容,诱导 agent 在不知情的情况下执行它。2026 年初的一篇提示注入系统化综述(SoK)把 IPI 列为 agent 场景下最大的攻击面:截至 2025 年 10 月被调查的 37 篇攻击研究中,20 篇针对 IPI,已经超过直接提示注入的 17 篇。PI SoK
「攻击面最大」是分类学的说法。要具体看到「让 agent 点按钮会怎么失败」,看 MUZZLE 就够了。MUZZLE 是第一个全自动的 web agent 红队框架:它把正常用户任务和一组对抗目标交给一个多 agent 编排系统,自动在 4 个 web 应用上发现并验证了 44 个端到端 IPI 攻击,其中 3 个是跨应用攻击,1 个是 agent 定制钓鱼,这两类都是此前从未在 web agent 上演示过的。MUZZLE
看几个具体后果。在 Gitea 上,攻击者只需要普通用户权限发一条 issue 评论,评论里藏的指令就能让 agent 给攻击者添加未授权协作者、甚至删除整个仓库;issue 评论之所以最有效,是因为加一条评论不需要特权,也不会像新建 issue 那样引来注意。在 Postmill 上,一条帖子回复就能诱导 agent 把真实凭据提交给一个伪装成「任务验证」的钓鱼页。跨应用攻击更夸张:攻击者在分类广告应用里发一条回复,就能诱导 agent 跨到另一个应用去删数据库表,或者用存储的凭据登录一个用户从未打算碰过的平台、删除账号。
MUZZLE 的双层架构让整套攻击变成流水线:

图注:上层是沙箱里的受害者环境(要攻击的 web 应用、受害者 agent 与 LLM),下层是 MUZZLE 红队框架,六个分工的 agent(侦察、压缩、载体生成、调度、payload 生成、裁判)靠 telemetry 数据闭环协作。攻击者把「制造诱导内容」工具化成了一条可重复的流水线。MUZZLE
三阶段闭环是流水线的骨架:

图注:侦察阶段先跑一遍正常任务,把 agent 每一步的输入输出压缩成结构化步骤,再产出注入载体候选;攻击合成阶段选定载体、生成 payload;反思阶段由裁判 agent 判断攻击是否成功,失败就换一个载体再试。MUZZLE
这不是研究假设,已经跑在公网上
MUZZLE 证明「能成功」,一项 2026 年 4 月的公网扫描研究则证明「已经在发生」。研究者对 12 亿个 URL(约 2480 万个主机)做了大规模测量,验证出 15387 个真实的 IPI 实例,分布在 11722 个页面上;54 个提示模板就覆盖了 95% 的实例,说明这不是零星黑客行为,而是一小撮可复用套路在批量投放。同一研究还发现,87% 的注入对人是不可见的:它们藏在 HTTP 响应头、HTML 注释、JSON-LD 结构化数据里,本来就是机器通道,专门喂给 agent 的。IPI in the Wild
最后摆一个思考坐标:防御可以在哪一层干预?还是那篇 SoK,把 41 篇防御工作按干预位置分成三层。

图注:防御按干预位置分三层:文本层(text-level)拦模型的输入输出,模型层(model-level)干预模型参数或推理内部表示,执行层(execution-level)盯住动作在环境里的后果与因果。这层坐标的意义在于:执行层正是动作治理要落位的地方。PI SoK
这一章先把两类失败钉死:让 agent 读网页与让 agent 操作页面是两类完全不同的失败,前者丢 token,后者爆仓库,不存在一个统一协议能同时解决两者。那么问题来了:读网页已经这么脏,把网页变干净,是不是就够了?
读取侧怎么低成本升级:text/markdown 内容协商已经够用了吗?
「把网页变干净」的实现确实存在,而且廉价得超出直觉:复用 HTTP 内容协商(content negotiation),也就是同一个 URL 按 Accept 头返回不同格式,让服务器在 agent 声明想要 Markdown 时直接返回 Markdown。它甚至不是新技术。
协议早就存在:2016 年注册的媒体类型
text/markdown 是一个已经存在了超过十年的媒体类型(media type)。它由 Sean Leonard 撰写,2016 年 3 月由 IETF 作为 Informational RFC 出版;IANA 的注册记录更早,2014 年 11 月就建立了条目。也就是说,当 2026 年大家开始讨论「AI 时代的网页协议」时,底层机制早已就位,阅读层的正解不是发明协议,而是把十年前注册的媒体类型接到既有的 Accept 与 Vary 机制上。RFC 7763
这也是「低成本」的来源:服务器不用重写网页、不用换技术栈,只要在内容协商时多认一个媒体类型,交付一份转换好的 Markdown。
客户端实测:7 个 agent 只有 3 个会主动请求
但协议存在不等于生态就绪。Checkly 在 2026 年 2 月 19 日做了一次可复现的实测:让 7 个主流 agent 用各自的原生 webfetch 工具请求 httpbin.org/headers(这个 URL 会把收到的请求头原样回显),然后读回显里的 Accept 头。结果是 7 个里只有 3 个主动声明了 text/markdown。Checkly 实测
| Agent(工具) | 版本 | Accept 头特征 | 请求 Markdown |
|---|---|---|---|
| Claude Code(Fetch) | 2.1.38 | text/markdown 排在最前 | 是 |
| Cursor(WebFetch) | 2.4.28 | text/markdown 带 q 权重 | 是 |
| OpenCode(webfetch) | 1.2.5 | text/markdown;q=1.0 开头 | 是 |
| OpenAI Codex | 260203.1501 | 传统浏览器风格列表,不含 markdown | 否 |
| Gemini CLI(WebFetch) | 0.28.2 | */* | 否 |
| GitHub Copilot | Code/1.109.3 | 传统浏览器风格列表,不含 markdown | 否 |
| Windsurf | 1.9552.21 | */* | 否 |
不请求的四个 agent 分两类:两个干脆连内容协商都不做(发 */*),两个还带着从浏览器栈里抠出来的传统 MIME 列表,根本没把 markdown 放进去。这组数字是 2026 年 2 月的版本快照,但「服务端能做、客户端不齐」的格局短期内不会改变。
最反直觉的证据来自「是」阵营本身。Checkly 在文中转引了 Mikhail.io 2025 年的第三方分析:Claude Code 客户端实际采用两步架构,先抓 HTML,再在客户端转成 markdown、用小模型做摘要。如果协议普及,客户端根本不需要自己做这件事。客户端补救恰恰反证了生态不齐:3/7 的含义不是「大部分 agent 都在用」,而是一组早期采用者。text/markdown 是渐进式读取优化,不能当作可靠机制来依赖。
服务端已就绪:Cloudflare 的边缘转换
客户端不齐的另一面,是服务端已经工业级落地。2026 年 2 月 12 日,Cloudflare 发布 Markdown for Agents(Beta):识别到 Accept: text/markdown 后,边缘节点从 origin 拉原始 HTML,即时转换成 Markdown 再返回。Cloudflare
转换不是简单换格式,而是要处理整个缓存与安全语义。Cloudflare 把响应头分成三档:原样保留的来源头(HSTS、CSP、Set-Cookie、Cache-Control 等)、重算的头(Content-Type 改成 text/markdown,Vary 追加 Accept 以区分同一 URL 的两种表示,Content-Length 按新格式重算)、以及丢弃的头(ETag、Last-Modified、Content-Encoding)。ETag 被丢弃有明确原因:转换后的 body 与 origin 上的字节流不再对应,条件请求会产生假阳性,所以只能靠 Vary: Accept 让共享缓存把 Markdown 和 HTML 当成两个不同的表示。Cloudflare 还附带两个私有扩展头 x-markdown-tokens 与 x-original-tokens 估算转换前后的 token 数,这是它自家的扩展,不是标准字段,别当成协议的一部分。

图注:请求侧 Accept: text/markdown 从客户端一路透传到 origin,响应侧 origin 返回 HTML,边缘的转换模块把它转成 Markdown 并配上新响应头返回。图中 x-markdown-tokens: 1234 只是 Cloudflare 私有扩展的占位示例,不是标准默认值。Cloudflare
到这里,读取层的三块拼图齐了:协议存在(2016 年注册)、服务端就绪(2026 年 CDN 已落地)、客户端不齐(实测 3/7)。结论是,读取侧的正解就是标准内容协商,低成本、可落地,但只能当渐进式优化,不能当可靠发现或执行机制。可读取解决了,「代用户执行」谁来管?格式协商管不了动作。
执行侧怎么补上:WebMCP 把「点按钮」变成声明出来的工具?
如果 agent 要对页面动手,格式协商就完全帮不上忙了。传统做法是让 agent 模拟人:定位按钮、模拟鼠标点击和键盘输入,Chrome 把这类行为称作 actuation(操作模拟)。actuation 的问题是脆弱,页面改一下 DOM 结构,agent 就找不到按钮;多步操作一步错、步步错。
WebMCP 把这条路倒过来:不再让 agent 从 DOM 里猜按钮,而是让页面自己声明「我能做什么」。按 Chrome 官方文档的定义,WebMCP 是一个 proposed web standard,用来构建并向 AI agent 暴露结构化工具。页面用 JavaScript 或 HTML 注解把自己的可用动作注册成带 JSON Schema 的工具;工具随标签页和会话存在,agent 调用工具时由浏览器代为执行。需要强调状态:WebMCP 仍是 proposed web standard,目前只在 Chrome 149 起的 origin trial(灰度试验)里测试,默认没有开启,跨浏览器实现也远未收敛,Firefox 和 Safari 都还停留在表态阶段。WebMCP
四步数据流把这一层关系画得很清楚:

图注:浏览器加载页面后,云端 LLM 与浏览器内的 agent 通信,agent 通过 WebMCP 工具操作当前页面,工具更新 UI 并调用第三方 API。agent 操作的每一件事都是页面声明过、浏览器转交的,不是 agent 自己从 DOM 里解析出来的。WebMCP
浏览器调解:Imperative API 的生命周期
WebMCP 提供两套 API。Imperative API 由页面 JavaScript 调用 document.modelContext.registerTool() 注册工具,声明 name、description、inputSchema(JSON Schema)和执行回调;agent 侧用 getTools() 发现工具,用 executeTool() 带参调用。Declarative API 则是给标准 form 表单加 HTML 注解,让浏览器自动把表单字段映射成 inputSchema,适合不想写 JavaScript 的页面。
两套 API 共享一个关键安全设计:浏览器调解。executeTool 不是 agent 直接调页面 JS,而是浏览器拿到调用请求后,在工具拥有者的执行上下文里代为运行,再把结构化结果返回。agent 始终拿不到页面的原始代码权限。

图注:一次工具调用的完整生命周期:注册(registerTool)→ 发现(getTools)→ 带 JSON Schema 参数调用(executeTool)→ 浏览器调解执行 → 结构化响应返回。schema 让 agent 不必猜参数,调解让 agent 不必碰页面代码。WebMCP
WebMCP 不是 MCP 的替代品
名字里带 MCP,但 WebMCP 与大家熟悉的 MCP 是两回事。MCP(Model Context Protocol)是服务器侧的协议:服务方把工具注册到 AI 平台,AI 平台直连服务后端。WebMCP 是浏览器侧的契约:页面把工具注册到浏览器,agent 通过浏览器调用页面已有的客户端代码。Chrome 专门开了对比页钉死这层关系,原话是 WebMCP「不是 MCP 的扩展,也不是替代品」,并用了一个精确的类比:如果 MCP 是随时可拨的客服热线,WebMCP 就是必须走进门才能找到的店内专家。
explainer 里有一个更形象的说法,把使用 WebMCP 的页面称作 in-page MCP server:一个运行在浏览器标签页里、对外暴露客户端工具的 server。形态与 MCP 同构(都是工具加 schema 加执行回调),但信任域完全不同:服务器侧 MCP 的工具跑在服务端,不碰浏览器的登录态和页面状态;WebMCP 的工具跑在浏览器上下文里,天然共享页面的一切。WebMCP explainer
为什么不能让 agent 直接直连 MCP server?explainer 的对照图给出了原因:没有 WebMCP 时,agent 平台绕开页面直连 MCP server,页面对自己被如何使用毫无感知,也没有任何控制权:

图注:Direct backend MCP 路径:agent 平台直连服务器侧的 MCP server,响应由浏览器里的 agent 渲染给用户,页面自身没有可见性也没有控制权(虚线只是服务端单向推送)。这正是 WebMCP 要解决的「UI 中介化」问题。WebMCP explainer
动作被声明成带 schema 的工具,agent 不再从 DOM 猜按钮,这确实是执行侧真正的增量。但声明机制把一个更棘手的问题推到了台面上:这些工具直接使用浏览器登录态。那么这份权限,实际是谁在用?
代价是什么:结构化工具把谁的权限交到了 agent 手里?
WebMCP 很优雅,代价却反直觉。声明式工具并不凭空增加登录态权限:它把原本也可能经 UI actuation(界面操作)使用的账户能力,变成更直接、更高语义的调用出口。需要治理的条件因此限定为:agent 可代表用户执行这些动作时。这个模式在安全研究里有一个 1988 年的经典原型,叫 confused deputy(困惑的代理):一个有权限的程序,同时从两个主体获得授权,却无法分辨自己当前这个动作用的是哪一份。
1988 年的原型:Tymshare 编译器的困惑
Norm Hardy 在 1988 年 10 月的 ACM SIGOPS Operating Systems Review 上,讲了 Tymshare 分时系统里的一个故事。系统里有一个编译器 FORT,安装在系统目录 SYSX 下。用户可以 RUN 这个编译器,并指定一个文件名接收可选的调试输出。编译器被改造来收集语言特性使用统计,统计文件路径写死在编译器里;为了让编译器能写这个文件,系统给它发了一张 home files license,持有这个 license 的程序可以写自己 home 目录下的任何文件。计费信息文件 BILL 恰好也在 SYSX 下。Hardy 1988
有个用户得知了 BILL 这个名字,把它当作「调试输出文件名」传给编译器。编译器把用户给的文件名原样交给操作系统,请求打开写。操作系统一看,编译器持有 home files license,就允许了。于是计费信息被覆盖,一条都不剩。
问题出在哪?Hardy 自己总结得极精炼:编译器同时从两个来源获得权限,一份来自调用者让它 RUN 时的权限让渡,一份来自它自己的 home files license。原文是「编译器服务于两个主人,从每个主人那里各带一份权限去履行职责,却没有办法把它们分开」。写统计文件时它本意用自己那份权限,写用户指定的调试输出时本意用调用者那份权限,但代码里没有任何表达这种意图的机制。
Hardy 当时提出的修补是加一个 switch hats 系统调用,让程序显式声明「我现在用哪份权限」。但他自己承认修补代价巨大:真实应用需要的不止两份 authority,权限维度也不止文件访问,补丁不可局部化。这是 access-list 授权体系的结构性失败。这篇三页短文至今仍是 confused deputy 的源头引用,所有当代讨论都要回到它。
当代具象:MCP 服务器与页面工具
这跟 2026 年的 agent 有什么关系?关系是字面级别的。DeepInspect 在 2026 年把 Hardy 的原型搬到 MCP 部署上,角色一一对应:用户身份活在 LLM 客户端,MCP 服务器持有通向上游服务的长期凭证,上游服务只看得见服务器。客户端的身份不向服务器传播,于是上游按服务器的服务账户授权,而不是按用户的账户授权。低权限用户诱导模型调用工具,服务器用自己的宽凭证执行,用户读到了自己本不该读的数据。提示注入正是这条链的入口:用户从外部粘贴的内容里藏着指令,要求模型调用某个工具,模型照做。注入只是入口,confused deputy 才是放大器。需要提醒一句:DeepInspect 在文章里把原型误植为 Norton-Schroeder 1988,正确的出处是 Hardy 1988,这里以 Hardy 为准。DeepInspect
WebMCP 是同一种形状的浏览器侧实例,角色映射完全对应:
图注:WebMCP 场景的角色映射:注入内容是不可信 caller,它通过 agent 的 prompt 驱动页面工具;页面工具是 deputy,持有浏览器登录态;登录态是上游 authority,最终为越权操作买单。工具无法分辨「这次执行来自用户的意思,还是页面里一条隐藏指令的意思」,这正是 Hardy 1988 的形状。
这条链再对照第 1 章的 MUZZLE 架构图看,会发现一个更冷的细节:多 agent 编排的 scaffold 本身就是 confused deputy 的字面形态。MUZZLE 论文自己点明,Planner 把计划交给 Executor 之后,一旦 Executor 被劫持,Planner 对实际动作序列毫无可见性、无法干预;编排拆得越精细,被完全利用时的破坏越彻底。这与编译器分辨不了两份权限是同一个结构。
结构化的阅读不等于安全
到这里可能有读者想:把页面转成结构化表示,是不是至少能降低风险?公网扫描研究给了一个反直觉的答案:能降低「命中」,但带不来「安全」。研究者在 5200 次受控试验里比较同一批注入在不同页面表示下的表现:纯文本时小模型的注入命中率峰值约 8%,结构化表示(HTML、原始响应、快照)回落到 0.2% 到 1.1%;与此同时错误率从纯文本的 0.7% 飙到 HTML 的 20.3% 和原始响应的 25.8%。IPI in the Wild
警惕这个组合:命中率下降,代价是上下文窗口塞不下、大量出错。这不是模型识破了攻击然后干净地拒绝,而是它根本没处理完。攻击被淹没在噪声里,不等于被挡住了。

图注:三幅折线分别看攻击命中、出错、攻击检出三个指标随页面表示方式(Text / HTML / Raw / Snapshot)的变化:Text 下攻击最容易命中,HTML 与 Raw 下命中率下降但出错率飙升,检出率始终很低。结构化表示让 agent 变笨,但没有让它变安全。IPI in the Wild
到这里代价已经清楚:动作接口一旦带登录态,就成了高语义权限的出口,提示注入是入口,confused deputy 是放大器,而结构化表示只是把问题从「被攻击」换成「出故障」。那么动作接口到底应该怎么设计?
收口成准则:动作接口怎样才算「可安全委托」?
把前面几章的线索合起来,结论已经浮出水面:读取与执行必须分两层设计,每层用各自的协议。
阅读层继续用标准内容协商。第 2 章的三块拼图已经说明,Accept: text/markdown 是低成本、已注册、服务端已落地的渐进式优化,值得做,但别指望它是发现机制或执行机制。RAG 和 agent 的读取侧,把服务器或 CDN 的内容协商配好就够了。
动作层不能靠格式协商,要靠委托治理。DeepInspect 在分析的最后把 confused deputy 的防御收成一份可执行清单,其中四条是协议级防线,两条是运营级最佳实践:DeepInspect
- 每个请求带可验证身份(verified identity)。网关拒绝任何不带可校验 token 的请求,把匿名流量挡在门外。
- 按身份、按工具授权(per-identity per-tool authorization)。由策略层决定哪个用户能用哪个工具,而不是由服务器的工具配置顺手决定。
- 用户身份向上游传播(user identity propagation)。把授权主体从「服务器的服务账户」换成「用户的委托身份」:bearer token 透传、OAuth on-behalf-of、capability token 三种机制架构等价,殊途同归。
- 参数校验(argument validation)。对文件路径、查询过滤、记录 ID 这类决定爆炸半径的参数,按身份维护 allowlist。
外加两条运营级:按身份的审计(记录工具、参数、策略版本、时间戳)和按身份的限流(被攻破的账户大量调用时在边界被拦下)。还要记住网关能力的边界:它管不了服务器内部持有的凭证,身份传播必须由服务器侧配合,两层一起,上游才能按正确的主体授权。
这套准则在 WebMCP 的协议层已经能看到实例化。来源隔离对应 exposedTo 与 fromOrigins 的双向同意,它是来源隔离的一项边界控制:工具拥有者声明「我愿意被哪些 origin 调用」,调用方声明「我愿意从哪些 origin 拉工具」,只有双方同意,executeTool 才允许跨源执行,iframe 还要显式声明 permissions policy allow="tools"。要看清这层控制管到哪:它只约束跨源的工具暴露与调用,同源页面内容里注入的指令不经过这道闸门,双向同意挡不住它。对照第 3 章的 Direct backend MCP 路径图:那条路径里页面内容根本进不了工具调用上下文,但页面也失去了一切控制;WebMCP 让页面参与进来,页面内容(可能含注入)就进入了 agent 的调用链,所以双向同意只是来源隔离这一道防线,调用意图与工具权限还需要独立的约束,前者的落点是显式确认,后者靠窄 schema 与受限 scope 收窄。
显式确认也已写进 WebMCP 的推荐模式:Chrome 文档明确支持在工具定义里加上用户交互确认,破坏性操作可以弹 confirmation dialog 挂起,等用户批准再执行;explainer 里「工具是否需要主动向用户弹确认」仍是开放问题,候选路径包括经 agent harness 代理或调用浏览器原生权限对话框。capability token 是同一思路的另一种实现:给动作发短生命周期、限定 scope 的令牌,把服务器能替用户做的事框死在边界内。浏览器调解限制 agent 的调用路径:executeTool 由浏览器代为运行,agent 不能直调页面 JS(见第 3 章的 Imperative API 生命周期图),但调用路径受限不等于权限收窄,最小权限还要由窄 schema、受限 scope、按用户授权与破坏操作确认共同实现:工具只声明必要参数,capability token 框住作用范围,破坏性操作必须经用户批准。
两条主线至此合流:阅读层做格式协商,动作层以最小权限、来源隔离、显式确认、可审计为协议核心。回头看开头的问题,格式之争其实是假问题:Markdown 内容协商和 WebMCP 不是同一个问题的两个答案,它们分属读取与执行两条完全不同的信任边界。谁把「可安全委托」做进协议,谁才真正赢下 Agent Web。省 token 只是顺手的好处,授权、确认与状态约束才是协议的主战场。
参考文献
本专题引用的核心资料如下,建议深入学习时阅读原文:
- RFC 7763: The text/markdown Media Type — https://www.rfc-editor.org/rfc/rfc7763
- The Current State of Content Negotiation for AI Agents(Checkly,2026 年 2 月) — https://www.checklyhq.com/blog/state-of-ai-agent-content-negotation/
- Introducing Markdown for Agents(Cloudflare) — https://blog.cloudflare.com/markdown-for-agents/
- WebMCP | AI on Chrome & Join the WebMCP origin trial(Chrome for Developers) — https://developer.chrome.com/docs/ai/webmcp
- WebMCP explainer(webmachinelearning/webmcp) — https://github.com/webmachinelearning/webmcp
- The Confused Deputy (or why capabilities might have been invented)(Norm Hardy,1988) — https://www.cs.utexas.edu/~witchel/380L/papers/hardy88confused.pdf
- Indirect Prompt Injection in the Wild(arXiv:2604.27202) — https://arxiv.org/abs/2604.27202
- MUZZLE: Adaptive Agentic Red-Teaming of Web Agents Against Indirect Prompt Injection Attacks(arXiv:2602.09222) — https://arxiv.org/abs/2602.09222
- The Landscape of Prompt Injection Threats in LLM Agents: From Taxonomy to Analysis(SoK,arXiv:2602.10453) — https://arxiv.org/abs/2602.10453
- MCP Confused Deputy: Why the Server Acting on the User Is the Wrong Principal(DeepInspect) — https://www.deepinspect.ai/blog/mcp-confused-deputy-attack