OpenClaw 的 Agent 循环怎么转:从 agent RPC 到落盘的一整条链路
用 OpenClaw 的人迟早会撞上同一类问题:消息发出去了什么都没回;或者回了两遍;或者一个任务跑半天不结束,也不知道卡在哪一环。这些现象背后是同一件事——你不知道一次「Agent 运行」内部被拆成了哪些阶段。
官方文档把这条链路叫 agent loop(Agent 循环),定义写得很干脆:按会话串行执行的一次运行,把一条消息变成动作和一个回复,中间包含接收、上下文装配、模型推理、工具执行、流式输出、持久化这几段。注意「串行」两个字,它是后面一堆行为的根因。
这篇顺着 concepts/agent-loop 和 concepts/agent 两页把链路一段段拆开,只讲机制和配置项——知道哪一段管什么,排查时才能直接跳到对的地方。
入口只有两个,但 RPC 是「立刻返回」的
官方列的入口就两处:
- Gateway RPC:
agent和agent.wait - CLI:
openclaw agent
第一个坑就藏在这儿。agent 这个 RPC 会校验参数、解析会话(sessionKey / sessionId)、持久化会话元数据,然后立刻返回 { runId, acceptedAt }。返回成功只代表「这次运行被受理了」,不代表模型跑完、更不代表回复生成了。真正等结果的是 agent.wait(内部是 waitForAgentRun),它在某个 runId 上等 lifecycle 的 end 或 error 事件,返回 { status: ok|error|timeout, startedAt, endedAt, error? }。
还有一条语义值得记住:agent.wait 超时只是「不等了」,并不会停掉底层那次运行。套脚本的人看到 wait 返回 timeout 就当任务死了、重发一遍——原来那次还在跑,你制造的是两次运行。
一次运行被拆成的五步
官方给的运行序列按职责重排成表,方便对号入座:
| 环节 | 干什么 | 出问题的表现 |
|---|---|---|
agent RPC | 校验参数、解析会话、持久化会话元数据,返回 runId | 参数/会话解析错误 |
agentCommand | 解析模型与 thinking/verbose/trace 默认值、加载技能快照、调用 runEmbeddedAgent;内嵌循环没发生命周期事件时补发 end/error 兜底 | 模型解析、技能加载类 |
runEmbeddedAgent | 通过「每会话队列」和「全局队列」串行化运行、解析模型与鉴权配置、构建会话、订阅运行时事件、流式输出助手与工具增量、执行运行超时(到期即中止),返回负载与用量元数据 | 排队等待、超时中止 |
subscribeEmbeddedAgentSession | 把运行时事件桥接到 agent 流:工具进 tool,助手增量进 assistant,生命周期进 lifecycle(phase: "start" | "end" | "error") | 流式没数据、事件不齐 |
agent.wait | 等 lifecycle end/error 并返回状态 | 只影响「等」,不影响「跑」 |
runEmbeddedAgent 还有个 Codex 特例:对 Codex app-server 类型的回合,一次已受理的回合若在终止事件之前停止产出 app-server 进度,它也会被中止。
为什么同一个会话不会并行跑:队列 + 写入者认领
运行是按会话键(session lane)串行的,还可以再走一条全局通道,官方给的理由是防止工具和会话竞态。消息渠道会选一个队列模式(steer / followup / collect / interrupt)喂给这套通道系统。
比串行更硬的一道保险是写入者认领(writer claim):一次被准入的运行在开始流式输出之前记录一个持久化的 activeWriterRunId 认领;之后每次转录追加或重写都要带 expectedWriterRunId;同步提交事务校验它是否仍等于当前活跃认领。结果是——一次被取代的运行,没法把过期的转录数据提交进去。 后续的重写、压缩、截断用的都是同一道事务内的栅栏。
再往下还有两层:SQLite 写入队列给每个 agent 的变更排序;Gateway 状态目录锁防止另一个 Gateway 或 openclaw agent --local 进程同时占用同一状态目录。多网关部署靠的就是这把锁。网关本身的层次划分可以对照读 OpenClaw 整体架构。
跑之前先准备什么:工作区、引导文件、技能
模型还没被调用之前,这几件事已经做完:工作区被解析并创建(沙箱运行可能重定向到沙箱工作区根目录);技能被加载或从快照复用,并注入环境变量和提示词;引导/上下文文件被注入系统提示词;会话转录目标和写入者认领在流式开始前就准备好。
工作区这块 concepts/agent 页说得更细:每个 agent 用单一一个工作区目录(agents.defaults.workspace,或按 agent 配 agents.entries.*.workspace)作为工具和上下文的唯一工作目录(cwd)。启用 agents.defaults.sandbox 后,非主会话可以用 agents.defaults.sandbox.workspaceRoot 下的每会话工作区覆盖它。
工作区里 OpenClaw 期待存在这几个用户可编辑的文件:
| 文件 | 用途 |
|---|---|
AGENTS.md | 操作说明 +「记忆」 |
SOUL.md | 人设、边界、语气 |
IDENTITY.md | Agent 名字/气质/emoji |
USER.md | 用户档案与称呼偏好 |
BOOTSTRAP.md | 一次性的首次运行仪式(完成后删除) |
MEMORY.md | 根级长期记忆文件,存在才注入 |
注入规则有几条容易忽略:新会话的第一个回合才把这些文件注入系统提示词的 Project Context;MEMORY.md 只有存在于工作区根目录时才注入;空文件跳过;大文件被修剪截断并留标记(想看全文得让它去读文件);除 MEMORY.md 外缺失的文件注入一行「缺失文件」标记,openclaw setup 会为它创建安全的默认模板。
BOOTSTRAP.md 只在全新工作区(没有其它引导文件)时创建。挂起期间它被留在 Project Context 里,并在系统提示词里加引导指引,而不是把内容抄进用户消息;仪式做完删掉它,之后重启也不会重建。
一条运维上很重要的:工作区被观察过之后,设置状态和认证信息存在共享 SQLite 库 ~/.openclaw/state/openclaw.sqlite 里。近期认证过的工作区若消失或被清空,启动时会拒绝静默重新播种 BOOTSTRAP.md——得恢复工作区,或做一次完整 onboard 重置把两边一起清掉。老版本的工作区 JSON 和 .attested 边车文件运行时已不读,用 openclaw doctor --fix 导入。
工作区如果是预先播好种的,可以整个关掉引导文件创建:
{ agents: { defaults: { skipBootstrap: true } } }
提示词装配这一步,系统提示词由基础提示词、技能提示词、引导上下文和按运行的覆盖项拼成,并执行模型专属限制和压缩预留 token——长会话牵扯上下文压缩就是从这儿来的,详见 上下文压缩与会话修剪。
钩子挂在循环的哪几个位置
OpenClaw 有两套钩子。内部钩子(Gateway hooks)是事件驱动脚本,管命令和生命周期事件:agent:bootstrap 在构建引导文件、系统提示词定稿之前运行,用来增删引导上下文文件;另有 /new、/reset、/stop 这类命令钩子。
插件钩子是跑在 agent 循环或网关流水线内部的扩展点,官方列了这些:before_model_resolve(会话前、无 messages,确定性地覆盖 provider/model)、before_prompt_build(会话加载后带 messages,可注入 prependContext、systemPrompt、prependSystemContext、appendSystemContext,支持的运行时上还能用 toolsAllow 收窄本回合工具面)、before_agent_reply(内联动作后、LLM 调用前,插件可接管这一回合并返回合成回复或静默)、agent_end、before_compaction / after_compaction、before_tool_call / after_tool_call、before_install、tool_result_persist、message_received / message_sending / message_sent、session_start / session_end、gateway_start / gateway_stop。
三条判定规则最好背下来,写错了不报错、只是不生效:before_tool_call 的 { block: true } 是终止性的、会停掉更低优先级的处理器,而 { block: false } 是空操作、不会清掉之前的 block;before_install 语义相同,但官方明确说运营者自己的安装放行/告警/拦截决策该用 security.installPolicy,因为只有它才覆盖 CLI 安装和更新路径;message_sending 的 { cancel: true } 终止、{ cancel: false } 空操作且不清除之前的 cancel。配置和示例接着看 hooks 怎么用。
流式、工具与回复
助手增量以 assistant 事件从运行时流出;块式流可在 text_end 或 message_end 上发出部分回复;推理内容可以走单独的流,也可以走块回复。块式流默认是关的(agents.defaults.blockStreamingDefault: "off"),边界由 agents.defaults.blockStreamingBreak 调(默认 text_end),软分块由 agents.defaults.blockStreamingChunk 控制(默认 800–1200 字符,优先段落断点,其次换行,句子最后),agents.defaults.blockStreamingCoalesce 用空闲合并减少单行刷屏。非 Telegram 渠道要显式打开 *.streaming.block.enabled: true;QQ Bot 反过来,除非 channels.qqbot.streaming.mode 设成 "off",否则默认流块回复。
工具这边:start/update/end 事件走 tool 流;工具结果在记录和发出前按体积和图片负载做净化;消息类工具的发送会被跟踪,用来抑制重复的助手确认。
最终回复由助手文本(外加可选推理内容)、内联工具摘要(verbose 且被允许时)、模型出错时的助手错误文本拼成,再加三条过滤:精确的静默令牌 NO_REPLY 从外发负载里过滤掉;消息工具的重复项从最终负载列表里移除;如果最后没有可渲染负载而又有工具报错,会发一个兜底的工具错误回复,除非某个消息工具已经发过用户可见的回复了——「回了两遍」和「什么都没回」,答案基本都在这一节。
插话怎么插进正在跑的运行
中途到达的入站提示词默认会被导入当前运行(steer)。运行时在两个检查点上查有没有 steer:未启动的工具调用启动前,以及下一次模型调用前。已经在跑的工具继续跑完;未启动的顺序调用被跳过;并行调用在这批越过启动检查点之后继续。被跳过的调用会收到合成的配对结果,然后模型才看到这次插话。
/queue steer 是活跃运行的默认行为,/queue followup 和 /queue collect 让消息等下一回合,/queue interrupt 直接中止当前运行。边界行为展开在 队列与插话。
超时不是一个数字,是一组
这是排查「卡住」时最该先看的表。官方给的默认值如下:
| 超时项 | 默认值 | 说明 |
|---|---|---|
agent.wait | 30s | 只管等,timeoutMs 参数可覆盖;不会停掉底层运行 |
Agent 运行时 agents.defaults.timeoutSeconds | 172800s(48 小时) | 由 runEmbeddedAgent 的中止定时器执行;设 0 为不限预算,但模型流活性看门狗仍生效 |
| CLI 后端无输出看门狗 | 按每次全新/恢复的 CLI 运行计算 | 独立于 agent 运行时,由注册的后端插件拥有 |
| Cron 隔离的 agent 回合 | 由 cron 拥有 | 调度器执行开始时启动自己的计时器,到期中止,有界清理后记录超时,避免过期子会话卡住通道 |
| 模型空闲超时 | 云端 120s;自托管 300s | 空闲窗口内没有响应分片就中止模型请求 |
| 提供方 HTTP 请求超时 | models.providers.<id>.timeoutSeconds | 覆盖连接、响应头、响应体、SDK 请求超时、受保护 fetch 的中止处理及该提供方的模型流空闲看门狗 |
两条官方点明的取舍:跑慢的本地/自托管提供方(文档举的例子是 Ollama)应先调 models.providers.<id>.timeoutSeconds,而不是先抬整个 agent 运行时超时,但模型请求确实要跑更久时,运行时超时也得跟着不低于它;提供方级空闲看门狗始终被更低的有限值兜住,agents.defaults.timeoutSeconds 或运行专属超时更小时以小的为准。另外,带显式 cron 运行超时时,云端模型流的停滞上限压到 60s,好让配置的模型回退还来得及在外层 cron 截止前跑。
会话「卡住」时,三个诊断标签怎么读
开启诊断后,有一个内建的两分钟阈值,给长时间处于 processing 且没观察到回复、工具、状态、块或 ACP 进度的会话分类:
session.long_running:活跃的内嵌运行、模型调用、工具调用。自己拥有的静默模型调用保持这个状态直到中止阈值,免得慢速或非流式提供方被过早判成停滞。session.stalled:有活跃工作但近期无进展。自己拥有的模型调用在到达中止阈值时切到这里;无主的陈旧模型/工具活动不会被藏成 long_running。session.stuck:保留给可恢复的陈旧会话簿记,包括带无主陈旧模型/工具活动的空闲排队会话。
中止阈值至少 5 分钟,且是告警阈值的 3 倍。停滞的内嵌运行要到中止阈值之后才被中止排空——排队的工作能继续,又不会把只是慢的运行一刀切掉。
官方还列了「运行可能提前结束」的四处:agent 超时(中止)、AbortSignal(取消)、Gateway 断连或 RPC 超时、agent.wait 超时(只是不等了,不停运行)。
几条容易忽略的落点
- 核心工具(读/执行/编辑/写入及相关系统工具)始终可用,但受工具策略约束。
apply_patch对 OpenAI 模型默认开启,由tools.exec.applyPatch的enabled、workspaceOnly、allowModels管控。 AGENTS.md里的## Tools小节不决定哪些工具存在,它只是你想怎么用它们的指引,这条常被误解。- 技能加载优先级从高到低:
<workspace>/skills、<workspace>/.agents/skills、~/.agents/skills、~/.openclaw/skills、内置、skills.load.extraDirs。 - 会话行存在
~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite。同级sessions/下的转录 JSONL 只是迁移输入、归档和支持材料。 - 模型引用按第一个
/拆分。模型 ID 自身带/的(OpenRouter 那种)必须带提供方前缀,例如openrouter/moonshotai/kimi-k2。
另外,Gateway 把生命周期和工具的开始/终止事件投影进一个有界的、只含元数据的审计账本,只记来源和结果码,不复制提示词、消息、工具参数、结果或原始错误。想靠它反查「模型当时说了什么」是查不到的。
什么时候它帮不上你
上面全部内容来自 OpenClaw 官方文档的 concepts/agent-loop 和 concepts/agent 两页,我们没有安装运行过,不涉及实测耗时、资源占用或界面行为,文档没写的也不做推断。
有几处是文档留白:CLI 后端无输出看门狗的具体计算方式,官方只说「按每次全新/恢复的 CLI 运行计算」,公式未说明;压缩预留 token 的数值和各模型专属限制这页没给;每会话队列与全局通道的容量上限,官方文档未说明;各类钩子的完整 API,这页只列挂载点。
把这条链路记住的收益很朴素:下次「没回复」,先看 lifecycle 有没有 end/error 而不是盲目重发;「回了两遍」,先看是不是消息工具已发过一次、兜底回复又补了一次;「一直转」,先看是哪一级超时该管却没管到。
延伸阅读
- 从头读起:OpenClaw 架构总览:网关、Agent、渠道、节点、插件五层各管什么,出问题该往哪查
- 本专题共 40 篇,完整分组目录见专题页
- OpenClaw 会话模型讲清楚:消息落到哪个 session、主会话汇进来什么、状态怎么不串
- OpenClaw 记忆架构拆解:分层文件、来源标记与两条召回通道各自负责什么
本文依据 OpenClaw 官方仓库(github.com/openclaw/openclaw)docs/ 下的官方文档整理,核对日 2026-08-17。
我们没有安装或运行过 OpenClaw,因此不涉及界面外观、操作手感与实测耗时的任何描述;
文中的默认值、命令与配置项均为文档口径,不构成对实际运行结果的保证。
该项目迭代很快,请以仓库最新内容为准。接入即时通讯平台前,请自行确认所在平台的规则与合规要求。