iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0

開場故事

如果你曾經看過一個 AI Agent 一直重複做同一件事,你應該會有一種很熟悉的焦躁感。

它可能在那邊一直查同一個狀態、一直重送同一個請求、一直換湯不換藥地呼叫工具,明明畫面上看起來很忙,實際上卻沒有任何進展。那種感覺很像你跟一個人說「幫我找資料」,結果他每隔幾秒就跑回來跟你說「我再查一次看看」,查到最後把自己查暈了,事情還是沒往前走。

第 6 天我想看的,就是這個很現實的問題:

  • 工具叫太多的時候,Agent 到底怎麼壞掉?
  • OpenClaw 怎麼知道這不是正常工作,而是開始繞圈?
  • 它是等整個系統爆掉才處理,還是會在前面先攔住?
  • 什麼叫 no-progress?
  • 什麼叫 ping-pong loop?

這一篇我想把它寫成「OpenClaw 怎麼避免自己把自己忙死」。

今天要解的問題

  • OpenClaw 怎麼定義「工具 loop」?
  • tools.loopDetection 的設定怎麼影響行為?
  • before_tool_call hook 什麼時候會先擋下工具?
  • 什麼是 repeated same-tool、unknown-tool、poll no-progress、ping-pong?
  • 為什麼工具呼叫不是越多越安全,反而可能越做越錯?

先講結論

OpenClaw 對「工具叫太多」不是用單一規則判斷,而是靠一套分層的 loop detection:

  1. 先看這個工具是不是一直被重複呼叫
  2. 再看這些呼叫有沒有帶來新結果
  3. 如果是輪詢類工具,就看它是不是一直在沒進展地問同一件事
  4. 如果是兩個工具互相來回,就看是不是 ping-pong
  5. 如果重複次數高到一定程度,直接從 warning 升級成 critical,最後阻止執行

也就是說,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
		}
	};
}

這段意思很直白:

  • 全域可以先定一層預設
  • agent 可以再覆蓋自己的規則
  • detectors 與 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
  • 但一旦開了,就會有歷史窗口、警告門檻、嚴重門檻、全域保險絲
  • 它不是只防一種 loop,而是同時防 generic repeat、poll no-progress、ping-pong

接著看真正的偵測邏輯。

📄 原始碼: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);
	...
}

這一小段其實已經把思路講完了。

它先做幾件事:

  • 把設定正規化
  • 只看同一個 scope 裡的歷史
  • 把本次工具呼叫 hash 化
  • 看是不是 unknown tool 一直失敗
  • 看是不是同參數重複呼叫卻沒有新結果
  • 看是不是 known poll 工具
  • 看是不是 ping-pong

這裡很像在辦案。
不是只問「你有沒有再做」,而是問「你再做的東西是不是同一個、結果是不是也一樣、是不是根本沒進展」。

再往下看,當它判定真的卡住時,會分 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
	};
}

這一段就是兩個工具互相打來打去,像乒乓球一樣。

例如:

  • A 工具查狀態
  • B 工具修正格式
  • A 又查一次
  • B 又修正一次

如果這兩者交替出現,但沒有帶來任何進展,OpenClaw 會把它看成一個 loop,而不是兩個正常動作。

白話拆解

1. 不是叫很多次就一定錯,而是看有沒有進展

這是這一篇最重要的觀念。

有些工具的確本來就會多次呼叫,例如:

  • 查任務是否完成
  • 等外部服務回應
  • 分段讀 log

但 OpenClaw 不是看次數本身,它看的是:

  • 呼叫是不是一樣
  • 結果是不是也一樣
  • 中間有沒有新狀態、新輸出、新線索

如果沒有,那就不是工作節奏,而是原地打轉。

2. unknown tool 不是「再試一下」就會變好

這個很像人類很容易犯的錯。

「欸剛剛那個工具失敗了,再呼叫一次試試看。」

如果失敗原因是工具根本不存在,那再叫一百次也不會憑空長出來。
OpenClaw 會把這種情況直接升級為 critical,因為這不是可恢復錯誤,而是方向本來就錯了。

3. polling 最容易讓人誤判

輪詢工具很容易讓人覺得「再多查幾次就好了」。

但如果每次查到的資訊完全一樣,代表不是時間不夠,就是系統根本卡住。
OpenClaw 的 knownPollNoProgress 就是要把這種情況挑出來。

4. ping-pong 比單點重複更陰險

單一工具一直重複,通常一眼就看得出來。

但 ping-pong 很容易偽裝成「流程正常」:

  • 先查
  • 再修
  • 再查
  • 再修

看起來很像工作流,實際上可能只是兩個工具互相打架。

OpenClaw 會把它的交替尾巴拉出來看,並且要求真的要有 no-progress evidence,才會判定成 loop。

5. 先 warning,後 critical,最後 block

這個分級很像人類在現場處理問題的方式:

  • 先提醒你:你有點重複了
  • 再警告你:這看起來真的不對
  • 最後直接切斷:不要再浪費資源了

這種設計很合理,因為不是每個重複都要立刻死刑。
但當系統很明確看到「沒有進展」時,就不能再放任它一直轉。

設計取捨

好處

  • 可以提早阻止無限 loop
  • 可以區分正常 polling 和真正卡死
  • unknown toolgeneric repeatping-pong 各自有不同處理方式
  • session history 會留下跡象,方便之後追查

代價

  • 規則會變多
  • 判斷不是完全黑白,容易碰到邊界案例
  • 某些正常但長時間的工作流,可能被誤判成 loop

這也是為什麼 OpenClaw 沒有把 loop detection 當成單一大招,而是做成可以配置的 detectors。

如果換另一種做法會怎樣

如果完全不做 loop detection,模型可能會在這些情況失控:

  • 一直呼叫不存在的工具
  • 一直輪詢沒結果的狀態
  • 一直在兩個工具之間來回跳
  • 一直產生重複結果,最後把 context 塞爆

短期看起來只是「模型比較有耐心」。
長期看就是 token 燒光、時間拖長、session 卡死、使用者體驗崩掉。

額外一層:不是只防 loop,也防 context 壓力

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。

今天的結論

  • OpenClaw 不把「工具叫很多次」直接等同於錯誤,而是看有沒有進展
  • tools.loopDetection 可以針對全域或單一 agent 疊加設定
  • before_tool_call 是第一道防線,會在工具真正執行前先做 loop 檢查
  • unknown_tool_repeatknown_poll_no_progressping_ponggeneric_repeat 是四種很實際的壞法
  • warning / critical / block 的分級,讓系統可以先提醒,再阻止
  • OpenClaw 不只是防 loop,也在想 context 壓力,避免工具結果把 session 撐爆

下一步

前 6 天看下來,我覺得 OpenClaw 已經有一個很明確的態度了:

它不是只想讓 Agent 會做事,而是想讓它「有秩序地做事」。

第 7 天我會接著往下看另一個很關鍵的主題:

記憶不是記越多越好,而是記對的東西

因為一個會做事的 Agent,如果不會記得什麼該留、什麼該忘,最後還是會慢慢失控。


上一篇
第 5 天:工具不是裝飾品,是 OpenClaw 的手腳
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言