跳到主要内容

fara — 本课题摘录

读了哪几篇: 01-agent-loop(主循环:一步是怎么走完的)、04-trajectory-and-resume(轨迹格式与续跑)。 其余四篇(提示词与动作空间、浏览器环境、评测框架、巧妙之处)本轮没读——属界面与评测课题。

这一家贡献两条:一条是停机方式的三分法(含一个诚实的缺陷),一条是"一次运行该怎么落盘"。

它对本课题回答了什么

决定四:三种停机,而且状态各不相同

停机方式触发状态最终答案是什么
模型主动结束动作是"终止"完成模型给的答案
交还用户动作是"问用户"等待用户那句问题的文本
步数耗尽循环走完也标成"完成"最后一步的观察文本

(依据:前沿库 · Fara · 主循环:一步是怎么走完的 —— 三种停机分别是 terminate(COMPLETE)、ask_user_question(WAITING_FOR_USER)、步数耗尽;步数耗尽也被标成 COMPLETE,尽管枚举里有 MAX_ROUNDS 却没被用上)

第三行是这一篇最值钱的地方:步数用完也被标成"完成",枚举里明明有"步数用尽"这个状态却没被用上。

它没放过这个缺陷,而是说清了后果:下游要区分"真的做完了"和"跑超时了",只能靠"最后一个动作是不是终止动作"来判断。 评测那边确实就是这么补的——第一件事就是检查最后一个动作,不是终止动作直接判零分。

这条要抄进配方,而且要抄成反面教材: 停机状态必须区分"完成"和"用尽",不能合并。 一旦合并,判断"这次到底成没成"的责任就转嫁给了下游,而下游只能靠猜。

"交还用户"这一档是很多家没有的。 cline / codex / vercel 都只有"完成 / 上限",没有"我需要问你一句"这个出口。

决定四补充:一步的七个阶段

step N 开始
(a) 验证码闸门 —— 挂起等外部条件解除,超时按策略降级
(b) 存动作前的画面 + 取当前状态
(c) 拼提示词 → 调模型 → 解析出**唯一一个**动作
(d) 记录动作到轨迹
(e) 执行动作 → 生成一句文字观察
(f) 存动作后的画面 + 落盘
(g) 判停

(依据:前沿库 · Fara · 主循环:一步是怎么走完的 —— 一步拆成验证码闸门→存前置截图→拼提示词调模型→解析出唯一动作→执行→存后置截图→落盘→判停七个阶段,每步只执行一个动作、每步存前后两张截图)

两个特点:每步只执行一个动作;每步存动作前后两份现场。

后者不是为了调试好看——打分器要靠这两帧来判断"这一步到底有没有产生预期效果"。

"每步存前后两份现场"这条对我们有间接用处: 我们的工具是读文件、跑命令,同样可以在动作前后各记一次相关状态。这是让"这一步到底有没有起作用"变得可判定的最便宜办法。

(a) 那个闸门的思路也可以泛化: 环境层维护一个"现在能不能安全观察"的信号,agent 每步开头等它。外部环境处于半死状态时喂给模型的观察只会让它做蠢动作。

决定一:轨迹用一维事件流,不用步骤数组

它先说清最自然的设计为什么不行。

最自然的是一个步骤数组:每个元素装"截图 + 动作 + 结果"。问题在于:一个步骤里的东西是分几次产生的——截图在动作前,动作在中间,观察在动作后。于是只能二选一:

  • 等一步全做完再写 → 崩了就丢一整步;
  • 写一半再回头改 → 追加写就不成立了

它的做法:把一切拍平成一维事件流,每个事件一产生就写。 (依据:前沿库 · Fara · 轨迹格式与"问完用户再续跑 —— 轨迹以一维事件流落盘而非步骤数组,理由是一个 step 里的东西分几次产生——等全做完再写会丢一整步,写一半再改则追加写不成立)

关键就一个字段:"这是哪个动作之后的观察"。

这个字段的值含义
这是动作之前的观察
某个动作的 id这是那个动作之后的观察

于是"哪些事件属于哪一步"不需要在写的时候确定,读的时候现算就行。

这跟 deepseek-harness 的 "append-only 会话日志 + 可见面投影"、deepchat 的 "tape 系统"是同一族思路。 fara 这一份最简单,而且它把"为什么不能用步骤数组"讲透了——这正是我们最需要的那一层解释。

读的时候两趟重建: 第一趟顺序扫,遇到动作就开一个新步、把攒着的"动作前观察"挂上;第二趟把"动作后观察"按 id 挂回它的步。

一个很实际的考虑——"迟到的观察": 异步环境里,一个动作的结果可能拖到两三步之后才回来。它会被同时挂到"它属于的那一步"和"它实际出现的那一步",下游按需选择。

引用不到的动作 id 直接抛错——宁可炸也不静默丢数据。

决定一补充:动作与观察必须交替

一条硬规则:连续两个动作之间必须至少有一个观察。 (依据:前沿库 · Fara · 轨迹格式与"问完用户再续跑 —— add_action 有硬规则「连续两个动作之间必须至少有一个观察」,保证轨迹一定是观察-动作-观察-动作的交替形态)

这保证了轨迹一定是"观察-动作-观察-动作"的交替形态,不会出现两步之间没有任何环境反馈的畸形数据。

这是一条很便宜的不变量,但能挡掉一整类脏数据。跟 rig / nanobot 强调的"每个调用必须有一个结果"是同族。

它的做法(可以抄的部分)

两套并存的持久化,冗余但各有各的用处:

机制特点为什么要
事件流(追加写,立刻落盘)一行一个事件进程被杀也不丢已发生的事件
整份快照(每步重写整个文件)一个文件装全部下游读起来简单,一次加载搞定

(依据:前沿库 · Fara · 轨迹格式与"问完用户再续跑 —— 事件流追加写保证进程被 kill 也不丢已发生事件、整份快照每步重写保证下游读起来简单,两套冗余但各有用处)

每个事件自动补 id 和时间戳,调用方不用操心。

它没回答什么

  • 工具调用怎么解析——它的动作空间是固定的几个屏幕操作,不是通用工具。
  • 历史怎么压——这两篇没讲。
  • 多个动作怎么并发——它每步只执行一个动作,没有这个问题。

坑与代价

  • "步数耗尽被标成完成"是一个真实缺陷(见上)。这是本课题里最值得记住的一条反面教材。
  • 每步存两张现场的代价是磁盘和时间。 对我们的场景(命令行工具)成本低得多,但要想清楚"存什么才算一份现场"。
  • 事件流的代价是读的时候要重建结构。 写便宜、读贵——如果下游读得比写得频繁,这个取舍就反了。 它用"整份快照"这套冗余把这个代价补回来了。