你的 AI 智能体为何在真实网站上失败

在你拿来测试的那些页面上,你的智能体跑得好好的。等你把它放到开放的网络上,答案开始变得含糊,或者自信满满地出错,又或者它说页面是空的——可你在浏览器里明明看得见内容。

第一反应是去改提示词。多数时候提示词没毛病。故障发生在模型看到任何东西之前,就在那个把网址抓下来、变成文本的层里。

我们在真实页面上测了这一层。它有五种坏法,下面是每一种实际的样子。

1. JavaScript 不跑,页面上就没有文字

atlassian.com/software/jira/pricing 用常规方式抓取,是 1.37 MB 的 HTML,产出 一个 token 的可读内容。不是一段,是一个 token,一个换行符。

价格就在里面。它们待在一个 <script> 里,作为应用状态,等着浏览器拿它们把页面搭出来。

用无头浏览器抓同一个网址,它会变成 3,048 个 token,开头是:

## **Transparent pricing for every team.**

同一个网址。同一天。是零内容还是整个页面,完全取决于你有没有执行 JavaScript。

这就是那种看起来像模型问题、其实不是的故障。你的智能体说“页面没有提到价格”,是因为在给它的那堆字节里,这句话是真的。

2. 但渲染并不是你指望的那个解法

显而易见的对策是全都渲染。在 stripe.com/docs 上试试:

FetchError: Rendered fetch failed: Navigation timeout of 25000 ms exceeded

一个普通抓取不到一秒就返回的页面,在无头浏览器里 25 秒都没加载完。没渲染的那版尽管有种种毛病,至少回来了。

渲染的代价是每个页面一个浏览器进程、几百毫秒到几秒的延迟,外加一类新的故障:超时、被拦的资源、永远不触发 load 的页面。如果你的智能体默认渲染,它在一部分网络上会更慢、更可靠,而不是更可靠。

可行的形态是:先廉价抓取,检查是否真拿到了东西,只对空手而归的页面升级到渲染。而那道检查,恰恰是大家会省掉的部分。

3. 你在为用不上的标记付钱

在我们测的十二个知名页面上,从服务器活到模型的比例,中位数不到 1%shopify.com/pricing 发送 410,515 个 token,产出 2,479 个。

如果你的智能体把原始 HTML 塞进上下文窗口,你就是在按输入 token 的价格为包裹用的 <div> 和内联 SVG 付费,同时挤掉了你本来也想读的那些页面。发送之前先抽取。更好的做法是,问问这个站点会不会直接给你 Markdown。有些已经会了:我们测的十二个页面里,有五个提供了某种形式的 Markdown 表示,其中一个是我们自己的。

4. 抽取留下了错的那部分

抽取是一套启发式规则。它去找正文、丢掉家具,而在那些形状不像文章的页面上,它会猜错。

stripe.com/docs 被缩减成 1,057 个字符,开头是这样:

$ stripe payment_intents create --amount 1099 --currency "usd"

这是页面上真实存在的片段。它同时也是一条终端示例,而不是一句说明 Stripe 做什么的话。只拿到这个的智能体,只能从一段代码里去推断这是什么产品。

所以:永远不要因为抽取成功了,就认定这次抽取是有用的。一个便宜的防线——如果抽出来的东西没有成句的文字,或者在一个发了半兆字节的页面上还不到几百个字符,就把它当成读取失败并升级处理,而不是当作事实交给模型。

5. 同一个网址不会返回同一个页面

普通抓取 shopify.com/pricing 返回的是英文。在同一台机器上渲染同一个网址,返回的是西班牙文:普通抓取写着 Pay monthly€32 EUR/mo 的地方,变成了 Pago mensual32 € EUR/mes

这里没有任何东西是坏的。无头浏览器发出的 Accept-Language 和地理位置信号和我们的普通抓取不同,于是拿到了一个不同但正确的表示。可如果你只按网址做缓存,或者拿今天的读取去比昨天的,这个差异看上去就像站点改了价格。

明确地发送 Accept-Language。缓存的键,要按你实际请求的内容来建。

那个没有发生的故障

我们预期会遇到拦截。我们把十二个页面各探测两次——一次作为浏览器,一次作为 OAI-SearchBot——并跨 Cloudflare、Vercel 和 CloudFront 比对状态码。

没有一个区别对待机器人。 一个都没有。

这不代表没人拦截智能体;拦的人很多,你依然应该读 robots.txt 并遵守它。它代表的是:如果你的智能体在一个足够宽的网络样本上失败,原因多半不是被拦。空屋子远比锁着的门常见。

一个经得起真实网络的读取层

1. 先要 Markdown      → Accept: text/markdown,或 /page.md
2. 普通抓取           → 便宜,不跑 JS
3. 检查拿到了什么      → 是成句的吗?相对发送的字节量够长吗?
4. 不合格就升级        → 渲染,带超时和预算
5. 诚实地放弃          → “这个页面我读不了”

第 5 步比看上去重要。一个会报告“页面读不了”的智能体是可以调试的。一个把换行符递给模型、任它即兴发挥的智能体,才是那个会编造你价格的家伙。

简短版

  • 智能体的多数失败是读取的失败,不是推理的失败。
  • 没有 JavaScript 就为空,是其中最大的一种,渲染能治它——有代价,而且并非总能治。
  • 相信之前,先验证每一次抽取。
  • 注意力都给了拦截。真正造成伤害的是空白。

看看一个页面怎么被读到 · 智能体真正看到什么 · 向智能体提供 Markdown