静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
Q
QianXun @QianXun · 2026-08-21 03:10

AgentEvolver 源码解读 · 第 5 章:Agent 执行流与上下文管理

> 前三章讲了「任务从哪来」「经验怎么攒」「奖励怎么拆」。然则 agent 究竟如何一步一步与环境过招?答案在 agentevolver/module/agent_flow/ 与 context_manager/。此章专攻「一回合之内」的微观机制:消息如何拼接、token 如何计、loss 如何遮、长文如何剪。

---

一、Why:多轮交互不是「拼字符串」那么简单

普通单轮 RL 只需把一条 prompt 喂给模型、收回一条回答。AgentEvolver 是多轮——agent 每步都要看环境回信、再写下一步代码,循环至多 max_steps=30 次。其间有三件麻烦事必须解决:

1. 上下文拼接:每步要把「历史全部消息 + 刚刚环境回信」重新拼成模型输入,且不同 CMT(上下文管理器)拼法不同。 2. loss 掩码:只让模型的生成参与梯度,环境的回信、初始指令不当作「学习对象」。 3. 长轨迹治理:30 步累积下来,上下文极易爆窗;需有策略压缩历史而不丢关键信息。

AgentFlow 是「导演」,ContextManager(CMT)是「场记」——导演喊 action,场记负责把每一镜记进带 token 与掩码的长卷。

---

二、导演:AgentFlow.execute 的主循环

agent_flow.py 的 execute 是整段执行流的心脏,九步一目了然:

def execute(self, context_manager, init_messages, env, instance_id, ...):
    add_nothink = self.config...use_qwen3          # qwen3 加 <think:6124c78e> 抑制
    # ① 经验注入:把 ReMe 召回的失败经验拼进 init_messages 末条
    init_messages, traj_exp_config = self.exp_worker.manage_rollout_context(init_messages, traj_exp_config)
    self.cmt.metadata["task_train_exp_mode"] = traj_exp_config.train_mode
    self.cmt.save_init_input(init_messages, add_nothink)

    for act_step in range(self.max_steps):
        # ② 准备下一步模型输入(各 CMT 拼法不同)
        step_input = self.cmt.prepare_next_llm_context()
        # ③ token 溢出检查
        if not self.cmt.check_context_token_num_safe(step_input):
            self.cmt.is_terminated = False ; break      # 没跑完,但先撤
        # ④ 调模型
        llm_output = self.llm_chat_fn(step_input, request_id=request_id)
        # ⑤ 落盘模型输出(author=llm)
        self.cmt.save_llm_output(llm_output, input_msg_ref=step_input)
        # ⑥ 与世界交互:把刚生成的内容交给环境执行
        env_output = env.step(instance_id, {"content": self.cmt.prepare_world_interaction(),
                                            "role": "assistant"})
        if env_output["state"]["role"] == "tool":
            env_output["state"] = convert_tool_to_user_message(env_output["state"], tokenizer, format="qwen")
        # ⑦ 落盘环境输出(author=env,截断到 max_env_len)
        state = env_output["state"] ; state.pop('tool_calls', None)
        self.cmt.save_env_output(state, input_msg_ref=step_input, add_nothink=add_nothink)
        # ⑧ 终止判定
        self.cmt.is_terminated = env_output["is_terminated"]
        if self.cmt.is_terminated: break

    # ⑨ 算分:优先用 RewardCalculator,否则直接 env.evaluate
    if self._reward_calculator is not None:
        grader_res = self._reward_calculator.calculate_reward(self.cmt, env, instance_id)
        score = grader_res["score"] ; reason = grader_res["reason"]
    else:
        score = env.evaluate(instance_id, params={"sparse": self.sparse})
        reason = "Outcome 1 = success, 0 = failure."
    self.cmt.reward = Reward(outcome=score, success_rate=1.0 if score>=1 else 0.0,
                              madness=self.cmt.compute_madness(), description=reason)
    self.cmt.remove_last_context()                     # 末条若非 llm,剔除,保证以模型结尾
    self.cmt.generate_log(task_id=task_id)             # 彩色日志
    return self.cmt

两个关键拐弯:

  • 经验注入在循环之前(第①步):这是第 3 章 ReMe 的落地点——manage_rollout_context 已把召回的经验塞进 init_messages 末尾,故 agent 从第一步起就「带着前车之鉴」。
  • 终止的两种语义:is_terminated=True 表示任务正常完成(complete_task 被调用);若因 token 溢出跳出,则 is_terminated=False——这条轨迹算「没跑完」,训练时仍能部分利用(取决于 remove_last_context 是否清掉不完整的末步)。
env.step 喂进去的是 prepare_world_interaction() 的返回值:对 Linear_CMT 就是 full_context[-1].content(即模型刚写的整段);对 MemoryCMT 则只抽 # next-step instruction code 那一节代码——详见第四节。

BaseAgentFlow(base_agent_flow.py)只是抽象基类,守住三把尺:max_steps、max_model_len、max_env_len,具体 execute 由子类实现。

---

三、场记之基:ExtendedMessage 与 loss 掩码

一切上下文操作的原子是 ExtendedMessage(cmt_base.py):

class ExtendedMessage:
    author          # "llm" / "env" / "initialization" / "memory" / "llm(do_not_train)"
    role            # "assistant" / "user" / "system"
    content         # 原始文本
    token_arr       # 该消息自身的增量 token(非整段!)
    clip            # 是否截断
    uuid            # 全局唯一,供 SelfContextClip 反查定位

need_training 决定谁进梯度:

NEED_TRAIN_AUTHORS   = ["llm"]
NON_TRAIN_AUTHORS    = ["env", "initialization", "user", "memory", "llm(do_not_train)"]
return self.author in NEED_TRAIN_AUTHORS

get_loss_mask 是掩码的精密手术:

def get_loss_mask(self, blackout_token_combo):
    if self.need_training:
        mask = [1] * len(self.token_arr)
        # 1. 首次出现 `<|im_start|>assistant\n` 之后的 token 全黑(不让模型学「扮演自己开场」的模板)
        mask = blackout_specific_token_ids_first_encounter(mask, self.token_arr, blackout_token_combo)
        # 2. EOS 之后的所有 token 全黑(生成结束后的尾巴不学)
        mask = blackout_everything_after_eos_but_keep_eos(mask, self.token_arr, self.eos_token_id)
        return mask
    else:
        return [0] * len(self.token_arr)   # 环境/指令/记忆:一律不学

一句话:只有 author=llm 的消息参与损失,且其中「开场模板 token」与「EOS 之后」被抠掉。这正是多轮 RL 里「只训模型决策、不训环境回信」的标准做法。

clip=True 的消息(典型是环境回信)在构造时即按 clip_token_limit=max_env_len(默认 8192)截断,长回信只留前段、尾加 ... truncate ...,避免单步环境输出撑爆上下文。

---

四、五式 CMT:线性 / 思考 / 幽灵提示 / 记忆 / 自剪

ContextManagerBase 定义抽象接口(save_init_input / prepare_next_llm_context / check_context_token_num_safe / prepare_world_interaction / save_llm_output / save_env_output / group_tokenize)。五套实现各管一类训练范式:

1. Linear_CMT(线性,最朴素)

  • prepare_next_llm_context 直接返回 full_context 全量('future' 模式:用 content_for_future)。
  • save_env_output 把环境回信记作 author=env, role=user,并 clip。
  • tokenize_steps 是 tokenizer 总装:遍历所有消息拼 input_ids/attention_mask/loss_mask,按首个 need_training 消息切分 prompt / response,再算 position_ids(用 verl 的 compute_position_id_with_mask)。
  • group_tokenize 只产 1 个 Sample(整条轨迹当一条样本)。
  • compute_madness 查 llm_output_mistakes:若某步触发重复惩罚(repetition_penalty_reward_scalar),且低于阈值,则 madness=-1.0,最终 reward.outcome 被 reward_patch 压成 −1——对「胡言乱语」直接判负。

2. LinearThinkCMT(先思考,后行动)

继承 Linear_CMT,专为「... + 动作」范式设计。差异在 prepare_next_llm_context:

# 历史里的 llm 消息:剥掉 <think>...</think>,且改 author="llm(do_not_train)"(历史思考不当作学习对象)
new_content = re.sub(r'<think>.*?</think>', '', ext_msg.content, flags=re.DOTALL)
author_override = "llm(do_not_train)"
# 末条(最新环境回信):追加 think_hint,逼模型「先想后动」
if is_last:
    content += self.think_hint

think_hint 随 force_think 与 env_feedin_preference 而变:box 模式要 \{...} 收尾,code 模式要 ___CODE_BLOCK_5___python

save_llm_output 时,把本步抽成一条 memory 消息

memory_construct = f">> step: {current_step}\n>> instruction:\n{next_step_code}\n>> feedback:\n{feedback}\n---" self.full_context += [ExtendedMessage(author="memory", role="assistant", content=memory_construct)]

prepare_next_llm_context 时,把所有 memory 拼成 "Previous steps:" 段喂回

___CODE_BLOCK_6___python def after_save_llm_output(self, this_interaction): clip_token_cnt = self.config...context_template_clip_trigger_token_num if self._get_seq_length(this_interaction) < clip_token_cnt: return # 未到阈值不剪 if self.clipped_before: return # 只剪一次 # 调 alien LLM:审每条 AR/ER,判 keep / remove / compress ... ___CODE_BLOCK_7___python class GraderResult(TypedDict): score: float reason: str | None

class RewardCalculator(ABC): def __init__(self, task: Task): self._task = task @abstractmethod def calculate_reward(self, trajectory, env, instance_id) -> GraderResult: ... `

AgentFlow 有 RewardCalculator 则走它算分(可做稀疏/稠密、可带 reason 文本),否则退回 env.evaluate。评分结果打包成 Reward(outcome, success_rate, madness, description)——注意 madness 来自 CMT 的重复惩罚检测,与 outcome 共同决定最终奖励。description 会进 reward_scores 随样本落盘,便于复盘「这条为啥正负」。

---

六、小结

第 5 章的骨架:AgentFlow.execute 是九步主循环(经验注入→拼上下文→查 token→调模型→落盘→交环境→落盘→判终止→算分);ExtendedMessage 是带 token 与掩码的原子,借 need_training + get_loss_mask 实现「只训模型决策、不训环境回信」;五式 CMT 各司其职——Linear 朴素全量、Think 剥历史思考逼当下想、Memory 用 markdown 四段积累摘要、SelfContextClip 唤外部模型自动归档防爆窗、phantom_hint` 为预留位。一句话:上下文管理器,是把「多轮对话」编译成「带掩码的 token 序列」的编译器;导演管流程,场记管记法。

下一章,看这套执行流对外依赖什么——环境服务与基建(env_service / AppworldEnv / schema)。

暂无表态