乐于分享
好东西不私藏

OpenClaw源码解析(九):Agent执行的真实流程

OpenClaw源码解析(九):Agent执行的真实流程

这一篇回答什么

前面两篇文章我们已经了解了

这一篇再往前一步,不按晦涩的源码结构,还原真实任务执行时的具体流程。

  • 哪些任务通常会在一个 attempt 内闭环。
  • 哪些任务更容易把控制权抛回 run.ts
  • 一旦抛回外层,新的 attempt 到底继承什么、不继承什么。
  • context、memory、compaction、tool result 在不同任务中分别扮演什么角色。

先给总判断:复杂任务不只有“长短”之分,更有“逃逸概率”之分

关键的分类是:

  1. 低逃逸任务

大概率在一个 attempt 内完成,几乎不触发外层恢复。

  1. 高上下文压力任务

逻辑本身没问题,但容易触发 compaction、overflow、tool-result truncation。

  1. 高基础设施风险任务

容易遇到 timeout、auth、rate limit、failover、provider 差异。

  1. 高历史依赖任务

新的 attempt 能否继续质量高度依赖 transcript 和 memory。

同一个业务任务可以同时属于多类。


Case 1:局部解释类任务,通常完整闭环于一个 attempt

用户例子:

“解释一下这个函数为什么会产生竞态条件。”

典型运行路径

run.ts 启动一次 attempt-> attempt 组装 prompt / context / tools-> 内部 agent loop 搜索并读取少量代码-> 模型形成解释-> 输出最终答案-> attempt 返回-> run.ts 收束 payload 并结束

为什么它通常不会逃逸到外层

因为它的压力点主要是“局部信息不够”,而不是:

  • transcript 太长
  • tool result 过大
  • 多次跨 provider 调整
  • 长时间执行

对两层 loop 的观察结论

这类任务最能说明一件事:

正常情况下,外层 run.ts 只负责开场和收场,中间的任务推进无异常完全由一次 attempt 内部完成。


Case 2:代码修复类任务,最能体现“一个 attempt 就是一整个执行容器”

用户例子:

“修复 webhook 重复发送,并验证。”

正常路径

run.ts 发起 attempt A-> attempt A 内部:   -> 搜索相关代码   -> 读候选文件   -> 形成假设   -> 运行命令验证   -> 修改文件   -> 运行测试   -> 根据测试再调一次   -> 形成最终回答-> attempt A 返回成功-> run.ts 结束

这里再次强调

虽然表面上你看到了很多“轮次”,但这些轮次都不属于 run.ts

它们都属于:

同一次 attempt 背后的内部模型-工具-模型循环。

这类任务为什么强依赖 transcript

因为它通常会累计很多中间状态:

  • 哪些文件已经读过
  • 哪个假设被证伪
  • 哪段代码已经改过
  • 哪个测试失败过
  • 最后一次验证结果是什么

如果这些状态只存在瞬时推理里,而不进入 transcript,那么一旦 attempt 失败,新的 attempt 很难接上之前的进度。


Case 3:第 3 轮内部失败,外层恢复到底怎么发生

假设任务已经走到这个状态:

attempt A:第 1 轮:搜索文件第 2 轮:读取文件并形成假设第 3 轮:运行命令 / 获取大 tool result-> 然后发生异常

异常可能发生在哪

  1. activeSession.prompt(…) 直接抛错

例如 prompt submission error。

  1. assistant 返回 error stop reason

例如 provider 侧上下文溢出、auth、rate limit。

  1. compaction 等待阶段失败或超时

模型主调用结束,但 compaction 没收敛。

控制权如何转移

内部 agent loop 出错-> attempt 捕获并整理成 promptError / lastAssistant / timedOut / messagesSnapshot-> attempt 返回给 run.ts-> run.ts 分类判断

外层会不会恢复到“第 3 轮”

不会。

外层只能做这些处理:

看 attempt 留下的最新 snapshot / session file / error-> 修复上下文或运行条件-> 再启动 attempt B

因此:

  • 不会恢复底层内部 loop 的 program counter
  • 不会从“第 3 轮 tool call 的下一条 token”继续
  • 不会把旧 attempt 的内部 in-flight 状态搬进新 attempt

那新 attempt 到底基于什么继续

基于:

  • 当时已经落入 session 的 transcript
  • 可能已经发生的 compaction/truncation 结果
  • 新的 provider / profile / thinking 条件
  • 再次运行的 sanitize / validate / limit / assemble

所以正确描述是:

从最新稳定工作记忆重新开始一次新的执行容器,而不是恢复旧容器的中间帧。


Case 4:上下文溢出任务,最能看清 run.ts 的真正职责

用户例子:

“基于我们前面 100 多轮对话和刚才的一堆命令输出,继续完成重构。”

这类任务往往不是“推理能力不够”,而是“上下文工程压力过大”。

典型路径

attempt A 执行到某处-> provider 报 context overflow-> attempt 返回 overflow 诊断-> run.ts 进入 overflow 恢复分支   -> 如有必要:等待已有 in-attempt compaction 后直接重试   -> 否则:调用 contextEngine.compact(...)   -> 还不行:尝试 truncate oversized tool results   -> 修复后 continue-> run.ts 发起 attempt B

这一条链路说明:

run.ts 不是负责继续思考,而是负责修复“这次 attempt 已经无法继续工作的执行环境”。

为什么新 attempt 不是坏事

因为一旦外层做了 compaction 或 truncation,旧 attempt 的上下文已经变了。

此时最合理的动作就是:

让新的 attempt 在新的上下文形态上重新起跑。

而不是强行把内部 loop 无缝衔接。


Case 5:provider/auth 失败任务,最能看清“任务求解”和“环境恢复”的分层

用户例子:

“完成这次修改并提交总结。”

任务本身没有任何问题,但中途当前 provider profile 失效了。

路径

attempt A 内部本来正常推进-> 某次模型调用返回 auth / rate limit / overload / billing / timeout-> attempt 返回错误事实-> run.ts:   -> 标记 profile 状态   -> advanceAuthProfile()   -> 或抛 FailoverError 进入更外层 model fallback   -> continue-> attempt B 在新 profile / 新 provider 上启动

这里最值得学习的点

这类任务能很清楚地区分:

  • 任务逻辑没有错
  • agent 也未必需要改变推理策略
  • 真正需要改变的是运行资源

Case 6:高历史依赖任务

用户例子:

“继续我们昨天讨论到一半的设计,沿用上次做出的权衡,不要推翻之前结论。”

这类任务最关键的问题不是“当前 prompt 是什么”,而是:

新 attempt 启动时,系统还能不能保住之前的关键状态。

可能依赖的东西

  • 当前 session transcript
  • compaction summary
  • session memory
  • 已 flush 到长期记忆的项目事实或用户偏好

如果没有异常

它仍然可能在一个 attempt 内完成。

如果发生异常并重新 attempt

这个任务的质量几乎完全取决于:

  • 上下文压缩后还有多少有效状态
  • memory 能否把已经沉淀的信息召回

一个复杂任务在 runner 眼里有哪些“结束方式”

方式 1:attempt 成功,run 直接收束

这是最理想路径。

方式 2:attempt 失败,run 恢复后新起 attempt,再成功

这是最典型的工程恢复路径。

方式 3:attempt 失败,run 认为不可恢复,直接返回最终错误

例如:

  • retry limit 用尽
  • compaction 失败且无法再缩
  • 明确的 role ordering / image size 等硬错误

方式 4:attempt 超时且没有任何可展示 payload

外层会专门构造 timeout 错误,避免用户收到“这轮消息像丢了一样”的体验。


“新的 attempt 到底会不会重复劳动”

会,而且这是当前架构的有意取舍。

会重复的

  • 某些搜索步骤
  • 某些局部推理
  • 某些工具选择

不一定重复的

  • 已经持久化的 transcript 内容
  • 已经被 compaction 摘要化的上下文
  • 已经被 memory 沉淀的长期事实
  • 已经更换好的 provider/profile/thinking 配置

在修复后的环境里,带着目前还保留下来的工作记忆重新求解。


为什么这套设计在复杂任务上仍然能工作

  1. transcript

保留本次任务的在线工作记忆。

  1. context compaction

在过长时压成更短但仍可继续工作的形式。

  1. memory

把跨轮、跨天、跨 session 的重要信息沉淀出来。

  1. hooks / system prompt

每次新 attempt 都能重建行为约束和环境说明。

因此新的 attempt 并不是“裸奔重来”,而是在已有状态之上重新启动。


把一条完整复杂任务压成最终运行图

用户发复杂任务-> run.ts 发起 attempt A-> attempt A 内部 agent loop 多轮推进任务-> 如果一直顺利:   -> attempt A 成功返回   -> run.ts 收束-> 如果中途上下文/账号/provider 出错:   -> attempt A 返回诊断   -> run.ts 恢复环境   -> 发起 attempt B   -> attempt B 基于最新稳定上下文继续推进-> 直到:   -> 成功收束   -> 或达到不可恢复条件

最终结论

  1. 真正决定一个任务“是否会跳回 run.ts”的,不是任务类型,而是它的上下文压力、基础设施风险和历史依赖强度。
  1. 正常复杂任务的大部分执行都发生在一个 attempt 内部。

外层 run.ts 只在执行容器无法继续工作时才接管。

  1. 一旦逃逸到外层,恢复语义就是“新的 attempt”,不是“恢复旧 attempt 的中间轮次”。
  1. 因为恢复是 attempt 级,所以 transcript、compaction 和 memory 成了决定任务连续性的核心基础设施。

下一篇预告

最重要的上下文管理和记忆系统终于要登场了。