跳到主要内容

对外形态:本地 vs 托管 SDK 与 MCP 服务

30 秒导读: 前四章讲的是 Notte 内部怎么把网页变成动作、怎么跑 agent 循环、怎么落地一次点击、怎么做企业级增强。这一章换个角度:这套东西怎么被真正用起来。同一份 Session/Agent 代码,改一行 import 就能从「本地开源、自带浏览器、用你自己的 LLM key」切到「Notte 云托管浏览器、附带隐身/验证码/代理/保险库/身份」。再往外包一层 MCP,Notte 就成了别的 AI agent 的「浏览器手脚」。

本章偏使用面与边界,内部机制点到为止——想看感知/循环/执行的原理,回 感知层Agent 主循环动作执行;想看保险库/抓取/表单,看 企业级增强


1. 两种运行形态:local vs hosted(先建立直觉)

Notte 最核心的产品决策:同一套对象,两种后端。你先在本地把逻辑跑通,然后换个 import 就搬到云上,拿到一堆本地没有的能力。

1.1 一句话对比

本地(local)托管(hosted)
入口import nottenotte.Session / notte.Agentfrom notte_sdk import NotteClientclient.Session / client.Agent
浏览器在哪你机器上的 Chromium(patchright 装的)Notte 云端
LLM key你自己的(.env 里的 GEMINI_API_KEY 等)也是你自己的推理 key,但浏览器与 premium 由云端提供
premium 能力隐身(stealth)、验证码求解、代理、保险库、数字身份等
计费只花你的 LLM 钱LLM + Notte API

1.2 用起来什么样:两段几乎一样的代码

本地模式——notte.Session 就是 NotteSession(见 src/notte/__init__.py,把 notte_browser.session.NotteSession 直接 as Session):

# 示意,改编自 README:44-56
import notte
from dotenv import load_dotenv
load_dotenv() # 载入你自己的 LLM key

with notte.Session(headless=False) as session: # 本地浏览器
agent = notte.Agent(session=session, reasoning_model='gemini/gemini-2.5-flash', max_steps=30)
response = agent.run(task="doom scroll cat memes on google images")

托管模式——NotteClient.SessionRemoteSession,浏览器在云端。改动只有:多创建一个 client、把 notte. 换成 client.:

# 示意,改编自 README:62-71
from notte_sdk import NotteClient
import os

client = NotteClient(api_key=os.getenv("NOTTE_API_KEY"))

with client.Session(open_viewer=True) as session: # 云端托管浏览器
agent = client.Agent(session=session, reasoning_model='gemini/gemini-2.5-flash', max_steps=30)
response = agent.run(task="doom scroll cat memes on google images")

README 自己的话:先在本地实验,然后 "drop-in replace the import and prefix notte objects with cli" 就切到云端(README.md:73)。

1.3 「无缝切换」是怎么做到的:同名门面

秘密在 NotteClient 上用了一堆 @property,把本地类名映射到远程类名——名字一样、构造签名兼容,只是背后连的后端不同:

notte.Session → NotteSession (notte_browser,本地浏览器 + 本地 pipeline)
client.Session → RemoteSession (notte_sdk,HTTP 打到 api.notte.cc)

notte.Agent → Agent (notte_agent,本地循环,见 02 章)
client.Agent → RemoteAgent (notte_sdk,云端跑循环,SDK 只收日志)

NotteClient 里每个能力都做成属性工厂(client.py:79-113),用 functools.partial 把对应的子客户端预绑进去:

# 真实源码 packages/notte-sdk/src/notte_sdk/client.py:83-89
@property
def Session(self) -> type[RemoteSession]:
return cast(type[RemoteSession], partial(RemoteSession, _client=self.sessions, headless=True))

@property
def Agent(self) -> type[RemoteAgent]:
return cast(type[RemoteAgent], partial(RemoteAgent, _client=self.agents))

一句话:client.Session(...) 等价于 RemoteSession(..., _client=..., headless=True)——所以调用点看起来和本地一模一样。Vault/Persona/FileStorage/Workflow/AgentFallback 同理(client.py:91-113)。

premium 从哪来? hosted 的能力不是 SDK「实现」的,而是云端浏览器提供、SDK 只是把参数传过去。例如 client.Session(solve_captchas=True, proxies=True, browser_type="chrome")(README.md:167-172)——这些字段最终进 SessionStartRequest,由 Notte 后端消费。本地 NotteSession 若设 solve_captchas=True 但本地没装求解器,会直接在 __init__CaptchaSolverNotAvailableError(packages/notte-browser/src/notte_browser/session.py:139-140)。


2. SDK 端点地图:notte-sdk 是怎么组织的

托管模式下,notte_sdk 就是一组按资源切分的 HTTP 客户端NotteClient.__init__ 把它们一次性装配好(client.py:52-74),每个都是 BaseClient 子类,负责一类云端资源。

2.1 endpoints/ 目录各管什么

文件客户端类 / 面向用户的对象管什么
sessions.pySessionsClient / RemoteSession会话生命周期:start/stop/status/list、cookie 上传下载、replay 录像、viewer 直播、以及代理到 observe/execute/scrape
agents.pyAgentsClient / RemoteAgentagent 的 start/run/wait、WebSocket 拉实时日志(watch_logs)、status/stop、把 agent 步骤转成可复用脚本(workflow)
vaults.pyVaultsClient / NotteVault凭据保险库:create、add/get/list/delete credentials、信用卡(见 企业级增强)
personas.pyPersonasClient / NottePersona数字身份:create、分配电话号(create_number)、读邮件/短信(list_emails/list_sms),给账号注册 + 2FA 流程用
files.pyFileStorageClient / RemoteFileStorage会话级文件:upload 给 agent 用、download 取回 agent 抓到的文件
workflows.pyWorkflowsClient / RemoteWorkflow把脚本/agent 生成的代码部署成可复用 workflow,run 触发(functions.py 是同一套的别名,client.py:74self.functions = self.workflows)
page.pyPageClient最底层的页面操作 HTTP:对某个 session_idscrape/observe/execute 请求
profiles.pyProfilesClient浏览器 profile 持久化(跨会话保留登录状态等)
base.pyBaseClient / NotteEndpoint公共 HTTP 管道:鉴权、URL 拼接、请求/响应模型校验

RemoteSession.observe/execute/scrape 并不自己发 HTTP,而是转手给 PageClient。例如:

# 真实源码 packages/notte-sdk/src/notte_sdk/endpoints/sessions.py:1281
return self.client.page.scrape(self.session_id, raise_on_failure=raise_on_failure, **data)

也就是 RemoteSession 是「面向用户的对象」,PageClient 是「干活的 HTTP 客户端」,两层分工。

2.2 请求模型如何与 core 复用

types.py 里定义了所有请求/响应的 Pydantic 模型,关键点:这些模型大量复用 notte_core 的类型,保证本地和云端说的是同一种「话」。

  • 顶部从 core 导入动作、观测、凭据、数据空间等类型(types.py:10-37,如 from notte_core.agent_types import AgentCompletion)。
  • 所有请求继承自 SdkRequest,它 extra="forbid"——多传一个字段就报错,防止拼错参数悄悄被吞(types.py:66-68)。

SessionStartRequest(types.py:774)就是那张「开会话时能配什么」的清单,premium 字段都在这里:

字段含义
solve_captchas云端自动求解验证码
proxiesTrue 用默认代理,或传自定义代理列表
browser_typechromium / chrome
use_file_storage是否挂 FileStorage
cdp_url接入你自己的外部 CDP 浏览器提供商
vault_id会话默认用哪个保险库

agent 侧,AgentRunRequest(types.py:1905)只装一次任务的输入:task(必填)、urlresponse_format(Pydantic 模型做结构化输出)、session_offset。而 reasoning_model/max_steps/vault_id/persona_id 属于「创建 agent 时的配置」,在 __AgentCreateRequest(types.py:1886 附近)。

同一个 AgentRunRequest 本地和云端都用:本地 Agent.arun 和远程 RemoteAgent.run 都吃 AgentRunRequestDict,只是一个在你机器上跑循环、一个把请求 POST 到云端。


3. 会话生命周期与资源管理

会话是所有操作的容器。理解它的生命周期,就理解了 with 背后发生了什么。

3.1 Session 是个「资源」:AsyncResource / SyncResource

本地 NotteSession 同时实现两套资源协议,同步/异步都能用 with:

# 真实源码 packages/notte-browser/src/notte_browser/session.py:115
class NotteSession(AsyncResource, SyncResource):

远程 RemoteSessionSyncResource(sessions.py:574)。两者的 __enter__ 都会 start()__exit__ 都会 stop()——所以 README 里所有例子都用 with,这不是风格,是保证会话被关掉(云端会话不关会一直计费/占额度)。

3.2 __init__ 装配了哪些东西

本地 NotteSession.__init__(session.py:120-161)是理解「一个会话由什么组成」的最好切口:

构造参数作用
perception_type感知强度,默认走 config(见 §5 的 fast/deep)
raise_on_failure动作失败是抛异常还是返回失败结果
cookie_file启动时自动载入、退出时自动回写的 cookie 文件(session.py:159)
storage文件存储;本地会话拒绝 RemoteFileStorage,传了直接报错(session.py:134-137)
tools附加工具列表
vault凭据保险库(见 04 章)
persona数字身份;附加时会顺带把它的 vault 和 PersonaTool 装进来(attach_persona,session.py:171-176)
keep_alive退出 with跳过 stop,让会话留活

keep_alive 有个很诚实的自我提醒(注意源码里连拼写错误都保留原样):

# 真实源码 packages/notte-browser/src/notte_browser/session.py:161
self._keep_alive_msg: str = "🌌 Keep alive mode enabled, skipping session stop... Use `session.close()` to manually stop the session. Never `keep_alive=True` is production."

RemoteSession.start(sessions.py:730-805)比本地多了云端特有的健壮性处理:

  • 集群过载重试:遇到 HTTP 529(cluster overload)会退避 30 秒重试(sessions.py:783-789);4XX 或未知状态直接抛。
  • cookie 自动化:start 时若 cookie_file 存在就自动载入(sessions.py:800-805),stop 时把会话 cookie 回写文件(sessions.py:841-846)——方便跨会话保持登录。
  • viewer 直播:open_viewer=True 时 start 完自动开直播窗口(sessions.py:797-798)。

装配组合关系(本地会话):

NotteSession
├── window 浏览器窗口(本地 Chromium / patchright)
├── controller BrowserController,把动作落到 Playwright(见 03 章)
├── *_pipe 感知/抓取/选择 三条 pipeline(见 01 章)
├── vault? 凭据保险库(可选)
├── persona? 数字身份 → 自动带出 vault + PersonaTool
├── tools[] 附加工具
└── trajectory 轨迹记录(agent 循环读它,见 02 章)

4. MCP 暴露:让 Notte 成为别的 AI 的浏览器手脚

前面讲的是「你写代码调 Notte」。MCP(Model Context Protocol,让 LLM 通过标准协议调用外部工具的协议)反过来:让别的 AI agent 把 Notte 当工具调

notte-mcp/server.pyFastMCP 起一个服务器,内部持有一个 NotteClient(server.py:70),把 Notte 的核心能力包成一组 MCP 工具。

4.1 暴露了哪些工具

MCP 工具背后调用给外部 AI 的能力
notte_start_session / notte_stop_session / notte_get_session_status / notte_list_sessionsSessionsClient管理云端浏览器会话
notte_observesession.observe(perception_type="fast")列出当前页可交互的动作空间(见 01 章)
notte_executesession.execute(action=...)在当前页执行一个动作(见 03 章)
notte_scrapesession.scrape(...)抓取当前页,支持结构化 response_format(见 04 章)
notte_screenshotsession.observe(...) 的截图看当前页长什么样
notte_operatornotte.Agent(session).start(...) + async_watch_logs_and_wait()把「整个任务」交给 Notte agent 自主完成

notte_operator 是最高层的一个——外部 AI 只给一句自然语言任务,Notte 内部跑完整 agent 循环再把答案返回:

# 真实源码(节选) packages/notte-mcp/src/notte_mcp/server.py:291-306
session = await get_session()
agent = notte.Agent(session=session)
_ = agent.start(task=task, url=url)
if vizualize_in_browser:
session.viewer()
response = await agent.async_watch_logs_and_wait() # 等 agent 跑完
...
return response.answer if response.success else f"Failed to run agent with error: {response.answer}. ..."

4.2 会话是单例、按需重建

MCP 服务器持一个全局 session,并用 asyncio.Lock 串行化访问(server.py:35-36)。get_session() 会先检查状态,只有当会话不存在或不 active 时才新建(server.py:146-160),避免每个工具调用都开一个新浏览器。这也是「同一个 session 一次只服务一个操作」这条约束在 MCP 层的体现。

一句话:MCP 层把「感知 / 执行 / 抓取 / 整任务」四档能力标准化对外,让 Notte 成为任意 MCP 客户端(Claude、Cursor 等)可插拔的浏览器执行器。


5. 边界与局限(诚实清单)

用之前该知道它刻意不做什么、会在哪崩。素材取自 agent.py 的 TODO 与各 case 分支。

5.1 人在环(human-in-the-loop)未实现

agent 若判断需要人帮忙(HelpAction),不是暂停等人,而是立刻失败返回:

# 真实源码 packages/notte-agent/src/notte_agent/agent.py:164-176
case HelpAction(reason=reason):
# Human in the loop is not implemented yet => fail immediately
...
error_msg = f"Agent requested human help: {reason}"
...
return CompletionAction(success=False, answer=error_msg)

5.2 验证码:不开 solve_captchas 就直接失败

agent 遇到验证码,而会话没开求解能力时,同样立即失败,并在错误信息里告诉你怎么办:

# 真实源码 packages/notte-agent/src/notte_agent/agent.py:177-191
case CaptchaSolveAction(captcha_type=captcha_type) if (
not self.session.window.resource.options.solve_captchas
):
error_msg = f"Agent encountered {captcha_type} captcha but session doesnt solve captchas: create a session with solve_captchas=True"
...
return CompletionAction(success=False, answer=error_msg)

也就是说:验证码求解是 hosted premium,要显式 client.Session(solve_captchas=True) 才有(README.md:167);本地默认没有。

5.3 一个 agent 只能 run 一次

跑完想再跑?得重建一个新 agent:

# 真实源码 packages/notte-agent/src/notte_agent/agent.py:363-366
if self.has_run:
raise ValueError("Agent can only be ran once, recreate a new agent to run a second time")
self.has_run = True

远程侧类似:用 agent_id 重建的 RemoteAgent 是「只读句柄」,对它调 run()/stop()/wait() 都会报错(agents.py:1007-10081136-1137)。

5.4 元素 ID 每步会漂移——别用 ID 引历史

observe() 给的元素 ID(如 B1I1)是跟当前页结构绑定的,会随页面变化而变。源码在 execute 的文档里明确建议:自动化工作流优先用 Playwright selector 而非 ID。

原话:"IDs are dependent on the page structure and can change over time"(sessions.py:1527)。

含义:ID 只在当次 observe 的即时结果里可靠;跨步骤/跨会话引用旧 ID 有风险。这也是 Notte 支持 selector/xpath/css 定位的原因(sessions.py:1518-1527)。

5.5 感知默认 fast:deep 更强但更贵

observe 默认用 perception_type='fast',因为快;想要「LLM-ready 的动作空间」可以传 perception_type='deep',但它多一次 LLM 调用,更慢更贵:

文档原话:"a very simple page perception is used ... (i.e perception_type='fast') to make the query fast";deep 则 "at the cost of a slower query since this uses an LLM call"(sessions.py:1328-1336)。

感知强度的原理见 感知层

5.6 其它已知待办(agent.py 顶部 TODO)

框架作者自己标记的未完成项(agent.py:42-46):

  • 更好的 agent 记忆(RAG 记忆管理器)——目前记忆是把轨迹塞进对话
  • 对支持工具调用的 LLM 用原生 tool calling
  • 文件上传/下载在 agent 循环里的一等支持
  • DOM 变化的 diff 渲染
  • 从当前状态里剥掉 base64 图片以省 token

6. 代码地图(导航索引)

主题文件路径符号
本地入口(Session/Agent 别名)src/notte/__init__.pyNotteSession as SessionAgent
托管入口(属性工厂)packages/notte-sdk/src/notte_sdk/client.pyNotteClient.Session / NotteClient.Agent
远程会话 + 生命周期/重试packages/notte-sdk/src/notte_sdk/endpoints/sessions.pyRemoteSessionRemoteSession.start
本地会话装配packages/notte-browser/src/notte_browser/session.pyNotteSession.__init__attach_persona
远程 agent(run/日志/workflow)packages/notte-sdk/src/notte_sdk/endpoints/agents.pyRemoteAgent.runAgentsClient.watch_logs
本地 agent 循环(run-once/边界)packages/notte-agent/src/notte_agent/agent.pyNotteAgent.arunNotteAgent.step
请求模型(与 core 复用)packages/notte-sdk/src/notte_sdk/types.pySessionStartRequestAgentRunRequestSdkRequest
底层页面 HTTPpackages/notte-sdk/src/notte_sdk/endpoints/page.pyPageClient.execute / .observe / .scrape
MCP 暴露packages/notte-mcp/src/notte_mcp/server.pynotte_observenotte_executenotte_scrapenotte_operator

上一章: 企业级增强:凭据保险库 · 结构化抓取 · 表单填充 · 混合脚本 · 回到: Notte 总览