Skip to content

第 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 系统尝试读取微博首页内容:

python
# 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 被识别为非浏览器)

三种失败,对应三个根本原因:

  1. 内容在登录墙后面requests.get("https://weibo.com") 得到的是登录跳转,不是内容。
  2. 内容在 JavaScript 里:现代 SPA 渲染的 DOM 在浏览器里执行 JS 后才存在,HTTP GET 只能拿到空 HTML 外壳。
  3. 反爬机制识别你: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 的 hashpushState 变化,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 的设计选择是不对抗反爬。原因:

  1. L3/L4 级别的反爬需要极复杂的模拟,且越来越强——军备竞赛没有终点
  2. 使用用户真实 Chrome profile 天然绕过 L1/L2,是合法且稳定的方案
  3. 三层 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 骨架,验证概念端到端可行:

python
# 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 应该看到:

  1. Chrome 打开一个新标签页
  2. 自动导航到 github.com/browser-use/browser-use
  3. 终端输出 star 数量

如果报错 ConnectionRefusedError: [Errno 61] Connection refused,检查 CDP 是否已启动:

bash
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 同时操作同一 Chromefcntl.flock 文件锁

扩展 1:Tab 保护

不覆盖已有 tab 是铁律,原因是 Chrome profile 里可能有用户正在编辑的文档、未保存的表单、活跃的视频会议。

python
# 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%。

python
# 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):

python
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:审批门控

浏览器有真实副作用——点击"提交订单"就真的下单了。任何写操作(提交表单、购买、删除)必须有人工确认。

python
# 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"

验证:

python
await gate_before_action("点击搜索按钮")   # → True(立即通过)
await gate_before_action("点击购买按钮")   # → 打印警告,等待用户输入

扩展 4:进程锁

cron 任务可能重叠执行,两个 CDP 采集进程同时操作同一个 Chrome 会导致竞态。

python
# 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 任务一:查微博新消息

目标:进入微博,检查是否有新消息,返回消息摘要。

python
# 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": [...]}

预期输出

json
{
  "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。

python
# 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 文件应该包含正确的表头和数据行,如:

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 次搜索等待。

python
# 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
SafetyTab 保护 + 审批门控铁律:永不覆盖现有 tab;高风险操作人工确认
Specialization从通用 Lena 派生 Browser AgentLenaBrowserAgent 继承 LenaAgent 基类

这不是偶然的。Browser Agent 之所以是全书大结局,是因为它是唯一一个能同时考验所有六个支柱的场景。


6 条 CDP 血泪教训:生产工程实现

这六条教训来自真实生产系统(一个基于 CDP 的多媒体采集 pipeline),每一条都有具体的错误现象、根本原因和修复代码。

教训 1:不能发 Origin 头

现象:CDP WebSocket 连接被 403 拒绝。

根因:Chrome CDP 的 WebSocket 安全策略只允许 localhost Origin 或无 Origin。发送任何其他 Origin 头都会被拒绝,即使是 http://127.0.0.1

python
# 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 本应直连)。

python
# 在任何 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 分钟,第二个任务会在第一个还在跑时启动。

python
# 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 里。

python
# 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 会判断"页面是空白的",导致错误决策。

python
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。

python
# 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

python
# 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 完整实现

python
# 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 从"只会说话"变成了"能干活"。你理解了为什么工具需要 isReadOnlyisDestructive 标志——不是为了程序员自己看,而是为了让 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 还能变成什么

这最后几页,属于你。

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 Agent 四大挑战DOM 无穷大 → 选择性提取;页面跳变 → 状态重置;反爬 → 三层 fallback;登录态 → Chrome profile
三条路径决策CDP(精确采集)/ Playwright(固定流程)/ browser-use(LLM 动态决策)
browser-use 核心循环感知(截图+DOM提取)→ LLM 决策 → Playwright 执行 → 结果验证
ChatBrowserUse3-5x 速度,适合固定步骤任务,不适合需要复杂推理的场景
接入 Chrome profilecdp-start.sh 启动 → 无 Origin 头 → PUT /json/new 创建 tab
6 条血泪教训Origin/代理/进程锁/Tab清理/截图空白/PUT方法
三层 fallbackRSSHub 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 能做什么,永远取决于你给她什么目标。 去吧。"


本章媒体资源

🎙️ 播客版 · 边读边听通勤可听

💡 建议:用播客版配合文字版一起学习,通勤/散步时收听效果极佳

构建通用 Agent Runtime,从这里开始