OpenClaw-RL 实战 03|捕捉“评估信号”实战:如何把用户的“重问”变成标量奖励?

5 阅读6分钟

当用户不耐烦地重复问题时,你的AI不仅要知道“我错了”,还要知道“错了几分”

引言:让AI学会“察言观色”

在上一篇中,我们深入拆解了OpenClaw-RL的四大异步组件,理解了它们如何“并行不悖”地协同工作。现在,是时候让这些组件真正“动起来”——捕捉交互中隐藏的评估信号,并将其转化为模型进化的养料。

想象这样一个场景:你问AI“帮我查一下2026年3月的财报”,它回复了一大段无关信息。你不耐烦地重复了一遍“直接给关键数字就行”。这一刻,AI收到了两个信号:

  • 显式信号:用户重复提问 → 隐含“不满意”
  • 隐式信号:用户强调“直接给数字” → 隐含“回答应更简洁”

传统的AI系统只会把这句话当作下一轮对话的上下文,然后……转身就忘。而OpenClaw-RL会做什么?它会通过过程奖励模型(PRM) 将“用户重复提问”这个行为转化为标量奖励(比如 -1 分),然后把这个分数反馈给训练引擎,让模型学会下次避免类似错误。

本文将通过实战带你完成:

  • 理解评估信号:哪些交互行为可以转化为奖励?
  • 配置PRM评判器:如何让模型学会“打分”?
  • 实现信号捕获:从用户重问、工具报错中提取信号
  • 奖励计算与聚合:多数投票机制如何工作?
  • PPO策略更新:让模型根据奖励调整行为

一、什么是“评估信号”?

1.1 核心定义

评估信号(Evaluative Signals) 是指智能体动作之后的下一个状态信号中,隐含着对动作质量的评分信息。这些信号天然存在,无需人工标注:

信号来源具体表现隐含评分
用户重问用户重复同一问题不满意 → -1
工具报错终端返回错误码失败 → -1
测试通过单元测试全部通过成功 → +1
用户反馈“谢谢”“很棒”等满意 → +1
无响应用户沉默/离开中性 → 0
GUI无变化点击后界面未更新可能失败 → -1

1.2 为什么需要评估信号?

在传统的RLHF中,我们需要人工标注大量偏好数据——成本高、周期长。而评估信号的妙处在于:它是交互的自然产物,随每一次对话免费产生

OpenClaw-RL的核心洞察正是:所有场景的下一个状态信号具有通用性,可通过同一框架训练同一策略。无论是个人对话、终端操作还是GUI交互,评估信号都可以被统一处理。

1.3 评估信号 vs. 指导信号

在进入实战前,我们需要先区分OpenClaw-RL捕捉的两类信号:

维度评估信号(Evaluative)指导信号(Directive)
信息类型好/坏评分具体怎么改
输出形式标量奖励(+1/-1/0)Token级方向监督
覆盖范围所有交互轮次仅含明确修正的轮次
对应方法Binary RLHindsight-Guided OPD
本篇重点✅ 本篇实战❌ 下一篇

简单来说:评估信号告诉AI“你做错了”,指导信号告诉AI“你应该怎么做”。本文聚焦前者。

二、PRM评判器:让AI学会“打分”

2.1 PRM是什么?

PRM(Process Reward Model,过程奖励模型) 是OpenClaw-RL中负责将“下一个状态信号”转化为标量奖励的核心组件。它的输入是:

  • 智能体动作 ata_t(如回复内容)
  • 下一个状态 st+1s_{t+1}(如用户反馈、工具输出)

输出是 {+1,1,0}\{+1, -1, 0\} 中的一个值。

2.2 PRM评判逻辑

PRM的评判逻辑可以用伪代码表示:

def prm_judge(action, next_state):
    """
    过程奖励模型评判器
    返回:+1(好)、-1(差)、0(中性)
    """
    # 判断用户是否重问
    if is_user_repeat(next_state):
        return -1
    
    # 判断工具执行是否成功
    if is_tool_success(next_state):
        return +1
    
    # 判断用户反馈是否积极
    if is_positive_feedback(next_state):
        return +1
    
    # 判断用户反馈是否消极
    if is_negative_feedback(next_state):
        return -1
    
    # 默认中性
    return 0

2.3 多数投票机制

为了缓解单次判定的随机性,OpenClaw-RL会对同一个交互对进行 m次独立查询,通过多数投票决定最终奖励:

r_final = MajorityVote([r1, r2, ..., rm])

在论文实验中,m通常取3-5次,投票机制显著提升了奖励的稳定性。

2.4 实战配置PRM评判器

在OpenClaw-RL中,PRM评判器可以基于智谱API或本地模型实现。我们先用智谱API快速搭建一个:

# prm_judge.py
import json
import requests
import numpy as np
from typing import List, Dict, Any

class PRMJudge:
    """基于智谱API的过程奖励模型评判器"""
    
    def __init__(self, api_key: str, model: str = "glm-4-flash", num_votes: int = 3):
        self.api_key = api_key
        self.model = model
        self.num_votes = num_votes
        self.base_url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
    
    def _call_api(self, action: str, next_state: str) -> int:
        """单次API调用,返回评分"""
        prompt = f"""你是一个AI交互质量评判器。请根据用户的反馈,判断AI的上一次回复质量。

AI的回复:{action}
用户的反馈:{next_state}

请从以下选项中选择一个:
+1:用户满意/任务成功(如用户说“谢谢”“很好”、工具执行成功)
-1:用户不满意/任务失败(如用户重复提问、工具报错)
0:中性(无法判断好坏)

请只返回数字:+1、-1或0"""

        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        
        data = {
            "model": self.model,
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 0.1
        }
        
        try:
            response = requests.post(self.base_url, headers=headers, json=data)
            result = response.json()
            content = result['choices'][0]['message']['content'].strip()
            
            # 解析返回的数字
            if '+1' in content or '1' in content and '+' in content:
                return 1
            elif '-1' in content:
                return -1
            else:
                return 0
        except Exception as e:
            print(f"API调用失败:{e}")
            return 0
    
    def judge(self, action: str, next_state: str) -> int:
        """多数投票机制,返回最终评分"""
        votes = []
        for i in range(self.num_votes):
            score = self._call_api(action, next_state)
            votes.append(score)
            print(f"投票{i+1}: {score}")
        
        # 多数投票
        final_score = np.argmax(np.bincount(votes)) if votes else 0
        # 将0/1/2映射回-1/0/1
        mapping = {0: -1, 1: 0, 2: 1}
        return mapping.get(final_score, 0)

# 使用示例
judge = PRMJudge(api_key="your-zhipu-api-key", num_votes=3)

# 测试:用户重问
score = judge.judge(
    action="2026年3月的财报数据我暂时无法获取,建议你关注官方公告。",
    next_state="直接给关键数字就行,别说这么多废话"
)
print(f"最终评分: {score}")  # 预期输出 -1

💡 小贴士:如果你想在本地运行更快的PRM,可以部署一个轻量级模型(如Qwen2.5-7B)作为评判器,论文中使用的正是这种方案。

三、环境集成:让OpenClaw“感知”评估信号

3.1 环境服务器的会话感知

在OpenClaw-RL架构中,环境服务器负责将用户交互流接入系统。关键设计是会话感知(Session-Aware)——区分“主线轮次”(主交互)和“支线轮次”(辅助操作)。

# env_server.py
from openclaw_rl import EnvironmentServer
import json

class OpenClawEnvServer(EnvironmentServer):
    """OpenClaw环境服务器"""
    
    def __init__(self):
        super().__init__()
        self.sessions = {}  # 会话存储
        
    def classify_request(self, request):
        """
        分类请求类型
        返回:'main'(主线)或 'side'(支线)
        """
        # 主线:用户问题、工具执行、用户反馈
        if request['type'] in ['user_query', 'tool_result', 'user_feedback']:
            return 'main'
        
        # 支线:心跳、状态查询、内存整理
        if request['type'] in ['heartbeat', 'status_check', 'memory_organize']:
            return 'side'
        
        return 'side'
    
    def handle_request(self, request):
        """处理用户请求"""
        session_id = request['session_id']
        req_type = request['type']
        content = request['content']
        
        # 分类请求
        turn_type = self.classify_request(request)
        
        # 记录交互
        if session_id not in self.sessions:
            self.sessions[session_id] = []
        
        self.sessions[session_id].append({
            'type': req_type,
            'turn_type': turn_type,
            'content': content,
            'timestamp': request['timestamp']
        })
        
        # 如果是主线轮次,触发PRM评判(针对上一轮)
        if turn_type == 'main' and len(self.sessions[session_id]) > 1:
            prev_turn = self.sessions[session_id][-2]
            if prev_turn['turn_type'] == 'main':
                # 上一轮也是主线,可以作为训练样本
                self.send_to_prm(prev_turn, request)
        
        # 返回响应
        return self.forward_to_policy(request)
    
    def send_to_prm(self, prev_turn, current_turn):
        """将交互对发送给PRM评判器"""
        action = prev_turn['content']
        next_state = current_turn['content']
        
        # 异步发送给PRM服务器(不阻塞主流程)
        self.prm_queue.put({
            'action': action,
            'next_state': next_state,
            'session_id': current_turn['session_id'],
            'timestamp': current_turn['timestamp']
        })

3.2 捕获典型评估信号

下面我们实现一个函数,专门从用户反馈中提取评估信号:

# signal_extractor.py
import re
from typing import Tuple, Optional

class SignalExtractor:
    """评估信号提取器"""
    
    @staticmethod
    def detect_user_repeat(current_query: str, history: list) -> bool:
        """
        检测用户是否在重复问题
        通过语义相似度或关键词匹配
        """
        if len(history) < 2:
            return False
        
        last_query = history[-2]['content'] if history[-2]['type'] == 'user_query' else None
        if not last_query:
            return False
        
        # 简化版:检查关键词重叠
        words1 = set(re.findall(r'\w+', last_query.lower()))
        words2 = set(re.findall(r'\w+', current_query.lower()))
        
        # 如果主要关键词重合度高,可能是重问
        common_words = words1.intersection(words2)
        if len(common_words) >= 3 and len(words1) > 0:
            overlap_ratio = len(common_words) / len(words1)
            return overlap_ratio > 0.7
        
        return False
    
    @staticmethod
    def detect_tool_error(tool_output: str) -> bool:
        """检测工具执行是否报错"""
        error_patterns = [
            r'error',
            r'fail',
            r'exception',
            r'traceback',
            r'permission denied',
            r'not found',
            r'exit code \d+'
        ]
        
        output_lower = tool_output.lower()
        for pattern in error_patterns:
            if re.search(pattern, output_lower):
                return True
        return False
    
    @staticmethod
    def detect_positive_feedback(feedback: str) -> bool:
        """检测用户反馈是否积极"""
        positive_patterns = [
            r'thanks?',
            r'thank you',
            r'great',
            r'excellent',
            r'perfect',
            r'good job',
            r'works?',
            r'正确',
            r'谢谢',
            r'很棒'
        ]
        
        feedback_lower = feedback.lower()
        for pattern in positive_patterns:
            if re.search(pattern, feedback_lower):
                return True
        return False
    
    @staticmethod
    def detect_negative_feedback(feedback: str) -> bool:
        """检测用户反馈是否消极"""
        negative_patterns = [
            r'wrong',
            r'incorrect',
            r'not what I wanted',
            r'not helpful',
            r'bad',
            r'terrible',
            r'useless',
            r'不对',
            r'错了',
            r'不好'
        ]
        
        feedback_lower = feedback.lower()
        for pattern in negative_patterns:
            if re.search(pattern, feedback_lower):
                return True
        return False

3.3 完整PRM评判服务

将上述组件整合成一个完整的PRM评判服务:

# prm_service.py
import asyncio
import json
from queue import Queue
from threading import Thread
from typing import Dict, Any

class PRMService:
    """PRM评判服务"""
    
    def __init__(self, judge_model, extractor):
        self.judge = judge_model  # PRM评判器
        self.extractor = extractor  # 信号提取器
        self.input_queue = Queue()  # 输入队列
        self.output_queue = Queue()  # 输出队列
        self.reward_buffer = []  # 奖励缓冲区
        
    def start(self):
        """启动服务(异步处理)"""
        def worker():
            while True:
                # 从队列获取待评判的交互对
                item = self.input_queue.get()
                if item is None:
                    break
                
                # 提取评估信号
                reward = self._compute_reward(item)
                
                # 将结果放入输出队列
                item['reward'] = reward
                self.output_queue.put(item)
                
                # 添加到训练缓冲区
                self.reward_buffer.append(item)
        
        Thread(target=worker, daemon=True).start()
        
    def _compute_reward(self, item: Dict[str, Any]) -> int:
        """计算奖励值"""
        action = item['action']
        next_state = item['next_state']
        
        # 1. 检测用户重问
        if self.extractor.detect_user_repeat(next_state, item.get('history', [])):
            return -1
        
        # 2. 检测工具报错
        if self.extractor.detect_tool_error(next_state):
            return -1
        
        # 3. 检测积极反馈
        if self.extractor.detect_positive_feedback(next_state):
            return 1
        
        # 4. 检测消极反馈
        if self.extractor.detect_negative_feedback(next_state):
            return -1
        
        # 5. 用PRM模型评判
        return self.judge.judge(action, next_state)
    
    def get_batch(self, batch_size: int = 16):
        """获取一批训练样本"""
        if len(self.reward_buffer) >= batch_size:
            batch = self.reward_buffer[:batch_size]
            self.reward_buffer = self.reward_buffer[batch_size:]
            return batch
        return []

3.4 集成到OpenClaw

在OpenClaw主配置文件中添加PRM服务配置:

// ~/.openclaw/config.json
{
  "gateway": {
    "port": 18789
  },
  "rl": {
    "enabled": true,
    "prm_service": {
      "type": "zhipuai",
      "model": "glm-4-flash",
      "num_votes": 3,
      "batch_size": 16,
      "update_interval": 60
    },
    "signal_extraction": {
      "detect_user_repeat": true,
      "detect_tool_error": true,
      "detect_sentiment": true
    }
  }
}

四、PPO策略更新:让奖励“落地”

4.1 PPO损失函数

OpenClaw-RL采用带有非对称边界的PPO损失函数:

Lpg=Et[min(ρtAt,clip(ρt,1ε,1+εhigh)At)]+βKLLKL\mathcal{L}_{pg} = -\mathbb{E}_{t}\left[\min \left(\rho_{t} A_{t}, \text{clip}\left(\rho_{t}, 1-\varepsilon, 1+\varepsilon_{\text{high}}\right) \cdot A_{t}\right)\right] + \beta_{KL} \cdot \mathcal{L}_{KL}

其中:

  • ρt=πθ(atst)πold(atst)\rho_t = \frac{\pi_\theta(a_t|s_t)}{\pi_{old}(a_t|s_t)} 是概率比率
  • AtA_t 是优势函数,在Binary RL中直接取 rfinalr_{final}
  • ε=0.2\varepsilon = 0.2εhigh=0.28\varepsilon_{\text{high}} = 0.28,确保更新稳定

4.2 实战实现PPO更新

# ppo_trainer.py
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import List, Dict, Any

class PPOTrainer:
    """PPO训练器"""
    
    def __init__(self, policy_model, lr=1e-5, beta_kl=0.1, clip_eps=0.2, clip_eps_high=0.28):
        self.policy = policy_model
        self.old_policy = None  # 定期保存旧策略
        self.optimizer = torch.optim.Adam(policy_model.parameters(), lr=lr)
        self.beta_kl = beta_kl
        self.clip_eps = clip_eps
        self.clip_eps_high = clip_eps_high
        
    def compute_ppo_loss(self, batch: List[Dict[str, Any]]):
        """计算PPO损失"""
        total_loss = 0
        
        for item in batch:
            # 提取数据
            state = item['state']  # 输入状态
            action = item['action']  # 动作(生成的token序列)
            reward = item['reward']  # 奖励(-1/0/1)
            old_logprobs = item['logprobs']  # 旧策略的log概率
            
            # 计算新策略的log概率
            new_logprobs = self.policy.get_logprobs(state, action)
            
            # 计算概率比率
            ratio = torch.exp(new_logprobs - old_logprobs)
            
            # 优势函数 = 奖励(简化版)
            advantage = torch.tensor(reward, dtype=torch.float32)
            
            # 非对称裁剪
            if advantage >= 0:
                # 正优势:鼓励增加概率
                clipped_ratio = torch.clamp(ratio, 1 - self.clip_eps, 1 + self.clip_eps_high)
            else:
                # 负优势:鼓励减少概率
                clipped_ratio = torch.clamp(ratio, 1 - self.clip_eps_high, 1 + self.clip_eps)
            
            # PPO损失
            pg_loss = -torch.min(ratio * advantage, clipped_ratio * advantage)
            
            # KL散度惩罚(防止策略变化过大)
            kl_div = (new_logprobs - old_logprobs).mean()
            
            # 总损失
            loss = pg_loss + self.beta_kl * kl_div
            total_loss += loss
        
        return total_loss / len(batch)
    
    def update(self, batch):
        """更新策略"""
        self.optimizer.zero_grad()
        loss = self.compute_ppo_loss(batch)
        loss.backward()
        self.optimizer.step()
        return loss.item()

4.3 集成训练循环

# rl_train_loop.py
import time
from threading import Thread

class RLTrainLoop:
    """RL训练循环"""
    
    def __init__(self, prm_service, trainer, update_interval=60):
        self.prm_service = prm_service
        self.trainer = trainer
        self.update_interval = update_interval  # 更新间隔(秒)
        
    def start(self):
        """启动训练循环"""
        def loop():
            while True:
                time.sleep(self.update_interval)
                
                # 获取一批训练样本
                batch = self.prm_service.get_batch(batch_size=16)
                
                if len(batch) >= 4:  # 至少4个样本才更新
                    loss = self.trainer.update(batch)
                    print(f"[{time.strftime('%H:%M:%S')}] 策略更新完成,损失: {loss:.4f}")
                    
                    # 更新旧策略
                    self.trainer.old_policy = self.trainer.policy.clone()
                    
                    # 记录训练统计
                    rewards = [item['reward'] for item in batch]
                    pos_ratio = sum(1 for r in rewards if r > 0) / len(rewards)
                    neg_ratio = sum(1 for r in rewards if r < 0) / len(rewards)
                    print(f"  正样本: {pos_ratio:.1%}, 负样本: {neg_ratio:.1%}")
        
        Thread(target=loop, daemon=True).start()

五、实战验证:从0.17到0.76的跃迁

5.1 实验设置

根据论文的实验配置,我们设置以下参数:

参数说明
基础模型Qwen3-4B个人智能体场景
训练触发每16个样本更新一次异步更新
PRM投票次数3次多数投票
更新间隔60秒后台静默更新
初始分数0.17学生场景基线

5.2 运行实验

# run_experiment.py
from openclaw_rl import OpenClawRL

# 初始化系统
system = OpenClawRL(
    policy_model="Qwen3-4B",
    prm_model="glm-4-flash",
    env_type="personal"
)

# 启动系统
system.start()

# 模拟用户交互(学生场景)
interactions = [
    {"query": "帮我解这道数学题:一个书包的价格是220元,计算过程...", 
     "response": "The handbag cost $220. Here's how: - Shoes cost $80 - 3 times the shoe cost = 3 × 80 = 240 - Subtract $20 = $240 − 20 = 220 Final Answer: $220",
     "feedback": "直接给数字就行,别说这么多废话"},
    
    {"query": "还是刚才那道题,重新说",
     "response": "220美元。",
     "feedback": "嗯,简单明了,挺好"},
    
    # ... 更多交互
]

# 运行36次交互(论文实验数据)
for i, interaction in enumerate(interactions[:36]):
    system.process_interaction(interaction)
    
# 查看训练统计
stats = system.get_stats()
print(f"总交互数: {stats['total_interactions']}")
print(f"训练样本数: {stats['train_samples']}")
print(f"正样本比例: {stats['pos_ratio']:.1%}")
print(f"负样本比例: {stats['neg_ratio']:.1%}")
print(f"当前得分: {stats['current_score']:.2f}")

5.3 预期结果

根据论文数据,经过36次交互后:

  • 学生场景:个性化得分从0.17提升至0.76
  • 优化效果:AI回复从“机械的步骤化风格”变为“自然流畅的口语化表达”

论文中的定性对比:

优化前(AI风格明显):

The handbag cost $220. Here's how:
- Shoes cost $80
- 3 times the shoe cost = 3 × 80 = 240
- Subtract $20 = $24020 = 220

Final Answer: $220

优化后(自然表达):

The jacket costs 30 and two pairs of shoes at 20 each, so that's 40 total for shoes. Adding the jacket gives us 70 for everything…

5.4 可视化训练过程

# visualize.py
import matplotlib.pyplot as plt

def plot_training_progress(log_file):
    """绘制训练过程"""
    import json
    
    steps = []
    scores = []
    
    with open(log_file, 'r') as f:
        for line in f:
            data = json.loads(line)
            steps.append(data['step'])
            scores.append(data['score'])
    
    plt.figure(figsize=(10, 6))
    plt.plot(steps, scores, 'b-', linewidth=2)
    plt.axhline(y=0.17, color='r', linestyle='--', label='Baseline (0.17)')
    plt.axhline(y=0.76, color='g', linestyle='--', label='After 36 steps (0.76)')
    plt.xlabel('Training Steps')
    plt.ylabel('Personalization Score')
    plt.title('OpenClaw-RL Training Progress (Student Scenario)')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()

六、常见问题与排错

问题可能原因解决方案
PRM一直返回0评判prompt不够清晰优化评判prompt,增加示例
奖励长期为负模型质量确实差正常,说明需要更多训练
训练更新后性能下降学习率过大降低学习率,增加KL惩罚
PRM调用太慢API延迟高改用本地模型,或增加投票并行度
样本积累过慢交互稀疏降低update_intervalmin_samples

七、下一步预告

恭喜!你已经完成了OpenClaw-RL的核心实战——捕捉评估信号,并通过PRM将其转化为标量奖励,驱动PPO策略更新。现在,你的AI已经具备了“察言观色”的能力,能够从用户反馈中感知好坏。

下一篇文章,我们将进入更精细的层面——指导信号的捕捉。当用户说“你应该先检查文件再修改”时,如何从这句话中提取具体的修正方向,并通过OPD实现Token级别的精细化优化。

敬请期待:《OpenClaw-RL 实战 04|捕捉“指导信号”实战:如何从用户纠正中提取Token级监督?》

附录:核心命令速查

# 启动PRM服务
python prm_service.py

# 运行训练实验
python run_experiment.py

# 查看训练日志
tail -f ~/.openclaw-rl/logs/training.log

# 可视化训练过程
python visualize.py --log training_log.jsonl

本文发布于稀土掘金社区


(本文为「OpenClaw-RL实战」系列第三篇,共12篇。欢迎关注、收藏、转发,与更多开发者一起探索AI的“边用边学”新范式!)