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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

在我们测的十二个知名页面上，[从服务器活到模型的比例，中位数不到 1%](/zh/blog/what-ai-agents-see)。`shopify.com/pricing` 发送 410,515 个 token，产出 2,479 个。

如果你的智能体把原始 HTML 塞进上下文窗口，你就是在按输入 token 的价格为包裹用的 `<div>` 和内联 SVG 付费，同时挤掉了你本来也想读的那些页面。发送之前先抽取。更好的做法是，问问这个站点会不会直接给你 Markdown。[有些已经会了](/zh/blog/markdown-for-agents)：我们测的十二个页面里，有五个提供了某种形式的 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 就为空，是其中最大的一种，渲染能治它——有代价，而且并非总能治。
- 相信之前，先验证每一次抽取。
- 注意力都给了拦截。真正造成伤害的是空白。

[看看一个页面怎么被读到](/zh) · [智能体真正看到什么](/zh/blog/what-ai-agents-see) · [向智能体提供 Markdown](/zh/blog/markdown-for-agents)
