Skip to content

Commit ce4edc6

Browse files
committed
fix: 检测 finish_reason=length 循环和重复内容,自动重试
1 parent 926d651 commit ce4edc6

File tree

1 file changed

+61
-0
lines changed
  • app/src/main/java/com/xiaomo/androidforclaw/agent/loop

1 file changed

+61
-0
lines changed

app/src/main/java/com/xiaomo/androidforclaw/agent/loop/AgentLoop.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,43 @@ class AgentLoop(
10061006
rawContent = response.thinkingContent
10071007
}
10081008

1009+
// Detect token-loop: finish_reason=length + no tool calls → model hit max_tokens in repetitive loop
1010+
// Also detect repetitive content (e.g. same sentence repeated 100+ times)
1011+
val isTokenLoop = response.finishReason == "length" && response.toolCalls.isNullOrEmpty()
1012+
val isRepetitiveContent = rawContent != null && rawContent.length > 500 && isHighlyRepetitive(rawContent)
1013+
if ((isTokenLoop || isRepetitiveContent) && emptyResponseRetryAttempts < MAX_EMPTY_RESPONSE_RETRY_ATTEMPTS) {
1014+
emptyResponseRetryAttempts++
1015+
val reason = if (isTokenLoop) "finish_reason=length, no tools" else "repetitive content (${rawContent!!.length} chars)"
1016+
writeLog("⚠️ LLM 陷入 token 循环: $reason (retry $emptyResponseRetryAttempts/$MAX_EMPTY_RESPONSE_RETRY_ATTEMPTS)")
1017+
Log.w(TAG, "⚠️ Token loop detected: $reason, retry $emptyResponseRetryAttempts")
1018+
1019+
if (contextManager != null) {
1020+
writeLog("🔄 压缩上下文后重试...")
1021+
val recoveryResult = contextManager.handleContextOverflow(
1022+
error = Exception("Token loop: $reason"),
1023+
messages = messages
1024+
)
1025+
when (recoveryResult) {
1026+
is ContextRecoveryResult.Recovered -> {
1027+
messages.clear()
1028+
messages.addAll(recoveryResult.messages)
1029+
_progressFlow.emit(ProgressUpdate.ContextRecovered(
1030+
strategy = recoveryResult.strategy,
1031+
attempt = recoveryResult.attempt
1032+
))
1033+
}
1034+
is ContextRecoveryResult.CannotRecover -> {
1035+
val ctxTokens = resolveContextWindowTokens()
1036+
pruneOldToolResults(messages, ctxTokens)
1037+
ToolResultContextGuard.enforceContextBudget(messages, ctxTokens)
1038+
}
1039+
}
1040+
} else {
1041+
writeLog("🔄 无 contextManager,直接重试...")
1042+
}
1043+
continue
1044+
}
1045+
10091046
// Detect suspicious default text from LLM (e.g. "无响应")
10101047
// Instead of silently accepting, try to compact context and retry
10111048
val isSuspiciousResponse = rawContent == "无响应" || rawContent == "无响应。" || rawContent == "没有响应"
@@ -1515,6 +1552,30 @@ class AgentLoop(
15151552
yieldSignal = null
15161553
Log.d(TAG, "AgentLoop reset for steer-restart")
15171554
}
1555+
1556+
/**
1557+
* Detect highly repetitive content (model stuck in a loop).
1558+
* Takes the first 50 chars as a "chunk" and checks how many times it repeats.
1559+
* Returns true if >50% of the content is the same chunk repeated.
1560+
*/
1561+
private fun isHighlyRepetitive(content: String): Boolean {
1562+
if (content.length < 500) return false
1563+
val chunkSize = 50
1564+
val chunk = content.take(chunkSize)
1565+
if (chunk.isBlank()) return false
1566+
var repeatCount = 0
1567+
var pos = 0
1568+
while (pos + chunkSize <= content.length) {
1569+
if (content.regionMatches(pos, chunk, 0, chunkSize)) {
1570+
repeatCount++
1571+
} else {
1572+
break // Stop at first mismatch — repetitive content is typically at the start
1573+
}
1574+
pos += chunkSize
1575+
}
1576+
val repeatedChars = repeatCount * chunkSize
1577+
return repeatedChars > content.length * 0.5
1578+
}
15181579
}
15191580

15201581
/**

0 commit comments

Comments
 (0)