OpenClaw源码解析(九):Agent执行的真实流程
这一篇回答什么
前面两篇文章我们已经了解了
-
谁在调度谁 OpenClaw源码解析(七):Agent Runner 全执行链 | 上集 -
openclaw agent 属于什么范式OpenClaw源码解析(八):OpenClaw 的 Agent 范式:内部求解循环、外层恢复循环
这一篇再往前一步,不按晦涩的源码结构,还原真实任务执行时的具体流程。
-
哪些任务通常会在一个 attempt 内闭环。 -
哪些任务更容易把控制权抛回 run.ts。 -
一旦抛回外层,新的 attempt 到底继承什么、不继承什么。 -
context、memory、compaction、tool result 在不同任务中分别扮演什么角色。
先给总判断:复杂任务不只有“长短”之分,更有“逃逸概率”之分
关键的分类是:
-
低逃逸任务
大概率在一个 attempt 内完成,几乎不触发外层恢复。
-
高上下文压力任务
逻辑本身没问题,但容易触发 compaction、overflow、tool-result truncation。
-
高基础设施风险任务
容易遇到 timeout、auth、rate limit、failover、provider 差异。
-
高历史依赖任务
新的 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-> 然后发生异常
异常可能发生在哪
-
activeSession.prompt(…) 直接抛错
例如 prompt submission error。
-
assistant 返回 error stop reason
例如 provider 侧上下文溢出、auth、rate limit。
-
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 配置
在修复后的环境里,带着目前还保留下来的工作记忆重新求解。
为什么这套设计在复杂任务上仍然能工作
-
transcript
保留本次任务的在线工作记忆。
-
context compaction
在过长时压成更短但仍可继续工作的形式。
-
memory
把跨轮、跨天、跨 session 的重要信息沉淀出来。
-
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 基于最新稳定上下文继续推进-> 直到:-> 成功收束-> 或达到不可恢复条件
最终结论
-
真正决定一个任务“是否会跳回 run.ts”的,不是任务类型,而是它的上下文压力、基础设施风险和历史依赖强度。
-
正常复杂任务的大部分执行都发生在一个 attempt 内部。
外层 run.ts 只在执行容器无法继续工作时才接管。
-
一旦逃逸到外层,恢复语义就是“新的 attempt”,不是“恢复旧 attempt 的中间轮次”。
-
因为恢复是 attempt 级,所以 transcript、compaction 和 memory 成了决定任务连续性的核心基础设施。
下一篇预告
最重要的上下文管理和记忆系统终于要登场了。
夜雨聆风