qinghuan
← Agent 算法与学习 / 全部笔记

单元 37 / Agent 算法与学习

P0 · 必会主线L3 · 深入

ReAct 与交互轨迹:建立可学习的 Agent 环境

让 ReAct 中的观察与行动真正发生在环境里,记录能重新回放的过程,并规定怎样才算任务成功。

本篇目录 · 按需要跳转

这篇重点学什么

本篇重点:按步骤记录观察、动作、奖励和结束原因,区分任务真正结束与预算用尽的截断,再决定哪些内容作为监督训练标签。

开始读正文 ↓
学过相近内容?查看哪些可以跳过

两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。

工具调用与轨迹

共同基础模型、工具、观察与行动的交互结构相通。

另一条路线补充什么?

先规定允许调用哪些工具(白名单),再记录调用 ID,处理重复与冲突,并设置执行预算和停止条件。

读完这篇,你能做什么

运行三步走廊环境,区分成功、提前结束和预算用尽;用 Java 审计器拒绝伪造观测与任务结束后的动作。

学习理由、准备要求与知识点

为什么学它

Agent 自称成功,不代表环境里的任务已经完成。环境和轨迹格式要能检查,记录的数据才能继续用于策略训练。

先读这些内容

知识点清单

  • 观察与真实状态
  • 动作空间与解析
  • 决策摘要与环境证据
  • 轨迹事件结构
  • 任务终止与预算截断
  • 可回放环境版本
  • 动作掩码与训练标签
  • 可执行成功判定

运行前先确认ReAct论文v3、Gymnasium v1.2.0接口基线;Java17算法与Python3.14标准库确定环境。未执行语言模型、GPU或真实外部工具。

01先弄懂它是什么

研究 Agent 是否会做事,要看它实际怎样与环境一步步交互。例如在时间 t,策略收到观测 o_t,根据已有历史选择动作 a_t,环境执行后再返回新观测、奖励和结束信息。这样从开始到结束的记录叫轨迹。真实状态 s_t 还可能包含它看不见的网页内部信息或任务目标;如果把完整答案直接放进观测,就等于提前泄露了本该通过任务获得的信息。

ReAct 把语言推理和任务动作交替安排,让模型收到外部反馈后调整下一步。实验中可以记录简短决策摘要、结构化动作和环境返回,但摘要只是记录方式,不能当作模型内部计算过程的可靠证明。例如说“查到了资料”,应有工具实际返回作证;说“任务完成”,应检查最终环境状态,不能让策略自己写一个 success=true 就算过关。

一条训练记录至少区分任务标识、环境版本、初始条件、动作、观测、奖励和终止原因。若将工具返回文本和模型动作全部拼成无角色长字符串,后续做模仿学习或策略梯度时容易把环境输出当成策略生成标签。先建立可执行的小环境,能够在引入语言模型前验证这些数据边界。

文中这几个词,先认一认

观测
环境允许策略看到的内容,可能只是完整状态的一部分。
轨迹
一次任务从初始条件出发,到完成、终止或被截断之间的一连串交互记录。
截断
因为时间或预算限制而停止,即使任务本身还没有自然结束。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

本篇环境是一条状态0、1、2的走廊,初始在0,动作只有left、right和finish。移动在边界处停留,每个合法动作消耗一步;仅在位置2执行finish才奖励1。到达位置2却没有提交finish仍不是成功,这使“状态接近目标”和“按协议完成”成为两个可以单独测试的条件。

环境返回 observation、reward、terminated、truncated 和 info。要分清“任务自然结束”和“因为限制被迫停止”:本实验提前 finish 算自然终止但失败,预算耗尽仍未 finish 算截断,并约定两标志互斥、任务结束优先。真实框架可能允许两者同时为真,应按具体环境解释。训练时要不要估计末状态后续价值,也取决于任务定义,不能把所有停止都当作后续价值为零。

轨迹审计器从已知初始状态重算每步转移,再对照记录的观测与奖励。如果日志声称从0向右一步就到2,或者结束后仍执行动作,审计立即失败。对真正随机环境需要固定随机数流或保存足够事件证据;仅记录一个种子不总能跨实现重建结果。这里使用确定性环境,是为了把回放契约与模型能力分开验证。

先跑通一个最小例子

Python标准库实验,执行 python experiment.py。Corridor是原创确定环境,不继承Gymnasium,不访问网络,也不训练模型。

展开收起代码 · experiment.pypython · 43 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

experiment.py · python
class Corridor:
    def __init__(self, budget=3):
        if type(budget) is not int or budget<1:
            raise ValueError("positive budget")
        self.budget=budget
        self.reset()
    def reset(self):
        self.state=0;self.used=0;self.closed=False
        return self.state, {"version":"corridor-v1"}
    def step(self, action):
        if self.closed:
            raise RuntimeError("reset required")
        if action not in ("left","right","finish"):
            raise ValueError("unknown action")
        if action=="left": self.state=max(0,self.state-1)
        if action=="right": self.state=min(2,self.state+1)
        self.used+=1  # 每个合法动作消耗一次预算,包括finish。
        terminated=action=="finish"
        truncated=not terminated and self.used>=self.budget
        reward=int(terminated and self.state==2)  # 由环境状态判定,不信任动作说明。
        self.closed=terminated or truncated
        return self.state,reward,terminated,truncated,{"used":self.used}

env=Corridor()
trace=[]
for action in ["right","right","finish"]:
    trace.append((action,env.step(action)))
assert trace[-1][1][1:4]==(1,True,False)
try: env.step("right")
except RuntimeError: pass
else: raise AssertionError("acted after end")
assert env.reset()[0]==0
assert Corridor().step("finish")[1:4]==(0,True,False)
short=Corridor(1)
assert short.step("right")[1:4]==(0,False,True)
edge=Corridor()
assert edge.step("left")[0]==0
try: edge.step("teleport")
except ValueError: pass
else: raise AssertionError("unknown action accepted")
assert edge.used==1  # 非法动作拒绝前没有改变状态和预算。
print("success=1 steps=3; early_finish=0; budget_end=truncated")
print("PASS 8 checks; deterministic environment, no model call")

核对运行结果输出success=1 steps=3、early_finish=0、budget_end=truncated,并通过8项检查。

正在加载流程图…

动作由环境验证后产生观测,轨迹结束与预算耗尽均阻止继续执行。
查看流程图源码(Mermaid)
flowchart TD
  N0["定义可观察状态与允许动作"]
  N1["重置环境并记录版本"]
  N2["验证动作后执行状态转移"]
  N3["分别记录奖励与停止原因"]
  N4["回放审计后形成训练轨迹"]
  N0 --> N1
  N1 --> N2
  N2 --> N3
  N3 --> N4

按顺序看,每一步做了什么

  1. 定义可观察状态与允许动作
  2. 重置环境并记录版本
  3. 验证动作后执行状态转移
  4. 分别记录奖励与停止原因
  5. 回放审计后形成训练轨迹

03对照实现代码

Gymnasium的step契约与ReAct交互形式 (新标签页)↗

Gymnasium v1.2.0;ReAct arXiv:2210.03629v3 · Env.step / Env.reset;ReAct第2节交互定义

原创调度片段,validate_action与record为应用占位;不是框架源码。独立Python实验无需安装Gymnasium,也不调用语言模型。

核心机制代码 · python
def controlled_step(env, proposed_action):
    action = validate_action(proposed_action)  # 自定义解析器先检查允许动作与参数。
    observation, reward, terminated, truncated, info = env.step(action)  # 五项来自环境。
    record(action, observation, reward, terminated, truncated, info)  # 轨迹保留各字段身份。
    return observation, terminated or truncated  # 调度器停止后须按契约重置。

Gymnasium核心Env类规定step返回五元组,reset返回观测与info。基类并未替每种业务实现状态转移,具体环境需要自己定义动作与观测空间。本文Corridor类借用这一返回形状,但没有继承Gymnasium,也没有通过其环境检查器;它是可运行的标准库教学环境。

ReAct论文第2节把策略条件写成交互历史,并扩展语言推理与环境动作的组织方式。原论文的若干任务有各自的搜索、查找或导航动作,不能把本文三个走廊动作当作论文原始API。我们保留的是行动产生外部观测、观测影响后续决策的闭环结构。

终止与截断拆分是为了保留停止原因,尤其关系到价值自举与评测分母。日志接口应原样保留两种标志及错误类型,不能为了方便显示统一写成done后丢掉原始信息。超时、格式错误与自然失败可能有不同恢复策略,也可能暴露不同模型弱点。

短代码里的validate_action和record是应用上下文占位,不是Gymnasium真实函数。完整实验给出这类验证的具体小实现,Java部分进一步用环境规则审计轨迹。这样读者既能找到真实框架契约,也能识别哪些部分是研究者必须自己设计和测试的职责。

代码先调用谁,再调用谁

  1. 策略读取允许观测
  2. 动作解析与预算检查
  3. 具体Env.step执行
  4. 记录五元组与来源
  5. 结束或将新观测交回策略

04动手写一个例子

确定性轨迹重放审计

输入走廊动作日志与预算,返回成功、失败、截断或部分轨迹;拒绝伪造观测、奖励、终止标记和越预算动作。

输入 1
right,right,finish,预算3
输出 1
SUCCESS
输入 2
right,right,预算2
输出 2
TRUNCATED

审计从可信初始状态开始,而不是把日志第一条观测直接当真。每个动作经固定规则更新状态,再算奖励与终止值,对照记录。这个顺序保证待审数据只能被检查,不能反过来决定检查器认为什么状态应该出现。

循环不变量是当前state恰好等于已验证动作前缀的真实执行结果。一旦进入终止状态,后续任何动作都非法。预算在入口检查总步数,并在未终止时区分完整预算和不足预算,避免把尚在采集的部分日志误判为失败任务。

算法只做一次扫描,不搜索所有可能动作,也不判断决策是否最优。合法但绕路的轨迹可以通过审计,最后可能截断;这正是数据真实性与策略质量的区别。对于非确定环境,不能直接复用状态重算结论,需要记录随机结果或引入独立可核验事件。

为什么这样写是对的state由已验证动作前缀唯一决定;结束后没有后续动作,所有观测与奖励均与规则一致。

时间 O(T)额外空间 O(1)额外空间

逐步重算常数大小状态,输入日志空间另计。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 42 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static void near(double expected,double actual) {
        if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
            throw new AssertionError(expected+" != "+actual);
    }
    static void check(boolean ok) { if(!ok) throw new AssertionError(); }
    static void bad(Runnable r) {
        try { r.run(); } catch(IllegalArgumentException e) { return; }
        throw new AssertionError("invalid input accepted");
    }
enum Action { LEFT, RIGHT, FINISH }
    record Step(Action action,int observation,int reward,boolean terminal) {}
    static String audit(List<Step> steps,int budget) {
        if(steps==null||budget<1||steps.size()>budget) throw new IllegalArgumentException("budget");
        int state=0;boolean ended=false;boolean success=false;
        for(Step s:steps) {
            if(s==null||s.action()==null||ended) throw new IllegalArgumentException("after end");
            if(s.action()==Action.LEFT) state=Math.max(0,state-1);
            if(s.action()==Action.RIGHT) state=Math.min(2,state+1); // 环境重算状态,不信任日志自述。
            ended=s.action()==Action.FINISH;
            success=ended&&state==2;
            int reward=success?1:0; // 只有在目标处提交结束才算成功。
            if(s.observation()!=state||s.reward()!=reward||s.terminal()!=ended)
                throw new IllegalArgumentException("forged transition");
        }
        if(ended) return success?"SUCCESS":"FAILED";
        return steps.size()==budget?"TRUNCATED":"PARTIAL"; // 预算耗尽与正常终止分开。
    }
    public static void main(String[] args) {
Step r1=new Step(Action.RIGHT,1,0,false),r2=new Step(Action.RIGHT,2,0,false);
        check(audit(List.of(r1,r2,new Step(Action.FINISH,2,1,true)),3).equals("SUCCESS"));
        check(audit(List.of(new Step(Action.FINISH,0,0,true)),3).equals("FAILED"));
        check(audit(List.of(r1,r2),2).equals("TRUNCATED"));
        check(audit(List.of(),3).equals("PARTIAL"));
        bad(()->audit(List.of(new Step(Action.RIGHT,2,0,false)),3));
        bad(()->audit(List.of(new Step(Action.FINISH,0,0,true),r1),3));
        bad(()->audit(List.of(r1,r2),1));
        check(audit(List.of(new Step(Action.LEFT,0,0,false)),2).equals("PARTIAL"));
        System.out.println("PASS 8 cases");
    }
}

这些特殊情况也要试一试

  • 边界移动不越界
  • 提前finish合法但失败
  • 空日志是PARTIAL
  • 预算耗尽不是自动成功
进阶选读 · 部分可观测环境的数据协议

场景网页或工具环境隐藏内部状态且版本持续变化。

做法保存可见观测、动作参数、工具版本和事件ID,建立沙箱重放或可核验快照;明确哪些状态无法恢复,不把模拟重放等同真实外部副作用。

复杂度轨迹存储O(T×观测大小),去重和快照恢复成本取决于环境。

好处与代价完整记录提升诊断性,却增加存储与隐私管理成本。

05项目里怎么用

把环境成功条件写成独立函数并在训练前固定。若任务是创建工单,应查最终工单状态及归属,不能只检查回答里出现“已创建”。同样,检索任务应验证引用是否支持结论,而不是只计工具调用次数。可执行成功条件决定了之后奖励学习和统计比较的意义。

采集失败轨迹时保留错误原貌,同时做必要脱敏。提示注入、工具异常和权限拒绝应有独立标签,不要把环境的拒绝指令混入模型示范动作。共享训练数据前检查个人信息和环境凭据;实验版本变化时重新评估旧轨迹是否仍合法。

本文标准库实验没有调用外部模型,也没有训练策略,因此成功走到目标只证明环境与固定动作序列一致。后续可以替换成规则策略、模仿学习策略或语言模型,在相同任务与预算下比较。先有可重复基线,再谈ReAct提示或训练方法带来的实际增益。

容易踩的坑

  • 让模型自述代替环境成功判定
  • 把工具观测也作为生成标签
  • 丢掉终止与截断原始字段
  • 环境版本变化却复用旧结果
  • 只保存成功轨迹隐藏失败分布

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
确定性小环境容易回放与单步定位不能代表真实复杂任务接口与算法起步
沙箱真实环境保留更多真实交互启动与重置成本较高策略端到端评测
线上只读回放贴近真实资料分布难重建所有外部状态有明确证据边界的诊断

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01到目标为何仍需finish?

展开参考答案与推导

任务契约要求显式提交,可区分状态达成和流程完成。

02ReAct轨迹是否证明内部推理真实?

展开参考答案与推导

不证明;可记录文本只是外部输出,事实依赖环境证据。

03截断为何独立记录?

展开参考答案与推导

预算结束与自然终止有不同学习和诊断含义。

换个情况,还会处理吗

01工具返回成功但数据库没有变化怎么办?

展开参考答案与推导

以独立业务状态验证为准,检查工具适配、事务和观测是否伪成功。

02更换环境版本后旧轨迹不能回放怎么办?

展开参考答案与推导

保留版本并迁移或重新采集,不能默默修改旧记录使其通过。

03只训练成功轨迹有哪些风险?

展开参考答案与推导

缺少失败恢复状态与错误边界,策略部署后可能无法从偏离中恢复。

写代码试一试

01审计三步成功轨迹。

输入 / 条件
right,right,finish
输出 / 预期
SUCCESS

提示 从状态0重算

展开参考答案与推导

每步对照观测和奖励,末步必须终止。

02检测伪造观测。

输入 / 条件
0向right后记录2
输出 / 预期
抛异常

提示 规则实际只能到1

展开参考答案与推导

先执行可信规则再比较日志。

03区分停止原因。

输入 / 条件
预算1执行right
输出 / 预期
TRUNCATED

提示 没有finish

展开参考答案与推导

截断标志为真、任务终止为假。

读完之后,独立完成

课后作业:自己做一遍

本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

运行三步走廊环境,区分成功、提前结束和预算用尽;用 Java 审计器拒绝伪造观测与任务结束后的动作。

本篇重点加练

研究轨迹必须保留信息可见性、停止语义与训练标签,重点审计从执行记录到学习数据的转换。

  • 观察与真实状态
  • 任务终止与预算截断
  • 动作掩码与训练标签

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    设计一个两房间取物环境,分别列出真实状态、可见观察、合法动作和决策记录;举出观察相同但真实状态不同的两种情况。

    提交内容
    环境契约与状态对照
    怎样算完成
    • 观察和状态不混用
    • 决策文本不充当环境证据

    练到的知识点观察与真实状态 · 动作空间与解析 · 决策摘要与环境证据

  2. 作业 02代码推演

    自建一条包含一次无效动作、一次有效动作和预算结束的轨迹,逐事件标注奖励、观察、停止原因与成功判定依据。

    提交内容
    完整事件账本
    怎样算完成
    • 终止截断可区分
    • 成功由环境状态判断

    练到的知识点轨迹事件结构 · 任务终止与预算截断 · 可执行成功判定

  3. 作业 03原理问答

    两条轨迹的模型可见观察相同,但日志含不同隐藏环境状态。设计行为克隆输入字段白名单,说明哪些字段只可用于评估而不能进入策略输入。

    提交内容
    信息分层及训练字段契约
    怎样算完成
    • 可见时点明确
    • 监督字段与隐藏答案分开

    练到的知识点观察与真实状态 · 动作掩码与训练标签

  4. 作业 04代码推演

    同一任务分别在 3 步和 6 步预算下执行,构造前 3 步相同的轨迹,比较第 3 步事件能支持的状态与成功结论。

    提交内容
    双预算轨迹及结论表
    怎样算完成
    • 成功判据与预算独立
    • 未观察后续保持未知

    练到的知识点观察与真实状态 · 任务终止与预算截断

综合应用

  1. 作业 05编程实践

    实现 CPU 确定性环境回放器,输入初始快照、种子、动作事件和环境版本;测试篡改观察、遗漏事件与不兼容版本。

    提交内容
    回放器及审计结果
    怎样算完成
    • 回放身份完整
    • 不一致位置可定位

    练到的知识点轨迹事件结构 · 可回放环境版本 · 可执行成功判定

  2. 作业 06排错实验

    构造含工具观察和助手动作的训练序列,分别注入错误动作标签、观察被当策略目标、预算结束被当成功三类问题。

    提交内容
    标签审计器及故障样例
    怎样算完成
    • 每个标签归属可检查
    • 训练目标和任务结果分开

    练到的知识点动作空间与解析 · 任务终止与预算截断 · 动作掩码与训练标签

  3. 作业 07面试场景

    同一轨迹文本在新版环境失败。设计研究数据集的可复现协议,列出需要冻结的环境内容及不能由文字推理补足的证据。

    提交内容
    数据集身份及回放协议
    怎样算完成
    • 环境变化可追溯
    • 不伪造缺失执行证据

    练到的知识点决策摘要与环境证据 · 可回放环境版本 · 可执行成功判定

  4. 作业 08编程实践

    实现 CPU 轨迹切片器,把长度六的轨迹切成训练窗口,保留动作位置和终止/截断身份;审计切片边缘是否产生伪造成功标签。

    提交内容
    切片程序与位置对账
    怎样算完成
    • 原事件身份保留
    • 窗口结束不自动当任务终止

    练到的知识点任务终止与预算截断 · 动作掩码与训练标签

  5. 作业 09排错实验

    故意把工具返回时间之后才出现的证据拼入之前的策略训练输入,写时间一致性检查并在一条自建轨迹上定位泄漏。

    提交内容
    事件时间审计器
    怎样算完成
    • 事件发生与可见时间区分
    • 泄漏位置有具体依据

    练到的知识点观察与真实状态 · 动作掩码与训练标签

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    比较仅成功轨迹与成功失败混合轨迹用于动作预测的泛化表现,用 CPU 小分类器和保留初始状态实验;预先定义样本预算。

    提交内容
    研究协议、程序与报告
    怎样算完成
    • 学习目标明确
    • 保留状态不参与训练

    练到的知识点观察与真实状态 · 任务终止与预算截断 · 动作掩码与训练标签

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    “轨迹文本完全相同就代表同一决策问题”是否成立?改变隐藏环境状态或终止规则但保持已见文本,构造两种任务后续不同的例子。

    提交内容
    双环境反例及可见性分析
    怎样算完成
    • 当前观察确实一致
    • 规则差异可执行验证

    练到的知识点观察与真实状态 · 任务终止与预算截断

  2. 作业 12思维拓展 · 跨域迁移

    把轨迹窗口迁移到视频动作学习:窗口边界与动作结束不一致时,如何定义标签和未观测未来?给出不依赖视频模型的事件序列例子。

    提交内容
    序列映射与边界契约
    怎样算完成
    • 采样截止与事件终止分开
    • 标签不使用不可见未来

    练到的知识点任务终止与预算截断 · 动作掩码与训练标签

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗