运行在云服务器上的 AI 智能体会从数据中心 IP 出口。网站首先看到的就是这个 IP,于是智能体拿到的是错误国家的结果,撞上并非自己造成的频率限制,或者陷入无法脱身的 CAPTCHA 循环。好的 AI Agent 代理配置在浏览器配置里解决出口问题,而不是在提示词里。多步骤任务从头到尾保持同一个粘性 IP,并行抓取则轮换。下面给出 browser-use、Crawl4AI 和 MCP 服务器的可用配置,以及成本控制方法和最常见的故障模式。
为什么 AI Agent 的代理配置比脚本更重要
智能体需要代理的理由和爬虫一样,有三个:地理位置、频率限制和 IP 信誉。但智能体的需求更迫切,因为它们无人值守,并且自行决定是否重试。脚本被封就停了;智能体被封会再试一次、换个说法、打开新标签页再试,每次尝试都要消耗带宽和 LLM token。
- 地理位置。价格、搜索结果、库存和法律声明都因国家而异。在美国东部的服务器上为德国客户比价的智能体,采集到的是错误数据。在代理用户名中指定国家即可解决,无需改动智能体逻辑。原理见地理定位代理的工作原理。
- 云出口信誉。AWS、GCP、Azure 和 Hetzner 的 IP 段是公开的,IP 信誉服务会把它们评为托管(hosting),而非普通用户接入。许多网站一看到这些 IP 段就发起挑战或限速,不管请求本身是什么样子。评分机制见我们的 IP 信誉深度解析。
- 共享频率限制。位于同一个 NAT 网关后的多个智能体共享同一个公网 IP。单看每一个都很克制,合在一起就像一个激进的客户端。
代理解决不了的问题:住宅 IP 并不会让自动化浏览器看起来像人。行为检测和指纹检测依然有效,我们的姊妹篇网站如何检测 AI 浏览器智能体说明了具体做法。代理的用途是获取正确的、地域准确的数据,并公平地分散负载,而不是用来绕过已经明确拒绝自动化访问的网站。
粘性会话还是轮换:按任务决定,而不是按项目
经验法则:任务有状态就固定 IP,没有状态就轮换。登录 → 浏览 → 加入购物车 → 结账这样的流程必须从头到尾保持同一个出口 IP,因为许多网站会把会话 Cookie、CSRF token 或风险评分绑定到开启会话的 IP 上。流程中途 IP 一变,你往往会被登出或重新挑战。相互独立的页面抓取正好相反:把它们分散到不同 IP 上,可以让每个 IP 都低于单 IP 限额。
使用 backconnect 网关时,只需修改用户名即可切换模式。USERNAME-sid-abc123-ttl-10m 只要你一直复用这个会话 ID,就保持同一个出口 IP。普通的 USERNAME 会轮换。USERNAME-country-US 固定国家。会话时长的详细说明见粘性会话与轮换会话指南。
| 智能体任务 | IP 模式 | 原因 |
|---|---|---|
| 登录后在账户内操作 | 粘性,每个账户一个会话 ID | 会话 Cookie 和风险评分通常与登录时的 IP 绑定。会话中途换 IP 看起来像账户被盗。 |
| 多页表单或结账测试 | 整个流程使用粘性 | CSRF token 和购物车状态得以保留。失败的步骤可以从同一个 IP 重试。 |
| 研究型智能体阅读 50 个互不相关的页面 | 轮换 | 没有共享状态。分散请求可避免在任一域名上触发单 IP 限速。 |
| 为 RAG 入库做批量爬取 | 轮换,或少量粘性槽位 | 轮换分散负载。粘性槽位让 JS 密集型网站在几个页面之间保留 Cookie。 |
| 地域对比(同一 URL,多个国家) | 按国家定位,国家内轮换 | 你要的是每个市场一个答案,而不是每个 IP 一个。 |
| 每 N 分钟监控一个页面 | 轮换 | 同一 IP 反复访问会形成模式。轮换让每个 IP 的负载保持在低位。 |
browser-use:当前 API 中的代理配置
browser-use 在 Browser(BrowserSession 的别名)上接受 proxy=ProxySettings(...) 参数,包含 server、username、password 和 bypass 字段。凭据交给浏览器,永远不会进入 LLM 上下文。在底层,browser-use 用 --proxy-server 启动 Chromium,并通过 CDP(Fetch.authRequired)应答代理的认证质询,因此你无需把凭据写进 URL。注意导入路径:ProxySettings 位于 browser_use.browser,而不在包的根目录(参见浏览器参数参考和 profile.py)。
import asyncio
import os
import uuid
from browser_use import Agent, Browser, ChatOpenAI
from browser_use.browser import ProxySettings
PROXY_USER = os.environ["PROXYHAT_USER"]
PROXY_PASS = os.environ["PROXYHAT_PASS"]
async def run_checkout_test() -> None:
# 整个流程使用同一个粘性会话 ID,让登录 -> 购物车 -> 结账保持同一出口 IP。
session_id = uuid.uuid4().hex[:12]
browser = Browser(
headless=True,
proxy=ProxySettings(
server="http://gate.proxyhat.com:8080",
username=f"{PROXY_USER}-sid-{session_id}-ttl-10m",
password=PROXY_PASS,
bypass="localhost,127.0.0.1",
),
# 把智能体限制在被测网站内;同时也限制了 sensitive_data 可以输入到哪里。
allowed_domains=["https://staging.example-shop.com"],
# Chromium 开关:完全不下载图片。
args=["--blink-settings=imagesEnabled=false"],
)
agent = Agent(
task="Log in with x_user / x_pass, add the cheapest USB-C cable to the cart, "
"stop at the payment page and report the order total.",
llm=ChatOpenAI(model=os.environ["OPENAI_MODEL"]),
browser=browser,
# 模型只看到占位符 x_user / x_pass,看不到真实值。
sensitive_data={
"https://staging.example-shop.com": {
"x_user": os.environ["SHOP_USER"],
"x_pass": os.environ["SHOP_PASS"],
}
},
use_vision=False, # 不在每一步截图:更省 token
max_failures=2, # 默认为 5;每次失败都意味着又一轮页面加载
)
history = await agent.run(max_steps=25) # 默认为 500
print(history.final_result())
asyncio.run(run_checkout_test())
关于这种写法的说明:
- 带认证的代理请使用 HTTP 端口。Chromium 的网络栈文档写明 SOCKSv5 不支持任何认证方式,因此带凭据的
socks5://…:1080在任何基于 Chromium 的智能体中都会失败。请使用http://gate.proxyhat.com:8080。 - 代理凭据不属于
sensitive_data。sensitive_data用于智能体需要在页面中输入的值。代理凭据放在ProxySettings里,模型永远看不到。不要为了“让智能体知道”而把代理 URL 粘贴进task字符串。 allowed_domains比看上去更重要。如果传入了sensitive_data却没有锁定域名,browser-use 会记录警告,因为遭受提示词注入的页面可能诱使智能体把机密输入到别处。- 云模式是另一种出口。
Browser(use_cloud=True)会在 Browser Use Cloud 上运行浏览器,使用其自带的代理。你的ProxySettings只作用于你自己启动的本地浏览器或cdp_url浏览器。 - 默认扩展能省流量。
enable_default_extensions默认开启,会加载 uBlock Origin Lite 和一个 Cookie 横幅处理器,因此大多数广告和追踪器根本不会计入你的流量。
Crawl4AI:逐次爬取的代理、轮换与粘性会话
在 Crawl4AI 中,代理在 CrawlerRunConfig 上按每次爬取设置,可以用 proxy_config,也可以通过 proxy_rotation_strategy。代理文档将旧的 BrowserConfig proxy 参数标为已弃用,并建议按请求配置代理。若需粘性行为,proxy_session_id 会把策略中的同一个代理固定给所有共享该 ID 的爬取。
backconnect 网关只有一个主机,所以“一组代理”其实是一组会话用户名。每个会话 ID 是一个槽位,拥有自己的出口 IP,轮询策略把 URL 分散到各个槽位:
import asyncio
import os
import uuid
from crawl4ai import (AsyncWebCrawler, BrowserConfig, CacheMode,
CrawlerRunConfig, ProxyConfig, RoundRobinProxyStrategy)
USER = os.environ["PROXYHAT_USER"]
PASS = os.environ["PROXYHAT_PASS"]
def slot() -> ProxyConfig:
# 每个槽位一个粘性会话,因此每个槽位保持一个出口 IP。
sid = uuid.uuid4().hex[:10]
return ProxyConfig(
server="http://gate.proxyhat.com:8080",
username=f"{USER}-sid-{sid}-ttl-10m",
password=PASS,
)
strategy = RoundRobinProxyStrategy([slot() for _ in range(8)])
browser_cfg = BrowserConfig(
headless=True,
text_mode=True, # 尝试禁用图片及其他重量级内容
avoid_ads=True, # 屏蔽常见的广告/追踪域名
)
bulk_cfg = CrawlerRunConfig(
proxy_rotation_strategy=strategy, # 每个 URL 用下一个槽位
cache_mode=CacheMode.BYPASS,
check_robots_txt=True,
max_retries=1,
)
async def main(urls: list[str]) -> None:
async with AsyncWebCrawler(config=browser_cfg) as crawler:
results = await crawler.arun_many(urls, config=bulk_cfg)
for r in results:
print(r.url, r.success, len(r.markdown or ""))
# 有状态的流程:每一步使用相同的代理槽位,以及相同的浏览器页面。
step_cfg = bulk_cfg.clone(proxy_session_id="acct-42", session_id="acct-42")
await crawler.arun("https://example.com/login", config=step_cfg)
await crawler.arun("https://example.com/account/orders", config=step_cfg)
asyncio.run(main(["https://example.com/a", "https://example.com/b"]))
有两个设置容易混淆。session_id 保持同一个浏览器页面,跨越多次 arun() 调用,proxy_session_id 则保持同一个代理。有状态的流程两者都需要。如果想按地域轮换而不是使用粘性槽位,就用单个 ProxyConfig,用户名设为 USERNAME-country-US。
一个重试放大的陷阱:proxy_config 也接受一个代理列表作为备用,而 async_webcrawler.py 中的重试循环在每次尝试时都会遍历整个列表。因此一个被封的 URL 最多可能被抓取 (max_retries + 1) × len(list) 次。备用代理有 10 个、max_retries=3 时,一个注定加载不了的 URL 就会产生 40 次完整页面加载。保持列表简短,并有意识地设置 max_retries。
为 MCP 助手提供走代理的网络访问
有两种方案:运行微软的 Playwright MCP 服务器并在配置中设置代理,或者写一个经代理抓取并返回文本的小型 MCP 工具。助手需要点击和输入时用第一种;只需阅读页面时用第二种,因为每次调用的成本低得多。
方案一:带认证代理的 Playwright MCP
@playwright/mcp 提供 --proxy-server 和 --proxy-bypass 参数(README)。对于需要凭据的代理,请改用 JSON 配置文件。其中的 browser.launchOptions 会直接传给 Playwright,而 Playwright 的 proxy 选项有独立的 username 和 password 字段:
{
"browser": {
"browserName": "chromium",
"isolated": true,
"launchOptions": {
"headless": true,
"proxy": {
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME-sid-mcp01-ttl-10m",
"password": "PASSWORD"
}
}
},
"capabilities": ["core"]
}
npx @playwright/mcp@latest --config ./playwright-mcp.json --image-responses omit
不要把这个文件放进代码仓库,并确保只有运行服务器的用户可以读取。这里的会话 ID 意味着整个助手会话共享一个 IP,适合“登录后执行某操作”的场景。想换新 IP 时,用新的 ID 重启即可。--image-responses omit 阻止截图回传给模型。"isolated": true(即 --isolated 参数)把配置文件保存在内存中,避免 Cookie 在任务之间泄漏。README 还指出,--allowed-origins 和 --blocked-origins 不是安全边界。把它们当作卫生措施,而不是防护。
方案二:用官方 MCP Python SDK 写一个最小的代理抓取工具
Python SDK 的 v2 把 FastMCP 重命名为 MCPServer(迁移指南)。为 v1 编写、使用 from mcp.server.fastmcp import FastMCP 的代码需要改用新的导入。下面是一个只读工具,它经网关抓取页面,限制响应大小,并且绝不在输出中包含代理 URL:
import os
import re
import httpx
from mcp.server import MCPServer
mcp = MCPServer("proxied-fetch")
_USER = os.environ["PROXYHAT_USER"]
_PASS = os.environ["PROXYHAT_PASS"]
_MAX_BYTES = 1_500_000
_COUNTRY = re.compile(r"^[A-Za-z]{2}$")
_SESSION = re.compile(r"^[A-Za-z0-9]{1,24}$")
def _proxy(country: str | None, session: str | None) -> str:
user = _USER
# 校验模型提供的值:它们最终会进入代理用户名。
if session:
if not _SESSION.match(session):
raise ValueError("session must be 1-24 alphanumerics")
user = f"{_USER}-sid-{session}-ttl-10m"
elif country:
if not _COUNTRY.match(country):
raise ValueError("country must be a 2-letter code")
user = f"{_USER}-country-{country.upper()}"
return f"http://{user}:{_PASS}@gate.proxyhat.com:8080"
@mcp.tool()
async def fetch_page(url: str, country: str | None = None, session: str | None = None) -> str:
"""Fetch a public http(s) page through a residential proxy and return its text.
country: optional 2-letter exit country. session: optional ID to reuse one exit IP.
"""
if not url.startswith(("http://", "https://")):
return "error: only http(s) URLs are allowed"
try:
async with httpx.AsyncClient(proxy=_proxy(country, session), timeout=30,
follow_redirects=True) as client:
async with client.stream("GET", url) as resp:
body = b""
async for chunk in resp.aiter_bytes():
body += chunk
if len(body) > _MAX_BYTES:
break
text = body.decode(resp.encoding or "utf-8", errors="replace")
return f"HTTP {resp.status_code} {resp.url}\n\n{text[:200_000]}"
except ValueError as e:
return f"error: {e}"
except httpx.HTTPError as e:
# 只返回异常类型:某些异常消息可能回显代理 URL。
return f"error: fetch failed ({type(e).__name__})"
if __name__ == "__main__":
mcp.run(transport="stdio")
在你的 MCP 客户端中把它注册为 stdio 服务器。通过客户端的 env 配置块传入 PROXYHAT_USER 和 PROXYHAT_PASS,绝不要通过工具参数传递。以 Claude Code 为例,命令是 claude mcp add --env PROXYHAT_USER=… --env PROXYHAT_PASS=… --transport stdio proxied-fetch -- python server.py。--transport 要放在最后一个 --env 和服务器名称之间,否则 CLI 会把名称当作另一组环境变量。正则校验物有所值:遭受提示词注入的页面可能让模型用奇怪的参数调用工具,不加校验的输入会让它把任意文本写进你的代理用户名。需要 JavaScript 的页面请使用方案一;如果你要自己构建浏览器层,可以参考我们的 Playwright 轮换代理指南。
成本控制:智能体消耗带宽的方式与脚本不同
住宅流量按 GB 计费,而智能体加载完整页面,每一步往往加载好几次。以下控制措施效果最显著,大致按重要性排序:
- 模型不读的内容就不要加载。图片、视频、字体和广告脚本占了现代网页的大部分体积。在 Crawl4AI 中使用
text_mode=True和avoid_ads=True,在 browser-use 中使用imagesEnabled=false开关或 uBlock;页面是服务端渲染时,用抓取工具代替浏览器。 - 在两个地方限制重试。框架重试(
max_failures、max_retries)和智能体层面的重新规划会相互放大。遇到 403 后“换个思路”的智能体,往往只是重新加载同一个被封的页面。把max_steps设得足够低,让卡住的运行以低成本结束。 - 有状态流程中复用上下文。新的浏览器上下文意味着新的 Cookie、新的挑战,以及重新下载每一个已缓存的资源。在同一个任务内,保持上下文(以及粘性 IP)存活。
- 遇到封锁信号就停止。把 CAPTCHA 页面、机器人管理厂商返回的 403 或 429 视为终止状态,上报给人工处理。不要让智能体靠重试硬闯。退避的理由见我们的 CAPTCHA 处理指南。
- 按任务计量,而不是按月。记录每次智能体运行的字节数。突然跳升通常意味着出现了循环,而不是任务变大了。
值得提前设计应对的故障模式
大多数智能体代理事故都源于四个原因之一:CAPTCHA 循环、重试放大、会话悄悄换了 IP,或凭据泄漏进模型上下文。
- CAPTCHA 循环。启用视觉的智能体会尝试“解”挑战,失败、重新加载、再试。请识别挑战页面(已知的 iframe 主机、“Just a moment…”这类标题)并结束运行。大规模应答挑战不是正当的数据管道该做的事,而且同一住宅 IP 上的反复失败会损害该 IP 的信誉,影响所有使用它的人。
- 重试放大。前文已述:框架重试 × 代理备用列表 × 智能体重新规划。要关注每个 URL 的总尝试次数,而不仅仅是失败次数。
- 会话悄然中断。粘性会话在 TTL 到期时结束,如果上游节点下线还可能提前结束;之后的请求就会从新的 IP 发出。如果流程中途的某个请求突然重定向到登录页,就假定 IP 已经变了。用新的会话 ID 重新开始流程,而不是让智能体即兴发挥。
- 凭据进入 LLM 上下文。任何到达模型的内容都可能出现在日志、追踪、评测数据集里,或者被提示词注入的外传请求带走。把代理凭据放在由浏览器或 HTTP 客户端读取的环境变量中。不要放进任务字符串、系统提示词或工具输出,返回异常消息前要先清理。Playwright MCP 的
secrets选项可以从工具响应中遮盖已知字符串,但它自己的 README 称这只是便利功能,而非安全特性。
在怪罪智能体之前,先单独测试代理:用代理检测工具测试你配置的用户名变体,确认出口国家和 IP 类型符合预期。关于智能体工作负载的 IP 类型的更全面对比,请参阅适用于 AI 智能体与 LLM 网页数据采集的最佳代理。
这套配置解决不了什么
代理改变的是流量从哪里来,而不是流量的行为方式。它不会让智能体通过行为类机器人检测,也不等于获得了访问那些在 robots.txt 或服务条款中屏蔽自动化的网站的许可。它同样不会让采集个人数据变得合法。如果网站提供 API、数据源或签名智能体计划,这些途径通常比任何代理配置都更便宜、更可靠。把住宅 IP 用在它擅长的地方:正确的地理位置、公平的负载分配,以及不继承云 IP 段的不良信誉。






