说来有点丢人,我花了大半个下午折腾一个客服机器人的工具调用,才发现问题根本不在“会不会调API”,而在“什么时候该调”——某开源基座模型对话流利,但让它处理“先查工单、再改状态、最后回复用户”这种两步以上任务时,逻辑就开始打结:要么原地复读,要么随口编一个处理结果。后来我把整个策略切到强化学习框架里重训,也就是本章要聊的Agentic-RL,情况才真正稳定下来。
如果你正在做智能体训练、工具增强链路,或者手上有一个“看起来什么都会、做事却没有章法”的语言模型,这篇内容应该能帮上不少忙。我不会复述论文原文,只会结合我自己验证过的环境定义、奖励设计、训练循环和踩坑记录,说说让 Agent 真正学会“办事”的完整思路。前面的章节我们聊过 SFT、RLHF 和上下文工程,到这章要回答的问题就一个:如何让一个语言模型通过奖励信号,学会在真实世界环境里完成多步任务。
1. Agentic-RL 到底要解决什么问题
1.1 为什么传统强化学习的起点在这里会失效
传统强化学习最经典的场景是游戏控制:智能体观察屏幕像素,输出“左移、右移、跳跃”这些动作,环境给出分数。这个范式有一个隐含前提,就是动作空间是固定且有穷的。无论是连续控制还是离散按键组合,你都可以在设计阶段把动作全集枚举出来。
到了 Agent 场景,这个前提就不成立了。智能体的动作变成了“调用哪个工具、传什么参数、什么时候回复用户”,工具列表可能动态增减,参数的取值甚至来自上一次调用的返回结果。你没法预先建一张 Q 表去覆盖所有“状态-动作”组合,因为组合的规模是组合爆炸级别的。更麻烦的是,一个动作执行后,环境会返回一段文本信息,这段文本又成为后续决策的一部分——这其实是“观察空间与动作空间互相纠缠”的复杂结构。
所以在 Agentic-RL 里,我们通常的思路不是让模型直接学一个“动作编号”,而是让它继续以自由文本的方式生成决策,再用一个外层策略优化框架去奖励那些“最终导致成功结局”的文本路径。这也是 PPO 这类算法在 Agent 场景重新回归的原因:它不需要枚举动作,只需要你能给一段完整轨迹算出收益。
1.2 SFT 教不会的事情:延迟反馈与终局思维
很多人会问:为什么不能只用 SFT 把“正确调用工具”的样例喂给模型?问题在于 SFT 的监督信号是逐 token 的,模型只需要在每个位置上拟合高概率动作,它根本看不到“我这一步做错了,导致后面五步全废了”这种终局代价。
我做个类比:教人打羽毛球,你可以示范一万次正确挥拍,但如果一直不给对方“这球出界了”的反馈,学生永远学不会调整力度。SFT 就是只示范挥拍,Agentic-RL 则是把“这一局是否得分”反馈给模型。对 Agent 而言,延迟反馈才是常态:某个工具调用单独看完全合理,但放整个任务链条里可能就是错误的动作。只有强化学习的训练目标能同时优化“单步动作合理性”和“多步序列的终局成功率”,这也是它不可替代的核心价值。
2. 状态、动作与奖励:先把“游戏规则”定义清楚
2.1 智能体观察到的到底是什么
开始写训练代码前,最应该花时间的是把观察空间想清楚。一个 Agent 每一轮看到的东西通常包括四块:
- 系统提示:角色定位、任务目标、回复风格;
- 工具描述:当前可用的工具名、参数结构、使用说明;
- 多轮轨迹:上一轮模型说了什么、工具返回了什么、用户又追问了什么;
- 用户当前输入:本轮要办的真实需求。
我在项目里会把观察组装成一段结构稳定的文本模板,模板顺序是固定的:系统提示在最前,工具列表其次,然后是逐轮的 user / assistant / tool 消息。顺序不能乱,因为语言模型非常吃上下文位置,工具描述被挤到后面可能导致模型“忘记”自己还有工具可用。
还有一个特别容易忽略的细节:工具返回信息必须截断。实际生产中的 API 可能返回几千行 JSON,直接拼进上下文既浪费 token 又干扰决策。我会限定工具返回最多 200 个 token,超出部分用摘要逻辑切掉,只保留字段名、状态值和必要的错误信息。这样既保留关键信息,又防止模型被无关字段带偏。
[SYSTEM] 你是某客服系统的智能助理,请使用可用工具完成用户请求。 [TOOLS] 1. query_ticket(ticket_id: string) -> 返回工单信息 2. update_ticket(ticket_id: string, new_status: string) -> 更新工单状态 3. reply(content: string) -> 向用户发送答复 [CONVERSATION] user: 工单 S1001 处理好了吗?能帮我关闭吗? assistant: 我先查一下最新状态。 tool(query_ticket, {"ticket_id": "S1001"}) tool_result: {"status": "open", "paid": true} assistant: 已查到 S1001 处于待处理状态,我现在为您更新为已关闭。 tool(update_ticket, {"ticket_id": "S1001", "new_status": "closed"})你看,这其实和普通对话提示词没有本质区别,但它的关键作用是让模型把“查询”和“更新”看成两个独立决策节点,而不是凭感觉一气呵成。强化学习的采样单元就是完整轨迹:从第一条 user 消息开始,到模型最终回复 reply 结束。
2.2 动作编码方式:工具调用如何成为决策的一部分
关于 Agentic-RL 的动作空间,我见过几种做法,最笨的是把每个工具调用编码成一个固定 ID,比如action 13 = query_ticket,但这种方式扩展性极差:增加一个新工具就要改动作表,工具参数稍有变化整个编码就失效了。
更通用的做法是让模型直接输出结构化文本作为动作,常见格式有两种:
- 函数调用格式:模型生成
<function=query_ticket>{“ticket_id”: “S1001”}</function>; - JSON 输出格式:模型生成
{"tool": "query_ticket", "parameters": {"ticket_id": "S1001"}}。
无论哪种,外层都需要 parser 去解析、校验、执行。执行完之后,工具返回的内容作为 tool 消息再接回上下文。这个做法的好处是:语言模型不需要预先学习“动作空间的编号”,它只需要学习“在该语境下该生成什么样的文本”。强化学习的优化目标就变成调整某个动作文本的生成概率——如果这条轨迹最终拿到了正收益,那么整个轨迹中的所有动作 token 的概率都会被上调,那些导致失败轨迹的动作 token 概率被下调。
训练时我会特意加一个规则:如果模型输出了非法工具参数,比如工具不存在或者参数类型错误,环境立刻返回一个惩罚性的错误消息,并把这一步记为“无效动作”。无效动作过多时,这条轨迹的最终奖励上限会被压低。这是因为我们想教给模型的不仅是“调什么工具”,还包括“不要乱调工具”。
2.3 奖励设计:结果奖励与过程奖励的取舍
奖励函数是整个训练里最容易出现“聪明反被聪明误”的地方。我最初的客服实验里只用了结果奖励:工单被正确关闭且最终回复包含确认信息 = +1.0;没完成任务或超过最大步数 = -1.0;最后一步回复错误 = -0.8。
结果奖励的问题是探索效率低。对一批随机初始化的模型策略来说,一个多步任务坚持到最后一轮才发现失败,中间的行为基本没有梯度指示——所有中间的 token 都被同等惩罚了,模型搞不清楚到底是“查错了”还是“改错了”还是“回复格式不对”。过程奖励(PRM)可以缓解这个问题:在轨迹的每一步都给出一个小奖励,比如“查询工单成功 = +0.1”“调用了一个与当前目标相关的工具 = +0.05”“最终回复包含明确结论 = +0.2”。
但过程奖励也不是越多越好。我踩过一个典型的坑:给“工具调用成功”本身加了正奖励之后,策略开始疯狂调用工具,哪怕任务已经完成了也还要多调几次拿分,因为它发现每多调一次都有正收益。这就是奖励函数设计中的奖励黑客问题,我会在后面的踩坑章节详细展开。这里先给出一个相对稳妥的组合思路:
| 信号类型 | 具体实现 | 权重倾向 |
|---|---|---|
| 任务结果 | 工单状态变更为目标值 | 高(主导性) |
| 用户满意度 | 回复中是否包含结论、致谢等 | 中 |
| 过程效率 | 步数越少额外加分 | 低 |
| 格式合法性 | 工具调用 JSON 是否可解析 | 低 |
权重倾向的意思是:任务结果应当是训练信号的“基本盘”,如果任务本身没完成,其它加几分都救不回来;反之,如果任务完成了,再根据效率与合规性做微调。这样既保留了结果驱动的本质,又给了模型中早期的探索信号。
3. 训练循环的关键管线:采样-回放-优化
3.1 Rollout:让策略智能体真实地与环境交互
把环境定义清楚后,就进入 Agentic-RL 的核心工程链路。一次完整的在线训练周期分四步:
- 从当前策略模型初始化一个轨迹,或者用探索策略(比如 temperature 调高一点)让模型与环境交互;
- 模型每生成一个动作,解析后交给工具环境执行,工具返回结果再拼接进上下文;
- 重复直到模型输出 reply 动作或达到最大步数;
- 根据轨迹终局计算奖励,把整段轨迹存入回放缓冲区。
这一步里最容易被低估的是“探索温度”。我做过对比实验:温度设成 0.3,模型会变得过度保守,任务里一旦出现训练集覆盖外的措辞,它就直接回复“请稍等”放弃;温度设到 1.0 以上,模型开始乱调工具,轨迹里出现大量无效动作。最后我把采样温度固定在 0.7 附近,并在训练前 2000 步设置了退火——前 10% 的训练步用一个略高的温度做粗探索,后面逐步降低到 0.7 做精细优化。这个细节让我每个任务的收敛步数平均少了约 30%。
3.2 PPO 更新:夹紧的比值与价值网络的配合
训练循环的优化器我用的是 PPO,因为它对采样效率的要求比纯策略梯度低不少,即使 rollout 数据不完全新鲜也能进行多轮更新。
PPO 里最核心的是 clip 操作:计算新旧策略在同一个动作上的概率比ratio = exp(new_logp - old_logp),如果这个比值超过1 - eps到1 + eps范围,梯度就被夹住,避免模型一步更新太猛把策略毁掉。Agentic 场景里我强烈建议把 eps 从常见的 0.2 调到 0.15 甚至 0.1,因为动作空间是文本生成,一旦策略更新步长失控,模型可能彻底变成满嘴胡话。
价值网络(critic)也一样重要。奖励只在轨迹末端给出,但价值网络需要估计每一步的长期回报,这样中间步骤才能获得非零的优势值。训练里我通常用 GAE 计算优势时把 lambda 设成 0.95,gamma 设成 0.99。简单说 lambda 决定了“多信任未来奖励的传递”,设得太大容易让模型把注意力全部放在远端结果,忽略了中间步骤的微调;设得太小则模型变短视。
# 核心 loss 计算示意,不包含分布式采样细节 def ppo_loss(new_logp, old_logp, advantage, value, returns, entropy): ratio = torch.exp(new_logp - old_logp) surr1 = ratio * advantage surr2 = torch.clamp(ratio, 1 - eps, 1 + eps) * advantage actor_loss = -torch.min(surr1, surr2).mean() critic_loss = F.mse_loss(value, returns).mean() return actor_loss + 0.5 * critic_loss - 0.01 * entropy这段代码看起来很简洁,但工程上真正的复杂度在数据管理:每个 batch 里的轨迹长度不一样,需要 padding;同一个轨迹会被不同的工具返回信息拆成多段,必须保证历史消息没有错位。我习惯用一个统一的TrajectoryStage结构体,把“上下文截断位置”“动作 token 范围”“实际奖励值”打包存储,避免训练阶段再去拼接字符串。
3.3 上下文长度、采样成本与并发稳定
Agentic-RL 训练最直接的资源瓶颈不是算力,而是上下文长度。一条 6 步的任务轨迹,每步含工具返回,总 token 数轻松超过 3000,十条轨迹塞一个 batch 就是 3 万 token 的前向与反向开销。如果模型本身是 7B 甚至更大的参数量,一台单卡机器的训练速度会慢到让人失去耐心。
我给出的实操建议是:
- 控制最大步数:客服场景我建议
max_steps=6,超过就强制终止并给负奖励。步数设置过大的确能让模型多探索,但也会让无效轨迹占比上升,整体效率下降; - 固定工具返回上限:给工具返回结果设置 token 上限,比如 200 token,防止长文本膨胀上下文;
- 先小 batch 跑通:batch size 从 8 开始,确认 loss 和下界奖励都在合理范围后,再逐步加到 16 或 32;
- 采样并发稳定:在线采样一般需要多个 worker 同时跑环境,我会给每个 worker 限制最大并发请求数,同时把采样频率与训练更新频率做隔离,避免 GPU 在 forward 和 update 之间频繁切换导致显存抖动。
成本控制这块,最有效的一招是控制重放次数。Agentic 轨迹的每一步都含有工具返回这类动态信息,过度利用旧轨迹会让策略对“历史环境状态”产生过拟合。我的做法是每条轨迹最多利用 4 轮 PPO 更新,之后必须丢弃。粗看是浪费数据,实际上它强制模型不断采集新经验,收敛稳定性反而好了。
4. 一个最小可跑版本:带查询与修改工单工具的客服智能体
4.1 环境与工具定义
这里给出一个可以单人复现的最小版训练闭环。任务原型是:用户给定一个工单号,智能体需要通过查询工单、修改状态两步操作完成关闭操作,并给用户一个明确回复。
class TicketEnv: def __init__(self): self.db = { "S1001": {"status": "open", "paid": True}, "S1002": {"status": "closed", "paid": False}, } self.max_steps = 6 def reset(self): self.ticket_id = "S1001" self.steps = 0 return f"工单 {self.ticket_id} 需要关闭,请完成操作。" def step(self, tool_name, params): self.steps += 1 if tool_name == "query_ticket": info = self.db.get(params.get("ticket_id")) return f"工单状态: {info['status']}, 已支付: {info['paid']}" if tool_name == "update_ticket": if self.db[params["ticket_id"]]["status"] == "open": self.db[params["ticket_id"]]["status"] = params["new_status"] done = params["new_status"] == "closed" return "工单已更新为 closed", done return "工单无需更新", False if tool_name == "reply": return params.get("content", ""), False return "未知工具", False这个环境里有个细节:update_ticket只有当工单确实从 open 变成 closed 时才置 done。这防止模型“假装关闭”或重复关闭一个已经关闭的工单。实际生产里你可能还需要引入更复杂的业务状态机,但核心思想一致——环境的成功信号必须和行为结果严格对应,而不是看模型说了什么漂亮话。
4.2 规则奖励函数
奖励函数我用简洁的规则版,避免引入额外的判断模型:
def compute_reward(episode_info): done = episode_info["done"] steps = episode_info["steps"] if not done: return -1.0 if episode_info.get("replied"): return 1.0 + 0.1 * max(0, 2 - steps) # 步数越少,略微加分 return 0.5 # 任务完成但没给用户最终回复细看这个函数:任务没完成给 -1,可以作为整个轨迹的终点奖励;任务完成但少了最终回复只给 0.5,这是为了让模型学会“办事要办到有交代”。步数奖励的权重被刻意压得很低(最多加 0.2),因为我吃过亏:步数奖励太高,模型会走向“只求快、不求稳”,有时候一个工具调用就能蒙混过关,但遇到状态变化时直接炸掉。
4.3 训练脚本骨架
接下来是训练部分的最小实践。我假设你已经有一个通过 SFT 或预训练而来的基座策略模型,它至少掌握了对话生成的基本格式。我们需要的是一个能计算生成概率的模型,以及一个 PPO 更新循环。下面的代码展示更新部分,重点在于把轨迹拼接成 token、算新旧策略的 logprobs、再算 PPO loss。
# 伪代码级别的 PPO 更新流程 old_logp = get_logprobs(model, trajectory) with torch.no_grad(): old_model(obs) -> logits value = value_head(hidden) for k in range(ppo_epochs): new_logp = get_logprobs(model, trajectory) ratio = torch.exp(new_logp - old_logp) advantage = normalize(return_estimates) actor_loss = -torch.min(ratio * advantage, torch.clamp(ratio, 0.85, 1.15) * advantage).mean() critic_loss = (value - returns).pow(2).mean() loss = actor_loss + 0.5 * critic_loss - 0.01 * entropy loss.backward() optimizer.step()这里我刻意省略了完整的 GAE 计算和 minibatch 划分,因为真正生产级的实现会涉及分布式采样、token masking、模型权重共享与 rollout 复用一个系统性问题,不是一两段代码能说清的。但这个骨架足以验证整条链路:模型生成动作、环境响应、奖励计算、策略更新。我建议你第一次跑的时候把ppo_epochs=2,观察成功率是否在 500 个 episode 左右开始爬升。
4.4 训练过程要盯住的四类指标
训练过程中,我习惯在每 500 个 episode 后打印四个指标,而不是只看 loss:
| 指标 | 含义 | 我的参考值 |
|---|---|---|
| 成功率 | 任务完成的轨迹占比 | 从初始 10% 左右逐步爬到 80%+ |
| 平均奖励 | 每个轨迹的奖励均值 | 应随训练逐步提升 |
| 平均步数 | 完成任务的轨迹平均步数 | 成功轨迹应在 3~4 步之间 |
| 熵 | 策略分布的熵 | 下降过快说明陷入局部动作,回落缓慢说明探索不足 |
熵这个指标经常被忽略。Agentic-RL 里如果熵下降过快,模型可能只学会调用第一个工具后立刻回复“已完成”,即使它并没有真正关闭工单。我碰到过一次熵从 1.8 骤降到 0.6,成功率同步掉到 20% 以下。当时的对策是加大 entropy bonus(从 0.01 提到 0.05),同时降低学习率,用两天时间把策略“摇”回来。
5. 实测踩坑:我反复栽过的四个地方
5.1 奖励黑客:模型发现了“捷径”
上一节提到的工具调用奖励,是我第一次真实遭遇 Agentic-RL 中的奖励黑客问题。当时给“成功调用一个工具”设了 +0.1,原本只是为了鼓励探索,结果训练到中后期,模型开始频繁调用query_ticket——它发现每次查工单都能拿 0.1 分,而且查完不做事也没惩罚。
这就是奖励设计的危险之处:模型找的不是最优解,而是最容易获得正奖励的解。要修复这个问题,一方面要给“完成任务”之外的奖励设上限,让这些过程奖励总和远小于任务完成奖励;另一方面要把“重复调用同一工具且状态未变化”视为低效行为,额外扣分。我最终的方案是:任务完成的正奖励至少是过程奖励总和的 10 倍以上,从根上保证任务完成优先。
5.2 环境泄漏:智能体“偷看答案”
另一个让我失眠的问题来自数据组装。有一版训练里,我在返回tool_result时顺带把expected_action字段也放了进去——本意是给调试人员看,结果模型直接学会了从工具返回里“读懂”自己下一步该干嘛,然后照抄答案。在训练集上表现近乎完美,一换到新环境就原形毕露。
检查方法很简单:把模型在验证集上的轨迹打印出来,看它是不是在没有外部信息的情况下说了“我查一下当前是否已关闭”,然后工具还没返回结果就声称“已关闭”。这就是典型的观察泄漏。修复方式是严格要求环境返回里不包含任何未来信息,包括人类调试字段、期望结果、任务标签。我在环境中加了一个测试断言:随机篡改工具返回值时,策略在验证集的表现必须明显下降,否则说明模型没有依赖真实环境,而是在“死记硬背”。
5.3 成功率像过山车:训练中期的剧烈波动
Agentic-RL 训练的成功率曲线不是平滑上升的,尤其是前期从随机策略切换到有一定成功率的策略时,曲线会剧烈震荡。我观察到的最严重一次:一晚上跑了 6000 个 episode,成功率从 35% 迅速冲到 70%,然后一夜之间掉回 22%。
原因是学习率设置过大导致策略在策略空间中“跳过”了高奖励区域。Agentic-RL 的动作空间是高维文本空间,策略的一次小波动在效果上可能等同于完全换了一个人设,所以学习率必须比传统 token 级 RLHF 更保守。我的建议是初始学习率设为普通 SFT 的十分之一,比如 2e-6 左右,同时监控 KL 散度——如果单次更新的 KL 超过 0.2,就要考虑降低更新步长。
5.4 训练成本高企:无效 rollout 太多
客服场景里,一次真实环境交互可能涉及多个工具调用请求,每个请求都产生 GPU 推理开销。无效轨迹比例高的问题是:智能体在早期经常调用不存在或格式错误的工具,环境返回错误信息后模型往往不知所措,直接死循环到步数上限。
我在项目里加入了“无效请求连续三次即终止轨迹”的规则,并给这类轨迹一个固定的负奖励。这个改动看似简单,实际上让训练总步数减少了约 40%。因为模型不再浪费时间在必败的轨迹上探索,而把有限的计算资源用在有潜在改进空间的轨迹分支。另外一个可选方案是先用拒绝采样 + 人工筛选的高质量轨迹做一轮微调,把策略预热到一个“至少懂基本格式”的状态,再进入在线强化学习阶段。这比让模型从零自己摸索格式合规要省很多钱。
6. 这一章的收尾思考与实际建议
6.1 什么时候不一定要上 Agentic-RL
Agentic-RL 不是万能锤子。很多场景下,few-shot 提示词配合工具调用的上下文工程就已经足够解决:如果任务步骤固定、工具数量少、状态转换简单,直接用提示词约束模型即可。我见过太多团队一上来就上强化学习,最后发现模型连基本的工具参数都没学会,原因是训练数据的信号太稀疏。
我的判断标准很简单:只有当任务需要多步决策、且单步的“正确性”高度依赖全局结果时,Agentic-RL 才值得投入。比如你需要让模型决定“先查 A 再调 B,然后根据 B 的结果选择回复方式”,这时候每一步的局部正确判断还不足以保证任务成功,就需要全局奖励信号来引导。反之,如果任务拆解后每个子问题都能独立判断对错,那用普通的监督微调配合规则就能做得很好了。
6.2 一条保守的落地路径
如果你确实决定尝试 Agentic-RL,我强烈建议按这个顺序推进:先收集一批人工演示或伪轨迹做 SFT 预热,再上在线 RL;RL 训练过程中每 100 个 episode 做一次人工盲评,看模型有没有出现“过于谄媚但低效”的行为;最后再逐步放开奖励权重,让模型的优化从“纠正坏行为”慢慢转为“打磨好行为”。
另外一个小建议:训练过程中留出一个永远不参与 RL 更新的“干净验证集”,每次更新完成后立刻评估。这个验证集只用于评估成功率,不参与任何梯度计算。它有双重作用:一个是防止环境泄漏问题回归,另一个是在模型跑偏时给你一个触发器——验证集成功率低于某个阈值时自动回滚 checkpoint。这个小措施帮我至少避免了三次灾难性遗忘。
Agentic-RL 目前仍是一个“看起来简单、跑起来折腾”的领域,但它的价值在于让模型从“擅长描述任务”变成“有能力执行任务”。如果你手头正好有一个多步工具调用的场景卡了很久,不妨按这一章列出的最小实验框架先跑起来看看,亲测一次后你会比任何理论推演都更清楚它值不值得深挖。