☰
OpenClaw Agent Loop 机制源码深度分析(二):从事件循环到工具调用的可复现拆解
2026/10/8 12:40:54 网站建设 项目流程

1. OpenClaw Agent Loop 卡顿排查:从事件循环到工具调用的可复现拆解

OpenClaw 的 Agent Loop 本质上是一个「事件驱动 + 消息队列 + 工具回调」的调度器,它决定了用户消息进来之后,什么时候调用 LLM、什么时候执行工具、什么时候压缩上下文、什么时候把结果吐回前端。如果你正在读源码却总在executeLoop里绕不出来,或者本地跑起来发现循环卡在某个await上不动,这篇就是接着上一篇继续往下拆的实操记录。适合已经能跑起 OpenClaw 本地实例、想搞清楚「为什么我的 Agent 调完工具不继续」「为什么流式响应断在半路」的开发者。

我试过把runs.ts、compact.ts、executeLoop三块放在一起对照调试,发现大部分「卡顿」不是死循环,而是状态映射没命中:ACTIVE_EMBEDDED_RUNS里没有对应 sessionId,queueEmbeddedPiMessage直接返回 false,消息就静默丢了。下面按「问题场景 → 前置准备 → 可复制配置 → 断点验证 → 报错排查 → 后续接入」的顺序展开,每一步都能在你本地复现。

核心检索词先明确:OpenClaw Agent Loop 是什么?它是 OpenClaw 里负责「接收消息 → 构建提示词 → 调用 LLM → 执行工具 → 回写结果 → 判断是否压缩」的主循环,源码集中在runs.ts(运行状态)、compact.ts(上下文压缩)和主执行循环executeLoop三处。能做什么?让你定位循环卡顿、工具调用顺序错乱、上下文溢出重试失败这三类高频问题。适合谁?正在二次开发 OpenClaw、或想借鉴 Agent 调度设计的工程师。

2. TaoToken 前置准备:给 Agent Loop 一个稳定的模型出口

在拆循环之前,得先让 LLM 调用这一环是通的,否则你断点打到sessionManager.complete也看不出是调度问题还是网络问题。OpenClaw 的callLLM最终会走一个兼容 OpenAI 协议的 endpoint,我本地调试时用的是 TaoToken 作为模型出口,原因是它的 Base URL 和 Key 管理比较干净,切换模型不用改代码,只改配置。

你需要准备三件套:Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 OpenAI 兼容端点前缀。API Key 在控制台生成,路径是 API Keys 页面,生成后复制一次就存好,页面不再明文展示。Model ID 按你实际要调的模型填,比如做 Agent 调度验证时选一个支持 tool_calls 的模型,否则response.tool_calls永远是空,循环会直接走到return分支,你会误以为「工具没执行」。

这里有个容易踩的坑:OpenClaw 的buildToolDescriptions()会把工具 schema 拼进请求,如果模型不支持 function calling,返回里没有tool_calls字段,executeLoop的while(true)第一轮就退出了。所以前置准备阶段一定要确认 Model ID 对应的模型支持工具调用。我建议先在模型对话页面手动发一条带工具描述的请求,确认返回结构里有tool_calls,再回到本地跑循环。

另外,如果你打算长期跑 Agent 任务、频繁触发工具调用和上下文压缩,可以考虑 Coding Plan,它的额度模型更适合这种「一轮对话多次 LLM 调用」的场景,避免调试到一半额度耗尽。接入文档里有完整的 endpoint 说明和参数示例,配置前扫一眼能省不少试错时间。

3. 可复制配置:本地调试 OpenClaw Agent Loop 的 settings 片段

这一节给你可以直接粘贴的配置。OpenClaw 本地调试一般有一个 settings 文件或环境变量入口,我把它整理成 JSON 和 TOML 两种形式,你按自己项目的加载方式选一个。关键是三件套要写全:Base URL、Key、Model ID,缺一个sessionManager.complete就会抛认证或模型不存在错误。

先看 JSON 形式,适合settings.json或类似配置文件:

{ "llm": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "modelId": "你的模型ID", "timeoutMs": 60000, "maxRetries": 3 }, "agentLoop": { "contextWindow": 128000, "compactionThreshold": 0.9, "reserveTokensFloor": 8000, "maxToolRounds": 12 }, "debug": { "logActiveRuns": true, "logToolCalls": true, "logCompaction": true } }

再看 TOML 形式,适合config.toml:

[llm] base_url = "https://taotoken.net/api" api_key = "sk-你的Key" model_id = "你的模型ID" timeout_ms = 60000 max_retries = 3 [agent_loop] context_window = 128000 compaction_threshold = 0.9 reserve_tokens_floor = 8000 max_tool_rounds = 12 [debug] log_active_runs = true log_tool_calls = true log_compaction = true

配置里几个参数和源码是对应的。compactionThreshold对应manageContext里的info.tokenCount > config.contextWindow * 0.9,你把它调低到 0.7 就能更早触发压缩,方便调试compact.ts的分支。reserveTokensFloor对应resolveCompactionReserveTokensFloor(config),压缩时保留的 token 下限,设太小会导致压缩后仍然溢出,设太大又浪费窗口。maxToolRounds是我自己加的保护,防止while(true)在工具反复调用时无限循环,源码里没有硬上限,调试时建议加上。

debug.logActiveRuns打开后,setActiveEmbeddedRun和abortEmbeddedPiRun的调用会打日志,你能直接看到ACTIVE_EMBEDDED_RUNS这个 Map 的增删时机。这一步很关键,因为很多「循环卡住」其实是 run 已经 abort 了但前端还在等,日志一看就清楚。

4. 断点验证:从事件循环到工具调用的成功结果

配置好之后,开始断点验证。目标是把executeLoop的完整链路走一遍,确认每一步的输入输出符合预期。我按调用顺序给你标出断点位置和观察点。

第一个断点打在prepareRunContext返回处。这里会拿到sessionManager、bootstrapFiles、contextFiles。观察sessionManager是否成功加载,如果sessionId和sessionKey都没命中已有会话,它会新建一个,此时ACTIVE_EMBEDDED_RUNS里还没有这个 id,属于正常。如果这里就抛错,多半是workspaceDir路径不对或config缺字段。

第二个断点打在buildEmbeddedSystemPrompt的 return 前。看拼接结果里identity、bootstrap、skills、tools四段是否都在,用\n\n---\n\n分隔。如果tools段为空,说明buildToolDescriptions()没拿到工具定义,后面 LLM 不会返回tool_calls,循环会直接结束。这是「工具不执行」最常见的原因,不是循环问题,是提示词构建问题。

第三个断点打在executeLoop的while(true)内部,sessionManager.complete返回后。观察response.tool_calls的长度。如果大于 0,进入工具执行分支,for (const toolCall of response.tool_calls)会逐个执行。这里注意appendToolResult的参数顺序是(toolCall.id, toolCall.name, result),顺序错了会导致下一轮 LLM 拿不到正确的工具结果关联。

第四个断点打在executeTool内部。确认工具实际执行成功,返回结构里有result。如果工具抛异常,executeLoop没有 try/catch 包裹的话会直接冒泡出去,循环中断。建议在executeTool外层加一层错误捕获,把错误作为 tool result 回写,让 LLM 自己决定重试还是换工具。

第五个断点打在manageContext的压缩分支。当info.tokenCount > config.contextWindow * 0.9时进入sessionManager.compact。观察compactResult.success,成功则返回{ compacted: true, originalTokens },失败会抛Compaction failed。压缩成功后循环继续,这是长对话不崩的关键。

验证成功的标志:一轮用户消息进来,LLM 返回 2 个tool_calls,两个工具都执行成功并回写,第二轮 LLM 返回纯文本回复,executeLoop走到return { success: true, reply, usage }。整个过程ACTIVE_EMBEDDED_RUNS里该 sessionId 从有到无,EMBEDDED_RUN_WAITERS里的等待器被唤醒。你可以在abortEmbeddedPiRun里加日志,确认 run 结束时被正确清理。

流式响应这块单独验证:在handleStreaming的onChunk里打日志,看chunk.content和chunk.tool_use是否按序到达。如果tool_use先到但content一直不来,可能是模型把工具调用和文本混在一个 chunk 里,你的解析逻辑要兼容。accumulatedContent的拼接顺序决定了前端显示是否错乱。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

调试 Agent Loop 时,报错往往不在循环本身,而在 LLM 调用这一层。下面按真实报错逐个对照。

401 Unauthorized:Key 无效或没带上。检查配置里apiKey是否写进了请求头,OpenClaw 的sessionManager.complete最终会拼Authorization: Bearer <key>。如果你用的是环境变量注入,确认变量名和读取代码一致。还有一种情况是 Key 复制时带了空格或换行,肉眼看不出来,重新生成一次最稳。

local proxy failed:本地网络出口问题,不是 OpenClaw 的 bug。检查你的baseUrl是否可达,用 curl 直接打https://taotoken.net/api看返回。如果 curl 通但 OpenClaw 不通,多半是它内部用了不同的 HTTP 客户端或代理配置,检查timeoutMs是否太短导致连接被掐断。

reading choices相关报错:通常是响应结构解析失败。OpenClaw 期望response.choices[0].message,如果模型返回的是流式 SSE 但代码按非流式解析,就会在reading choices处抛错。确认你的complete调用是否开了 streaming,以及onChunk回调是否和解析逻辑匹配。流式场景下choices是分片的,不能一次性读。

OAuth相关报错:如果你用的是需要 OAuth 的模型出口,token 过期会导致认证失败。OpenClaw 的handleError里有isAuthError分支,会尝试attemptAuthFailover切换备选密钥。确认你的 failover 配置里有可用的备选 Key,否则切换失败后直接return { action: "fail" },循环终止。

还有一个隐蔽的:Tool "xxx" is blocked by policy。这是checkToolPolicy返回的,resolveSandboxToolPolicyForAgent根据agentId解析白名单/黑名单。如果你新加了工具但没更新策略,调用会被拒。检查配置里该 agent 的工具策略,把新工具加进白名单。

上下文溢出重试失败:isContextOverflowError命中后走sessionManager.compact,如果压缩后仍然溢出,retryCount达到MAX_RETRIES就放弃。这时候要么调大contextWindow,要么调低compactionThreshold让它更早压缩,要么减少单轮工具返回的数据量。

6. 语义一致 CTA:把 Agent Loop 接到真实模型出口

源码读到这里,你应该能定位循环卡顿和工具调用顺序问题了。下一步是把它接到一个稳定的模型出口上跑真实任务。三件套再强调一次:Base URL 用https://taotoken.net/api,API Key 在 API Keys 页面生成,Model ID 选支持 tool_calls 的模型。

如果你在排障阶段反复遇到认证或接入问题,直接看接入文档,里面有完整的请求示例和错误码说明。想先验证模型是否支持工具调用,去模型对话页面手动发一条带工具描述的请求,确认返回结构。长期跑 Agent 任务、频繁触发工具循环和上下文压缩的话,Coding Plan 的额度模型更适合这种多轮调用场景。

配置改完记得重启本地实例,ACTIVE_EMBEDDED_RUNS是内存态,热重载不会清空,残留的 run 会导致新消息命中旧 handle。调试完把debug日志关掉,生产环境开着会拖慢流式响应。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询