AgentEvolver 源码解读 · 第 4 章:步骤归因(Self-Attributing / ADCA-GRPO)
> 三大机制之三 Self-Attributing,也是最锋利的一环。代码全在 agentevolver/module/adv_processor/:semantic_attribution.py(标注)+ adca_grpo.py(融合优势)。
---
一、Why:长轨迹的信用分配难题
GRPO 只拿「终局成败」当组间Baseline——一条 30 步轨迹,成则步步有功、败则步步背锅,分不清哪步真妙、哪步瞎搞。Self-Attributing 借 LLM 的因果判断,给每步贴 GOOD/BAD,把学习信号从「结果」拆成「过程质量 + 结果有效性」,做步级精细归因。论文报:约 +10% 性能、−40% 训练步数。
---
二、入口:compute_prm_grpo_advantages
adca_grpo.py 的入口把原始 batch 变成 token 级优势:
def compute_prm_grpo_advantages(batch, step_flags, hyper=None, scheme="decouple"):
step_ids = _ensure_tensor(batch.batch["step_ids"], ..., dtype=torch.long) # (B,L_resp), -1=非响应
# 对齐实际 response 长度(补 -1 或截断)
group_ids = _ensure_tensor(batch.batch["group_ids"], ..., dtype=torch.long).view(-1)
token_level_rewards = _ensure_tensor(batch.batch["token_level_rewards"], ...)
orm_scores = token_level_rewards.sum(dim=1) # 轨迹级 ORM 分
step_rewards, extra_metrics = _build_decouple(orm_scores, step_flags, step_ids, group_ids, hyper)
step_adv = suffix_sum_on_steps(step_rewards) # 步级 suffix sum
advantages = broadcast_step_adv_to_tokens(step_adv, step_ids) # 广播到 token
return {"advantages": advantages, "orm_scores": orm_scores, "metrics": extra_metrics}
step_ids 是关键索引:标记每个 token 属第几步,-1 为非响应 token。它串起「步 → token」的桥。
---
三、语义标注:每样本一次 API
semantic_attribution.py 的 evaluate_step_flags_parallel 是标注主力:
api_client = AsyncOpenAI(api_key=..., base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
semaphore = asyncio.Semaphore(max_concurrent)
# 每样本建一个 EvaluationTask(不是每步!),一次 API 调用评完全部步
for task in all_tasks:
coroutines.append(_evaluate_single_sample_api(api_client, model_name, task, semaphore, ...))
batch_results = await asyncio.gather(*coroutines, return_exceptions=True)
效率要点:一个样本一次调用(不是每步一次),max_concurrent 控并发,整体省下「步数/样本数」倍的 API。
省钱的 skip
if skip_type == "skip_small_adv":
if abs(advantage) < 1e-8: # 优势≈0 不值得标
should_skip = True
elif skip_type == "skip_all_neg":
if orm_reward <= THRESHOLD: # ORM 非正不标
should_skip = True
跳过者直接按 overall_score 符号给全 GOOD/全 BAD——把算力留给「有信息量」的样本。解析与容错
def parse_batch_evaluation_result(response, num_steps):
numbered = {int(m.group(1)): m.group(2).upper()=="GOOD"
for m in re.finditer(r"Step\s+(\d+)\s+Judgment:\s*(GOOD|BAD)", response, re.I)}
if len(numbered) == num_steps: return [numbered[i] for i in range(num_steps)]
...
_async_safe_query 是铁打的重试壳:max_retries=200,遇 429 指数退避(封顶 60s)+ 抖动;内容审核失败两次即返空串(不崩训练)。每条结果可落 EvaluationRecord 到 llm_evaluation_log_dir,便于复盘标注质量。---
四、decouple 融合:过程与结果各自归一
_build_decouple 是算法心脏,四步:
# 1. 原始 PRM 奖励:GOOD=+fix_base, BAD=-fix_base(fix_base 默认 0.2)
prm_rewards_raw = [[hyper.fix_base if f else -hyper.fix_base for f in flags] ...]
# 2. PRM 步级 z-score(组内)
prm_rewards_std = _group_zscore_on_steps(prm_rewards_raw, group_ids, hyper)
# 3. ORM 组级 z-score
orm_scores_std[i] = (orm - mean) / (std + 1e-12)
# 4. 融合
for j, prm_reward in enumerate(prm_std):
if orm_distribution == "last_step":
combined = alpha*prm_reward + orm_std if j == K-1 else alpha*prm_reward
elif orm_distribution == "all_steps":
combined = alpha*prm_reward + orm_std
decouple 的精髓:PRM(过程质量)与 ORM(结果)先各自独立 z-score 归一,再按 alpha 融合。归一是防一个信号淹没另一个——过程与结果各守各的尺度,互不抢戏。equal_trajectory_weight=True(GRPO)时每组轨迹等权;False 则扁平成 GSPO 大池(噪声环境更稳)。length_normalization 开启则乘 1/sqrt(K) 平衡长短轨迹。prm_steps 让标注只在前 N epoch 跑——后期模型已学会,关掉 API 省钱。
产出的 decouple_stats 一堆监控:outcome_share_last_mean(末步 ORM 占比)、alpha_effective(实际 α 比)、sum_step_reward_sign_agree(合成奖励与 ORM 符号一致率)——可当作「归因健康度仪表盘」。
---
五、步级 → token 级:两道桥
def suffix_sum_on_steps(step_rewards):
t = torch.tensor(r)
s = torch.flip(torch.cumsum(torch.flip(t,0),0),0) # 未来奖励后缀和
return s
def broadcast_step_adv_to_tokens(step_adv, step_ids):
out = torch.zeros((B, L), device=device)
for i in range(B):
valid = step_ids[i] >= 0
out[i, valid] = adv_i[step_ids[i][valid]] # 按 step_ids 映射
return out
suffix_sum_on_steps 给每步算「从这一步往后还能拿多少未来奖励」——这恰是优势函数的直觉定义。broadcast_step_adv_to_tokens 借 step_ids 把步优势铺到它名下每个 token;-1(非响应)填 0,不污染损失。
---
六、接驳点:优势如何改写训练
回到第 1 章 ae_ray_trainer 的 fit:当 attribution_driven_credit_assignment.enable=true,训练器在算出 token_level_rewards 后调 evaluate_step_flags_parallel 拿 step_flags,再 compute_prm_grpo_advantages 覆盖 batch.batch["advantages"]。整条 GRPO 链路不变,只换掉优势这一层——这就是「可插拔」的真意。
basic.yaml 里 enable: false,故最小化训练走原版 GRPO;全开时把 alpha 从 0.05~0.2 起调、prm_steps=20 控成本、skip_type=skip_small_adv 省 API,便是《深度研究》里 +10% 的来源。
---
七、小结
Self-Attributing 的代码骨架:语义标注(每样本一 API、skip 省钱、正则解析、重试壳)→ decouple 融合(GOOD/BAD 转 ±fix_base → 独立 z-score → α 平衡过程与结果)→ 后缀和 + step_ids 广播到 token → 覆盖 advantages。一句话:用 LLM 当「分步阅卷老师」,把终局成败拆成每步好坏,再各自归一、谨慎融合,让长轨迹里每一脚好棋都算数。
下一章,看 agent 到底怎么一步一步跟环境过招——Agent 执行流与上下文管理。