如果你曾經看過一個 AI Agent 一直重複做同一件事,你應該會有一種很熟悉的焦躁感。
它可能在那邊一直查同一個狀態、一直重送同一個請求、一直換湯不換藥地呼叫工具,明明畫面上看起來很忙,實際上卻沒有任何進展。那種感覺很像你跟一個人說「幫我找資料」,結果他每隔幾秒就跑回來跟你說「我再查一次看看」,查到最後把自己查暈了,事情還是沒往前走。
第 6 天我想看的,就是這個很現實的問題:
這一篇我想把它寫成「OpenClaw 怎麼避免自己把自己忙死」。
tools.loopDetection 的設定怎麼影響行為?before_tool_call hook 什麼時候會先擋下工具?OpenClaw 對「工具叫太多」不是用單一規則判斷,而是靠一套分層的 loop detection:
也就是說,OpenClaw 不是只看「叫了幾次」,而是看「叫這麼多次,有沒有真的往前走」。
這個差別很重要。因為有些工具多叫幾次是合理的,例如輪詢狀態、等待外部系統完成;但有些工具多叫幾次就只是卡住,繼續叫只是在浪費 token、時間和注意力。
這一層可以先想成一個保全系統。
工具準備要進場前,不是直接放行,而是先問:
OpenClaw 在 before_tool_call 這一層做的事情,就是在工具真正執行前先檢查一次。這樣做的目的不是刁難模型,而是讓系統不要一路跑進死胡同才回頭。
如果把它畫成流程,大概是這樣:
模型準備呼叫工具
-> before_tool_call hook
-> loop detection
-> 判斷 warning / block
-> 如果沒問題才真的執行工具
-> 執行完把 outcome 記進 history
-> 下一輪再拿 history 比對
真正厲害的地方在於,OpenClaw 不是只看這一輪,而是把前面的工具歷史一起算進來。
先看 loop detection 的設定怎麼被組起來。OpenClaw 會先把全域設定和 agent 自己的設定疊在一起,讓每個 agent 可以有不同的保護力度。
📄 原始碼:
src/agents/tool-loop-detection-config.ts:15-36
function resolveToolLoopDetectionConfig(params) {
const global = params.cfg?.tools?.loopDetection;
const agent = params.agentId && params.cfg ? resolveAgentConfig(params.cfg, params.agentId)?.tools?.loopDetection : void 0;
if (!agent) return global;
if (!global) return agent;
return {
...global,
...agent,
detectors: {
...global.detectors,
...agent.detectors
},
postCompactionGuard: {
...global.postCompactionGuard,
...agent.postCompactionGuard
}
};
}
這段意思很直白:
這就像公司有全公司標準流程,但某些部門可以再加自己的安全規則。財務部跟研發部不會用完全一樣的門禁強度。
再看設定預設值。
📄 原始碼:
src/agents/tool-loop-detection.ts:44-53
const DEFAULT_LOOP_DETECTION_CONFIG = {
enabled: false,
historySize: 30,
warningThreshold: 10,
unknownToolThreshold: 10,
criticalThreshold: 20,
globalCircuitBreakerThreshold: 30,
detectors: {
genericRepeat: true,
knownPollNoProgress: true,
pingPong: true
}
};
這裡可以直接抓到 OpenClaw 的態度:
enabled: false
接著看真正的偵測邏輯。
📄 原始碼:
src/agents/tool-loop-detection.ts:569-579
function detectToolCallLoop(state, toolName, params, config, scope) {
const resolvedConfig = resolveLoopDetectionConfig(config);
if (!resolvedConfig.enabled) return { stuck: false };
const history = selectHistoryForScope(state.toolCallHistory ?? [], scope);
const currentHash = hashToolCall(toolName, params);
const unknownToolStreak = getUnknownToolRepeatStreak(history, toolName);
const noProgress = getNoProgressStreak(history, toolName, currentHash);
const noProgressStreak = noProgress.count;
const knownPollTool = isKnownPollToolCall(toolName, params);
const pingPong = getPingPongStreak(history, currentHash);
...
}
這一小段其實已經把思路講完了。
它先做幾件事:
這裡很像在辦案。
不是只問「你有沒有再做」,而是問「你再做的東西是不是同一個、結果是不是也一樣、是不是根本沒進展」。
再往下看,當它判定真的卡住時,會分 warning 跟 critical。
📄 原始碼:
src/agents/tool-loop-detection.ts:585-587
if (unknownToolStreak.count >= resolvedConfig.unknownToolThreshold) return {
stuck: true,
level: "critical",
detector: "unknown_tool_repeat",
count: unknownToolStreak.count,
message: `CRITICAL: attempted unavailable tool ${unknownToolStreak.unknownToolName ?? toolName} ${unknownToolStreak.count} times. Stop retrying that missing tool and answer without it.`,
warningKey: `unknown-tool:${toolName}:${unknownToolStreak.unknownToolName ?? "unknown"}`
};
這段很有意思。
如果一直嘗試一個根本不存在或不可用的工具,那不是「努力」,那是死路。
OpenClaw 直接告訴你:不要再試了,換別的方法回答。
接著是全域 no-progress breaker。
📄 原始碼:
src/agents/tool-loop-detection.ts:592-601
if (noProgressStreak >= resolvedConfig.globalCircuitBreakerThreshold) {
log.error(`Global circuit breaker triggered: ${toolName} repeated ${noProgressStreak} times with no progress`);
return {
stuck: true,
level: "critical",
detector: "global_circuit_breaker",
count: noProgressStreak,
message: `CRITICAL: ${toolName} has repeated identical no-progress outcomes ${noProgressStreak} times. Session execution blocked by global circuit breaker to prevent runaway loops.`,
warningKey: `global:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
};
}
這段像是最後一條保險絲。
它不在乎你是不是輪詢工具,不在乎你是不是知名工具,只要你重複地拿到相同結果,而且次數已經超過全域門檻,就直接擋掉。
這代表 OpenClaw 的思路不是「允許模型一直努力」,而是「允許模型努力,但不允許它在沒有新資訊的情況下無限重試」。
再看最容易出現的 polling 情境。
📄 原始碼:
src/agents/tool-loop-detection.ts:615-617
if (knownPollTool && resolvedConfig.detectors.knownPollNoProgress && noProgressStreak >= resolvedConfig.criticalThreshold) {
return {
stuck: true,
level: "critical",
detector: "known_poll_no_progress",
count: noProgressStreak,
message: `CRITICAL: Called ${toolName} with identical arguments and no progress ${noProgressStreak} times. This appears to be a stuck polling loop. Session execution blocked to prevent resource waste.`,
warningKey: `poll:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
};
}
這裡的語氣很像在說:
「你如果是查狀態,我可以理解。但你查了十幾次都一樣,那就不是等待,是真的卡住了。」
最後是 ping-pong。
📄 原始碼:
src/agents/tool-loop-detection.ts:655-656
if (resolvedConfig.detectors.pingPong && pingPong.count >= resolvedConfig.criticalThreshold && pingPong.noProgressEvidence) {
return {
stuck: true,
level: "critical",
detector: "ping_pong",
count: pingPong.count,
message: `CRITICAL: You are alternating between repeated tool-call patterns (${pingPong.count} consecutive calls) with no progress. This appears to be a stuck ping-pong loop. Session execution blocked to prevent resource waste.`,
pairedToolName: pingPong.pairedToolName,
warningKey: pingPongWarningKey
};
}
這一段就是兩個工具互相打來打去,像乒乓球一樣。
例如:
如果這兩者交替出現,但沒有帶來任何進展,OpenClaw 會把它看成一個 loop,而不是兩個正常動作。
這是這一篇最重要的觀念。
有些工具的確本來就會多次呼叫,例如:
但 OpenClaw 不是看次數本身,它看的是:
如果沒有,那就不是工作節奏,而是原地打轉。
這個很像人類很容易犯的錯。
「欸剛剛那個工具失敗了,再呼叫一次試試看。」
如果失敗原因是工具根本不存在,那再叫一百次也不會憑空長出來。
OpenClaw 會把這種情況直接升級為 critical,因為這不是可恢復錯誤,而是方向本來就錯了。
輪詢工具很容易讓人覺得「再多查幾次就好了」。
但如果每次查到的資訊完全一樣,代表不是時間不夠,就是系統根本卡住。
OpenClaw 的 knownPollNoProgress 就是要把這種情況挑出來。
單一工具一直重複,通常一眼就看得出來。
但 ping-pong 很容易偽裝成「流程正常」:
看起來很像工作流,實際上可能只是兩個工具互相打架。
OpenClaw 會把它的交替尾巴拉出來看,並且要求真的要有 no-progress evidence,才會判定成 loop。
這個分級很像人類在現場處理問題的方式:
這種設計很合理,因為不是每個重複都要立刻死刑。
但當系統很明確看到「沒有進展」時,就不能再放任它一直轉。
unknown tool、generic repeat、ping-pong 各自有不同處理方式這也是為什麼 OpenClaw 沒有把 loop detection 當成單一大招,而是做成可以配置的 detectors。
如果完全不做 loop detection,模型可能會在這些情況失控:
短期看起來只是「模型比較有耐心」。
長期看就是 token 燒光、時間拖長、session 卡死、使用者體驗崩掉。
OpenClaw 不只怕工具呼叫一直重複,也怕工具結果把上下文撐爆。
在它的 context engine loop hook 裡,有一段很直白的註解:
📄 原始碼:
src/agents/embedded-agent-runner/tool-result-context-guard.ts(本機版本行號已變動,僅標到檔案)
/**
* Per-iteration `afterTurn` + `assemble` wrapper for sessions where
* the context engine owns compaction. Lets the engine compact inside
* a long tool loop instead of only at end of attempt.
*/
這段說明了另一個現實問題:
如果工具 loop 很長,context 不是只會浪費時間,也可能真的越塞越滿。
所以 OpenClaw 不只是在結果很糟時事後收尾,還會考慮在長工具 loop 中間就先做 compaction,避免 context 壓力一路累積到炸掉。
這讓我覺得它的設計很像真正跑現場的系統,而不是只會在理想狀態下工作的 demo。
tools.loopDetection 可以針對全域或單一 agent 疊加設定before_tool_call 是第一道防線,會在工具真正執行前先做 loop 檢查unknown_tool_repeat、known_poll_no_progress、ping_pong、generic_repeat 是四種很實際的壞法前 6 天看下來,我覺得 OpenClaw 已經有一個很明確的態度了:
它不是只想讓 Agent 會做事,而是想讓它「有秩序地做事」。
第 7 天我會接著往下看另一個很關鍵的主題:
記憶不是記越多越好,而是記對的東西
因為一個會做事的 Agent,如果不會記得什麼該留、什麼該忘,最後還是會慢慢失控。