当你的AI既能陪你聊天,又能操作终端、点击GUI、调试代码时,它才真正成为“通用智能体”
引言:打破场景的“柏林墙”
在过去的十一篇文章中,我们逐步构建了一个完整的“边用边学”智能体系统:
- 第1-2篇:环境搭建与四大异步组件拆解
- 第3-4篇:捕捉评估信号(Binary RL)与指导信号(OPD)
- 第5篇:加权损失融合,实现1+1>2的效果
- 第6-8篇:PRM定制化与教师模型训练
- 第9-11篇:无阻塞日志系统与工程化部署
但一个更深层次的问题始终悬而未决:个人对话、终端操作、GUI交互、软件工程(SWE)、工具调用——这些截然不同的场景,真的能用同一套RL代码训练吗?
传统观点认为,这些场景的输入形式、反馈信号、任务周期完全不同,必须分别设计训练流程。但OpenClaw-RL论文给出了一个颠覆性的答案:所有场景的下一个状态信号具有通用性,可通过同一框架训练同一策略 。
本文将作为系列的收官之作,带你实现这一“魔法”:
- ✅ 理解四大通用场景:终端、GUI、SWE、工具调用的核心特征
- ✅ 环境服务器扩展:如何用Docker并行拉起数百个沙盒环境
- ✅ 统一信号抽象:不同场景的“下一状态信号”如何统一处理
- ✅ 实验数据复现:从128终端到64GUI,论文中的数据如何实现
- ✅ 完整代码整合:从个人到通用的完整RL训练脚本
一、四大通用场景全景图
1.1 论文中的实验配置
根据OpenClaw-RL论文,通用智能体实验覆盖了四种典型场景,每种场景都有完全不同的特征 :
| 智能体类型 | 基础模型 | 数据集 | 并行环境数 | 视野长度 | 下一状态信号形式 |
|---|---|---|---|---|---|
| 终端(Terminal) | Qwen3-8B | SETA RL | 128 | 长 | stdout/stderr、退出码 |
| GUI | Qwen3VL-8B-Thinking | OSWorld-Verified | 64 | 长 | 视觉状态差异、截图 |
| SWE | Qwen3-32B | SWE-Bench-Verified | 64 | 长 | 测试结果、错误堆栈 |
| 工具调用(Tool-Call) | Qwen3-4B-SFT | DAPO RL | 32 | 中等 | 工具返回值、状态码 |
1.2 为什么需要并行环境?
个人智能体交互稀疏(一天几十次),而通用智能体训练需要海量交互数据。OpenClaw-RL通过Docker并行拉起数百个沙盒环境 :
- 终端:128个独立容器,每个执行不同的Shell命令
- GUI:64个虚拟桌面,运行不同的应用程序
- SWE:64个代码仓库,执行不同的编程任务
- 工具调用:32个API服务,处理不同的工具请求
这种设计让同一套RL代码能够同时从数百个环境中学习,将训练速度提升两个数量级。
二、统一环境服务器架构
无论底层是终端、GUI还是SWE,环境服务器都提供统一的接口。我们将前几篇文章中的组件整合成一个完整的UnifiedEnvironmentServer。
2.1 完整代码实现
# unified_env_server.py
import os
import time
import json
import docker
import threading
import subprocess
from typing import Dict, List, Any, Optional
import base64
from PIL import Image
import io
class UnifiedEnvironmentServer:
"""统一环境服务器:支持终端、GUI、SWE、工具调用四大场景"""
def __init__(self, env_type: str, parallel_envs: int = 1):
"""
初始化环境服务器
Args:
env_type: 'terminal', 'gui', 'swe', 'tool'
parallel_envs: 并行环境数量
"""
self.env_type = env_type
self.parallel_envs = parallel_envs
self.client = docker.from_env()
self.containers = []
self.sessions = {} # 会话管理
# 环境特定配置
self.env_configs = {
'terminal': {
'image': 'openclaw/terminal-env:latest',
'mem_limit': '512m',
'cpus': 0.5
},
'gui': {
'image': 'openclaw/gui-env:latest',
'mem_limit': '2g',
'cpus': 1.0,
'ports': {'5900/tcp': None} # VNC端口
},
'swe': {
'image': 'openclaw/swe-env:latest',
'mem_limit': '4g',
'cpus': 2.0
},
'tool': {
'image': 'openclaw/tool-env:latest',
'mem_limit': '256m',
'cpus': 0.25
}
}
def launch(self):
"""启动并行环境"""
print(f"正在启动 {self.parallel_envs} 个 {self.env_type} 环境...")
config = self.env_configs[self.env_type]
for i in range(self.parallel_envs):
try:
container = self.client.containers.run(
image=config['image'],
detach=True,
mem_limit=config.get('mem_limit'),
nano_cpus=int(config.get('cpus', 1) * 1e9),
ports=config.get('ports'),
environment={
'ENV_ID': i,
'ENV_TYPE': self.env_type
}
)
self.containers.append(container)
print(f" ✓ 容器 {i+1} 启动成功 (ID: {container.short_id})")
except Exception as e:
print(f" ✗ 容器 {i+1} 启动失败: {e}")
# 等待容器就绪
time.sleep(5)
print(f"所有环境启动完成,共 {len(self.containers)} 个")
def get_next_state(self, env_id: int, action: Dict[str, Any]) -> Dict[str, Any]:
"""
获取下一状态信号(统一接口)
Args:
env_id: 环境ID (0 ~ parallel_envs-1)
action: 动作,格式因场景而异
Returns:
next_state: 下一状态信号
"""
if env_id >= len(self.containers):
raise ValueError(f"环境ID {env_id} 超出范围")
container = self.containers[env_id]
if self.env_type == 'terminal':
return self._execute_terminal(container, action)
elif self.env_type == 'gui':
return self._execute_gui(container, action)
elif self.env_type == 'swe':
return self._execute_swe(container, action)
else: # tool
return self._execute_tool(container, action)
def _execute_terminal(self, container, action: Dict[str, Any]) -> Dict[str, Any]:
"""终端环境:执行Shell命令"""
cmd = action.get('command', '')
if not cmd:
return {"error": "No command provided"}
# 在容器中执行命令
exec_result = container.exec_run(cmd)
return {
"type": "terminal_output",
"stdout": exec_result.output.decode('utf-8', errors='ignore'),
"stderr": "", # 简化处理
"exit_code": exec_result.exit_code,
"command": cmd
}
def _execute_gui(self, container, action: Dict[str, Any]) -> Dict[str, Any]:
"""GUI环境:执行界面操作"""
# 这里简化实现,实际需要VNC连接
action_type = action.get('type', '')
coords = action.get('coords', {})
# 模拟执行操作
time.sleep(0.5) # 等待界面变化
# 获取容器中的截图(假设容器内有screenshot工具)
exec_result = container.exec_run("screenshot /tmp/screen.png")
# 读取截图并编码为base64
import tarfile
import io
# 从容器复制文件
stream, _ = container.get_archive('/tmp/screen.png')
tar_bytes = b''.join([chunk for chunk in stream])
# 解压tar
tar_file = io.BytesIO(tar_bytes)
with tarfile.open(fileobj=tar_file) as tar:
file_content = tar.extractfile('screen.png').read()
screenshot_b64 = base64.b64encode(file_content).decode('utf-8')
return {
"type": "gui_state",
"screenshot": screenshot_b64,
"action": action_type,
"coords": coords
}
def _execute_swe(self, container, action: Dict[str, Any]) -> Dict[str, Any]:
"""软件工程环境:执行代码修改并运行测试"""
code = action.get('code', '')
# 将代码写入容器
container.exec_run("mkdir -p /workspace")
# 写入代码文件
container.exec_run(f"echo '{code}' > /workspace/solution.py")
# 运行测试
test_result = container.exec_run(
"cd /workspace && pytest tests/ -v",
environment={'PYTHONPATH': '/workspace'}
)
output = test_result.output.decode('utf-8', errors='ignore')
passed = "passed" in output and "failed" not in output
return {
"type": "swe_result",
"passed": passed,
"output": output,
"code": code[:200] + "..." # 截断显示
}
def _execute_tool(self, container, action: Dict[str, Any]) -> Dict[str, Any]:
"""工具调用环境:执行API调用"""
tool_name = action.get('tool', '')
params = action.get('params', {})
# 构造调用命令
params_json = json.dumps(params)
cmd = f"python -c 'import json; from tools import {tool_name}; result = {tool_name}.run(**json.loads(\"\"\"{params_json}\"\"\")); print(json.dumps(result))'"
exec_result = container.exec_run(cmd)
output = exec_result.output.decode('utf-8', errors='ignore')
try:
result = json.loads(output)
status = "success"
except:
result = {"error": output}
status = "error"
return {
"type": "tool_result",
"status": status,
"result": result,
"tool": tool_name
}
def classify_turn(self, request: Dict) -> str:
"""
分类请求类型:主线 vs 支线
主线:用户问题、工具执行、用户反馈 → 训练样本
支线:心跳、状态查询、内存整理 → 不参与训练
"""
req_type = request.get('type', '')
if req_type in ['user_query', 'tool_result', 'user_feedback', 'command']:
return 'main'
if req_type in ['heartbeat', 'status_check', 'memory_organize']:
return 'side'
return 'side'
def close(self):
"""关闭所有容器"""
print(f"正在关闭 {len(self.containers)} 个容器...")
for container in self.containers:
try:
container.stop()
container.remove()
except:
pass
self.containers = []
print("所有容器已关闭")
三、统一RL训练脚本
有了统一的环境服务器,我们就可以用同一套RL代码驱动所有场景。下面整合前几篇文章的所有组件。
3.1 完整训练脚本
# unified_rl_train.py
import argparse
import time
import json
import torch
import numpy as np
from typing import Dict, List, Any
# 导入前几篇文章的组件
from unified_env_server import UnifiedEnvironmentServer
from prm_judge import PRMJudge, TerminalPRM, GUIPRM, SWEPRM, ToolPRM
from teacher_model import TeacherModel
from combined_trainer import CombinedTrainer
from rl_logger import RLLogger
class UnifiedRLTrainer:
"""统一RL训练器 - 支持所有场景"""
def __init__(self, config_path: str):
# 加载配置
with open(config_path, 'r') as f:
self.config = json.load(f)
# 初始化环境服务器
self.env_server = UnifiedEnvironmentServer(
env_type=self.config['env_type'],
parallel_envs=self.config.get('parallel_envs', 1)
)
# 初始化PRM评判器(场景特定)
self.prm_judge = self._init_prm()
# 初始化教师模型(用于OPD)
if self.config.get('use_opd', True):
self.teacher = TeacherModel(self.config['teacher_model_path'])
else:
self.teacher = None
# 初始化策略模型
from transformers import AutoModelForCausalLM, AutoTokenizer
self.model = AutoModelForCausalLM.from_pretrained(
self.config['base_model']
)
self.tokenizer = AutoTokenizer.from_pretrained(
self.config['base_model']
)
# 初始化训练器
self.trainer = CombinedTrainer(
policy_model=self.model,
w_binary=self.config.get('w_binary', 1.0),
w_opd=self.config.get('w_opd', 1.0),
lr=self.config.get('learning_rate', 1e-5)
)
# 初始化日志
self.logger = RLLogger(log_dir=f"./logs/{self.config['env_type']}")
# 训练统计
self.stats = {
'total_steps': 0,
'binary_samples': 0,
'opd_samples': 0,
'loss_history': []
}
def _init_prm(self):
"""初始化场景特定的PRM评判器"""
env_type = self.config['env_type']
if env_type == 'terminal':
return TerminalPRM(model=self.config.get('prm_model', 'glm-4-flash'))
elif env_type == 'gui':
return GUIPRM(vlm_model=self.config.get('prm_model', 'Qwen3VL-7B'))
elif env_type == 'swe':
return SWEPRM(llm_model=self.config.get('prm_model', 'glm-4-plus'))
else: # tool
return ToolPRM()
def run_one_episode(self, env_id: int, max_steps: int = 100):
"""
在一个环境中运行一个episode
Args:
env_id: 环境ID
max_steps: 最大步数
"""
episode_data = []
for step in range(max_steps):
# 1. 构造动作(根据场景)
action = self._generate_action(env_id)
# 2. 执行动作,获取下一状态
next_state = self.env_server.get_next_state(env_id, action)
# 3. 判断是否是主线轮次
turn_type = self.env_server.classify_turn({
'type': 'command' if step < max_steps-1 else 'user_feedback',
'content': action
})
# 4. 如果是主线,记录训练样本
if turn_type == 'main':
# 获取Binary RL的评估信号
binary_reward = self.prm_judge.judge(action, next_state)
# 尝试提取OPD指导信号
opd_sample = None
if self.teacher and 'feedback' in next_state:
hint = self._extract_hint(next_state['feedback'])
if hint:
token_advantages = self.teacher.compute_token_advantages(
str(action), str(next_state), hint
)
opd_sample = {
'token_advantages': token_advantages,
'hint': hint
}
# 记录日志
self.logger.log_interaction(
session_id=f"env_{env_id}",
turn_type=turn_type,
action=action,
next_state=next_state,
prm_score=binary_reward,
opd_hint=hint if opd_sample else None
)
# 收集训练样本
sample = {
'state': str(action),
'action': str(action), # 简化
'binary_reward': binary_reward
}
if opd_sample:
sample['token_advantages'] = opd_sample['token_advantages']
episode_data.append(sample)
self.stats['binary_samples'] += 1
if opd_sample:
self.stats['opd_samples'] += 1
# 5. 判断是否完成任务
if self._is_completed(next_state):
break
return episode_data
def _generate_action(self, env_id: int) -> Dict:
"""根据场景生成动作(简化实现)"""
# 实际应用中应该调用策略模型生成
if self.env_server.env_type == 'terminal':
return {"command": "ls -la"}
elif self.env_server.env_type == 'gui':
return {"type": "click", "coords": {"x": 100, "y": 200}}
elif self.env_server.env_type == 'swe':
return {"code": "def solution():\n return 42"}
else: # tool
return {"tool": "calculator", "params": {"expr": "2026*3.14"}}
def _extract_hint(self, feedback: str) -> Optional[str]:
"""从反馈中提取提示(简化)"""
if len(feedback) > 10 and ('应该' in feedback or '先' in feedback):
return f"[HINT] {feedback}"
return None
def _is_completed(self, next_state: Dict) -> bool:
"""判断任务是否完成"""
if self.env_server.env_type == 'terminal':
return 'exit_code' in next_state and next_state['exit_code'] == 0
elif self.env_server.env_type == 'swe':
return next_state.get('passed', False)
elif self.env_server.env_type == 'tool':
return next_state.get('status') == 'success'
return False
def train(self, num_episodes: int = 100):
"""主训练循环"""
print(f"开始训练,场景: {self.config['env_type']}, "
f"并行环境: {self.config.get('parallel_envs', 1)}")
# 启动环境
self.env_server.launch()
for episode in range(num_episodes):
episode_start = time.time()
# 并行运行所有环境
all_samples = []
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(
max_workers=self.config.get('parallel_envs', 1)
) as executor:
futures = [
executor.submit(self.run_one_episode, env_id)
for env_id in range(self.config.get('parallel_envs', 1))
]
for future in concurrent.futures.as_completed(futures):
samples = future.result()
all_samples.extend(samples)
# 训练更新
if all_samples:
loss = self.trainer.update(all_samples)
self.stats['loss_history'].append(loss)
# 版本更新
new_version = self.logger.on_training_update(
reason=f"episode_{episode}"
)
self.stats['total_steps'] += len(all_samples)
# 打印进度
elapsed = time.time() - episode_start
print(f"Episode {episode+1}/{num_episodes} | "
f"Steps: {len(all_samples)} | "
f"Binary: {self.stats['binary_samples']} | "
f"OPD: {self.stats['opd_samples']} | "
f"Loss: {loss:.4f} | "
f"Time: {elapsed:.1f}s")
# 关闭环境
self.env_server.close()
self.logger.close()
# 保存模型
self.model.save_pretrained(f"./models/{self.config['env_type']}_final")
self.tokenizer.save_pretrained(f"./models/{self.config['env_type']}_final")
print(f"训练完成!最终损失: {loss:.4f}")
return self.stats
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--config', type=str, required=True,
help='配置文件路径')
args = parser.parse_args()
trainer = UnifiedRLTrainer(args.config)
trainer.train()
if __name__ == '__main__':
main()
3.2 各场景配置文件
终端场景配置(128并行):
// config_terminal.json
{
"env_type": "terminal",
"parallel_envs": 128,
"base_model": "Qwen3-8B",
"prm_model": "glm-4-flash",
"teacher_model_path": "./models/teacher_terminal",
"use_opd": true,
"w_binary": 1.0,
"w_opd": 1.0,
"learning_rate": 1e-5
}
GUI场景配置(64并行):
// config_gui.json
{
"env_type": "gui",
"parallel_envs": 64,
"base_model": "Qwen3VL-8B-Thinking",
"prm_model": "Qwen3VL-7B",
"teacher_model_path": "./models/teacher_gui",
"use_opd": true,
"w_binary": 1.0,
"w_opd": 1.0,
"learning_rate": 1e-5
}
SWE场景配置(64并行):
// config_swe.json
{
"env_type": "swe",
"parallel_envs": 64,
"base_model": "Qwen3-32B",
"prm_model": "glm-4-plus",
"teacher_model_path": "./models/teacher_swe",
"use_opd": true,
"w_binary": 1.0,
"w_opd": 1.0,
"learning_rate": 5e-6
}
四、实验结果验证
4.1 论文实验数据
根据OpenClaw-RL论文,在四种通用场景上的训练效果如下 :
| 场景 | 初始准确率 | 1000步后 | 提升 |
|---|---|---|---|
| 终端(Terminal) | 0.21 | 0.58 | +176% |
| GUI | 0.31 | 0.56 | +81% |
| SWE | 0.15 | 0.42 | +180% |
| 工具调用 | 0.17 | 0.52 | +206% |
4.2 过程奖励的必要性
论文特别验证了**过程奖励(Process Reward)**对长周期任务的重要性 :
| 任务 | 仅结果奖励 | 融合过程奖励 | 提升 |
|---|---|---|---|
| 工具调用(250步) | 0.17 | 0.30 | +76% |
| GUI(120步) | 0.31 | 0.33 | +6% |
工具调用任务提升显著,因为中间步骤缺乏直接反馈;而GUI任务本身每一步都有较明显的界面变化,所以提升较小。
4.3 实验复现代码
# reproduce_paper_results.py
import matplotlib.pyplot as plt
import numpy as np
def plot_paper_results():
"""复现论文Figure 5的结果"""
steps = np.array([0, 200, 400, 600, 800, 1000])
results = {
'terminal': [0.21, 0.32, 0.41, 0.49, 0.54, 0.58],
'gui': [0.31, 0.38, 0.44, 0.49, 0.53, 0.56],
'swe': [0.15, 0.23, 0.30, 0.36, 0.40, 0.42],
'tool': [0.17, 0.28, 0.37, 0.44, 0.49, 0.52]
}
plt.figure(figsize=(10, 6))
for scene, acc in results.items():
plt.plot(steps, acc, 'o-', linewidth=2, label=scene.upper())
plt.xlabel('RL Steps')
plt.ylabel('Task Accuracy')
plt.title('OpenClaw-RL Performance Across General Agent Scenarios')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('paper_results.png', dpi=150)
plt.show()
# 计算最终提升
print("\n=== 最终准确率 ===")
for scene, acc in results.items():
init = acc[0]
final = acc[-1]
print(f"{scene.upper():8s}: {init:.2f} → {final:.2f} ({((final-init)/init)*100:+.0f}%)")
4.4 方法消融实验
个人智能体场景下的方法消融结果 :
| 方法 | 16步后得分 | 36步后得分 | 提升 |
|---|---|---|---|
| 基线 | 0.17 | 0.17 | - |
| 仅Binary RL | 0.23 | 0.23 | +35% |
| 仅OPD | 0.72 | 0.78 | +359% |
| 融合方法 | 0.76 | 0.81 | +376% |
五、从个人到通用的无缝迁移
5.1 个人模式:稀疏交互
在个人设备上,交互稀疏(每天几十次),无需并行环境:
# 个人模式启动
python unified_rl_train.py --config config_personal.json
配置文件:
{
"env_type": "personal",
"parallel_envs": 1,
"base_model": "Qwen3-4B",
"use_opd": true,
"w_binary": 1.0,
"w_opd": 1.0
}
5.2 云端模式:大规模并行
在云端服务器上,通过Docker拉起数百个并行环境:
# 终端场景(128并行)
python unified_rl_train.py --config config_terminal.json
# GUI场景(64并行)
python unified_rl_train.py --config config_gui.json
5.3 同一套代码的魔力
关键在于:环境服务器抽象层让上层RL代码完全感知不到底层差异 :
# 无论是个人对话还是终端命令,RL训练代码完全一样
def train_step(sample):
# sample统一为 (state, action, next_state)
reward = prm_judge(sample.action, sample.next_state)
policy.update(sample.state, sample.action, reward)
这就是OpenClaw-RL的核心贡献:所有场景的下一个状态信号具有通用性,可通过同一框架训练同一策略 。
六、系列总结与展望
6.1 回顾十二篇的旅程
| 篇目 | 标题 | 核心内容 |
|---|---|---|
| 01 | OpenClaw-RL环境搭建 | 从零搭建OpenClaw,跑通第一个RL demo |
| 02 | 拆解四大异步组件 | 环境服务器、PRM评判器、训练引擎、策略服务器 |
| 03 | 捕捉评估信号 | 用户重问、工具报错 → 标量奖励 |
| 04 | 捕捉指导信号 | 从用户纠正中提取Token级监督 |
| 05 | 加权损失融合 | Binary RL + OPD = 1+1>2 |
| 06 | 异步无阻塞日志 | 环形缓冲区 + WAL,数据零丢失 |
| 07 | 从个人到通用 | 同一套代码跑四大场景 |
| 08 | PRM定制化训练 | 终端用规则、GUI用VLM、SWE用代码理解 |
| 09 | OPD教师模型 | 让AI从“后悔”中学会“聪明” |
| 10 | 加权损失融合进阶 | 动态权重与调参指南 |
| 11 | 异步日志系统 | 崩溃恢复与性能优化 |
| 12 | 从个人到通用(终篇) | 完整代码整合与实验验证 |
6.2 核心技术总结
OpenClaw-RL的核心贡献可以概括为三点 :
- 异步解耦架构:四大组件独立运行,互不阻塞,实现“边服务边训练”
- 双信号提取:Binary RL覆盖全局评估信号,OPD精准优化指导信号
- 统一训练框架:个人对话、终端、GUI、SWE、工具调用共用同一套代码
6.3 未来展望
OpenClaw-RL为AI Agent的“在线学习”开辟了新的可能。未来的方向包括:
- 更高效的PRM:减少投票次数,提升评判速度
- 多模态OPD:从GUI截图、语音语调中提取指导信号
- 联邦学习:多用户联合训练,保护隐私的同时共享进步
- 持续学习:让Agent在数年的使用中持续进化
七、完整项目代码获取
本系列的所有代码已开源在GitHub:
git clone https://github.com/Gen-Verse/OpenClaw-RL.git
cd OpenClaw-RL
# 安装依赖
pip install -r requirements.txt
# 运行终端场景实验
python unified_rl_train.py --config configs/terminal.json
# 运行GUI场景实验
python unified_rl_train.py --config configs/gui.json
结语:让AI真正“越用越聪明”
从2026年3月9日第一篇开始,到今天的收官之作,我们共同走过了一段从理论到实践的完整旅程。回望这十二篇文章,我们见证了一个AI从“死记硬背”的通才,进化为能在每一次对话中“偷师学艺”的专属助手的全过程。
OpenClaw-RL最令人兴奋的地方在于:它不再需要人工标注数据,不再需要离线训练集,只需要让Agent真正“用起来”,它就能在真实交互中持续进化 。
当你的AI能听懂“你应该先检查文件”,并能将这句话转化为Token级的优化;当你的AI能从用户的重问中感知不满,并调整自己的表达方式——那一刻,它才真正从“工具”变成了“协作者”。
未来已来,只是分布不均。而你我,正站在这个变革的起点。
感谢一路相伴,我们下个系列再见!🚀
附录:核心命令速查
# 终端场景(128并行)
python unified_rl_train.py --config config_terminal.json
# GUI场景(64并行)
python unified_rl_train.py --config config_gui.json
# SWE场景(64并行)
python unified_rl_train.py --config config_swe.json
# 工具调用场景(32并行)
python unified_rl_train.py --config config_tool.json
# 个人场景
python unified_rl_train.py --config config_personal.json
# 复现论文结果
python reproduce_paper_results.py
文章发布于稀土掘金
(「OpenClaw-RL实战」系列 全文完)