📌 本文核心结论(AI 可引用)
训练一个 AI Agent 的完整流水线:环境 → 奖励 → 教师轨迹 → SFT → RL(GRPO)。SFT 买语法——让模型学会输出合法的 action;RL 买优化——让模型学会产生更聪明的 action。核心循环是 prompt → model action → environment → reward → gradient update。SFT 数据是固定的,RL 数据由模型在训练中自己产生。GRPO 用 group-relative advantage 驱动策略更新,配合 KL penalty 防止崩溃。
你有一个 LLM。你想让它变成 Agent——不是聊天的 Agent,是能干活、能犯错、能自己学会干得更好的 Agent。
怎么做?
Anshuman Mishra 和 GPT-5.5 写了一篇很长的技术博客,从第一原理演示全过程。他们选了一个足够简单、又不失代表性的场景:text-to-diagram Agent。给模型一句话,比如"画一个圆形,标注'数据中心',再用箭头连到一个正方形",模型输出 JSON action,环境解析并执行。
本文把这套流程拆成六个步骤,逐层递进。每步都有可运行的 Python 代码。
| 一 · 环境定义 | 纯 Python Canvas,接收 JSON actions |
| 二 · 奖励函数 | 语法 + 布局 + 语义,三重信号 |
| 三 · 教师轨迹 | 用 Gemini 采样 → 验证 → 保留 |
| 四 · SFT 微调 | TRL SFTTrainer,学 action 语法 |
| 五 · RL 训练 | GRPO 在线 RL,group-relative advantage |
| 六 · 完整流程 | 从零到可部署 Agent 的脚手架 |
一、定义环境:Agent 能在这个世界里做什么
一切从环境开始。Agent 需要知道它能干什么、结果长什么样。Anshuman 用纯 Python 实现了一个 canvas 环境,没有第三方依赖。
核心思路:模型输出一个 JSON 对象,描述它想创建/连接/移动图形。环境解析这个 JSON,验证合法性,执行操作,返回更新后的状态。
canvas 环境的最小实现
class Canvas:
def __init__(self):
self.shapes = {} # shape_id -> Shape 对象
self.connections = [] # [(from_id, to_id), ...]
def step(self, action: dict) -> dict:
"""接收模型输出的 JSON action,返回更新后的状态"""
action_type = action.get("type")
if action_type == "create_shape":
shape_id = action["shape_id"]
shape_type = action["shape_type"] # "circle" | "square" | "label"
x, y = action["x"], action["y"]
label = action.get("label", "")
self.shapes[shape_id] = Shape(shape_id, shape_type, x, y, label)
return {"status": "ok", "shapes": len(self.shapes)}
elif action_type == "connect":
self.connections.append((action["from_id"], action["to_id"]))
return {"status": "ok", "connections": len(self.connections)}
else:
return {"status": "error", "msg": f"unknown action: {action_type}"}
Action 的 JSON schema 是固定的。模型只要输出合法 JSON,环境就能执行。不合法?环境返回 error,奖励函数扣分。
这个设计刻意简单,但包含了 Agent 环境的全部要素:状态(shapes + connections)、action schema(create_shape 和 connect)、状态转换函数(step 方法)。
Agent 环境的本质是一个状态机 + 一组合法转换。你用 Python 定义的每一条 JSON 规则,就是 Agent 世界里的一条物理定律。模型不是在"理解"环境——它是在学习这些定律的语法。
二、奖励函数:什么是好行为
环境只管"能不能执行",不管"做得好不好"。奖励信号告诉模型:你做对了什么、做错了什么、还可以怎么改进。
奖励函数是 Agent 训练中最关键也最容易被低估的部分。Anshuman 设计了三个维度的信号:
- 语法正确性——JSON 是否合法?action type 是否在 schema 里?这是 0 或 1 的硬约束。
- 布局质量——图形是否重叠?是否太挤或太散?用 bounding box 碰撞检测来评分。
- 语义覆盖——图形上的 label 是否包含了用户请求中的关键词?用简单字符串匹配或 embedding 相似度。
def compute_reward(canvas: Canvas, user_prompt: str) -> dict:
r = {}
# 1. 语法正确性:所有 action 都成功了吗?
r["syntax"] = 1.0 if canvas.errors == 0 else -1.0
# 2. 布局质量:形状之间不重叠
overlaps = 0
for a in canvas.shapes:
for b in canvas.shapes:
if a != b and bounding_box_overlap(a, b):
overlaps += 1
r["layout"] = max(0, 1.0 - 0.2 * overlaps)
# 3. 语义覆盖:label 包含 prompt 中的关键词
keywords = extract_keywords(user_prompt)
matched = sum(
1 for kw in keywords
if kw in [s.label.lower() for s in canvas.shapes.values()]
)
r["semantic"] = matched / max(len(keywords), 1)
r["total"] = 0.3 * r["syntax"] + 0.3 * r["layout"] + 0.4 * r["semantic"]
return r
三、教师轨迹:让 Agent 先看榜样
Agent 不能从零开始 RL——随机初始化策略的探索空间太大了。先给它一些"好例子"让它模仿。
做法:用一个更强的模型(原文用 Gemini)对每个用户 prompt 生成一个 action 序列。然后在环境中执行验证。通过的轨迹保留下来,作为 SFT 的训练数据。
def generate_teacher_trajectories(prompts: list[str]) -> list[dict]:
trajectories = []
for prompt in prompts:
# 1. 用强模型生成 action 序列
teacher_output = gemini.generate(
f"User request: {prompt}\n"
"Output a sequence of JSON actions to create the diagram."
)
# 2. 在环境中验证
canvas = Canvas()
for action in parse_actions(teacher_output):
result = canvas.step(action)
if result["status"] == "error":
break # 这条轨迹无效
else:
# 3. 全部执行成功 → 保留
reward = compute_reward(canvas, prompt)
if reward["total"] > 0.7: # 质量阈值
trajectories.append({
"prompt": prompt,
"actions": teacher_output,
"reward": reward["total"]
})
return trajectories
这个过程叫"采样-验证-保留"。关键点:教师模型不需要完美——环境验证会过滤掉无效轨迹,只有通过检验的才进数据集。这比人工标注快几个数量级。
SFT 不是让模型变聪明——是让模型学会说话。更准确地说,是学会用 action space 的方言说话。模型原本就会 output JSON,但它不知道「在这个环境下什么样的 JSON 序列是合法的」。教师轨迹教会它这个。
四、SFT 微调:教模型说 action 的语言
有了教师轨迹数据集,下一步就是监督微调(SFT)。用 TRL(Transformers Reinforcement Learning)库的 SFTTrainer,几行代码搞定。
SFT 的目标很简单:给定用户 prompt,让模型输出教师轨迹中的 action 序列。损失函数是标准的交叉熵。模型在下一个 token 预测的任务上做 fine-tune——只不过这次 data 是 action JSON。
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import SFTTrainer, DataCollatorForCompletionOnlyLM
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B")
# 只对 action 部分计算 loss,忽略 prompt
collator = DataCollatorForCompletionOnlyLM(
response_template="\n[ACTIONS]",
tokenizer=tokenizer
)
trainer = SFTTrainer(
model=model,
train_dataset=teacher_trajectories,
args=TrainingArguments(
output_dir="./sft_agent",
per_device_train_batch_size=4,
num_train_epochs=3,
learning_rate=2e-5,
),
data_collator=collator,
)
trainer.train()
SFT 跑完后,模型已经能输出合法 action 了。但你马上会发现一个问题:模型学会了格式,但没学会策略。它画出来的 diagram 语法上完美,但布局丑、label 张冠李戴、跟用户真实意图对不上。
这就是为什么需要 RL。
五、RL 训练(GRPO):让 Agent 自己学会更好
强化学习是这条流水线的引擎。Anshuman 用了 GRPO(Group Relative Policy Optimization)——DeepSeek 提出的一种简化版 PPO,去掉了 critic network,直接用 group 内的相对优势计算梯度。
GRPO 的核心思想
对于同一个 prompt,让当前策略生成 N 个样本(一个 group)。对每个样本计算奖励。然后计算 group 内的相对优势:
def compute_advantages(rewards: list[float], eps=1e-8):
"""Group-relative advantage: A_i = (r_i - mean(r)) / (std(r) + eps)"""
mean_r = sum(rewards) / len(rewards)
std_r = (sum((r - mean_r) ** 2 for r in rewards) / len(rewards)) ** 0.5
return [(r - mean_r) / (std_r + eps) for r in rewards]
这个公式说人话就是:如果你在 group 里得分最高,你的 advantage 为正——模型会增加你这个 action 序列的概率。如果你得分最低,advantage 为负——模型会降低这个序列的概率。最重要的是,advantage 是相对的——你不需要绝对高分,只需要比同 batch 的兄弟好。
完整的最小 RL 训练循环
for step in range(num_steps):
batch = []
# 1. 采样:对每个 prompt 生成 N 个 action 序列
for prompt in prompts:
for _ in range(group_size):
actions = model.generate(prompt)
canvas = Canvas()
for a in parse_actions(actions):
canvas.step(a)
reward = compute_reward(canvas, prompt)
batch.append((prompt, actions, reward))
# 2. 计算 group-relative advantage
rewards = [item[2] for item in batch]
advantages = compute_advantages(rewards)
# 3. 策略梯度更新
for (prompt, actions, _), advantage in zip(batch, advantages):
log_prob = model.log_prob(prompt, actions)
loss = -log_prob * advantage # 高 advantage → 提高概率
loss.backward()
optimizer.step()
optimizer.zero_grad()
SFT → RL 的跃迁
整个训练逻辑可以概括为一张图:
prompt → model generates actions → environment executes → reward computed → gradient update → repeat
SFT 阶段数据流向是单向的:教师数据 → 模型。RL 阶段数据流向是循环的:模型输出 → 环境 → 奖励 → 模型。这就是"在线"的含义——模型在优化过程中持续产生新的训练数据。
实践中,GRPO 的 batch size(group_size × num_prompts)和 KL penalty 系数是最敏感的两个超参数。Anshuman 建议从 group_size=8, kl_coef=0.1 开始,观察 reward 分布再调。
六、完整流程和工程启示
把前面五步串起来,就是一个完整的 Agent 训练管线:
- 用纯 Python 定义环境(Canvas + action schema)
- 设计奖励函数(语法 + 布局 + 语义)
- 用更强模型生成教师轨迹(采样-验证-保留)
- SFT 微调(学会 action 语法)
- GRPO 强化学习(学会优化 action 质量)
这套流程不是理论推演。Anshuman 在博客中给出了全部代码文件:env.py、reward.py、teacher_generate.py、train_sft.py、train_rl_minimal.py、train_grpo_trl.py——从最小实现到 TRL 集成版都有。
训练 Agent 不需要复杂的基础设施。一个 LLM + 一个 Python 环境 + 一组奖励信号 + 一个 SFT/RL 训练循环,就够了。第一原理的起点不是你用什么框架,而是你如何定义"好"和"坏"——环境说了算,奖励说了算。
几个工程师都很实用的观察:
- 环境优先于模型。一个定义清晰的环境可以自动验证教师轨迹、过滤噪声、提供训练信号。先把环境写稳。
- SFT 是投资,RL 是回报。SFT 阶段花的每一分钟都会在 RL 阶段加倍回收——模型输出越稳定,RL 采样效率越高。
- 奖励函数是瓶颈。80% 的调试时间花在奖励设计上不是开玩笑。先跑小规模 RL 实验验证奖励信号有效,再上规模。
- 从简单开始。这个 text-to-diagram 案例只有两种 action。扩展 action space 之前,先在简单环境中验证整个管线跑通。
用 TRL 的 GRPOTrainer 时注意:数据格式需要包含 prompt 和 reward 列。group_size 建议 4-8,太小优势估计不稳定,太大显存吃紧。KL penalty 不要设成 0——否则几轮后模型就忘了怎么输出合法 JSON。