Agent RL 工具调用
阶段 9 · 教小模型学会"使用工具"——从只会说话,到会动手解决问题
模型的盲区
经过前面八个阶段的训练,你的 MiniMind 已经能流利对话、区分好坏回答、甚至学会推理。但它有一个根本性的局限——
你问 "256 x 37 = ?",它靠训练数据猜,可能猜对也可能猜错。你问"北京今天天气",它只能编一个。训练数据再好,也覆盖不了所有事实和计算。
怎么办?给它工具。让模型学会在需要的时候调用计算器、查天气、翻译文本——从"背书的书呆子"变成"会查资料的实用派"。
这就是 Agent RL:用强化学习教会模型使用工具。
成长路线
图:MiniMind 从"学会说话"到"学会使用工具"的成长路线
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一条从左到右的横向成长路线,包含四个阶段节点 - 节点1(灰色):"Stage 3-4 预训练+SFT = 学会说话" - 节点2(灰色):"Stage 5-8 LoRA/蒸馏/DPO/RL = 学会说好话" - 节点3(红色高亮,大边框):"Stage 9 Agent RL = 学会使用工具" - 节点4(绿色高亮,大边框):"MiniMind Agent 能说话+会思考+会用工具" - 各元素关系:节点1用箭头指向节点2指向节点3指向节点4,依次递进 - 关键标注:节点3旁标注"本阶段重点",节点4旁标注"最终目标" - 配色建议:浅蓝/浅灰背景,已完成阶段灰色,当前阶段红色,目标绿色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
前八个阶段教模型"怎么说"和"说得好"。本阶段教它一个全新能力:什么时候需要借助外部工具,怎么调用,怎么利用结果。
本阶段路线图
1. Agent 概念
LLM + 工具 = Agent,从回答者到执行者
2. 工具定义
6 个模拟工具 + 调用格式
3. 模型如何调用工具
解析调用意图 + 安全执行
4. 让对话动起来
生成-调工具-再生成的循环
5. 奖励计算
工具对齐 + GT 匹配 + 格式惩罚
6. 训练:从单轮到多轮
突出与 GRPO 的关键差异
Part 1: Agent 概念
理解什么是 Agent,以及为什么 LLM 需要工具。
模型再能说会道,也只会"靠记忆回答问题"——算术可能算错,天气只能瞎编。给它工具,它就从"背书的书呆子"变成了"会查资料的实用派"。这一节建立 Agent 的基本概念:LLM + 工具 = 能决策、能执行的智能体。
- 什么是 Agent:Agent = LLM + 外部工具。模型生成特殊格式的标记来请求执行函数,外部系统解析后返回结果——本质是纯文本协议
- 核心工作流:用户提问 → Agent 理解意图并决策用哪个工具 → 调用工具拿到结果 → 基于结果组织回答
- 架构全景:文本协议层是核心——所有交互都是纯文本,模型不直接调用函数,通过 tool_call/tool_response 标签通信
什么是 Agent
定义:Agent = LLM + 使用外部工具的能力。模型在生成文本的过程中,可以插入特定格式的标记来请求执行外部函数,然后基于函数返回结果继续生成。
白话:想象你雇了一个助手。没有工具时,你问 256 x 37,他靠心算,可能算错。给了他计算器后,他会主动拿计算器按一下——结果一定准确。
这个比喻只为建立直观感受。实际上,模型只是生成了特殊格式的文本标记,外部系统解析这些标记后执行函数并返回结果——没有真正的"助手"在工作。
# Agent 的核心循环(伪代码)
while 模型还在输出:
text = model.generate() # 模型生成文本
calls = parse_tool_calls(text) # 解析工具调用
if calls:
results = execute_tools(calls) # 执行工具
text += format_results(results) # 结果注入上下文
else:
break # 没有工具调用,回答完毕
核心工作流
图:AI Agent 查天气的完整交互时序
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个三栏时序图,从左到右分别是"用户"、"AI Agent(MiniMind)"、"外部工具" - 时序步骤:用户发问"北京今天天气怎么样?"到Agent思考"需要查天气"(标注"1.理解意图")到Agent调用 get_current_weather(location=北京)到工具返回 28度晴到Agent基于结果组织回答(标注"2.基于工具结果回答")到回复用户"北京今天晴朗,气温28度!" - 各元素关系:用户和Agent之间用实线箭头表示对话,Agent和工具之间用实线/虚线箭头表示调用和返回 - 关键标注:Agent侧两个黄色注释框分别标注"1.理解意图:需要查天气"和"2.基于工具结果组织回答" - 配色建议:浅蓝/浅灰背景,用户蓝色,Agent绿色,工具橙色,关键箭头用橙色标注 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
两个关键能力:决策(何时用工具、用哪个、传什么参数)+ 整合(把工具结果融入最终回答)。Agent RL 的训练目标就是教会模型这两个能力。
架构全景
图:LLM Agent 架构全景
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:中央是一个大框标注"LLM(MiniMind 模型)",左侧是"用户输入",右侧是"外部工具系统"(包含计算器、天气API、翻译服务等小图标) - LLM 和工具系统之间有一个"文本协议层"的虚线框,里面展示 tool_call 和 tool_response 标签的示例 - 各元素关系:用户输入→LLM思考→生成tool_call→工具执行→tool_response→LLM继续→最终回答 - 关键标注:文本协议层旁标注"所有交互都是纯文本,模型不直接调用函数" - 配色建议:浅灰背景,LLM蓝色,工具系统绿色,协议层橙色虚线 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
Part 2: 工具定义
6 个模拟工具的定义、调用格式和模拟数据机制。
模型只会输出文字,怎么让它"调用工具"?答案是约定一套文本协议:模型输出 <tool_call> 标签,外部系统就执行对应函数。这一节介绍 MiniMind 定义的 6 个训练工具、它们的调用格式,以及为什么训练时用模拟数据而不是真实 API。
- 标签协议:
<tool_call>和<tool_response>标签机制,标签内必须是合法 JSON,包含 name 和 arguments - 6 个内置工具:calculate_math、unit_converter、get_current_weather、get_current_time、get_exchange_rate、translate_text,每个遵循 OpenAI Function Calling 标准
- 模拟数据机制:训练时不调真实 API——太慢、不稳定、要花钱。模拟执行器用 eval 做计算(带安全限制),CHECK_ARGS 做参数校验
为什么用标签
核心矛盾:模型只会输出文字,不会真的"按按钮"。那怎么让它调用工具?
解决方案:约定一套文本协议。模型输出特殊标签 <tool_call> 时,外部系统就执行对应工具。本质上,工具调用只是"生成特殊格式的文本"。
<!-- 模型发出:工具调用 -->
<tool_call
{"name": "calculate_math", "arguments": {"expression": "256*37"}}
</tool_call
<!-- 系统返回:工具响应(模型不生成这个) -->
<tool_response
{"result": "9472"}
</tool_response
格式要求:标签内必须是合法 JSON,包含 name(工具名)和 arguments(参数字典)。为什么不用其他格式(比如纯 JSON 或 XML)?因为标签对正则解析更友好,且与模型已有的特殊 token(如 <|im_start|>)风格一致。
交互实例
用户问:"帮我把 100 公里换算成英里"。完整的交互过程如下:
用户: 帮我把100公里换算成英里
模型的第一次输出:
模型需要学会两件事:正确生成 tool_call(选择对的工具、传对的参数),以及看到 tool_response 后用它组织最终回答。这两件事正是 Agent RL 训练的目标。
6 个内置工具
MiniMind 定义了 6 个模拟工具用于训练。以 calculate_math 为例看看工具定义长什么样:
{"type": "function", "function": {
"name": "calculate_math",
"description": "计算数学表达式",
"parameters": {"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"]}
}}
每个工具遵循 OpenAI Function Calling 标准:name 标识工具,description 帮助模型理解何时使用,parameters 定义输入规范。其余 5 个工具结构相同,只是字段不同:
| 工具名称 | 功能 | 必需参数 | 示例调用 |
|---|---|---|---|
calculate_math | 计算数学表达式 | expression | {"expression": "256*37"} |
unit_converter | 单位换算 | value, from_unit, to_unit | {"value":100,"from_unit":"km","to_unit":"miles"} |
get_current_weather | 获取天气 | location | {"location": "北京"} |
get_current_time | 获取时间 | 无必需参数 | {"timezone": "Asia/Shanghai"} |
get_exchange_rate | 查询汇率 | from_currency, to_currency | {"from_currency":"USD","to_currency":"CNY"} |
translate_text | 翻译文本 | text, target_language | {"text":"你好世界","target_language":"english"} |
模拟数据机制
为什么要模拟?训练时不调用真实 API——太慢、不稳定、要花钱。就像在教室里用模拟题训练,等学会了方法,换真实工具没问题。
# 模拟执行器(部分)
MOCK_RESULTS = {
"calculate_math":
lambda args: {"result": str(
eval(args.get("expression", "0"),
{"__builtins__": {}}))},
"get_current_weather":
lambda args: {"city": args.get("location"),
"temperature": "28°C", "condition": "晴"},
}
两个关键设计
- eval 的安全限制:
calculate_math使用eval()执行表达式,但__builtins__设为空字典,禁止调用危险函数。 - 参数校验 CHECK_ARGS:每个工具有校验函数,用于奖励计算时判断调用是否合法(不是运行时校验)。
# 参数校验示例
CHECK_ARGS = {
"calculate_math":
lambda a: bool(a.get("expression")),
"get_current_weather":
lambda a: bool(a.get("location")),
# ... 其余工具类似
}
Part 3: 模型如何调用工具
模型生成了 <tool_call> 文本,系统如何提取意图并安全执行?
上一节定义了工具的"字典",这一节搞定工具的"执行管线":从模型输出的一大段文本中准确提取工具调用,然后安全地执行。两个核心问题——怎么从混合文本中提取 JSON?怎么防止 eval() 执行恶意代码把训练进程卡死?
- 解析设计:用
<tool_call>标签做"信封",正则提取标签内 JSON。容错策略:解析失败直接跳过,不崩溃 - parse_tool_calls 代码:正则
<tool_call\n(.*?)\n</tool_call非贪婪匹配 + re.DOTALL,JSON 解析失败静默跳过 - 安全执行 execute_tool:1 秒超时保护防止 eval() 执行死循环,出错返回 None 不中断训练
为什么要这样解析
问题:模型的输出是一大段文本,里面可能混杂着自然语言和工具调用。怎么准确提取工具调用?
思路:用 <tool_call> 标签做"信封"——正则表达式找到标签,再解析里面的 JSON。标签机制的好处是:模型可以同时输出自然语言和工具调用,两者不冲突。
模型输出:
"让我帮你查一下天气"
解析失败怎么办?模型可能生成格式错误的 JSON。设计上采用容错策略:失败的调用直接跳过,不会导致训练崩溃。宁可漏掉一个调用,也不能让整个流程挂掉。
解析核心逻辑
def parse_tool_calls(text):
calls = []
for m in re.findall(
r'
正则拆解
| 部分 | 作用 |
|---|---|
<tool_call\n | 匹配标签开始 + 换行 |
(.*?) | 非贪婪捕获 JSON 内容 |
re.DOTALL | 让 . 匹配换行符 |
容错示例
# 正常输入 → 提取成功
parse_tool_calls(
'
安全执行工具
为什么需要超时保护?calculate_math 使用 eval() 执行用户输入的表达式。如果模型传了 "while True: pass",没有超时保护会卡死整个训练进程。
def execute_tool(name, args):
fn = MOCK_RESULTS.get(name)
if not fn: return None # 工具不存在
try:
signal.alarm(1) # 1秒超时保护
return fn(args) # 执行工具
except:
return None # 出错就返回None
finally:
signal.alarm(0) # 取消超时
执行管线的完整流程:模型输出 → parse_tool_calls() 提取调用 → execute_tool() 执行 → 结果以 role='tool' 注入上下文 → 模型继续生成。参数校验(CHECK_ARGS)不在执行时使用,而是在奖励计算阶段判断调用是否合法。
Part 4: 让对话动起来
Agent 的核心循环——生成、调工具、拿结果、继续生成。
普通 RL 一次生成就够了,但 Agent 可能需要多轮:先查天气,再查汇率,最后综合回答。这一节是 Agent RL 最核心的部分——多轮交互循环的实现。理解了 rollout_single 的流程和 response_mask 的设计,后面所有训练逻辑都是在这个基础上搭建的。
- 多轮交互设计:rollout_single 实现"思考-行动"循环——每轮重新渲染完整上下文、模型生成、解析工具调用、执行并注入结果
- response_mask:Agent RL 与普通 RL 最核心的架构差异。mask=1 表示模型生成的 token(参与梯度),mask=0 表示工具结果(不参与梯度)
- 批量生成 rollout_batch:每个样本生成 num_generations 次,必须深拷贝 messages 防止数据污染
多轮交互设计
为什么需要多轮?普通 RL 只需生成一次回答。但 Agent 可能需要多轮交互——先查天气,再查汇率,最后综合回答。rollout_single() 就是实现这个"思考-行动"循环的核心函数。
图:Agent 多轮工具调用的完整时序(天气+汇率)
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个三栏时序图,从左到右分别是"Prompt"、"MiniMind"、"工具系统" - Turn 1区域:Prompt发出"北京天气?汇率多少?"到MiniMind调用 get_current_weather(location=北京)到工具系统返回 28度晴 - Turn 2区域:MiniMind将工具结果加入上下文后继续生成到调用 get_exchange_rate(USD到CNY)到工具系统返回 7.21 - Turn 3区域:MiniMind将结果加入上下文后到直接回复"北京今天28度晴,汇率7.21"到标注"没有工具调用,结束!" - 各元素关系:每一轮用虚线分隔,箭头表示调用方向,虚线箭头表示返回 - 关键标注:每个Turn左侧用标签标注"Turn 1/2/3" - 配色建议:浅蓝/浅灰背景,MiniMind绿色,工具系统橙色,轮次分隔虚线灰色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
每一轮都经历完整的"构建上下文 → 模型生成 → 解析工具调用 → 执行 → 结果注入"循环。关键变量:max_turns 控制上限,防止无限循环。
循环核心逻辑
图:rollout_single 多轮交互循环流程图
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个自上而下的流程图,描述 rollout_single 函数的完整循环 - 顶部蓝色圆角矩形"开始 rollout_single"到"初始化变量"到菱形判断"循环 turn=0..max_turns-1" - 循环体:"构建上下文 apply_chat_template"到"模型生成 rollout_engine.rollout()"到"保存本轮结果 mask=1"到黄色菱形判断"解析工具调用" - 无工具调用路径:到绿色圆角"结束!" - 有工具调用路径:到黄色菱形"最后一轮?"到是则"unfinished=True" / 否则"assistant+tool消息加入messages"到"execute_tool() 工具结果追加"到"obs_delta追加 mask=0不参与梯度"到回到循环顶部 - 各元素关系:箭头连接各步骤,菱形判断有两个出口 - 关键标注:起始蓝色,结束绿色,判断黄色 - 配色建议:浅蓝/浅灰背景,步骤白色,判断黄色菱形,开始蓝色,结束绿色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
伪代码概括:每轮重新渲染完整上下文(因为 messages 在增长)→ 模型生成 → 如果有工具调用,执行并把结果加入上下文。用 obs_delta 精确捕获模板渲染后的差异 token,标记为 mask=0。
response_mask
为什么需要 mask?模型只能对自己"说出来的话"负责。工具返回的结果不是模型说的,不该算它的错。mask=1 表示模型自己生成的 token(参与梯度),mask=0 表示工具结果(不参与梯度)。
图:多轮 Agent 交互中 token 序列的 mask 标注示意
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一条从左到右的横向 token 序列条带,被分成6个色块 - 色块依次为:灰色"Prompt (mask=0)"、绿色"Turn1生成 (mask=1)"、灰色"Turn1工具结果 (mask=0)"、绿色"Turn2生成 (mask=1)"、灰色"Turn2工具结果 (mask=0)"、绿色"Turn3最终回答 (mask=1)" - 各元素关系:色块从左到右依次排列,用箭头连接,展示多轮交互中 token 的来源 - 关键标注:绿色块旁标注"参与梯度更新",灰色块旁标注"不参与梯度" - 底部图例:绿色=模型生成(mask=1) 灰色=非模型生成(mask=0) - 配色建议:浅灰背景,绿色(mask=1)用亮绿色,灰色(mask=0)用中灰色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
绿色 mask=1:模型生成,参与 RL 梯度更新。灰色 mask=0:prompt / 工具结果,不参与。这是 Agent RL 与普通 RL 最核心的架构差异。
批量生成
rollout_batch() 对每个样本生成 num_generations 次(默认 4 次),用于后续的优势计算。一个关键细节:
必须深拷贝 messages:rollout_single() 会修改 messages 列表(追加 assistant 和 tool 消息)。每次生成都必须深拷贝,否则第 2 次生成会从第 1 次的状态开始,导致数据污染。
def rollout_batch(rollout_engine, tokenizer,
messages_batch, tools_batch, num_gen, ...):
for messages, tools in zip(messages_batch, tools_batch):
for _ in range(num_gen):
msgs_copy = [dict(m) for m in messages] # 深拷贝!
result = rollout_single(
rollout_engine, tokenizer, msgs_copy, tools, ...)
# 收集结果 ...
Part 5: 奖励计算
Agent RL 的奖励信号比普通 RL 更复杂——工具对齐、GT 匹配、标签格式、完成度。
普通 RL 只需要 RM 打分就行,Agent RL 则需要回答一连串问题:工具调对了没?参数传对了没?最终答案对不对?标签格式规范吗?模型是不是磨蹭到 max_turns 还没回答完?这一节拆解 Agent RL 的多层奖励设计——核心原则是"结果正确 > 过程正确 > 格式规范"。
- 奖励设计理念:GT 匹配分(0~+2.5,权重最高)> 工具对齐分(过程指标)> 标签格式惩罚(基础规范)> 未完成惩罚(效率信号)> 重复惩罚(质量底线)
- 计算流程:有工具调用和无工具调用走两条分支,最终总分 clip 到 [-3.0, +3.0] 保证训练稳定
- 标签格式与工具对齐:标签不闭合扣分;合法调用数与 GT 期望数的差距决定过程分
- GT 匹配与数值实例:支持文本子串匹配和数值近似匹配,通过成功/失败两个场景理解奖励叠加
奖励设计理念
先看全局:为什么要选这些奖励信号?每个信号的设计目的是什么?
| 奖励信号 | 分值范围 | 为什么需要它 |
|---|---|---|
| GT 匹配分 | 0 ~ +2.5 | 终极目标:答对了才算好。权重最高,因为过程再正确,答错也不行 |
| 工具对齐分 | +0.5 或 -0.5*gap | 过程指标:调了几个工具?合法吗?与 GT 期望的数量一致吗? |
| 标签格式惩罚 | -0.5 * N | 基础规范:标签不闭合说明模型还没学会协议格式 |
| 未完成惩罚 | -0.5 | 效率信号:达到 max_turns 还没给出最终回答,说明规划能力不足 |
| 重复惩罚 | 0 ~ -0.5 | 质量底线:防止模型生成"天气很好天气很好天气很好" |
| 总分 Clip | [-3.0, +3.0] | 稳定性保障:防止极端值导致训练不稳定 |
核心思想:结果正确(GT 匹配)> 过程正确(工具对齐)> 格式规范(标签闭合)。下面逐项展开。
计算流程
图:Agent RL 奖励计算的两条分支流程
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个自上而下的流程图 - 顶部蓝色圆角"开始计算奖励"到"标签格式扣分"到黄色菱形判断"调用了工具?" - 无工具分支(左):到"无工具分支:长度+思考+RM+重复惩罚"到"Clip到[-3.0, 3.0]" - 有工具分支(右):到"有工具分支:工具对齐+GT匹配"到"Clip到[-3.0, 3.0]" - 两条分支汇合到绿色圆角"返回奖励值" - 各元素关系:菱形判断分流两条路径,最终汇合到同一个出口 - 关键标注:判断节点黄色,起始蓝色,结束绿色 - 配色建议:浅蓝/浅灰背景,判断黄色菱形,步骤白色矩形,关键箭头用橙色标注 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
奖励计算分两条路径:有工具调用和无工具调用。无工具时走普通 RL 奖励逻辑(长度分 + RM 打分);有工具时走 Agent 专用逻辑(工具对齐 + GT 匹配)。
格式与对齐
标签格式惩罚
无论是否有工具调用,都检查 <tool_call> 标签是否成对:
reward -= 0.5 * sum(
abs(turn.count('
不闭合就扣 0.5 分/对。教会模型:调用工具,标签必须完整。
工具对齐分
衡量"合法调用数"与"GT 期望数"的差距:
# 合法 = 工具名有效 + 参数通过校验
valid = sum(1 for call in tool_calls
if name in valid_names
and CHECK_ARGS[name](args))
gap = abs(valid - len(gt)) + \
max(0, len(tool_calls) - valid)
reward += 0.5 if gap == 0 \
else -0.5 * gap
GT 期望 1 个调用,模型调了 3 个(1 合法)→ gap=2,扣 1.0 分。
GT 匹配分
为什么权重最高?工具对齐只是过程指标——调对了工具但答错了,依然不该得高分。GT 匹配直接衡量最终效果。最高 +2.5 分。
# 最终回答 = 最后一次 tool_call 之后的内容
final_text = answer.split('
validate_gt_in_text() 支持两种匹配模式:
| 模式 | 适用场景 | 示例 |
|---|---|---|
| 文本子串匹配 | 天气、翻译等文本 GT | GT="晴",回答含"晴"→匹配 |
| 数值近似匹配 | 计算、汇率等数字 GT | GT="9472",回答含 9472→匹配(误差 < 1e-6) |
加上未完成惩罚(-0.5)和重复惩罚(3-gram 重复率,上限 0.5),总分 clip 到 [-3, 3]。
奖励计算实例
假设 gt=["9472"],模型调用了 1 次正确的 calculate_math,最终回答是 "256*37=9472":
成功场景
# 标签格式:完整闭合 → 0
# 工具对齐:1合法 vs 1个GT → gap=0 → +0.5
# GT匹配:9472在回答中 → +2.5
# 重复惩罚:无 → 0
# 未完成:False → 0
total = 0 + 0.5 + 2.5 + 0 + 0 = 3.0
失败场景
# 标签格式:缺少闭合标签 → -0.5
# 工具对齐:调2次(1合法) vs 1个GT
# gap = |1-1| + max(0,2-1) = 1 → -0.5
# GT匹配:回答里没9472 → 0
# 重复惩罚:轻微重复 → -0.1
# 未完成:True → -0.5
total = -0.5-0.5+0-0.1-0.5 = -1.6
Part 6: 从单轮到多轮
Agent RL 训练与普通 GRPO 有什么关键差异?
前面五节铺垫了 Agent 的所有组件——工具、解析、多轮循环、奖励。这一节把它们串起来,看 Agent RL 的完整训练流程。核心是理解它与普通 GRPO 的三大差异:多轮 rollout(上下文动态增长)、工具感知奖励(不只是 RM 打分)、mask 区分来源(工具结果不参与梯度)。
- 对比普通 RL:生成方式从一次性变为多轮交互、上下文从固定变为动态增长、训练目标从"生成好回答"变为"何时调用、调哪个、怎么用结果"
- 训练数据格式:JSONL 包含 conversations、tools、gt 三个字段,messages[:-1] 去掉最后一条让模型自己生成
- 训练循环四阶段:多轮 Rollout → 工具感知奖励计算 → 组内标准化优势 → CISPO 策略更新(completion_mask 精确控制梯度)
对比普通 RL
| 对比项 | 普通 RL(GRPO) | Agent RL |
|---|---|---|
| 输入 | prompt | prompt + tools |
| 生成方式 | 一次性生成 | 多轮交互 |
| 上下文 | 固定不变 | 动态增长 |
| 奖励来源 | RM 打分 / 格式 | 工具对齐 + GT + 格式 + RM |
| mask 设计 | 区分 prompt/response | 还区分模型生成/工具结果 |
| 数据格式 | {prompt, answer} | {messages, tools, gt} |
| 训练目标 | 生成好回答 | 何时调用、调哪个、怎么用结果 |
记住这三个差异就够了:多轮 rollout(上下文动态增长)、工具感知奖励(不只是 RM 打分)、mask 区分来源(工具结果不参与梯度)。
训练数据
训练数据为 JSONL 格式(dataset/agent_rl.jsonl),每条数据包含三个字段:
{
"conversations": [
{"role": "system",
"content": "你是一个有用的AI助手...",
"tools": "[{\"type\":\"function\",...}]"},
{"role": "user",
"content": "帮我算一下 256 乘以 37"},
{"role": "assistant",
"content": "
为什么 messages[:-1] 去掉最后一条?最后一条是 assistant 的标准回复,模型需要自己通过 rollout 生成,而不是复制训练数据。AgentRLDataset 返回的是"问题 + 可用工具",答案由模型自行探索。
训练架构
图:Agent RL 训练架构全景(数据准备、模型初始化、训练循环)
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:三个水平排列的虚线大框,分别标注"数据准备"、"模型初始化"、"训练循环" - 数据准备框内:黄色"AgentRLDataset (agent_rl.jsonl)"到"DataLoader" - 模型初始化框内:"策略模型(要训练的)"到"参考模型(冻结)","奖励模型(可选)"+"策略模型"到"Rollout Engine" - 训练循环框内:"batch: messages, tools, gt"到红色"rollout_batch() 多轮交互生成"到绿色"calculate_rewards()"到"advantages=(r-mean)/std"到蓝色"CISPO Loss+KL惩罚"到紫色"梯度更新" - 各元素关系:DataLoader连接batch,Rollout Engine连接rollout_batch,参考模型虚线连接CISPO Loss(标注"参考logps") - 关键标注:rollout红色,奖励绿色,Loss蓝色,更新紫色 - 配色建议:浅蓝/浅灰背景,各模块按功能着色,关键箭头用橙色标注 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
训练循环
rl_train_epoch() 一个 batch 的处理分四个阶段。与普通 GRPO 相比,核心差异在 Phase 1(多轮 Rollout)和 Phase 2(工具感知奖励):
# Phase 1: 多轮 Rollout(与GRPO的最大区别)
with torch.no_grad():
results = rollout_batch(
rollout_engine, tokenizer, messages_batch,
tools_batch, num_generations=4, max_turns=3)
# Phase 2: 工具感知奖励(不只是RM打分)
rewards = calculate_rewards(
prompts, completions, gt_batch, tools_batch,
num_generations, reward_model)
# Phase 3: 优势计算(与GRPO相同:组内标准化)
advantages = (rewards - mean) / (std + 1e-4)
# Phase 4: CISPO策略更新(与GRPO类似)
# 用 completion_mask 只更新模型生成的 token
# mask=0的工具结果 token 被排除在 loss 之外
CISPO 与 PPO 的区别:PPO 用 clamp(ratio, 1-eps, 1+eps) 限制更新幅度,CISPO 只限制上界 clamp(ratio, max=epsilon_high),允许策略自由下降但限制上升幅度,训练更稳定。
精确控制梯度
为什么 completion_mask 这么重要?多轮 Rollout 生成的序列很长,包含 prompt、模型生成、工具结果、padding。训练时必须精确控制:只有模型自己生成的、EOS 之前的 token 参与 loss 计算。
# 构建流程:
# 1. response_mask 标记: mask=1(模型生成), mask=0(工具结果)
# 2. 找到 EOS 位置,mask 掉 EOS 之后的 token
# 3. 最终:completion_mask = response_mask ∩ (位置<=EOS)
# 效果:只有绿色部分参与梯度
# [prompt(0) | Turn1生成(1) | 工具结果(0) | Turn2生成(1) | ... ]
此外,超长序列会从右侧截断(保留最近的 token,通常是最终回答部分)。所有序列 pad 到 batch 内最大长度后组成 batch 张量。
关键超参数
| 参数 | 默认值 | 含义 |
|---|---|---|
--num_generations | 4 | 每个 prompt 生成几条回答(用于优势估计) |
--max_turns | 3 | 多轮交互最大轮数 |
--max_total_len | 2500 | 总序列长度上界(含多轮) |
--thinking_ratio | 0.1 | 开启 thinking 的概率 |
--learning_rate | 3e-7 | 学习率(比 SFT 小 100 倍) |
--beta | 0.1 | KL 散度惩罚系数 |
--loss_type | cispo | 损失类型(grpo/cispo) |
训练命令
python trainer/train_agent.py \
--data_path ../dataset/agent_rl.jsonl \
--from_weight full_sft \
--save_weight agent \
--epochs 1 \
--batch_size 2 \
--num_generations 4 \
--learning_rate 3e-7 \
--loss_type cispo \
--thinking_ratio 0.1
注意事项:
--from_weight full_sft:从 SFT 模型开始,不是从 RL 模型开始--batch_size 2:因为每个样本要生成 4 次 rollout,实际计算量是 8 倍--learning_rate 3e-7:远小于 SFT 的学习率,防止灾难性遗忘
Batch 生命周期
图:Agent RL 训练一个 Batch 的完整生命周期(三阶段时序)
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个六栏时序图,从左到右分别是"DataLoader"、"RolloutEngine"、"工具执行器"、"RewardModel"、"策略模型"、"参考模型" - Phase 1(多轮Rollout,绿色底色区域):DataLoader给RolloutEngine发batch,循环(模型生成到解析执行工具到返回结果到继续生成),最终回答 - Phase 2(奖励计算,橙色底色区域):RolloutEngine请求RewardModel打分,返回分数,综合计算奖励 - Phase 3(策略更新,蓝色底色区域):RolloutEngine到策略模型前向传播计算新logps,参考模型前向传播计算参考logps,策略模型CISPO loss+KL惩罚,反向传播梯度更新 - 各元素关系:三个阶段用虚线分隔,阶段内箭头表示数据流向 - 关键标注:每个Phase左侧用大标签标注名称 - 配色建议:浅蓝/浅灰背景,Phase1绿色区域,Phase2橙色区域,Phase3蓝色区域 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
Part 7: 扩展实战
如何添加新工具、评估模型、诊断常见错误。
理论学完了,这一节帮你把能力延伸到实战。三个主题:怎么给 Agent 加新工具(定义 → 执行 → 校验 → 数据)、怎么评估工具调用模型的好坏、训练中出了问题怎么排查。还有动手练习帮你巩固前面的知识点。
- 添加新工具:修改 TOOLS 列表(工具定义)→ MOCK_RESULTS(执行函数)→ CHECK_ARGS(参数校验)→ agent_rl.jsonl(训练数据),四处保持一致
- 评估方法:eval_toolcall.py 支持 local 和 api 两种后端,评估指标包括工具调用率、工具正确率、GT 匹配率
- 常见错误诊断:模型不生成 tool_call、格式错误 JSON 解析失败、unfinished 惩罚过高、奖励不收敛、OOM、GT 匹配分总是 0 等
添加新工具
要添加新工具,需要修改 train_agent.py 中的四个位置:
# 第一步:在 TOOLS 列表中添加工具定义
TOOLS.append({
"type": "function",
"function": {
"name": "search_web",
"description": "搜索网页",
"parameters": {"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]}
}
})
# 第二步:在 MOCK_RESULTS 中添加执行函数
MOCK_RESULTS["search_web"] = lambda args: {
"results": [f"搜索结果: {args.get('query')} 的相关信息..."]
}
# 第三步:在 CHECK_ARGS 中添加参数校验
CHECK_ARGS["search_web"] = lambda a: bool(a.get("query"))
# 第四步:更新训练数据 agent_rl.jsonl
# 在 system.tools 中包含新工具的定义,gt 中包含期望答案
扩展工具的四个步骤:定义 → 执行 → 校验 → 数据。确保工具名在四处保持一致。训练数据中必须包含使用新工具的样本。
扩展注意
工具描述要清晰
模型的工具选择依赖 description 字段。描述越精确,模型越容易学会何时调用。比如"搜索网页"比"搜索引擎"更好。
参数类型要准确
parameters 中的 type 字段影响模型的参数格式。数字用 number,字符串用 string。错误类型会导致模型传参失败。
MOCK 数据要合理
模拟数据应该覆盖典型用例。如果 MOCK 数据太简单,模型可能学到的是"记住答案"而非"学会调用工具"。
gt 字段要精确
gt 列表中的值会在最终回答中被精确匹配(数值用浮点近似,文本用子串)。确保 gt 值在回答中的表述方式一致。
评估方法
# 本地模型评估
python scripts/eval_toolcall.py \
--backend local \
--load_from ../model \
--weight agent \
--hidden_size 768 \
--num_hidden_layers 8
# API 模式评估(OpenAI 兼容接口)
python scripts/eval_toolcall.py \
--backend api \
--api_base_url http://localhost:11434/v1 \
--api_model jingyaogong/minimind-3:latest
评估脚本会运行一组预设测试用例,检查模型是否能正确调用工具并给出包含正确答案的最终回复。评估指标包括:工具调用率、工具正确率、GT 匹配率。
测试用例
| 类别 | 测试问题 | 期望工具 | GT |
|---|---|---|---|
| 数学计算 | "帮我算 256 乘以 37" | calculate_math | ["9472"] |
| 天气查询 | "北京今天天气怎样?" | get_current_weather | ["28°C", "晴"] |
| 单位换算 | "100公里换算英里" | unit_converter | ["62.1371"] |
| 汇率查询 | "美元兑人民币汇率" | get_exchange_rate | ["7.21"] |
| 文本翻译 | "把'你好世界'翻译成英文" | translate_text | ["Hello World"] |
| 当前时间 | "现在几点了?" | get_current_time | ["14:30"] |
| 组合调用 | "北京天气+东京天气" | weather x2 | ["28°C", "12°C"] |
| 多工具 | "天气+汇率" | weather + exchange | ["28°C", "7.21"] |
常见错误诊断
模型不生成 tool_call
症状:模型总是直接回答,从不调用工具。
原因:SFT 阶段没见过工具调用格式的数据,或 tokenizer 的 chat_template 不支持 tools 参数。
解决:确保 agent_rl.jsonl 中 system 消息包含 tools 字段。检查 tokenizer 的 chat_template 是否正确处理 tools。
tool_call 格式错误
症状:生成了 tool_call 标签但 JSON 解析失败。
原因:模型没学会正确的 JSON 格式,比如少了引号或括号。
解决:检查 parse_tool_calls() 的正则是否能匹配模型输出。增加 SFT 数据中的工具调用示例。
unfinished 惩罚过高
症状:奖励一直是负数,频繁触发 max_turns。
原因:max_gen_len 太小,模型没空间生成最终回答。
解决:增大 max_gen_len(默认 768)或减小 max_turns(默认 3)。
更多排错
奖励不收敛
症状:训练多 epoch,奖励值仍剧烈波动。
原因:learning_rate 太大或 num_generations 太少。
解决:降低 learning_rate(默认 3e-7),增大 num_generations(默认 4)来稳定优势估计。
OOM 显存溢出
症状:多轮 Rollout 后显存暴增。
原因:max_total_len(默认 2500)允许的总序列太长。
解决:减小 max_total_len、batch_size 或 max_turns。或用 --accumulation_steps 替代大 batch。
GT 匹配分总是 0
症状:validate_gt_in_text 始终返回空集。
原因:模型调了工具但最终回答没包含 GT 值。或 gt 格式不对。
解决:检查 gt 格式(应为字符串列表,如 ["9472"])。检查模型能否将工具结果整合进回答。
重复输出严重
症状:模型反复输出相同内容。
原因:rep_penalty 不够,或模型太小无法同时处理多轮上下文。
解决:降低 temperature(rollout 中默认 0.8),增大 thinking_ratio 让模型"先想后说"。
Debug 模式
训练脚本内置了 debug 模式,可以打印每个样本的完整信息:
python trainer/train_agent.py \
--debug_mode \
--debug_interval 20 \
...
Debug 日志输出示例:
============================== [DEBUG] gen[0][0] CONTEXT_BEGIN ==============================
[系统提示 + 用户问题 + 工具定义]
=============================== [DEBUG] gen[0][0] CONTEXT_END ================================
[DEBUG] gen[0][0] prompt_len=45, seq_len=312
============================ [DEBUG] gen[0][0] COMPLETION_BEGIN [45:312] ============================
Debug 模式每 debug_interval 步打印一次,包含完整上下文、模型输出、奖励值。强烈建议在调参时开启。
动手练习
练习 1:奖励计算
任务:假设 gt=["7.21"],模型调用了 1 次正确的 get_exchange_rate,最终回答是 "美元兑人民币汇率是7.21"。标签格式正确,无重复,finished。计算总奖励。
参考答案
# 标签格式:完整闭合 → 0
# 工具对齐:1合法调用 vs 1个GT → gap=0 → +0.5
# GT匹配:7.21在回答中 → +2.5*1/1 = +2.5
# 重复惩罚:无 → 0
# 未完成:False → 0
total = 0 + 0.5 + 2.5 + 0 + 0 = 3.0
练习 2:mask 追踪
任务:跟踪 rollout_single 执行过程。假设 max_turns=3,模型在第 1 轮调用了 weather,第 2 轮调用了 exchange,第 3 轮没调工具直接回答。画出 response_mask 的示意图。
参考答案
[1,1,...,1, 0,0,...,0, 1,1,...,1, 0,0,...,0, 1,1,...,1]
↑Turn1生成 ↑Turn1工具 ↑Turn2生成 ↑Turn2工具 ↑Turn3回答
mask=1 mask=0 mask=1 mask=0 mask=1
更多练习
练习 3:添加工具
尝试添加一个新工具 get_stock_price(查询股票价格)。需要修改哪四处?MOCK 数据怎么设计?写出一组训练数据样本。
练习 4:格式分析
写出一段包含两个工具调用的完整交互(天气 + 汇率)。模型需要生成几次?每次的 context 包含什么?
练习 5:数据设计
设计一条 agent_rl.jsonl 数据,要求模型同时调用 calculate_math 和 unit_converter。gt 应该怎么写?
练习 6:动手实验
开启 --debug_mode 运行训练,观察前 20 步的 reward 变化。如果 unfinished 频繁出现,尝试调大 max_gen_len 到 1024,观察变化。
知识体系
图:Stage 9 Agent RL 完整知识体系(七大模块)
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个从上到下的纵向知识链,包含7个彩色节点 - 节点依次为:红色"Agent概念 = LLM+工具使用"、橙色"调用格式 tool_call/tool_response"、黄色"6个内置工具 计算/天气/时间/汇率/翻译/换算"、浅绿"多轮Rollout 生成到调工具到再生"、蓝色"解析与执行 parse+execute+超时保护"、紫色"奖励计算 工具对齐+GT匹配+格式惩罚"、深蓝"训练流程 Rollout到Reward到CISPO" - 各元素关系:节点从上到下用箭头依次连接,形成学习路径 - 关键标注:每个节点用不同颜色,整体形成彩虹色系的学习路线 - 配色建议:白色背景,节点依次红到橙到黄到绿到蓝到紫到深蓝 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
代码速查
| 文件 | 作用 | 关键函数/类 |
|---|---|---|
trainer/train_agent.py | Agent RL 训练主脚本 | rollout_single(), rollout_batch(), calculate_rewards(), rl_train_epoch() |
dataset/lm_dataset.py | 数据加载 | AgentRLDataset 类 |
trainer/rollout_engine.py | Rollout 引擎 | create_rollout_engine(), compute_per_token_logps() |
scripts/eval_toolcall.py | 工具调用评估 | parse_tool_calls(), execute_tool() |
一句话总结
Agent RL 的本质,就是教模型学会三件事:什么时候该用工具、用哪个工具、怎么用工具的结果来回答问题。
不是让它背下所有答案,而是教它学会利用工具来解决问题。面对新问题、新工具,也能举一反三。
完整成长之路
图:MiniMind 全部九个训练阶段的完整成长之路
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一条从左到右的横向路线,包含9个阶段节点 - 节点依次为:S1预训练、S2 SFT、S3 RLHF、S4 DPO、S5 PPO、S6 GRPO、S7推理强化、S8 SPO、S9 Agent RL(红色高亮大边框) - 节点之间用实线箭头连接表示顺序 - 上方用弧线+文字标注学习目标:S1到S2"学说话",S2到S3"学对话",S3到S5"学对齐",S5到S6"学探索",S6到S7"学思考",S7到S9"学用工具" - 各元素关系:9个阶段线性递进,弧线标注学习能力的跃迁 - 关键标注:S9红色高亮+粗边框,标注"当前阶段" - 配色建议:浅蓝/浅灰背景,节点统一浅蓝,S9红色高亮,弧线标注橙色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
阶段 9 完成
让模型学会使用工具——从回答者进化为执行者。
下一章:阶段 10 · 模型推理与部署——让训练好的模型真正上线服务