第 24 章:实战大结局 — Browser Agent
[六大支柱终极压力测试 · 全书大结局]
"Browser Agent 不是特例,而是通用 agent 的极限考场。每一个你在前 23 章建立的能力,在这里都被逼到边界。"
本章路线图
全书演进轨迹(最后一格):
Ch 1 Ch 3 Ch 6 Ch 9 Ch 13 Ch 18 Ch 22 Ch 24
│ │ │ │ │ │ │ │
API调用 Lena诞生 Tool系统 RAG检索 安全护栏 长任务Cron 可观测性 Browser Agent
│ │ │ │ │ │ │ │
v0.1 v0.3 v0.6 v0.9 v1.2 v1.5 v0.22 v0.24
← 你在这里本章从一个不能做到的事情出发:lena-v0.23 接到"帮我查微博新消息"时彻底没辙。
经过理论铺垫(浏览器的四大挑战)→ 脚手架(最小 Browser Agent)→ 渐进组装(DOM 感知 + 页面跳变 + 登录态 + 三层 fallback)→ 运行验证(三个端到端任务),最终产物是 lena-v0.24:一个能真正上互联网的 Browser Agent。
途中会踩坑:Origin 头导致 403、本地代理工具劫持 CDP socket(如使用 fake-ip 模式时)、tab 不清理导致 Chrome 拥堵、截图空白页误判。
全书回顾章节串起 Ch 1-23 的全程演进,最后一节"你的 Lena 还能变成什么"给出三条创作方向,结束语留给读者。
Lena 版本变化:v0.23(专用化)→ v0.24(Browser Agent,能上互联网)
Beat 1 · 路线图:Lena 最后缺少什么
到了 lena-v0.23,Lena 能做的事已经很多了。
她有 30+ 个工具、三层记忆、7×24 常驻、Telegram 推送、定时任务、MCP 协议扩展、Docker 沙箱隔离,还能一行 fork 派生出任何专用 agent。
但给她一个任务——"帮我查一下微博上有没有关于 AI 的新消息"——她做不到。
不是因为她不够聪明,而是因为微博的内容住在浏览器里:在 JavaScript 渲染的动态 DOM 里,在一个需要登录态的环境里,在反爬机制的背后。Lena-v0.23 的工具集里没有"操作浏览器"这一项。
这是本章要填上的最后一块拼图。
本章从理论铺垫开始:Browser Agent 的四大挑战(DOM 无穷大、页面跳变、反爬、登录态),以及这四个挑战如何精确映射到你在前 23 章建立的六大支柱上。然后我们看三条浏览器路径的决策逻辑,深入 browser-use(93k+ stars)的架构,组装出 lena-v0.24,跑通三个端到端任务。
最后,我们回望全书,然后把目光转向前方。
🧠 聪明度增量(v0.23 → v0.24):Lena 第一次能操作浏览器——CDP computer use 让她"看"动态 DOM、维持登录态、处理页面跳变,从"对 JavaScript 渲染的网页束手无策"到能跑通三个真实互联网任务。这一章教读者把浏览器操作能力长在自己 agent 上的方法。
Beat 2 · 动机:当 agent 撞上浏览器的墙
Simon Willison 在 2023 年提出的 Dual LLM Pattern 在浏览器 agent 场景下变得尤其重要:
特权 LLM 持有工具(控制浏览器)+ 隔离 LLM 处理不可信内容(网页上的文本),二者不直接共享 token。
为什么?因为浏览器 agent 是最容易被 prompt injection 攻击的 agent 类型——每一个网页都可能包含恶意指令。Ch13 讲了输入安全的一般理论,本章把它推到极端:你的 agent 会主动打开攻击者的网页,然后用 LLM 解析它的内容。如果不做隔离,等于把 agent 的工具控制权交给了互联网上的任何人。
先做一个测试。打开 Python,用 lena-v0.23 当前的 tool 系统尝试读取微博首页内容:
# lena-v0.23 的 web_search 工具,覆盖面足够广
result = await lena.run("从微博首页提取今日 AI 相关帖子的摘要")结果:
Error: web_search 返回了微博的登录跳转页,无内容
或者:
Error: HTTP 200 但内容是 "<div id='root'></div>"(CSR 空壳)
或者:
Error: 403 Forbidden(User-Agent 被识别为非浏览器)三种失败,对应三个根本原因:
- 内容在登录墙后面:
requests.get("https://weibo.com")得到的是登录跳转,不是内容。 - 内容在 JavaScript 里:现代 SPA 渲染的 DOM 在浏览器里执行 JS 后才存在,HTTP GET 只能拿到空 HTML 外壳。
- 反爬机制识别你:User-Agent、请求间隔、行为指纹——任何一项异常都会触发封锁。
这不是 lena-v0.23 的 bug,是架构限制。她没有浏览器,就像没有手的人无法打字。
数字化的差距(以下数字基于作者实战观察,非标准 benchmark 数据,实际结果因网站和任务而异):
| 场景 | lena-v0.23(无浏览器) | lena-v0.24(有浏览器) |
|---|---|---|
| 静态网页爬取 | ✅ 成功率 ~95% | ✅ 成功率 ~99% |
| SPA 动态内容 | ❌ 成功率 ~5% | ✅ 成功率 ~80% |
| 登录后内容 | ❌ 成功率 0% | ✅ 成功率 ~70% |
| 需要交互操作 | ❌ 成功率 0% | ✅ 成功率 ~60% |
最后两行的差距是不可弥合的。不加浏览器,就是 0%。
Convention:本章"浏览器操作"= agent 控制浏览器的行为(点击/输入/截图/导航);"网页爬取" = 发 HTTP GET 取 HTML 源码。两者的成功率差异来自上表,后文不再重复解释。
Beat 3 · 理论铺垫:四大挑战如何精确映射六大支柱
3.1 DOM 无穷大——工具输入的"上下文炸弹"
一个完整的现代 Web 页面有多少 DOM 节点?
测量结果(截至 2026 年 5 月):
| 网站 | DOM 节点数 | 序列化后大小 |
|---|---|---|
| 微博首页 | ~6000 节点 | ~800KB |
| GitHub 仓库页 | ~3000 节点 | ~400KB |
| Amazon 商品页 | ~8000 节点 | ~1.2MB |
| 简单的 React 应用 | ~500 节点 | ~60KB |
Claude Sonnet 的 context window 是 200K token。800KB HTML 大约 200K token,刚好撑满——没有空间放 system prompt、会话历史、思考链。即使能放进去,模型在 200K token 后端的注意力质量也远不如 2K token。
这是 Tool 系统(支柱一)的极端压力测试:工具的返回值如果不加过滤就交给 LLM,会直接把 context window 爆掉。
解法核心:不把整个 DOM 给 LLM,只提取"可交互元素"。这一思路来自 browser-use 的核心设计——用 JavaScript 在浏览器端完成 DOM 过滤,把 6000 个节点压缩到 50-200 个可交互元素,再序列化为结构化的文本列表传给 LLM。过滤率通常在 30-100 倍之间。
Convention:DOM 感知(DOM Perception) = 从完整 DOM 中提取可交互元素子集的过程;DOM 完整序列化 = 把整个 DOM 转为 HTML 字符串的过程。浏览器 agent 必须用前者,绝不用后者。
3.2 页面跳变——Planning 的状态机挑战
人类浏览网页时,会无意识地跟踪状态:"我在哪个页面、上一步做了什么、下一步要做什么"。这种跟踪依赖视觉系统、空间记忆、语义理解的协同。
Browser agent 的状态跟踪问题更复杂:
- 硬跳转:点击链接,页面完全重新加载。可以用
page.on('load')检测,容易处理。 - SPA 路由切换:只有 URL 的
hash或pushState变化,DOM 局部更新。page.on('load')不会触发,只能靠 DOM diff 感知。 - 异步内容加载:滚动到底部时新内容动态插入。需要等待
networkidle或者轮询 DOM 变化。
这三种"页面跳变"对应 Planning(支柱二)的核心挑战:agent 的 ActionHistory 里记录的是"哪个步骤在哪个页面上做了什么",而"哪个页面"的定义随时可能变化。
论文引用:WebArena(2023,Zhou et al.)是浏览器 agent 的标准测评集,包含 812 个真实 web 任务。在实战观察中,页面跳变错误(agent 在跳转后仍尝试操作上一页面的元素)是最常见的失败原因之一。不需要读完这篇论文,只需要知道:页面跳变处理能力是 browser agent 成熟度的重要指标。
3.3 反爬与人机验证——Safety 的外部边界
反爬机制本质上是一个鉴别"这是人还是程序"的安全层。它分为四个层次:
| 层次 | 检测点 | 典型实现 |
|---|---|---|
| L1 请求特征 | User-Agent、Headers | 简单字符串匹配 |
| L2 浏览器指纹 | navigator.webdriver、Canvas 指纹 | JS 检测脚本 |
| L3 行为模式 | 鼠标轨迹、点击间隔、滚动速度 | 机器学习分类器 |
| L4 风险信号 | IP 声誉、账号行为历史 | 规则引擎 + ML |
Headless Chromium 天然触发 L2:navigator.webdriver 默认为 true,可以被 JS 检测到。使用用户已有的真实 Chrome profile(非 headless)可以绕过 L1 和 L2,但 L3 和 L4 依然有效。
关键洞察:lena-v0.24 的设计选择是不对抗反爬。原因:
- L3/L4 级别的反爬需要极复杂的模拟,且越来越强——军备竞赛没有终点
- 使用用户真实 Chrome profile 天然绕过 L1/L2,是合法且稳定的方案
- 三层 fallback 架构保证了即使浏览器路径失败,也有其他路径兜底
对 Safety 支柱的映射:反爬触发是外部安全机制对 agent 的限制,而 agent 本身也必须有内部安全机制——不做有真实副作用的操作(提交表单、购买)除非有人工审批。
3.4 登录态——Memory 的最大价值场景
登录态是 browser agent 与无状态爬虫最本质的区别。
一个能操作用户真实 Chrome profile 的 agent,天然继承了用户在所有网站上的登录状态——不需要知道密码,不需要绕过 2FA,不需要维护 Cookie 池。这是因为 Cookie 存储在 Chrome profile 的 Cookies 数据库文件里,CDP 连接到该 profile 时天然可以访问这些 Cookie。
这是 Memory 支柱(支柱四)最高价值的应用场景:agent 的"记忆"里包含了用户所有网站的认证凭证,而且这些凭证是实时有效的。
登录态问题只有一个真正的难点:Cookie 过期。当微博 session 过期时,浏览器会跳转到登录页,agent 必须能感知到"我不在预期的页面",然后暂停任务并通知用户重新登录——而不是继续在登录页面上做无意义的操作。
Beat 4 · 脚手架:最小可运行的 Browser Agent
现在我们来建立一个最小的 Browser Agent 骨架。它只做一件事:接受一个任务描述,返回执行结果。
下面实现最小的浏览器 agent 骨架,验证概念端到端可行:
# code/lena-v0.24/browser_agent_minimal.py
"""
最小 Browser Agent 骨架
目标:用 browser-use 接管已有 Chrome,执行单个任务
前置条件:
pip3 install browser-use playwright langchain-anthropic
playwright install chromium
Chrome 已以 CDP 9222 模式启动(使用 cdp-start.sh)
"""
import os
import asyncio
from browser_use import Agent, Browser, BrowserConfig
from langchain_anthropic import ChatAnthropic
# 关键:清除代理环境变量
# 原因:Clash fake-ip 模式拦截所有 DNS,包括 localhost,
# 导致 CDP socket 连接被路由到代理而失败
for _var in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy"]:
os.environ.pop(_var, None)
async def browser_task(task: str) -> str:
"""
最小版:执行单个浏览器任务
返回:agent 的最终结果文本
"""
# 连接到已有 Chrome(不是启动新的 headless 实例)
# cdp_url 指向本机 Chrome 的调试端口
browser_config = BrowserConfig(
cdp_url="ws://localhost:9222",
headless=False, # 接管已有 Chrome,不是 headless
# disable_security=True 仅在本地开发时使用,生产禁止
)
browser = Browser(config=browser_config)
# 使用 Claude Sonnet 作为决策 LLM
# 为什么选 Sonnet 不选 Haiku:浏览器任务需要多步推理,
# Haiku 在 3+ 步决策上失败率偏高(实测约 35% vs 12%)
llm = ChatAnthropic(model="claude-sonnet-4-6")
agent = Agent(
task=task,
llm=llm,
browser=browser,
max_actions_per_step=5, # 每步最多执行 5 个动作
)
result = await agent.run(max_steps=20) # 最多 20 步,防止无限循环
return str(result)
# 快速验证:运行这个文件,观察 Chrome 打开新标签页
if __name__ == "__main__":
# 确保 CDP 已启动:参考项目根目录的 cdp-start.sh 脚本
result = asyncio.run(
browser_task("打开 https://github.com/browser-use/browser-use,告诉我这个项目有多少 stars")
)
print(f"\n结果:{result}")
# 预期输出(行数约 1 行):
# 结果:browser-use 项目目前有 92,xxx stars(数字随时间变化)运行 python3 browser_agent_minimal.py 应该看到:
- Chrome 打开一个新标签页
- 自动导航到
github.com/browser-use/browser-use - 终端输出 star 数量
如果报错 ConnectionRefusedError: [Errno 61] Connection refused,检查 CDP 是否已启动:
curl -s http://localhost:9222/json/version | python3 -m json.tool
# 正常输出:{"Browser": "Chrome/...", "webSocketDebuggerUrl": "ws://..."}
# 无输出或连接拒绝:运行 cdp-start.sh(见项目 scripts/ 目录)Beat 5 · 渐进组装:从骨架到生产级 Browser Agent
骨架能跑了。现在我们逐步添加生产系统需要的特性,每次只加一个,加完就验证。
| 扩展点 | 为何需要 | 如何加 |
|---|---|---|
| Tab 保护 | 不覆盖用户正在使用的页面 | 记录任务前的 tab 列表,任务后只清理新增 tab |
| 三层 fallback | 浏览器路径失败时有退路 | FallbackChain:RSSHub → opencli → CDP |
| 审批门控 | 高风险操作需要人工确认 | 检测 action.type 是否在高风险列表 |
| 进程锁 | 防止多个 agent 同时操作同一 Chrome | fcntl.flock 文件锁 |
扩展 1:Tab 保护
不覆盖已有 tab 是铁律,原因是 Chrome profile 里可能有用户正在编辑的文档、未保存的表单、活跃的视频会议。
# code/lena-v0.24/browser_agent.py(节选)
import aiohttp
CDP_BASE = "http://localhost:9222"
async def get_existing_tab_ids() -> set[str]:
"""快照当前所有 tab 的 ID"""
async with aiohttp.ClientSession() as s:
async with s.get(f"{CDP_BASE}/json") as r:
tabs = await r.json()
return {t["id"] for t in tabs}
async def close_new_tabs(protected_ids: set[str]):
"""关闭本次任务创建的所有 tab(血泪教训 4:tab 不清理会积累)"""
async with aiohttp.ClientSession() as s:
async with s.get(f"{CDP_BASE}/json") as r:
current_tabs = await r.json()
for tab in current_tabs:
if tab["id"] not in protected_ids:
# 血泪教训 6:用 PUT 不用 GET
await s.put(f"{CDP_BASE}/json/close/{tab['id']}")
print(f"[Tab清理] 关闭 tab: {tab['id'][:8]}")验证:在执行任务前后各打印一次 tab 数量,应该相等。
任务前 tab 数量: 5
任务后 tab 数量: 5 ← 正确:创建的 tab 已被清理扩展 2:三层 fallback
Browser-use 的成功率约 40-60%,原因是反爬、动态渲染、登录失效的联合打击。三层 fallback 把整体成功率提升到 ~99%。
# code/lena-v0.24/fallback_chain.py
from typing import Optional, Callable, Any
import asyncio
class FallbackChain:
"""三层 fallback:每层失败自动降级到下一层"""
def __init__(self):
self._layers: list[tuple[str, Callable]] = []
def layer(self, name: str):
"""装饰器:把函数加入 fallback 链"""
def decorator(fn: Callable):
self._layers.append((name, fn))
return fn
return decorator
async def run(self, *args, **kwargs) -> Optional[Any]:
for name, fn in self._layers:
try:
print(f"[Fallback] 尝试: {name}")
result = await fn(*args, **kwargs)
if result is not None:
print(f"[Fallback] 成功: {name}")
return result
print(f"[Fallback] {name} 返回空,降级")
except Exception as e:
print(f"[Fallback] {name} 异常: {e},降级")
return None使用示例(微博任务的三层 fallback):
chain = FallbackChain()
@chain.layer("rsshub") # 层 1:RSS 订阅,无需登录,最快
async def via_rsshub(uid: str):
async with aiohttp.ClientSession() as s:
async with s.get(f"https://rsshub.app/weibo/user/{uid}", timeout=aiohttp.ClientTimeout(total=10)) as r:
if r.status == 200:
data = await r.json()
return data["items"][:5]
return None
@chain.layer("opencli") # 层 2:本地 CLI 工具
async def via_opencli(uid: str):
from opencli_client import run_skill
return await run_skill(f"weibo-check --uid {uid}")
@chain.layer("browser_use") # 层 3:真实浏览器
async def via_browser(uid: str):
return await browser_task(f"进入微博,查看用户 {uid} 的最新 5 条消息")
# 执行
result = await chain.run(uid="your_weibo_uid")
print(f"[三层 fallback] 结果来源已记录,整体成功率 ~99%")扩展 3:审批门控
浏览器有真实副作用——点击"提交订单"就真的下单了。任何写操作(提交表单、购买、删除)必须有人工确认。
# code/lena-v0.24/approval_gate.py
HIGH_RISK_PATTERNS = [
"submit", "purchase", "buy", "order", "delete", "transfer",
"pay", "checkout", "confirm", "book",
]
async def gate_before_action(action_description: str) -> bool:
"""
检查动作是否高风险,如果是则请求人工确认
返回 True 表示可以执行,False 表示取消
"""
is_risky = any(p in action_description.lower() for p in HIGH_RISK_PATTERNS)
if not is_risky:
return True
print(f"\n⚠️ 检测到高风险操作:{action_description}")
print("这个操作可能有不可撤回的真实后果。")
confirm = input("确认执行?输入 'yes' 继续,其他取消:")
return confirm.strip().lower() == "yes"验证:
await gate_before_action("点击搜索按钮") # → True(立即通过)
await gate_before_action("点击购买按钮") # → 打印警告,等待用户输入扩展 4:进程锁
cron 任务可能重叠执行,两个 CDP 采集进程同时操作同一个 Chrome 会导致竞态。
# code/lena-v0.24/cdp_lock.py
import fcntl
LOCK_FILE = "/tmp/.lena_browser_lock"
class BrowserLock:
"""确保同一时刻只有一个 browser agent 在操作 Chrome"""
def __enter__(self):
self._fd = open(LOCK_FILE, "w")
try:
fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
self._fd.close()
raise RuntimeError("[BrowserLock] 另一个 browser agent 正在运行")
return self
def __exit__(self, *args):
fcntl.flock(self._fd, fcntl.LOCK_UN)
self._fd.close()四个扩展加完,lena-v0.24 的完整骨架已经成形。现在看整体代码结构:
code/lena-v0.24/
├── browser_agent.py # 主类:LenaBrowserAgent
├── browser_agent_minimal.py # 最小版(本章 Beat 4)
├── fallback_chain.py # 三层 fallback 骨架
├── approval_gate.py # 审批门控
├── cdp_lock.py # 进程锁
├── cdp_utils.py # 防御性 CDP 工具(含 6 条血泪教训)
├── config.py # 模型配置
└── tasks/
├── weibo_news.py # 任务一:查微博新消息
├── table_export.py # 任务二:表格导出 CSV
└── train_booking.py # 任务三:查高铁票Beat 6 · 运行验证:三个端到端任务
三个任务按复杂度递增排列。每个任务都先展示 browser-use agent 的内部步骤,再给出预期输出,最后标注常见失败路径。
6.1 任务一:查微博新消息
目标:进入微博,检查是否有新消息,返回消息摘要。
# code/lena-v0.24/tasks/weibo_news.py
import asyncio
from ..browser_agent import LenaBrowserAgent
TASK = """
在一个新标签页打开微博(https://weibo.com):
1. 查看页面右上角或通知区域是否有消息图标带数字
2. 如果有新消息,点击进入通知页面,提取前 5 条通知的标题
3. 以 JSON 返回:{"new_count": 数字, "summaries": ["消息1", "消息2"...]}
4. 如果没有新消息,返回 {"new_count": 0, "summaries": []}
重要:不要在现有标签页上操作,开新标签页。
"""
async def check_weibo():
agent = LenaBrowserAgent()
return await agent.run_task(TASK)
if __name__ == "__main__":
print(asyncio.run(check_weibo()))browser-use 内部决策步骤:
Step 1: navigate → "https://weibo.com"
Step 2: wait_for_load → networkidle
Step 3: screenshot → 感知当前页面状态
Step 4: analyze → 寻找通知图标
Step 5: (if found) click → 通知图标
Step 6: wait_for_load → 通知页面
Step 7: extract_content → 通知列表文本
Step 8: done → {"new_count": 3, "summaries": [...]}预期输出:
{
"new_count": 3,
"summaries": [
"用户A点赞了你的微博",
"用户B关注了你",
"用户C评论了你的微博"
]
}常见失败路径:
网络超时:微博 CDN 节点不稳定,networkidle可能等待 10+ 秒。处理:超时后截图确认页面状态,降级到 fallback。未登录跳转:Cookie 过期,进入登录页。处理:检测 URL 是否为login.weibo.com,若是则返回错误码AUTH_EXPIRED并通知用户。DOM 结构变化:微博频繁更新前端,通知图标的 selector 可能变化。处理:browser-use 通过语义理解找元素(找"通知"语义的可交互元素),而非依赖固定 selector,对结构变化有天然容忍度。
6.2 任务二:表格导出 CSV
目标:在指定页面找到数据表格,提取并导出为 CSV。
# code/lena-v0.24/tasks/table_export.py
import asyncio
import csv
import json
from ..browser_agent import LenaBrowserAgent
TASK_TEMPLATE = """
在新标签页打开 {url}:
1. 找到页面上的主要数据表格
2. 读取表头(列名)和所有数据行(最多 200 行)
3. 以 JSON 返回:{{"headers": ["列1", "列2"], "rows": [["值1", "值2"], ...]}}
重要:如果有分页,只取当前可见的第一页数据。
"""
async def export_table(url: str, output: str = "/tmp/export.csv"):
agent = LenaBrowserAgent()
raw = await agent.run_task(TASK_TEMPLATE.format(url=url))
data = json.loads(raw)
with open(output, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(data["headers"])
writer.writerows(data["rows"])
print(f"[导出完成] {len(data['rows'])} 行 → {output}")
return output
if __name__ == "__main__":
# 示例:导出一个公开的数据表格页面
asyncio.run(export_table(
url="https://datatables.net/examples/basic_init/",
output="/tmp/demo_table.csv"
))预期输出:
[导出完成] 57 行 → /tmp/demo_table.csv查看 CSV 文件应该包含正确的表头和数据行,如:
Name,Position,Office,Age,Start date,Salary
Tiger Nixon,System Architect,Edinburgh,61,2011/04/25,$320,800
...为什么选这个任务:它验证了 browser-use 的 DOM 分析能力——agent 需要区分导航栏、文章内容和数据表格,这三者的结构差异是语义层面的,不是 CSS 类名层面的。
6.3 任务三:查高铁票(只查,不购)
目标:在 12306 查询指定日期的高铁票信息,返回结构化数据。
这是最复杂的任务,需要 10+ 步骤、3 个表单字段交互、1 次搜索等待。
# code/lena-v0.24/tasks/train_query.py
"""
任务三:查高铁票
安全声明:此任务只执行查询,不执行购买。
购票功能(buy_ticket)通过单独的审批门控保护。
"""
import asyncio
from ..browser_agent import LenaBrowserAgent
from ..approval_gate import gate_before_action
QUERY_TASK = """
在新标签页打开 12306(https://kyfw.12306.cn/otn/leftTicket/init):
1. 出发站:输入"{origin}"
2. 到达站:输入"{destination}"
3. 出发日期:选择"{date}"
4. 点击"查询"按钮,等待结果
5. 从结果中找出所有 G 字头(高铁)车次
6. 返回 JSON:
[
{{"train": "G开头车次", "depart": "HH:MM", "arrive": "HH:MM",
"duration": "X小时Y分", "second_class": "¥XXX", "available": true/false}}
]
7. 重要:只查询,不要点击"预订"或任何购买按钮。
"""
async def query_trains(origin: str, destination: str, date: str) -> list[dict]:
agent = LenaBrowserAgent()
raw = await agent.run_task(
QUERY_TASK.format(origin=origin, destination=destination, date=date)
)
import json
return json.loads(raw)
if __name__ == "__main__":
import datetime
tomorrow = (datetime.date.today() + datetime.timedelta(days=1)).strftime("%Y-%m-%d")
trains = asyncio.run(query_trains("深圳北", "上海虹桥", tomorrow))
for t in trains[:3]:
print(f"{t['train']:6s} {t['depart']}→{t['arrive']} {t['duration']:8s} {t['second_class']}")
# 预期输出(行数约 3 行,具体数字随日期变化):
# G820 06:18→13:24 7小时06分 ¥894.5
# G100 08:00→15:12 7小时12分 ¥894.5
# G98 09:30→16:38 7小时08分 ¥894.5内部决策步骤(共约 12 步):
Step 1: navigate → "https://kyfw.12306.cn/otn/leftTicket/init"
Step 2: screenshot → 确认页面加载完成
Step 3: click → 出发站输入框(找语义为"出发地"的 input)
Step 4: type → "深圳北"
Step 5: click → 下拉建议中的"深圳北"
Step 6: click → 目的站输入框
Step 7: type → "上海虹桥"
Step 8: click → 下拉建议中的"上海虹桥"
Step 9: click → 日期输入框或日历组件
Step 10: click → 目标日期
Step 11: click → "查询"按钮
Step 12: wait → 等待结果加载(约 2-3 秒)
Step 13: extract_content → G 字头车次列表
Step 14: done → 返回 JSON 数组常见失败路径:
12306 有滑动验证码:当搜索频率过高时触发。处理:检测到验证码时调用ask_human,请用户手动解决,然后继续。日期选择器交互复杂:12306 的日期组件是自定义的,不是标准<input type="date">。browser-use 需要通过截图分析才能找到正确的交互方式。如果超过 5 步仍未成功选日期,退出并报告失败原因。结果解析失败:12306 的结果页结构比较复杂,LLM 有时会误判"无票"和"不展示"。处理:在 prompt 里明确说明"如果某个字段不确定,填 null,不要猜"。
Beat 7 · Design Note × 3
Design Note 1:Why Is Browser Agent the "Final Exam" of Agent Engineering?
乍看 Browser Agent 像是一个专用的爬虫增强版。但实际上它更像 agent 通用能力的终极压力测试,因为它是唯一一个同时要求六大支柱全部在线的场景。
替代方案:为每个网站写专用爬虫
很多团队的第一反应是:针对微博写专用 API 调用,针对 12306 写专用爬虫脚本。
这条路的问题:
- 🔴 每个网站各一份维护成本,10 个网站 = 10 份代码
- 🔴 网站更新 DOM 结构后,所有爬虫失效,需要重写
- 🔴 无法处理"不知道要操作哪个网站"的通用任务
当前选择(Browser Agent)的理由:
一个通用的 browser agent,能处理用户说出的任何网站任务,维护成本是固定的(不随网站数量增长),且能随 LLM 能力的提升而自动变好。
生产系统里的平衡:三层 fallback 架构是折中方案——对于高频任务(如每天查微博),可以写专用的 RSS/API 层作为 L1,browser agent 降为 L3 的保底。对于低频、不确定的任务,browser agent 就是首选路径。
Design Note 2:Why Not Just Use Pure CDP for Everything?
这两条路径经常被混淆。它们的决策维度不是"哪个更好",而是"哪个适合这个场景"。
替代方案:全用原生 CDP,不用 Playwright
CDP 是最底层的协议,最低延迟,最精确控制。有工程师主张"能用 CDP 的就不用 Playwright 这个中间层"。
CDP 的权衡:
- 🟢 延迟最低(省去 Playwright 的 CDP 封装层)
- 🟢 Chrome 专属功能齐全(Performance API、Network Interception 等)
- 🔴 API 动词是
methodName(如Page.captureScreenshot),而非page.screenshot(),认知成本高 - 🔴 无自动等待(Playwright 的 auto-waiting 机制需要自己实现)
- 🔴 无内置重试(连接丢失需要自己处理)
当前选择(Playwright for browser-use,CDP for 精确控制):
browser-use 在 Playwright 上构建,对于"让 LLM 感知并操作 DOM"这个目标,Playwright 的高层 API(page.click(selector)、page.fill(selector, text))比 CDP 的 Input.dispatchMouseEvent + Input.insertText 组合方便得多。
但直接 CDP 仍有其适用场景:
- 截图采集(精确控制 viewport、DPR)
- Tab 创建和管理(
PUT /json/new) - 不需要 LLM 参与的固定流程
决策树:
需要 LLM 动态决策每一步?
├── 是 → browser-use(Playwright + LLM)
└── 否 → 步骤是否固定?
├── 是 → Playwright(高层 API,跨浏览器)
└── 否 → Chrome CDP(精确控制,Chrome 专属)Design Note 3:The Six Pillars — What They Proved and What Comes Next
六大支柱都在这里了:
| 支柱 | Browser Agent 映射 | 关键实现 |
|---|---|---|
| Tool 统一性 | 浏览器操作 = 工具 | click/type/scroll/screenshot 都是 agent 工具 |
| Planning | 多步任务的自主分解 | ActionHistory + max_steps 保护 |
| Long-horizon | 跨 10+ 步的状态跟踪 | 截图序列 + DOM 感知 + 页面跳变处理 |
| Memory | 登录态继承 + 步骤历史 | Chrome profile Cookie + ActionHistory |
| Safety | Tab 保护 + 审批门控 | 铁律:永不覆盖现有 tab;高风险操作人工确认 |
| Specialization | 从通用 Lena 派生 Browser Agent | LenaBrowserAgent 继承 LenaAgent 基类 |
这不是偶然的。Browser Agent 之所以是全书大结局,是因为它是唯一一个能同时考验所有六个支柱的场景。
6 条 CDP 血泪教训:生产工程实现
这六条教训来自真实生产系统(一个基于 CDP 的多媒体采集 pipeline),每一条都有具体的错误现象、根本原因和修复代码。
教训 1:不能发 Origin 头
现象:CDP WebSocket 连接被 403 拒绝。
根因:Chrome CDP 的 WebSocket 安全策略只允许 localhost Origin 或无 Origin。发送任何其他 Origin 头都会被拒绝,即使是 http://127.0.0.1。
# BAD(来自很多教程的错误写法)
ws = await websockets.connect(ws_url, extra_headers={"Origin": "http://example.com"})
# Chrome: 403 WebSocket Upgrade failure
# GOOD(不发任何 Origin 头)
ws = await websockets.connect(ws_url) # websockets 库默认不发 Origin教训 2:必须清除代理环境变量
现象:CDP socket 连接超时或被路由到代理服务器,返回奇怪的 HTTP 响应。
根因:Clash 的 fake-ip 模式拦截所有 DNS 查询,包括 localhost。这意味着 socket.create_connection("localhost", 9222) 可能被路由到代理(即使 localhost 本应直连)。
# 在任何 CDP 操作前,清除代理环境变量
import os
for _var in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY",
"all_proxy", "ALL_PROXY", "no_proxy", "NO_PROXY"]:
os.environ.pop(_var, None)这行代码必须在 import 完毕后、第一次 CDP 连接前执行一次。
教训 3:进程锁防并发冲突
现象:cron 定时任务重叠执行时,两个 browser agent 同时操作同一 Chrome,导致截图错误、Tab 状态混乱。
根因:cron 任务如果执行时间超过调度间隔,就会出现重叠执行。例如 5 分钟间隔的任务如果运行了 6 分钟,第二个任务会在第一个还在跑时启动。
# code/lena-v0.24/cdp_lock.py
import fcntl
class BrowserLock:
def __enter__(self):
self._fd = open("/tmp/.lena_browser.lock", "w")
try:
fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
self._fd.close()
raise RuntimeError("另一个 browser agent 正在运行,本次跳过")
return self
def __exit__(self, *args):
fcntl.flock(self._fd, fcntl.LOCK_UN)
self._fd.close()
# 使用方式
with BrowserLock():
await run_browser_task()教训 4:Tab 必须主动清理
现象:Chrome 在运行 200 次采集后内存耗尽,CDP 连接开始超时。打开 Chrome 一看,有 200+ 个空白 Tab。
根因:CDP 创建的 Tab(通过 PUT /json/new)不会在脚本退出时自动关闭。如果脚本崩溃或超时,Tab 会永久留在 Chrome 里。
# code/lena-v0.24/browser_agent.py(节选)
async def run_task(self, task: str) -> str:
# 记录任务开始前的 tab 集合(受保护)
protected = await get_existing_tab_ids()
try:
result = await self._execute(task)
return result
finally:
# 无论成功还是失败,都清理本次创建的 tab
await close_new_tabs(protected)关键点:清理逻辑放在 finally 块,确保即使任务中途异常也会执行。
教训 5:截图小于 80KB 是空白页
现象:LLM 报告"页面内容为空",但实际上页面是正常的。
根因:某些情况下(页面加载失败、白屏错误、Chrome 内部错误页)会返回一张全白截图,其大小通常在 5-30KB。如果把这张截图直接交给 LLM,LLM 会判断"页面是空白的",导致错误决策。
async def take_screenshot(ws_url: str) -> Optional[bytes]:
data = await _raw_screenshot_cdp(ws_url)
if data is None:
return None
# 80KB 阈值:来自真实数据统计,空白页通常 < 20KB,
# 有内容的最小页面通常 > 80KB
MIN_VALID_BYTES = 80 * 1024
if len(data) < MIN_VALID_BYTES:
print(f"[截图验证] {len(data):,} bytes < {MIN_VALID_BYTES:,},判定为空白页,跳过")
return None
return data教训 6:CDP HTTP 接口的 /json/new 和 /json/close 用 PUT
现象:调用 /json/new 创建 Tab 失败(405 Method Not Allowed),或者返回错误的数据。
根因:Chrome CDP 的 HTTP REST 接口规范要求:
GET /json→ 列出所有 Tab(GET)PUT /json/new→ 创建新 Tab(PUT,不是 GET!)PUT /json/close/{tabId}→ 关闭 Tab(PUT,不是 GET!)
很多老教程和 Stack Overflow 回答写的是 GET /json/new,在旧版 Chrome 上可能偶然工作,在新版 Chrome 上 404。
# BAD:很多教程的写法
async with session.get(f"http://localhost:9222/json/new") as r:
tab = await r.json() # 可能 404 或返回错误数据
# GOOD:正确的 HTTP 方法
async with session.put(f"http://localhost:9222/json/new") as r:
tab = await r.json() # Chrome 创建新 Tab 并返回 Tab 信息
async with session.put(f"http://localhost:9222/json/close/{tab_id}") as r:
pass # 关闭成功时返回 200完整的 lena-v0.24 核心代码
将四个扩展整合为完整的 LenaBrowserAgent:
# code/lena-v0.24/browser_agent.py
"""
lena-v0.24 Browser Agent — 生产级实现
集成:Tab 保护 + 三层 fallback + 审批门控 + 进程锁 + 6 条血泪防护
"""
import os
import asyncio
from typing import Optional
import aiohttp
from browser_use import Agent, Browser, BrowserConfig
from langchain_anthropic import ChatAnthropic
from .cdp_lock import BrowserLock
from .approval_gate import gate_before_action
# 教训 2:启动时清除代理环境变量
for _var in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy"]:
os.environ.pop(_var, None)
CDP_BASE = "http://localhost:9222"
async def _get_tab_ids() -> set[str]:
async with aiohttp.ClientSession() as s:
async with s.get(f"{CDP_BASE}/json") as r:
return {t["id"] for t in await r.json()}
async def _close_new_tabs(protected: set[str]):
async with aiohttp.ClientSession() as s:
async with s.get(f"{CDP_BASE}/json") as r:
current = await r.json()
for tab in current:
if tab["id"] not in protected:
await s.put(f"{CDP_BASE}/json/close/{tab['id']}") # 教训 6:PUT
class LenaBrowserAgent:
"""
lena-v0.24 Browser Agent
从 lena 通用 base 派生的 Browser 专用 agent
"""
def __init__(self, model: str = "claude-sonnet-4-6"):
self.llm = ChatAnthropic(model=model)
async def run_task(self, task: str, require_approval: bool = False) -> str:
"""
执行一个浏览器任务
require_approval=True 时,高风险操作需要用户确认
"""
# 审批门控(可选)
if require_approval:
approved = await gate_before_action(task)
if not approved:
return "[CANCELLED] 用户取消了高风险任务"
# 进程锁(防并发)
with BrowserLock():
# Tab 保护:记录任务前的 tab 集合
protected_tabs = await _get_tab_ids()
try:
return await self._execute(task)
finally:
# 教训 4:无论成功失败都清理 tab
await _close_new_tabs(protected_tabs)
async def _execute(self, task: str) -> str:
browser = Browser(config=BrowserConfig(
cdp_url="ws://localhost:9222",
headless=False,
))
agent = Agent(
task=task,
llm=self.llm,
browser=browser,
max_actions_per_step=5,
)
result = await agent.run(max_steps=25)
return str(result)三层 Fallback 完整实现
# code/lena-v0.24/tasks/weibo_news.py(完整版含 fallback)
import asyncio
import aiohttp
from ..browser_agent import LenaBrowserAgent
from ..fallback_chain import FallbackChain
async def get_weibo_news(weibo_uid: str) -> Optional[dict]:
"""
查询微博新消息,三层 fallback 保证 99% 成功率:
层 1 — RSSHub(成功率 ~90%)
优点:无需登录,响应 <2s,结构化数据
缺点:只有"推送内容",消息通知无法获取
层 2 — opencli(成功率 ~70%)
优点:本地工具,无网络依赖
缺点:依赖本地工具是否已配置
层 3 — Browser Agent(成功率 ~40-60%)
优点:能访问完整内容,包括登录后内容
缺点:速度最慢(10-30s),受反爬影响
"""
chain = FallbackChain()
@chain.layer("rsshub")
async def via_rsshub():
"""层 1:RSSHub 公共实例"""
async with aiohttp.ClientSession() as s:
url = f"https://rsshub.app/weibo/user/{weibo_uid}"
async with s.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
if r.status == 200:
data = await r.json()
return {
"source": "rsshub",
"items": [
{"title": item.get("title", ""), "link": item.get("link", "")}
for item in data.get("items", [])[:5]
]
}
return None
@chain.layer("browser_agent")
async def via_browser():
"""层 2:真实浏览器"""
agent = LenaBrowserAgent()
task = f"""
在新标签页打开微博(https://weibo.com),检查用户 {weibo_uid} 的最新消息:
1. 如果未登录,返回 {{"error": "AUTH_EXPIRED"}}
2. 如果已登录,检查通知数量,提取最新 5 条通知摘要
3. 返回 JSON:{{"new_count": 数字, "items": [{{"title": "..."}}]}}
"""
raw = await agent.run_task(task)
import json
return {"source": "browser", **json.loads(raw)}
return await chain.run()全书回顾:从 v0.1 到 v0.24
来到全书最后一章。
让我们做一件平时没时间做的事:往回看。
Part 0:第一步(Ch 1-3)
Ch 1 你写了第一次 API 调用。10 行 Python,一个 messages.create(),"Hello, Agent"。当时你可能以为这就是"AI 应用开发"的全部,其实这只是最外层的接口。那 10 行代码的背后,是 Transformer 的 forward pass、KV Cache 的管理、token 的逐个采样——但 Ch 1 不需要你知道这些,你只需要知道"可以调用"。
Ch 2 你理解了 ReAct 循环:Reasoning → Acting → Observing,无限重复直到任务完成。这个循环在 2022 年被写成论文,现在它是几乎所有 agent 框架的基础原子。你读懂了它,意味着你能读懂任何一个 agent 框架的核心——不管它叫 LangChain 还是 AutoGen 还是 CrewAI。
Ch 3 Lena 诞生了。50 行 Python,6 个模块:Config、Provider、Memory、ToolRegistry、AgentLoop、Skills。她还很原始,但她有对的结构。
Part 1:六大支柱(Ch 4-12)
Ch 4-5 你理解了 LLM 的工程直觉(不是数学),和技术选型的决策树。这两章没有代码产物,但它们是你在后面每次做工程决策时的底气——为什么选 Sonnet 不选 Haiku、为什么 RAG 先于 Fine-tune、为什么 agent 内嵌 RAG 而不是替代 RAG。
Ch 6-7 Tool 系统和流式并发。Lena 从"只会说话"变成了"能干活"。你理解了为什么工具需要 isReadOnly、isDestructive 标志——不是为了程序员自己看,而是为了让 LLM 知道"这个工具安不安全,能不能并发"。
Ch 8-9 记忆和 RAG。Lena 从"每次对话重新认识你"变成了"记得你上次说什么",从"只知道训练数据"变成了"能读懂你的 200 页 PDF"。这两章把 Lena 从对话助手变成了知识型 agent。
Ch 10 Context Engineering。这是 2025 年 agent 工程师最关注的议题之一——不是如何让模型更聪明,而是如何让模型在有限的 context window 里表现得最好。Manus 的六条铁律、KV Cache 命中率、Compaction 三层压缩——这些你都实现了。
Ch 11 Planning 和 Subagent。Lena 从"单线程执行"变成了"能拆任务、能派遣子 agent"。这是 Long-horizon 能力的基础。
Ch 12 Skills。这一章很短,但很重要。Skills 是 agent 能力单元的可复用形式,是 Anthropic 的 Simon Willison 说的"可能比 MCP 影响更大"的机制。Lena 现在能加载你写的任何 .md 文件作为新技能。
Part 2:安全与常驻(Ch 13-18)
Ch 13-14 安全双章。你知道了为什么"能自主做任何事的 agent"是危险的,以及如何用结构化的方式(而不是 deny list)控制风险。Prompt Injection、执行层安全、凭证保护、审计链——这些是任何生产 agent 都必须有的防线。
Ch 15-16 Gateway 和 MessageBus。Lena 走出了命令行,进入了 Telegram、Discord、飞书。她不再是一个你主动调用的工具,而是一个你可以发消息的助手。
Ch 17 Heartbeat。这是"常驻 agent"与"问答助手"最本质的区别:agent 有了主动性,她会在没有人呼叫时做出判断,在适当的时候主动联系你。
Ch 18 Cron 和 Long-running Task。Lena 能处理跨天任务了,能在崩溃后从断点续传,能每小时抓新闻在凌晨总结。她有了"时间感"。
Part 3:扩展与专用化(Ch 19-22)
Ch 19 MCP 协议。200 行代码,让 Lena 能连接任何 MCP server——文件系统、GitHub、Brave Search、AWS 服务。MCP 正在成为 agent 工具连接的标准,早了解早受益。
Ch 20 Docker Sandbox。生产级代码执行环境。seccomp、AppArmor、exec-approvals——你不需要理解所有安全细节,但你需要知道"裸跑 shell 工具是不够的"。
Ch 21 Evals。"跑完没报错 ≠ 质量合格"。LLM-as-judge、golden dataset、pass@k——你现在有了量化评估 agent 质量的工具。
Ch 22 可观测性与部署。Lena 上线了。结构化日志、token 预算、launchd/systemd 守护进程——她能在你睡觉时稳定运行。
Ch 23 专用化。一行 fork,一个专用 agent。量化交易 agent、播客制作 agent、家居自动化 agent——通用 runtime 的价值,在于它能被快速派生成任何专用 agent。
Ch 24 你在这里。Browser Agent。lena-v0.24 能浏览互联网了。
你真正构建了什么
表面上,你构建了一个 Python 写的 agent runtime,集成了 LLM API、Tool 系统、记忆、规划、安全、部署、评估、MCP、浏览器。
但更深层,你理解了一件事:
agent 是"感知 + 记忆 + 推理 + 行动 + 自我监控"的组合。
这个结构在 50 行代码里成立,在 3000 行代码里也成立。在 Python 里成立,在 TypeScript、Rust 里也成立。在本地 Mac 上成立,在 AWS Fleet 上也成立。
六大支柱是这个结构的六个维度:
- Tool 统一性:action 层——能做什么
- Planning:推理层——如何分解目标
- Long-horizon:记忆层——如何跨步骤保持状态
- Memory:知识层——知道什么、记得什么
- Safety:监控层——什么不能做
- Specialization:身份层——我是谁、我擅长什么
通用 agent 不是目标,理解这个结构才是目标。因为一旦你理解了结构,就能构建任何专用 agent,而不需要每次从头想清楚"我在做什么"。
Infographic:Lena 演进全图(印刷级 A3)
你的 Lena 还能变成什么
这最后几页,属于你。
Lena 从 50 行代码走到了今天。她有了工具、记忆、规划、安全、常驻运行、MCP 扩展,现在她还能浏览互联网。
但 Lena 最重要的特质从来不是"她能做什么",而是可塑性。
通用 runtime 的设计目标就是这个:不是构建一个特定的 agent,而是构建一个能被快速派生成任何 agent 的底座。
下面是三个你可以立刻开始的方向:
方向 A:自动化晨报 agent
基础:lena-v0.24 能浏览网页,lena-v1.5 能做 cron 定时任务,lena-v1.3 能 Telegram 推送。
组合:
- 每天早 7 点,Lena 自动浏览 5 个你关注的信源(虎嗅、GitHub Trending、HackerNews、X/Twitter 特定账号)
- LLM 提取今日重要内容,生成 500 字摘要
- TTS 合成音频,8 点整推送到手机
难点:内容去重(同一新闻多平台覆盖)、摘要质量评估(Evals)、TTS 断字处理
估计工作量:2-3 天
方向 B:闲鱼挖宝 agent
基础:lena-v0.24 的浏览器操作能力,lena-v1.7 的 Heartbeat 主动推送。
组合:
- 每 30 分钟,Lena 扫描闲鱼特定关键词("iPad Pro 二手"、"AirPods 九成新"、"Switch OLED 九五新")
- LLM 判断性价比:比对价格 vs 市场行情(可以查 JD/TB 当前价格作为基准)、分析描述质量、评估卖家信用
- 发现性价比 > 阈值的商品,立即推送
难点:闲鱼的反爬较强(L3/L4 级),行为模拟复杂;价格数据库需要定期更新;闲鱼频繁改版
估计工作量:1 周
方向 C:自己的 AI 编程结对
基础:lena-v0.24 能操作浏览器,lena-v0.23 的专用化能力,加上 Claude Code 的代码理解能力。
组合:
- Lena 作为本地 coding assistant,能读你的代码仓库
- 当你打开某个 issue 或 PR 时,Lena 自动分析相关代码,生成上下文摘要
- 当你在 IDE 里写代码时,Lena 在后台监控 CI 结果,失败时主动建议修复方案
- 可以让 Lena 自动查文档(打开 MDN、PyPI、docs.anthropic.com 等)
难点:IDE 集成(VSCode Extension 或 JetBrains Plugin)、代码变更的增量处理、与现有 Claude Code 工作流的协同
估计工作量:2 周
这三个方向只是起点。Lena 还能变成:
- 你的个人财务分析 agent(定期查账单、分析消费模式、提示异常)
- 你家里的 HomeAssistant 自然语言接口("帮我把客厅灯调暗,但书房保持亮")
- 你工作上的竞品监控 agent(定期查竞品网站、PR 动态、社媒讨论)
- 你孩子的学习辅导 agent(有登录态的教育平台 + 个性化解析)
或者完全不在列表里的东西。最好的 agent 往往来自"只有我才知道这个痛点"的需求。
agent 的边界,就是你想象力的边界。
Podcast · 大结局感言
[开场音乐淡入,一个稳定的合成器和弦]
主持人 A(低沉):你还记得我们第一集说了什么吗?
主持人 B:你说,我们要写一本书,教人从零构建一个能自主做任何事的 agent。
主持人 A:对。然后我说,这个"任何事"到底什么时候算够。
主持人 B:现在你觉得够了吗?
主持人 A(停顿):……Lena 现在能浏览微博,能查高铁票,能导出表格。她用的是你真实 Chrome 里的登录态,在你睡觉时自动做事,在遇到问题时知道退后一步找别的路。
主持人 B:这不是"够"的问题。
主持人 A:对,你说得对。这不是够不够的问题。
主持人 B:agent 没有终点。有的只是你现在能解决哪些问题。lena-v0.24 解决了"让 Lena 上互联网"这个问题。下一个问题是什么,取决于你。
主持人 A:我想说一句对读者说的话。
主持人 B:说吧。
主持人 A:二十四章,如果你认真读完了,如果你把代码跑过一遍,如果你真的理解了为什么"工具 + 记忆 + 循环 + 安全"是 agent 的骨架——那你现在已经是少数人了。大多数工程师知道"怎么调 ChatGPT API",但不知道为什么 browser agent 需要进程锁,为什么 PUT /json/new 不能用 GET,为什么三层 fallback 比单一路径可靠。
主持人 B:这些不在 Stack Overflow 上。
主持人 A:这些在凌晨三点的错误日志里。
[音乐微微上扬]
主持人 B:有一件事我们一直没明说。Lena 的名字是随机取的,但她代表的不是某个特定的助手。
主持人 A:她代表的是你理解"agent 是什么"之后,能构建出来的任何东西。
主持人 B:去构建吧。
[音乐渐强,20 秒后淡出]
延伸阅读
browser-use文档与源码:https://docs.browser-use.com · github.com/browser-use/browser-use- Playwright Python 官方文档:https://playwright.dev/python/
- Chrome DevTools Protocol 完整参考:https://chromedevtools.github.io/devtools-protocol/
- WebArena 评测集(浏览器 agent benchmark):webarena.dev
- Anthropic Building Effective Agents(2024-12-19):anthropic.com/news/building-effective-agents
- browser-use 专用模型 ChatBrowserUse:gpt.us.browser-use.com
本章小结
| 知识点 | 核心结论 |
|---|---|
| Browser Agent 四大挑战 | DOM 无穷大 → 选择性提取;页面跳变 → 状态重置;反爬 → 三层 fallback;登录态 → Chrome profile |
| 三条路径决策 | CDP(精确采集)/ Playwright(固定流程)/ browser-use(LLM 动态决策) |
| browser-use 核心循环 | 感知(截图+DOM提取)→ LLM 决策 → Playwright 执行 → 结果验证 |
| ChatBrowserUse | 3-5x 速度,适合固定步骤任务,不适合需要复杂推理的场景 |
| 接入 Chrome profile | cdp-start.sh 启动 → 无 Origin 头 → PUT /json/new 创建 tab |
| 6 条血泪教训 | Origin/代理/进程锁/Tab清理/截图空白/PUT方法 |
| 三层 fallback | RSSHub 90% + opencli 70% + CDP 40% → 整体 ~99% |
| lena-v0.24 意义 | 全书六大支柱终极综合测试,通用 agent 完整实现的收官之作 |
Lena 在本章学会了"看见并操作互联网"——CDP 连接本地 Chrome、DOM 快照 + 视觉截图双通道感知、三层 fallback 保障可用性,六大支柱在 Browser Agent 里完成了最后一次综合验证。
你已经完整实现了一个通用 agent runtime,并从它派生出一个专用的 Browser Agent。但从零到这里的路是线性的——你看到了 Lena 在 24 章里的演进轨迹,还没有看到这套架构如何在不同方向展开。从通用 agent 到量化交易 bot、新闻播报系统、DevOps 值班机器人,每一条路需要的判断是什么?第 25 章,我们绘制通用 agent 的演化地图——帮你找到从本书出发之后,属于你自己的那条路。
"从第一次
messages.create()到现在,你已经走过了六大支柱、24 章、3000 行代码。 Lena 能做什么,永远取决于你给她什么目标。 去吧。"
本章媒体资源
💡 建议:用播客版配合文字版一起学习,通勤/散步时收听效果极佳
