FunReason 核心技术深度剖析
📊 技术全景图
┌─────────────────────────────────────────────────────────┐
│ FunReason Framework │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ FCDR Pipeline │───▶│ SRML Training │ │
│ │ (数据质量保证) │ │ (训练优化方法) │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ ▲ │ │
│ │ ▼ │
│ └────── Self-Refinement ──────┘ │
│ (迭代提升) │
└─────────────────────────────────────────────────────────┘
第一部分:FCDR - Function Call Data Refinement
1. 设计动机:为什么需要FCDR?
问题1:现有数据集的质量问题
# 原始xLAM数据集的典型问题
问题类型1: CoT推理错误
User: "查询北京明天的天气"
CoT: <think>用户想查天气,应该调用get_news函数...</think> # ❌ 逻辑跳跃
Result: [get_news(city="北京")] # ❌ 函数选错
问题类型2: 参数错误
User: "发邮件给张三"
Result: [send_email(to="张三")] # ❌ 应该是邮箱地址
问题类型3: 格式错误
Result: [get_weather('city'="北京")] # ❌ 参数名不应加引号
问题2:Token不平衡的根源
统计发现 (Table 1):
CoT部分平均token: 350.74 (中位数248)
Result部分平均token: 31.07 (中位数27)
比例: 约 11:1
影响: 传统SFT会让模型"夸夸其谈但执行不准"
问题3:合成数据的可靠性
- 用GPT-4o生成的CoT可能包含策略痕迹
- 需要验证生成数据的可执行性
- 需要确保格式统一性
2. FCDR架构设计
2.1 整体流程图
┌──────────────┐
│ 输入:xLAM-cot│
└───────┬──────┘
│
┌───────▼──────────┐
│ Stage 1: 是FC吗? │
└───────┬──────────┘
│ Yes
┌───────▼──────────────┐
│ Stage 2: 能用工具解决?│
└───────┬──────────────┘
│ Yes
┌───────▼──────────────┐
│ Stage 3: CoT推理对吗?│
└───────┬──────────────┘
│ Yes
┌───────▼──────────────┐
│ Stage 4: 函数名参数对?│◄─┐
└───────┬──────────────┘ │
│ Yes │Regenerate
┌───────▼──────────────┐ │
│ Stage 5: 格式对吗? │──┘
└───────┬──────────────┘
│ Yes
┌───────▼──────────────┐
│ 保存到FCDR数据集 │
└──────────────────────┘
注: Stage 1-3不通过 → Drop
Stage 4-5不通过 → Regenerate
3. 五阶段详细剖析
Stage 1: Response Identification
目标:区分function call和普通对话
技术实现:
# Prompt结构(简化版)
prompt = f"""
判断以下内容是function_tool调用还是普通回复:
常见格式:[func_name(param1=value1, param2=value2)]
输入:{reference_answer}
输出:<think>...</think><judge>True/False</judge>
"""
# QwQ-32B处理
response = qwq_model.generate(prompt)
is_function_call = parse_judge_tag(response)
if not is_function_call:
save_as_dialogue_data() # 保存为普通对话数据
return
示例:
输入1: "[get_weather(city='北京')]"
输出: <think>符合function call格式</think><judge>True</judge>
输入2: "今天北京天气不错,温度15度"
输出: <think>这是普通回复</think><judge>False</judge>
Stage 2: Query & Tool Identification
目标:验证查询是否可用给定工具解决
核心逻辑:
def check_query_tool_match(query, tools, reference_answer):
"""
检查三个关键点:
1. 工具列表中是否有相关功能的函数
2. 查询中是否包含足够的参数信息
3. 参数值能否从query中提取
"""
prompt = f"""
分析以下内容:
1. 函数名能从候选工具中找到吗?
2. 参数值能从查询中分析出来吗?
User Query: {query}
Candidate Tools: {tools}
Reference Answer: {reference_answer}
输出:<think>...</think><judge>True/False</judge>
"""
result = qwq_model.generate(prompt)
return parse_judge(result)
典型Case:
Case 1: 通过
Query: "查北京天气"
Tools: [get_weather(city, date), send_email(to, content)]
Reference: [get_weather(city="北京")]
→ ✅ 工具有get_weather,参数city能从query提取
Case 2: 不通过(工具不匹配)
Query: "帮我订机票"
Tools: [get_weather(city), send_email(to, content)]
Reference: [book_flight(from, to, date)]
→ ❌ 工具列表中没有订票功能 → Drop
Case 3: 不通过(参数不足)
Query: "发邮件"
Tools: [send_email(to, content)]
Reference: [send_email(to="unknown@example.com", content="...")]
→ ❌ query中没有收件人信息,reference硬编码了地址 → Drop
Stage 3: CoT Identification
目标:验证推理过程的连贯性和正确性
三重检查机制:
def verify_cot_quality(cot, reference_fc):
"""
1. 起点检查:推理是否从合理的位置开始
2. 步骤检查:每一步是否紧密跟随上一步
3. 终点检查:最后一步是否指向正确答案
"""
checks = {
'start_valid': check_reasoning_start(cot),
'steps_coherent': check_step_connections(cot),
'conclusion_correct': check_final_step(cot, reference_fc)
}
return all(checks.values())
示例分析:
Example 1: 优质CoT(通过)
Query: "检索一个长度至少7个字符的动词变位词"
CoT:
<think>
1. 用户需要一个词,所以要调用词典相关函数
2. 要求是"动词变位",参数verbeconjugue应该为True
3. 要求"长度至少7",参数minlong应该设为'7'
4. 需要定义,参数avecdef设为True
→ 应该调用get_random_word函数
</think>
Reference: [get_random_word(verbeconjugue=True, minlong='7', avecdef=True)]
验证结果:
✅ 起点:从"用户需要词"开始,合理
✅ 连贯:每步都基于上一步和需求推进
✅ 终点:正确导出函数和所有参数
→ Pass
Example 2: 劣质CoT(不通过)
Query: "查北京天气"
CoT:
<think>
1. 用户想知道天气
2. 应该搜索新闻
3. 调用get_news函数
</think>
Reference: [get_news(city="北京")]
验证结果:
✅ 起点:从"天气"开始,合理
❌ 连贯:步骤2跳跃到"新闻",逻辑断裂
❌ 终点:导出了错误函数
→ Drop
Stage 4: Function & Parameter Identification
目标:验证并修正函数名和参数
这是唯一会Regenerate的阶段(4和5)
检查流程:
def verify_and_fix_function(query, tools, reference_fc):
"""
1. 函数名是否在tools中?
2. 参数名是否匹配函数定义?
3. 参数值类型是否正确?
4. 参数值是否合理?
"""
prompt = f"""
检查function call的函数名和参数:
1. 函数名是否正确?
2. 参数是否都有效?
若有问题,生成修正后的function call。
Query: {query}
Tools: {tools}
Reference FC: {reference_fc}
输出:
<think>...</think>
<judge>True/False</judge>
<NewFC>[修正后的函数调用]</NewFC>
"""
response = qwq_model.generate(prompt)
is_valid, corrected_fc = parse_response(response)
return corrected_fc if not is_valid else reference_fc
修正案例:
Case 1: 参数名错误
Reference: [get_weather(location="北京")]
Tools定义: get_weather(city: str, date: str)
QwQ-32B分析:
<think>
工具定义中参数名是city,不是location
应该修正为city
</think>
<judge>False</judge>
<NewFC>[get_weather(city="北京")]</NewFC>
→ 自动修正并保存
Case 2: 参数值类型错误
Reference: [set_alarm(time=8)]
Tools定义: set_alarm(time: str) # 要求格式"HH:MM"
QwQ-32B分析:
<think>
time参数应该是字符串格式"08:00"
当前是整数8,需要转换
</think>
<judge>False</judge>
<NewFC>[set_alarm(time="08:00")]</NewFC>
→ 修正类型并保存
Case 3: 参数缺失
Reference: [get_weather(city="北京")]
Tools定义: get_weather(city: str, date: str = "today")
QwQ-32B分析:
<think>
date是可选参数,有默认值"today"
当前未提供date,但有默认值可用
可以接受
</think>
<judge>True</judge>
→ 直接通过
Stage 5: Format Identification
目标:统一JSON格式规范
严格的格式要求:
# 正确格式规范
CORRECT_FORMAT = """
[func_name(param_name=param_value, ...)]
关键规则:
1. 参数名(param_name)不加引号
2. 字符串参数值(param_value)加双引号
3. 数字/布尔值不加引号
4. 多个函数用逗号分隔
"""
# 常见错误格式
WRONG_FORMATS = [
'[func("param_name"="value")]', # ❌ 参数名加了引号
"[func(param_name='value')]", # ❌ 应该用双引号
'[func(param_name=value)]', # ❌ 字符串值漏了引号
]
格式修正示例:
Example 1: 参数名引号问题
输入:
[getPrivacyViolationRisk("data"="personal_info", "purpose"="marketing")]
QwQ-32B修正:
<think>
参数名data和purpose不应该加引号
参数值是字符串,保留双引号
</think>
<judge>False</judge>
<NewFC>[getPrivacyViolationRisk(data="personal_info", purpose="marketing")]</NewFC>
→ 修正保存
Example 2: 单引号问题
输入:
[get_weather(city='北京', date='today')]
QwQ-32B修正:
<think>
应该统一使用双引号
</think>
<judge>False</judge>
<NewFC>[get_weather(city="北京", date="today")]</NewFC>
→ 修正保存
Example 3: 数字类型
输入:
[set_volume(level="50")]
QwQ-32B修正:
<think>
level是数字类型,不应该加引号
</think>
<judge>False</judge>
<NewFC>[set_volume(level=50)]</NewFC>
→ 修正保存
4. FCDR的技术细节
4.1 为什么用QwQ-32B?
选择理由: 1. 强推理能力: 32B参数,专为推理优化 2. 自我纠错: 能够识别自己生成内容的问题 3. 遵循指令: 严格按照prompt格式输出
对比其他选择:
GPT-4o:
✅ 推理能力强
❌ 闭源,无法完全控制
❌ API调用成本高(60K样本 × 5阶段)
Claude-3.5:
✅ 推理能力强
❌ 闭源
❌ 成本高
QwQ-32B:
✅ 开源可控
✅ 推理能力足够(见Table 2: 78.70%)
✅ 可本地部署(vLLM)
✅ 成本可控
4.2 Prompt Engineering技巧
关键设计原则:
# 1. 明确的输出格式要求
prompt_template = """
<think>思考过程</think>
<judge>True/False</judge>
<NewFC>[修正结果]</NewFC> # 仅Stage 4-5需要
"""
# 2. 具体的评判标准
"""
✅ 给出正面和负面示例
✅ 列出明确的检查点
✅ 说明边界情况处理
"""
# 3. 结构化的输入
"""
分块提供:Query、Tools、Reference
使用标签:<query>、<tools>、<refFC>
实际Prompt示例(Stage 4):
Judge whether the function names and parameters meet requirements:
1) Check if function names and parameter values are correct
2) If valid, output <judge>True</judge>
3) If invalid, output <judge>False</judge> and provide corrected version
Output format:
<think>analysis process</think>
<judge>True/False</judge>
<NewFC>[corrected_function_call]</NewFC>
Input:
User query: <query>查北京天气</query>
Candidate tools: <tools>
{
"get_weather": {
"parameters": {
"city": {"type": "string", "required": true},
"date": {"type": "string", "default": "today"}
}
}
}
</tools>
Reference FC: <refFC>[get_weather(location="北京")]</refFC>
4.3 数据流转机制
class FCDRPipeline:
def __init__(self, model="QwQ-32B"):
self.model = load_model(model)
self.stats = {
'input': 0,
'stage1_drop': 0,
'stage2_drop': 0,
'stage3_drop': 0,
'stage4_regenerate': 0,
'stage5_regenerate': 0,
'output': 0
}
def process(self, data):
self.stats['input'] = len(data)
# Stage 1: Response Identification
data = [d for d in data if self.stage1(d)]
self.stats['stage1_drop'] = self.stats['input'] - len(data)
# Stage 2: Query & Tool Identification
data = [d for d in data if self.stage2(d)]
self.stats['stage2_drop'] = self.stats['input'] - len(data)
# Stage 3: CoT Identification
data = [d for d in data if self.stage3(d)]
self.stats['stage3_drop'] = self.stats['input'] - len(data)
# Stage 4: Function & Parameter (with regenerate)
data = [self.stage4_fix(d) for d in data]
# Stage 5: Format (with regenerate)
data = [self.stage5_fix(d) for d in data]
self.stats['output'] = len(data)
return data
def report(self):
print(f"""
输入: {self.stats['input']}
Stage 1 丢弃: {self.stats['stage1_drop']}
Stage 2 丢弃: {self.stats['stage2_drop']}
Stage 3 丢弃: {self.stats['stage3_drop']}
Stage 4 修正: {self.stats['stage4_regenerate']}
Stage 5 修正: {self.stats['stage5_regenerate']}
最终输出: {self.stats['output']}
保留率: {self.stats['output']/self.stats['input']*100:.1f}%
""")
5. FCDR的实验效果
5.1 数据质量提升
Table 3 实验结果(3种模型的对比):
模型 | xLAM-cot | FunReason-FCDR | 提升 |
---|---|---|---|
Llama-3.1-8B | 79.50% | 81.46% | +1.96% |
Llama-3.2-3B | 67.38% | 72.97% | +5.59% |
Qwen2.5-Coder-7B | 78.70% | 81.94% | +3.24% |
结论: - ✅ 所有模型都受益于FCDR - ✅ 小模型受益更明显(Llama-3.2-3B提升5.59%) - ✅ 证明数据质量比数量更重要
5.2 各阶段的过滤率(估算)
根据论文描述推测:
输入: 60,000 samples (xLAM-cot)
Stage 1: ~2% drop → 58,800 samples
Stage 2: ~5% drop → 55,860 samples
Stage 3: ~8% drop → 51,391 samples
Stage 4: ~10% regen → 51,391 samples (修正约5,139个)
Stage 5: ~5% regen → 51,391 samples (修正约2,570个)
最终输出: ~60,000 samples (FunReason-FCDR)
注:Stage 4-5修正后补齐到60K
第二部分:SRML - Self-Refinement Multiscale Loss
1. 设计动机:为什么需要新损失函数?
问题根源:Token不平衡导致的优化偏差
传统SFT的问题:
# 一个典型的训练样本
sample = {
"think": "<think>...</think>", # 350 tokens
"result": "[func()]" # 31 tokens
}
# 传统Cross-Entropy Loss
total_tokens = 350 + 31 = 381
L_total = (1/381) × Σ[-log P(token)]
# 问题:350个"think" tokens的loss贡献远大于31个"result" tokens
# 导致梯度更新主要优化推理部分,而函数调用部分被"淹没"
数学上的表现:
传统SFT隐含权重:
w_think = N_think / N_all = 350 / 381 ≈ 0.92
w_result = N_result / N_all = 31 / 381 ≈ 0.08
影响:
- 模型学会生成"看起来合理"的长推理
- 但对函数调用的准确性关注不足
- 类似于"夸夸其谈但不办实事"
2. SRML数学原理
2.1 问题形式化
定义符号:
- M: Language Model
- N_t: CoT部分的token数量
- N_f: Function call部分的token数量
- N_all = N_t + N_f
- L_think: CoT部分的平均cross-entropy loss
- L_result: Function call部分的平均cross-entropy loss
2.2 传统SFT损失函数推导
完整形式:
L_total = (1/N_all) × Σ[i=1 to N_all] -log P(y_i | y_<i, x)
分解为两部分:
L_total = (1/N_all) × [
Σ[i=1 to N_t] -log P(y_i | y_<i, x) # Think部分
+ Σ[i=N_t+1 to N_all] -log P(y_i | y_<i, x) # Result部分
]
改写为加权形式:
L_total = (N_t/N_all) × (1/N_t) × Σ[i=1 to N_t] -log P(...)
+ (N_f/N_all) × (1/N_f) × Σ[i=N_t+1 to N_all] -log P(...)
= w_t × L_think + w_f × L_result
其中: - w_t = N_t / N_all ≈ 0.92 - w_f = N_f / N_all ≈ 0.08 - w_t + w_f = 1
问题明确:权重严重不平衡!
2.3 SRML改进方案
核心思想:手动设置权重α和β,打破token数量的束缚
L_MSL = α × L_think + β × L_result
约束: α + β = 1, α,β ∈ [0,1]
关键区别:
传统SFT: 权重由数据决定 (α≈0.92, β≈0.08)
SRML: 权重由设计者决定 (α可调节)
2.4 梯度分析
对think部分的梯度:
∂L_MSL/∂θ_think = α × ∂L_think/∂θ
传统SFT: 系数为 0.92
SRML: 系数为 α (可调,通常0.5-0.7)
对result部分的梯度:
∂L_MSL/∂θ_result = β × ∂L_result/∂θ
传统SFT: 系数为 0.08
SRML: 系数为 β (可调,通常0.3-0.5)
效果: - ✅ 降低α:减少对长推理的过度关注 - ✅ 提高β:增强对函数调用准确性的重视 - ✅ 平衡优化:推理和执行都重要
3. 超参数α的选择
3.1 消融实验(Figure 4b)
实验设置: - 模型:Qwen2.5-Coder-7B / Llama-3.2-3B - 评估指标:Live Accuracy(真实场景准确率) - α范围:0.1 - 0.9
Qwen2.5-Coder-7B结果:
α=0.1: 67.5% # β太大,过度关注result,推理不足
α=0.2: 69.0%
α=0.3: 70.5%
α=0.4: 71.2%
α=0.5: 71.8% # ← 最优点
α=0.6: 71.7%
α=0.7: 71.5%
α=0.8: 70.0%
α=0.9: 68.5% # 接近传统SFT,result权重过低
Llama-3.2-3B结果:
α=0.1: 65.0%
α=0.3: 68.0%
α=0.5: 71.5% # ← 最优点
α=0.7: 71.0%
α=0.9: 67.0%
统计分析:
传统SFT隐含α值:0.92
FunReason最优α值:0.5-0.7
改进幅度:
Qwen: 68.5% → 71.8% (+3.3%)
Llama: 67.0% → 71.5% (+4.5%)
3.2 α选择的理论指导
推荐公式:
def recommend_alpha(N_think_mean, N_result_mean):
"""
基于token统计推荐α值
"""
ratio = N_think_mean / N_result_mean # xLAM约为11:1
if ratio > 10:
# 极度不平衡,需要大幅调整
alpha = 0.5
elif ratio > 5:
# 中度不平衡
alpha = 0.6
else:
# 轻度不平衡
alpha = 0.7
return alpha
# xLAM case
alpha = recommend_alpha(350.74, 31.07) # 返回 0.5
实验验证: - ✅ α=0.5在两个模型上都接近最优 - ✅ 与理论推荐一致 - ✅ 具有跨模型泛化性
4. SRML实现细节
4.1 代码实现(简化版)
import torch
import torch.nn as nn
class MultiscaleLoss(nn.Module):
def __init__(self, alpha=0.5):
super().__init__()
self.alpha = alpha
self.beta = 1 - alpha
self.ce_loss = nn.CrossEntropyLoss(reduction='none')
def forward(self, logits, labels, think_mask):
"""
Args:
logits: [batch, seq_len, vocab_size]
labels: [batch, seq_len]
think_mask: [batch, seq_len] # 1表示think部分,0表示result
"""
# 计算逐token的loss
token_loss = self.ce_loss(
logits.view(-1, logits.size(-1)),
labels.view(-1)
).view(labels.size())
# 分离think和result的loss
think_loss = (token_loss * think_mask).sum() / think_mask.sum()
result_loss = (token_loss * (1-think_mask)).sum() / (1-think_mask).sum()
# 多尺度加权
total_loss = self.alpha * think_loss + self.beta * result_loss
return total_loss, {
'think_loss': think_loss.item(),
'result_loss': result_loss.item(),
'total_loss': total_loss.item()
}
# 使用示例
loss_fn = MultiscaleLoss(alpha=0.5)
loss, metrics = loss_fn(logits, labels, think_mask)
4.2 如何生成think_mask?
方法1:基于特殊token
def create_mask_from_tokens(input_ids, tokenizer):
"""
假设格式: <think>...</think>\n[func()]
"""
think_start_id = tokenizer.convert_tokens_to_ids('<think>')
think_end_id = tokenizer.convert_tokens_to_ids('</think>')
mask = torch.zeros_like(input_ids)
in_think = False
for i, token_id in enumerate(input_ids):
if token_id == think_start_id:
in_think = True
elif token_id == think_end_id:
in_think = False
if in_think:
mask[i] = 1
return mask
方法2:基于位置索引
def create_mask_from_positions(seq_len, think_end_pos):
"""
think_end_pos: CoT结束位置
"""
mask = torch.zeros(seq_len)
mask[:think_end_pos] = 1 # 前think_end_pos个token为think
return mask
4.3 训练循环
def train_with_srml(model, dataloader, alpha=0.5, epochs=3):
loss_fn = MultiscaleLoss(alpha=alpha)
optimizer = torch.optim.AdamW(model.parameters(), lr=4e-5)
for epoch in range(epochs):
for batch in dataloader:
# 前向传播
logits = model(batch['input_ids'])
# 计算SRML loss
loss, metrics = loss_fn(
logits,
batch['labels'],
batch['think_mask']
)
# 反向传播
loss.backward()
optimizer.step()
optimizer.zero_grad()
# 记录metrics
if step % 100 == 0:
print(f"Epoch {epoch}, Step {step}")
print(f" Think Loss: {metrics['think_loss']:.4f}")
print(f" Result Loss: {metrics['result_loss']:.4f}")
print(f" Total Loss: {metrics['total_loss']:.4f}")
5. Self-Refinement策略
5.1 Self-Refinement循环
完整流程:
┌──────────────────────────────────────────────────┐
│ Self-Refinement Loop │
└──────────────────────────────────────────────────┘
Round 0: 初始训练
xLAM原始数据 → [用QwQ生成CoT] → xLAM-cot
↓
[FCDR清洗] → FunReason-FCDR-60K
↓
[SRML训练] → Model_v1
Round 1: 自我精炼
xLAM原始数据 → [Model_v1生成CoT] → Model_v1-cot
↓
[FCDR清洗] → FunReason-FCDR-v2
↓
[SRML训练] → Model_v2
Round 2+: 持续迭代
...可以继续,但论文只做了1轮
5.2 为什么自我精炼有效?
理论基础:
1. 初始模型已经学会基本的function calling
2. 让模型生成 → 暴露模型的弱点
3. FCDR识别并修正 → 针对性改进
4. 再次训练 → 强化正确行为
类比: 学生做题 → 老师批改 → 学生订正 → 再次练习
实验证据:
Qwen2.5-Coder-7B:
- 初始训练(FCDR-SFT): 81.94%
- +SRML: 83.66% (+1.72%)
- +Self-Refinement: (论文未单独测试,但整体pipeline有效)
5.3 实现代码
def self_refinement_loop(base_model, xlam_data, num_rounds=2):
"""
自我精炼训练循环
"""
current_model = base_model
for round_idx in range(num_rounds):
print(f"\n=== Self-Refinement Round {round_idx} ===")
# Step 1: 用当前模型生成CoT
cot_data = []
for sample in xlam_data:
cot = current_model.generate_cot(
query=sample['query'],
tools=sample['tools']
)
cot_data.append({
'query': sample['query'],
'tools': sample['tools'],
'cot': cot,
'reference': sample['answer']
})
# Step 2: FCDR清洗
fcdr_pipeline = FCDRPipeline()
refined_data = fcdr_pipeline.process(cot_data)
print(f"FCDR保留率: {len(refined_data)/len(cot_data)*100:.1f}%")
# Step 3: SRML训练
current_model = train_with_srml(
model=current_model,
data=refined_data,
alpha=0.5,
epochs=3
)
# Step 4: 评估
score = evaluate_on_bfcl(current_model)
print(f"Round {round_idx} BFCL Score: {score:.2f}%")
return current_model
6. FCDR + SRML 协同效应
6.1 单独使用 vs 联合使用
对比实验(推测,论文未明确单独测试):
配置 | Live Acc | Overall | 说明 |
---|---|---|---|
Baseline (纯SFT) | 77.60% | 81.94% | xLAM-cot + 传统loss |
+FCDR only | ~78.5% | ~82.5% | 数据清洗提升 |
+SRML only | ~79.0% | ~82.8% | 损失改进提升 |
+FCDR+SRML | 80.31% | 83.66% | 协同效应 |
协同原因:
FCDR: 提供高质量数据 → 减少噪声
SRML: 平衡优化目标 → 提高效率
1 + 1 > 2 的效果
6.2 为什么不是简单叠加?
数据质量对SRML的影响:
# 劣质数据 + SRML
数据: CoT逻辑错误,但格式正确
SRML: 仍会学习错误的推理模式
结果: 损失函数平衡了,但学习了错误知识
# 优质数据 + SRML
数据: FCDR清洗过,CoT和Result都正确
SRML: 在正确知识上做平衡优化
结果: 事半功倍
损失函数对数据利用的影响:
# 优质数据 + 传统SFT
数据: FCDR清洗过的高质量数据
传统SFT: 过度关注think部分
结果: 浪费了result部分的高质量标注
# 优质数据 + SRML
数据: FCDR清洗过的高质量数据
SRML: 充分利用think和result
结果: 数据价值最大化
7. 对比其他方法
7.1 vs 传统SFT
维度 | 传统SFT | FunReason |
---|---|---|
损失函数 | 统一CE loss | 多尺度加权 |
数据清洗 | 无/简单过滤 | 5阶段自动验证 |
训练策略 | 单轮训练 | Self-Refinement |
灾难性遗忘 | 严重(-46%) | 轻微(-3%) |
Live Acc | 77.60% | 80.31% |
7.2 vs RL-based方法
RL方法(如ToolRL):
# 强化学习范式
reward = execution_success + format_correctness
policy_gradient = ∇log P(action) × reward
优点: - ✅ 直接优化成功率 - ✅ 可以处理不可微的奖励
缺点: - ❌ 需要设计reward function - ❌ 训练不稳定(高方差) - ❌ 样本效率低(需要大量rollout)
FunReason (SFT-based):
# 监督学习范式
loss = α × L_think + β × L_result
优点: - ✅ 训练稳定 - ✅ 样本效率高 - ✅ 实现简单
缺点: - ❌ 依赖高质量标注(通过FCDR解决) - ❌ 难以优化不可微指标(通过精心设计loss缓解)
性能对比:
Tool-N1-7B (xLAM-RL): 82.01%
FunReason-7B: 83.66% (+1.65%)
结论: SFT配合好的数据和loss可以超越RL
7.3 vs Loss Masking方法
Loss Masking(如Instruction Tuning常用):
# 只计算response部分的loss
mask = create_response_mask(input_ids)
loss = CrossEntropyLoss(logits[mask], labels[mask])
问题: - 完全忽略instruction/CoT部分 - 可能导致推理能力下降
SRML vs Loss Masking:
Loss Masking: [思考过程不算loss] + [结果部分算loss]
SRML: [思考过程loss×0.5] + [结果部分loss×0.5]
区别: SRML不完全丢弃think的学习,只是降低权重
8. 实践建议
8.1 如何应用到自己的任务?
Step 1: 数据统计
# 统计你的数据集
def analyze_dataset(dataset):
think_tokens = []
result_tokens = []
for sample in dataset:
think_len = count_tokens(sample['reasoning'])
result_len = count_tokens(sample['function_call'])
think_tokens.append(think_len)
result_tokens.append(result_len)
print(f"Think平均: {np.mean(think_tokens):.1f}")
print(f"Result平均: {np.mean(result_tokens):.1f}")
print(f"比例: {np.mean(think_tokens)/np.mean(result_tokens):.1f}:1")
Step 2: 选择α
ratio = mean_think_tokens / mean_result_tokens
if ratio > 10:
alpha = 0.5
elif ratio > 5:
alpha = 0.6
elif ratio > 2:
alpha = 0.7
else:
alpha = 0.8 # 轻微不平衡,接近传统SFT
Step 3: 实现FCDR(简化版)
# 如果没有QwQ-32B,可以用其他模型
def simple_fcdr(data, model="gpt-4o-mini"):
"""
简化的3阶段清洗:
1. 格式检查(规则)
2. 参数检查(规则)
3. 逻辑检查(LLM)
"""
cleaned = []
for sample in data:
# Stage 1: 格式检查(纯规则)
if not check_format(sample['function_call']):
sample = fix_format(sample)
# Stage 2: 参数检查(规则+schema)
if not check_parameters(sample, schema):
sample = fix_parameters(sample, schema)
# Stage 3: 逻辑检查(用LLM)
if not llm_verify_logic(sample, model):
continue # 逻辑不通就丢弃
cleaned.append(sample)
return cleaned
Step 4: 训练
# 完整训练脚本
model = load_base_model("Qwen2.5-7B-Instruct")
# FCDR清洗
cleaned_data = simple_fcdr(raw_data)
# SRML训练
alpha = 0.5 # 根据Step 1结果调整
trained_model = train_with_srml(
model=model,
data=cleaned_data,
alpha=alpha,
epochs=3,
lr=4e-5
)
# 评估
score = evaluate(trained_model, test_set)
8.2 资源受限时的简化方案
方案A: 只用SRML(不用FCDR)
# 适用场景: 数据质量已经较好
# 节省: FCDR的计算成本
alpha = 0.5 # 根据token统计确定
model = train_with_srml(model, data, alpha)
方案B: 只用FCDR(不用SRML)
# 适用场景: token比例还算平衡(ratio<5)
# 节省: 不需要修改训练代码
cleaned_data = fcdr_pipeline.process(data)
model = train_standard_sft(model, cleaned_data)
方案C: 两者简化版
# FCDR简化: 只用Stage 4+5(格式和参数修正)
# SRML简化: 固定α=0.5,不做消融
quick_cleaned = format_and_param_check(data)
model = train_with_srml(model, quick_cleaned, alpha=0.5)
9. 未来改进方向
9.1 动态权重调整
当前: α固定为0.5 改进: 根据训练阶段动态调整
def dynamic_alpha(epoch, total_epochs):
"""
早期: 更关注think(学习推理)
后期: 更关注result(精确执行)
"""
# 线性衰减
alpha = 0.7 - 0.2 * (epoch / total_epochs)
# epoch 0: α=0.7, β=0.3
# epoch final: α=0.5, β=0.5
return alpha
9.2 样本难度加权
当前: 所有样本权重相同 改进: 根据样本复杂度调整权重
def difficulty_weighted_loss(loss, difficulty):
"""
difficulty: 样本难度(0-1)
简单样本: 降低权重
困难样本: 提高权重
"""
weight = 0.5 + 0.5 * difficulty
return loss * weight
9.3 多任务联合训练
当前: 只训练function calling 改进: 同时训练code generation
L_total = α × L_think
+ β × L_result
+ γ × L_code # 额外的代码生成任务
10. 总结
核心takeaway
组件 | 核心思想 | 关键创新 |
---|---|---|
FCDR | 数据是根本 | 5阶段自动清洗,LLM辅助LLM |
SRML | 损失要平衡 | 打破token数量束缚,手动设权重 |
Self-Ref | 自我提升 | 模型生成→清洗→再训练 |
可复用的方法论
- 数据驱动设计: 先统计→发现问题→针对性解决
- 分而治之: 复杂问题拆解为多阶段处理
- 质量优于数量: 60K高质量>100K低质量
- 自动化优先: 用LLM代替人工标注
- 理论与实验结合: 数学推导+消融实验
适用场景
✅ 适合用FCDR+SRML的场景: - 有推理过程+执行结果的任务(如function calling, code generation) - Token比例失衡(ratio>5) - 数据质量参差不齐
❌ 不适合的场景: - 纯生成任务(没有明确的"推理vs执行"划分) - Token比例本来就平衡 - 数据已经非常高质量
最后: 这两个技术的精髓在于"数据质量"和"损失设计"的协同,单用一个效果有限,组合使用才能发挥1+1>2的效果。