第 10 章

Atropos RL 框架:从轨迹到能力

第十章:Atropos RL 框架:从轨迹到能力

强化学习的历史充满了在简单游戏上的辉煌胜利,但将其应用于开放式 Agent 任务始终是一个未竟的难题。Atropos 的出现,标志着这一难题第一次在开源社区得到了系统性的工程解答。


10.1 Atropos 的设计目标与哲学

10.1.1 命名的隐喻

Atropos 在希腊神话中是三位命运女神之一,负责切断生命之线——决定命运的终结与转折。NousResearch 选择这个名字,暗示 Atropos 框架的核心职责:判断哪些 Agent 行为轨迹值得保留,哪些应该被终止和丢弃

这不仅是命名上的诗意,更是系统设计的写照:Atropos 的核心机制正是对 Agent 轨迹进行质量裁决,然后用这些裁决来改进模型本身。

10.1.2 三个核心设计目标

目标一:可扩展的轨迹采集

传统 RL 系统依赖于封闭的模拟环境(Atari 游戏、棋盘游戏)。Atropos 需要在真实工具调用环境中采集轨迹——这意味着每一步都可能调用真实的代码执行器、网络搜索、文件系统,且结果不可预测。

目标二:可靠的轨迹评判

对于 Agent 完成的任务("帮我分析这份报告"),没有单一正确答案,评判本质上是主观的。Atropos 需要一套可扩展、一致性高的评判机制,在不依赖大量人工的前提下给出合理评分。

目标三:闭环改进循环

最终目标是让模型通过自己的经验不断改进——像人类学习新技能时那样,通过反复尝试和反馈来提升能力。

10.1.3 设计约束

Atropos 在设计时面临几个关键约束:

约束 具体要求 解决方案
计算成本 大规模轨迹采集代价高昂 异步并行采集 + 优先级队列
评判一致性 不同 Judge 对同一轨迹评分可能不一致 多评判者投票 + 评判者校准
分布漂移 模型改进后,旧轨迹可能不再有价值 在线数据混合 + 经验回放
安全边界 Agent 行为可能产生真实危害 沙箱隔离 + 动作白名单

10.2 轨迹采集机制

10.2.1 轨迹的定义与结构

在 Atropos 中,一条**轨迹(Trajectory)**是一个完整的任务执行序列:

from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any

@dataclass
class ToolCall:
    tool_name: str
    parameters: Dict[str, Any]
    result: str
    error: Optional[str] = None
    latency_ms: int = 0

@dataclass
class TrajectoryStep:
    step_id: int
    thought: Optional[str]      # <think> 内容
    tool_call: Optional[ToolCall]
    observation: str             # 环境反馈
    timestamp: float

@dataclass
class Trajectory:
    task_id: str
    task_description: str
    model_id: str
    steps: List[TrajectoryStep] = field(default_factory=list)
    final_response: Optional[str] = None
    success: Optional[bool] = None
    judge_score: Optional[float] = None  # 0.0 - 1.0
    token_count: int = 0
    wall_time_seconds: float = 0.0

10.2.2 采集管道架构

┌─────────────────────────────────────────────────────────────┐
│                    Atropos 轨迹采集管道                      │
│                                                              │
│  ┌──────────────┐                                           │
│  │  任务池      │ ← 人工设计 + 自动生成                    │
│  │ Task Pool    │                                           │
│  └──────┬───────┘                                           │
│         │ 任务分发                                          │
│         ↓                                                    │
│  ┌──────────────────────────────────┐                       │
│  │      Agent 工作池(并行)         │                       │
│  │  ┌────────┐ ┌────────┐ ┌───────┐│                       │
│  │  │Worker 1│ │Worker 2│ │Worker N││                       │
│  │  │(模型实例)│ │(模型实例)│ │(模型实例)││                  │
│  │  └───┬────┘ └───┬────┘ └───┬───┘│                       │
│  └──────┼──────────┼──────────┼────┘                       │
│         │          │          │ 原始轨迹                    │
│         ↓          ↓          ↓                             │
│  ┌─────────────────────────────────┐                        │
│  │         轨迹收集器              │                         │
│  │      Trajectory Collector       │                         │
│  └──────────────┬──────────────────┘                        │
│                 │                                            │
│                 ↓                                            │
│  ┌──────────────────────────────────┐                        │
│  │          评判者集群              │                         │
│  │         Judge Cluster            │                         │
│  │  ┌────────┐  ┌────────┐         │                         │
│  │  │Judge A │  │Judge B │  ...    │                         │
│  │  │(GPT-4) │  │(Claude)│         │                         │
│  │  └────────┘  └────────┘         │                         │
│  └──────────────┬───────────────────┘                        │
│                 │ 评分轨迹                                    │
│                 ↓                                            │
│  ┌──────────────────────────────────┐                        │
│  │        训练数据集                │                         │
│  │  正例(高分轨迹)+ 负例(低分) │                         │
│  └──────────────────────────────────┘                        │
└─────────────────────────────────────────────────────────────┘

10.2.3 Agent 运行阶段

每个 Worker 执行以下循环:

class AtroposWorker:
    def __init__(self, model, tools, sandbox):
        self.model = model
        self.tools = tools
        self.sandbox = sandbox
        self.max_steps = 50
    
    async def execute_trajectory(self, task: str) -> Trajectory:
        trajectory = Trajectory(
            task_id=generate_id(),
            task_description=task,
            model_id=self.model.model_id
        )
        
        context = [{"role": "user", "content": task}]
        
        for step_num in range(self.max_steps):
            # 1. 模型生成下一步动作
            response = await self.model.generate(
                context,
                tools=self.tools.get_schemas()
            )
            
            # 2. 解析 <think> 和工具调用
            thought, tool_call = parse_model_response(response)
            
            # 3. 如果没有工具调用,则任务完成
            if tool_call is None:
                trajectory.final_response = response
                break
            
            # 4. 在沙箱中执行工具
            try:
                result = await self.sandbox.execute(
                    tool_call.tool_name,
                    tool_call.parameters,
                    timeout=30
                )
                observation = str(result)
                error = None
            except Exception as e:
                observation = f"Error: {str(e)}"
                error = str(e)
            
            # 5. 记录步骤
            step = TrajectoryStep(
                step_id=step_num,
                thought=thought,
                tool_call=ToolCall(
                    tool_name=tool_call.tool_name,
                    parameters=tool_call.parameters,
                    result=observation,
                    error=error
                ),
                observation=observation,
                timestamp=time.time()
            )
            trajectory.steps.append(step)
            
            # 6. 更新上下文
            context.append({"role": "assistant", "content": response})
            context.append({"role": "tool", "content": observation})
        
        return trajectory

10.2.4 评判者打分机制

Atropos 使用多评判者投票来评分轨迹,避免单一评判者的偏见:

class TrajectoryJudge:
    def __init__(self, judge_models: List[str]):
        self.judges = [LLMClient(model) for model in judge_models]
    
    async def score_trajectory(self, trajectory: Trajectory) -> float:
        scores = []
        
        for judge in self.judges:
            prompt = self._build_judge_prompt(trajectory)
            response = await judge.generate(prompt)
            score = self._parse_score(response)  # 0.0 - 1.0
            scores.append(score)
        
        # 去掉最高分和最低分,取平均(鲁棒性处理)
        if len(scores) > 2:
            scores = sorted(scores)[1:-1]
        
        return sum(scores) / len(scores)
    
    def _build_judge_prompt(self, trajectory: Trajectory) -> str:
        return f"""你是一个 AI Agent 任务评判专家。请评估以下 Agent 的任务完成质量。

任务描述:{trajectory.task_description}

Agent 执行轨迹:
{self._format_trajectory(trajectory)}

请从以下维度评分(每项 0-10 分):
1. 任务完成度:最终响应是否完整回答了用户需求
2. 推理质量:思考过程是否合理、高效
3. 工具使用:工具选择和参数是否恰当
4. 错误处理:遇到错误时是否正确处理
5. 效率:是否避免了不必要的步骤

最终综合评分(0.0-1.0):"""

10.3 Atropos 与 RLHF/DPO 的本质区别

10.3.1 三种对齐方法的对比

┌───────────────────────────────────────────────────────────┐
│                   对齐方法演进时间线                       │
│                                                           │
│  RLHF (2022)          DPO (2023)        Atropos RL (2024) │
│                                                           │
│  人类标注 ─────────── 人类偏好对 ─────── Agent 轨迹        │
│  奖励模型训练  ─────── 直接偏好优化 ──── 评判者打分        │
│  PPO 优化    ─────── 无需 RL      ──── PPO/GRPO           │
│  单步响应    ─────── 单步响应     ──── 多步轨迹            │
│  对话对齐    ─────── 对话对齐     ──── 任务完成对齐        │
└───────────────────────────────────────────────────────────┘
维度 RLHF DPO Atropos RL
数据来源 人工偏好标注 人工偏好对比 Agent 自主执行轨迹
评判主体 人类标注者 隐式(偏好对) LLM 评判者
优化目标 最大化人类偏好 最大化偏好对数比 最大化任务完成率
时序建模 单步/短序列 单步/短序列 完整多步轨迹
可扩展性 受人力瓶颈限制 中等(需标注对) 高(自动采集)
反事实学习 有限 显式正负例 成功/失败轨迹对比
成本 极高 中等 中等(主要是计算)

10.3.2 RLHF 的根本局限

RLHF 的设计假设是:人类知道什么是好的响应。但对于复杂的 Agent 任务,这个假设往往不成立:

任务:优化这个 Python 程序的性能

RLHF 困境:
- 人类标注者看到两个响应,哪个"更好"?
- 响应 A:理论分析充分,但实际加速效果未知
- 响应 B:代码改动大,但实际运行快 3 倍

RLHF 无法判断,因为它没有执行工具的能力。
Atropos 可以:直接运行代码,测量实际加速比。

10.3.3 DPO 的局限

DPO 解决了 RLHF 的工程复杂性,但本质上仍然是模仿学习

# DPO 的核心假设
# 模型应该输出"被选择的"响应,而不是"被拒绝的"响应
# 但这个"被选择"是基于人类直觉,而非实际任务效果

# 问题:如果人类选择了次优响应呢?
# DPO 没有办法区分——它只能忠实地模仿人类偏好

DPO 对多步轨迹的时序信用分配也无能为力:如果第 3 步的工具调用失败导致了任务失败,DPO 无法将这个"失败"归因到具体的步骤。

10.3.4 Atropos 的创新:时序信用分配

Atropos 使用 GRPO(Group Relative Policy Optimization) 来处理时序信用分配问题:

def grpo_update(model, trajectories_batch, gamma=0.99):
    """
    GRPO:组相对策略优化
    对同一任务的多条轨迹进行相对排名,而非绝对评分
    """
    # 按任务ID分组
    task_groups = group_by_task(trajectories_batch)
    
    losses = []
    for task_id, group in task_groups.items():
        # 获取该任务所有轨迹的得分
        scores = [t.judge_score for t in group]
        
        # 计算相对优势(组内归一化)
        mean_score = sum(scores) / len(scores)
        std_score = statistics.stdev(scores) + 1e-8
        
        for trajectory, score in zip(group, scores):
            # 相对于同组平均水平的优势
            advantage = (score - mean_score) / std_score
            
            # 对轨迹中每个步骤计算折扣奖励
            discounted_returns = compute_discounted_returns(
                trajectory.steps, 
                final_reward=advantage,
                gamma=gamma
            )
            
            # 计算策略梯度损失
            loss = policy_gradient_loss(
                model, trajectory.steps, discounted_returns
            )
            losses.append(loss)
    
    return torch.stack(losses).mean()

10.4 持续改进循环的工程实现

10.4.1 在线学习与离线学习的混合

Atropos 采用混合训练策略,避免遗忘旧能力:

class AtroposTrainer:
    def __init__(self, model, replay_buffer_size=100000):
        self.model = model
        self.replay_buffer = PrioritizedReplayBuffer(replay_buffer_size)
        self.online_ratio = 0.7   # 70% 在线新数据
        self.offline_ratio = 0.3  # 30% 经验回放
    
    def training_step(self):
        # 采集新轨迹
        new_trajectories = self.collect_trajectories(n=256)
        
        # 评判打分
        scored = [self.judge(t) for t in new_trajectories]
        
        # 高质量轨迹加入回放缓冲
        for t in scored:
            if t.judge_score > 0.7:
                self.replay_buffer.add(t, priority=t.judge_score)
        
        # 混合训练批次
        online_batch = scored[:int(256 * self.online_ratio)]
        offline_batch = self.replay_buffer.sample(
            int(256 * self.offline_ratio)
        )
        
        training_batch = online_batch + offline_batch
        
        # GRPO 更新
        loss = grpo_update(self.model, training_batch)
        self.optimizer.step(loss)
        
        return loss.item()

10.4.2 任务课程设计

Atropos 的一个关键工程细节是任务课程(Curriculum)——按难度递进地安排训练任务:

class TaskCurriculum:
    """
    课程学习:从简单任务开始,逐步增加难度
    """
    def __init__(self):
        self.difficulty_levels = {
            1: ["单步工具调用", "简单文件操作", "基础计算"],
            2: ["多步信息检索", "代码调试", "数据格式转换"],
            3: ["复杂数据分析", "多工具协作", "错误恢复"],
            4: ["跨系统集成", "长期规划", "模糊任务澄清"],
            5: ["开放式研究", "创意问题解决", "自主项目规划"]
        }
        self.current_level = 1
        self.success_threshold = 0.75  # 达到此成功率后升级
    
    def should_advance(self, recent_scores: List[float]) -> bool:
        avg_score = sum(recent_scores[-100:]) / 100
        return avg_score > self.success_threshold
    
    def get_next_task(self) -> str:
        task_type = random.choice(
            self.difficulty_levels[self.current_level]
        )
        return self.task_generator.generate(task_type)

10.4.3 模型崩溃的防护机制

强化学习训练容易导致模型在优化过程中"崩溃"(丢失已有能力)。Atropos 的防护机制包括:

class SafetyMonitor:
    def __init__(self, baseline_benchmarks):
        self.baselines = baseline_benchmarks
        self.regression_threshold = 0.05  # 允许 5% 以内的退化
    
    def check_regression(self, model, check_frequency=100):
        """每 100 步检查一次能力退化"""
        current_scores = run_benchmarks(model, self.baselines)
        
        regressions = {}
        for benchmark, baseline_score in self.baselines.items():
            current = current_scores[benchmark]
            if current < baseline_score * (1 - self.regression_threshold):
                regressions[benchmark] = {
                    "baseline": baseline_score,
                    "current": current,
                    "drop": baseline_score - current
                }
        
        if regressions:
            # 触发回滚或混入基础数据
            self.handle_regression(regressions)
        
        return regressions
    
    def handle_regression(self, regressions):
        # 增加基础能力数据的比例
        self.trainer.adjust_data_ratio(
            capability_data_ratio=0.5  # 从 0.3 提升到 0.5
        )
        logging.warning(f"能力退化检测: {regressions}")

10.5 开放给社区的接口

10.5.1 Atropos 的开源策略

NousResearch 采取了选择性开源策略:

组件 开源状态 说明
轨迹采集接口 开源 允许社区贡献轨迹
评判者接口 开源 支持自定义评判标准
核心训练循环 开源(简化版) 完整版内部使用
预训练轨迹数据集 部分开源 ~2M 条轨迹公开
Hermes 4 权重 开源 Apache 2.0

10.5.2 社区贡献接口

# 社区可以通过标准接口贡献轨迹数据

from atropos import TrajectoryContributor, TaskSchema

class MyCustomTask(TaskSchema):
    """自定义任务:网络安全漏洞分析"""
    
    task_type = "security_analysis"
    difficulty = 3
    required_tools = ["code_exec", "web_search", "file_read"]
    
    def generate_task(self) -> str:
        return f"分析以下代码片段中的潜在安全漏洞:\n{self.get_vulnerable_code()}"
    
    def evaluate(self, trajectory: Trajectory) -> float:
        """自定义评判函数"""
        final_response = trajectory.final_response or ""
        
        score = 0.0
        # 检查是否识别了所有漏洞
        for vuln in self.known_vulnerabilities:
            if vuln.cve_id in final_response or vuln.name in final_response:
                score += 1.0 / len(self.known_vulnerabilities)
        
        return score

# 注册并提交
contributor = TrajectoryContributor(api_key="your_api_key")
contributor.register_task(MyCustomTask)
contributor.submit_trajectories(
    task=MyCustomTask(),
    n_trajectories=100,
    model="hermes-4"
)

10.5.3 本地 Atropos 训练

社区可以在本地运行简化版 Atropos 来微调自己的模型:

# 安装
pip install atropos-rl

# 配置文件
cat > atropos_config.yaml << 'EOF'
model:
  base_model: "NousResearch/Hermes-4-405B"
  quantization: "q4_k_m"
  
training:
  max_steps: 1000
  batch_size: 8
  learning_rate: 1e-5
  
trajectory_collection:
  n_workers: 4
  max_steps_per_trajectory: 30
  tools: ["python_exec", "web_search", "file_ops"]
  
judge:
  model: "gpt-4o"
  n_judges: 3
  
tasks:
  task_file: "./my_tasks.json"
  difficulty_range: [2, 4]
EOF

# 启动训练
atropos train --config atropos_config.yaml --output ./my_hermes_ft

10.5.4 轨迹数据集贡献

NousResearch 建立了一个公共轨迹数据集库(Trajectory Commons)

# 访问公共轨迹数据集
from atropos.datasets import TrajectoryCommons

# 加载特定类型的轨迹
dataset = TrajectoryCommons.load(
    task_types=["code_debugging", "data_analysis"],
    min_score=0.8,          # 只加载高质量轨迹
    min_steps=5,            # 至少 5 步的多步轨迹
    max_steps=40,
    split="train"           # train / validation / test
)

print(f"加载了 {len(dataset)} 条轨迹")
print(f"平均得分: {dataset.avg_score:.3f}")
print(f"平均步数: {dataset.avg_steps:.1f}")

10.6 Atropos 的局限与未来方向

10.6.1 当前局限

评判者偏见问题

使用 GPT-4 作为评判者会引入 GPT-4 的偏好偏见:

已知问题:
- GPT-4 偏好较长、格式整齐的响应
- GPT-4 对某些编码风格有隐性偏好
- 评判者本身的知识边界影响打分准确性

探索效率问题

对于极难任务(成功率 <5%),随机探索效率极低——需要采集大量失败轨迹才能得到少量成功案例。

奖励稀疏性

# 二元奖励(成功/失败)太稀疏
reward = 1.0 if task_completed else 0.0

# Atropos 的缓解方案:过程奖励
def compute_process_rewards(trajectory: Trajectory) -> List[float]:
    rewards = []
    for step in trajectory.steps:
        r = 0.0
        if step.tool_call and not step.tool_call.error:
            r += 0.05  # 成功工具调用的小额奖励
        if step.thought and len(step.thought) > 50:
            r += 0.02  # 有实质性思考的小额奖励
        rewards.append(r)
    # 最后一步的终态奖励
    rewards[-1] += trajectory.judge_score
    return rewards

10.6.2 未来方向


本章小结

思考题

  1. Atropos 使用 LLM 作为评判者,但 LLM 本身有偏见。如何设计一套更客观的评判机制?
  2. GRPO 通过"组内相对排名"而非绝对分数来更新策略,这个设计解决了什么问题?又引入了什么风险?
  3. 如果你要为"医疗问答"场景设计 Atropos 任务,你会如何定义"成功的轨迹"?如何设计评判标准?
  4. 在线学习(新轨迹)和经验回放(旧轨迹)的混合比例应该如何动态调整?请设计一个自适应策略。
本章评分
4.5  / 5  (49 评分)

💬 留言讨论