跳转到正文

22.1 浏览器 RL 动作空间与 harness 工程

24.1 介绍了 Deep Research 的任务定义和主流模型。但当你真正动手训练一个 Deep Research Agent 时,会立刻撞上两个工程问题:(1) 动作空间怎么设计——浏览器有成百上千种操作,哪些该暴露给 agent?(2) harness 怎么搭——agent 生成的动作要落到真实浏览器上,需要一套完整的执行、监控、奖励计算环境。本节解决这两件事,给出可复现的工程模板。

浏览器作为 RL 环境

Deep Research 的"环境"是浏览器(或搜索引擎 API)。从 RL 视角看,这是一个部分可观 + 长程 + 稀疏奖励的 MDP:

  • :浏览器状态空间,包括当前 URL、DOM 树、可见文本、滚动位置、Cookie/Session 等
  • :动作空间(见下文)
  • :环境转移函数(由真实浏览器决定,对 agent 未知)
  • :稀疏二值奖励,通常 ,中间步
  • :折扣因子,Deep Research 任务 步,(无折扣)
  • :最大步数(budget),通常 30-50

第 23 章 Computer Use 的 GUI MDP 相比,Deep Research 的关键差异:

维度Deep ResearchComputer Use
观察空间DOM 文本 / 截图截图为主
动作粒度抽象(search / click_link / extract)原子(pixel click / key)
状态转移可预测性较高(搜索结果相对稳定)低(GUI 动画、弹窗)
奖励稀疏度极稀疏(最后一步)极稀疏(最后一步)
典型步数20-5050-500

动作空间设计 与 三种主流方案

搜索 API 抽象

最简单的方案——不暴露真实浏览器,只给 agent 一个搜索 API

python
ACTIONS = {
    "search":   {"query": str},          # 调用搜索引擎,返回 top-K 结果
    "visit":    {"url": str},            # 抓取指定 URL 的纯文本
    "answer":   {"text": str},           # 提交最终答案
}

这是 Search-R1、R1-Searcher 用的方案。优点:

  • 动作空间只有 3 个原子操作,易学
  • 每步观察是干净的 Markdown,不需要视觉模型
  • 工程简单,一个 requests.get() 搞定

缺点:

  • 无法处理需要 JavaScript 的页面(SPA、动态加载)
  • 无法点击/滚动/翻页(只能取第一屏)
  • 不接近真实"上网研究"体验

适合:开放域 QA、学术论文检索等"文本为主"的任务。

Playwright 真实浏览器

用 Playwright / Puppeteer 暴露完整浏览器能力:

python
ACTIONS = {
    "goto":         {"url": str},
    "click":        {"selector": str},        # CSS selector 或文本匹配
    "fill":         {"selector": str, "value": str},
    "scroll":       {"dx": int, "dy": int},
    "back":         {},
    "extract_text": {"selector": str},        # 提取指定元素文本
    "screenshot":   {},
    "answer":       {"text": str},
}

这是 DeepResearcher、Tongyi DeepResearch 用的方案。优点:

  • 真实浏览器能力,能处理任意网页
  • 可截图作为视觉观察(用于 VLM agent)
  • 接近人类研究行为

缺点:

  • 动作空间大(7-10 个),需要更多训练数据
  • 真实浏览器慢(每步 1-3 秒),训练成本高
  • CSS selector 失败率高(页面变化导致 selector 失效)

适合:金融调研、产品对比、需要交互式翻页的任务。

Set-of-Mark 混合

借鉴 第 23 章 GUI Grounding 的 SoM 思路:每步把页面所有可交互元素编号,agent 只需输出编号:

Agent observes:
[页面截图 + 编号]
  [1] 搜索框
  [2] "下一页" 按钮
  [3] 第一个搜索结果的链接
  [4] 第二个搜索结果的链接
  ...

Agent action: click(3)  # 点击第一个搜索结果

这是 BrowseComp 评测里多数 SOTA 系统用的方案。优点:

  • 动作空间退化为"选编号",极简
  • 不依赖 CSS selector 的脆弱性
  • 兼容 VLM(看截图)和 LLM(看编号列表)

缺点:

  • 需要 OCR / DOM 解析做编号(额外组件)
  • 编号错误的代价高(点错链接)

Harness 工程的五个核心模块

无论选哪种动作空间,Deep Research 的训练 harness 都需要以下五个模块:

环境封装(Environment Wrapper)

python
class BrowserEnv:
    def __init__(self, mode='api' | 'playwright' | 'som'):
        self.mode = mode
        self.browser = None  # Playwright instance
        self.history = []    # 轨迹历史

    def reset(self, query: str) -> Observation:
        """开始新 trajectory,返回初始观察"""
        self.history = [{'role': 'user', 'content': query}]
        return self._get_obs()

    def step(self, action: Action) -> Tuple[Observation, float, bool, dict]:
        """执行动作,返回 (next_obs, reward, done, info)"""
        # 1. 解析 action
        # 2. 调用浏览器 / API
        # 3. 抓取新观察
        # 4. 判断是否 done(agent 主动 answer 或超出 budget)
        # 5. 计算 reward(done 时才有,否则 0)
        ...

关键工程点

  • 超时处理:真实网页可能 hang,必须有 timeout(通常 10 秒)
  • 错误恢复:CSS selector 失败、网络断开、JS 报错——都要捕获并返回友好的 error obs
  • 状态持久化:Cookie / Session 跨步骤保留(否则登录态丢失)

动作解析与验证(Action Parser)

LLM 输出的是文本,需要解析成结构化 action:

python
def parse_action(output: str, mode: str) -> Action:
    """从 LLM 输出解析 action,失败时返回 NoOp"""
    try:
        if mode == 'api':
            # 期望格式: <action>search</action><query>...</query>
            return ApiAction.from_xml(output)
        elif mode == 'playwright':
            # 期望格式: ```python\nAction(...)\n```
            return PlaywrightAction.from_code(output)
        elif mode == 'som':
            # 期望格式: click(3)
            return SomAction.from_text(output)
    except ParseError as e:
        # 解析失败:返回错误观察,让 agent 重试
        return ErrorAction(f"Parse failed: {e}")

关键工程点

  • 格式容错:LLM 输出经常有格式错误,parser 要 robust
  • 重试机制:解析失败时返回 error obs,让 agent 自纠错(这是 emergent behavior 的重要来源)
  • 动作白名单:禁止危险动作(如 format_diskrm -rf),即使 agent 想做

奖励计算器(Reward Verifier)

Deep Research 的奖励是任务完成度,需要分任务类型设计:

python
class RewardVerifier:
    def __call__(self, query: str, answer: str, task_type: str) -> float:
        if task_type == 'qa':
            # 答案匹配(EM / F1 / LLM-as-Judge)
            return self.qa_score(query, answer)
        elif task_type == 'citation':
            # 引用准确性(CaRR 指标)
            return self.citation_score(query, answer)
        elif task_type == 'multi_doc':
            # 多文档综合(需要 LLM 评判)
            return self.multi_doc_score(query, answer)
        elif task_type == 'browse_comp':
            # BrowseComp 基准:精确字符串匹配
            return self.browse_comp_score(query, answer)

关键工程点

  • 奖励稀疏化缓解:可加过程奖励(PRM)作为辅助,但主奖励仍是端到端
  • LLM-as-Judge 偏置:用 GPT-4 / Claude 做 judge 时有已知偏置(长答案偏好、自身风格偏好),需要校准
  • 反作弊:检测 agent 是否"复述问题"或"拼接搜索摘要"等作弊策略

进度跟踪(Progress Tracker)

长程任务(30+ 步)必须可视化进度,否则训练时无法 debug:

python
# claude-progress.txt 风格的进度文件
[2026-06-25 10:23:15] Step 1: search("2024 US GDP")
[2026-06-25 10:23:18] → Got 10 results, top: bea.gov
[2026-06-25 10:23:22] Step 2: visit("https://bea.gov/...")
[2026-06-25 10:23:25] → Page loaded, 15KB text
[2026-06-25 10:23:29] Step 3: extract("main table")
[2026-06-25 10:23:32] → Extracted table: 4 rows × 3 cols
[2026-06-25 10:23:36] Step 4: answer("2024 US GDP was $28.5T")
[2026-06-25 10:23:38] → Reward: 1.0 (correct)

这个文件有两个用途:

  1. 训练 debug:失败 trajectory 一眼看出哪步错
  2. 数据合成:成功 trajectory 可作为 SFT 数据

并行 Rollout(Parallel Rollout Engine)

Deep Research 单条 trajectory 30-50 步 × 每步 1-3 秒 = 60-150 秒。训练 batch size 1024 时,串行需 25 小时/step。必须并行:

python
async def parallel_rollout(
    agent, prompts: list[str], num_parallel: int = 256
) -> list[Trajectory]:
    semaphore = asyncio.Semaphore(num_parallel)

    async def rollout_one(prompt):
        async with semaphore:
            env = BrowserEnv(mode='playwright')
            obs = await env.reset(prompt)
            trajectory = []
            for t in range(MAX_STEPS):
                action = await agent.act(obs)
                next_obs, r, done, info = await env.step(action)
                trajectory.append((obs, action, r))
                if done:
                    break
                obs = next_obs
            return trajectory

    return await asyncio.gather(*[rollout_one(p) for p in prompts])

关键工程点

  • 浏览器池:复用浏览器实例(启动开销大)
  • 网络代理:避免被目标网站封 IP(用住宅代理)
  • 失败隔离:单条 trajectory 崩溃不影响其他

完整训练流水线

把这五个模块拼起来:

┌─────────────────────────────────────────────────┐
│ 1. Prompt Batch (1024 questions)                │
└────────────────────┬────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 2. Parallel Rollout (256 concurrent browsers)   │
│    ├─ Environment Wrapper (Playwright)          │
│    ├─ Action Parser (XML / code / SoM)          │
│    ├─ Progress Tracker (claude-progress.txt)    │
│    └─ Reward Verifier (QA / Citation / Browse)  │
└────────────────────┬────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 3. Trajectory Buffer                            │
│    {(s_t, a_t, r_t)}_{t=1..T} per trajectory    │
└────────────────────┬────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 4. GRPO Update                                  │
│    ├─ Group normalization (G=8 per prompt)      │
│    ├─ Advantage estimation                      │
│    └─ PPO-Clip policy update                    │
└─────────────────────────────────────────────────┘

实测在 8×H100 GPU + 64-core CPU server 上,单次 GRPO step 处理 1024 prompts 约需 8-12 分钟。训练一个 7B Deep Research 模型到收敛通常需要 5000-10000 step,即 4-7 天。

第 16 章 GRPO 的衔接

Deep Research 的 RL 训练流水线和 第 16 章 讲的 GRPO 没有本质区别——都是 group-normalized advantage + PPO-Clip。差异只在环境(浏览器 vs 文本 sandbox)和奖励(任务完成 vs 答案正确)。如果你已经跑通过 18.8 金融 API 工具调用 GRPO,迁移到 Deep Research 只需要换 Environment WrapperReward Verifier 两个模块。

本节总结

Deep Research 的 harness 工程核心是五个模块:环境封装、动作解析、奖励计算、进度跟踪、并行 rollout。其中环境封装奖励计算是最难复现的——前者需要真实浏览器工程经验,后者需要任务特定的 verifier 设计。

下一节 24.3 评测基准与开源项目 介绍如何衡量 Deep Research Agent 的好坏——你会发现,评测本身比训练更难。

现代强化学习实战课程