mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(agent): dedup repetition output + mark INCOMPLETE instead of NORMAL
This commit is contained in:
parent
fb0ae2b6cc
commit
f71f49bda1
@ -302,8 +302,8 @@ public class NodeStreamingChatHelper {
|
||||
* (removed at 42d406ff for being brittle on legitimate long-form
|
||||
* content), just the cheap specific check that catches this loop.
|
||||
*/
|
||||
private static final int CONTENT_REPEAT_MIN_PERIOD = 24;
|
||||
private static final int CONTENT_REPEAT_MAX_PERIOD = 240;
|
||||
public static final int CONTENT_REPEAT_MIN_PERIOD = 24;
|
||||
public static final int CONTENT_REPEAT_MAX_PERIOD = 240;
|
||||
private static final int CONTENT_REPEAT_MAX_OCCURRENCES = 4;
|
||||
/**
|
||||
* Re-scan every N chars of new content. Smaller = faster reaction,
|
||||
@ -1627,6 +1627,49 @@ public class NodeStreamingChatHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse a content buffer's trailing run of verbatim repeats to a
|
||||
* single copy. Used to clean up the persisted final answer after
|
||||
* {@link #hasRepeatingSuffix} fires — the streamed text already
|
||||
* contains the duplicates (SSE chunks can't be unsent), but the
|
||||
* DB-persisted message and the IM channel reply should show ONE
|
||||
* clean copy of the looping unit, not a wall.
|
||||
*
|
||||
* <p>Algorithm: find the smallest period in {@code [minPeriod,
|
||||
* maxPeriod]} where the buffer ends with that unit repeated 2+
|
||||
* times consecutively, then return everything up to (and including)
|
||||
* the FIRST copy of that unit. Conservative — if no period yields
|
||||
* 2+ consecutive matches, returns the buffer unchanged.
|
||||
*
|
||||
* <p>Public for unit-testing alongside {@link #hasRepeatingSuffix}.
|
||||
*/
|
||||
public static String dedupTrailingRepeats(String content, int minPeriod, int maxPeriod) {
|
||||
if (content == null || content.isEmpty()) return content;
|
||||
int len = content.length();
|
||||
if (minPeriod <= 0 || maxPeriod < minPeriod) return content;
|
||||
int periodCap = Math.min(maxPeriod, len / 2);
|
||||
for (int p = minPeriod; p <= periodCap; p++) {
|
||||
int unitStart = len - p;
|
||||
// Walk backward as far as the unit keeps matching.
|
||||
int copies = 1;
|
||||
int blockStart = unitStart - p;
|
||||
while (blockStart >= 0
|
||||
&& content.regionMatches(blockStart, content, unitStart, p)) {
|
||||
copies++;
|
||||
blockStart -= p;
|
||||
}
|
||||
if (copies >= 2) {
|
||||
// Keep prefix + ONE copy. The first copy starts at
|
||||
// (blockStart + p) since the loop walked back one step
|
||||
// past the last match.
|
||||
int firstCopyStart = blockStart + p;
|
||||
int trimEnd = firstCopyStart + p;
|
||||
return content.substring(0, trimEnd);
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether {@code accum} ends with the same {@code period}-sized
|
||||
* unit repeated at least {@code minOccurrences} times consecutively,
|
||||
|
||||
@ -489,6 +489,46 @@ public class ReasoningNode implements NodeAction {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
if (result.partial() && "content_repetition".equals(result.errorMessage())) {
|
||||
// Reasoning loop: the helper disposed the stream because the
|
||||
// model emitted the same paragraph 4+ times in a row (qwen3.6
|
||||
// / deepseek-r1 self-arguing pattern). The streamed text
|
||||
// already showed the duplicates to the user — we can't unsend
|
||||
// SSE chunks — but the persisted finalAnswer should be ONE
|
||||
// clean copy so the IM channel reply and any page-reload
|
||||
// history don't show the wall of repetition. Skip
|
||||
// FinalAnswerNode's evidence validation: the answer is
|
||||
// already truncated, applying validateAnswer on top would
|
||||
// double-stamp warnings on something the user already knows
|
||||
// is incomplete.
|
||||
String rawContent = result.text() != null ? result.text() : "";
|
||||
String dedupedAnswer = NodeStreamingChatHelper.dedupTrailingRepeats(
|
||||
rawContent,
|
||||
NodeStreamingChatHelper.CONTENT_REPEAT_MIN_PERIOD,
|
||||
NodeStreamingChatHelper.CONTENT_REPEAT_MAX_PERIOD);
|
||||
log.warn("[ReasoningNode] Content-repetition cap hit (raw={} chars → deduped={} chars); " +
|
||||
"INCOMPLETE",
|
||||
rawContent.length(), dedupedAnswer.length());
|
||||
var builder = reasonOutput()
|
||||
.needsToolCall(false)
|
||||
.shouldSummarize(false)
|
||||
.finalAnswer(dedupedAnswer.isEmpty()
|
||||
? "(模型反复输出同一段内容,已自动截断。请尝试重新生成或换个问法。)"
|
||||
: dedupedAnswer)
|
||||
.llmCallCount(nextLlmCallCount)
|
||||
.finishReason(FinishReason.INCOMPLETE)
|
||||
// contentStreamed=true because the user already saw
|
||||
// the looping text in their bubble; persisting again
|
||||
// via streamedContent would replay it.
|
||||
.contentStreamed(true)
|
||||
.thinkingStreamed(result.thinking() != null && !result.thinking().isEmpty())
|
||||
.mergeUsage(state, result);
|
||||
if (result.thinking() != null && !result.thinking().isEmpty()) {
|
||||
builder.finalThinking(result.thinking());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// Fatal error:直接设置 finalAnswer 为错误文案 + ERROR_FALLBACK,
|
||||
// 不走 LimitExceededNode(后者会再发一次 LLM 调用,语义不对且对认证/配额错误会再失败)。
|
||||
// ReasoningDispatcher 看到 !needsToolCall && !shouldSummarize → finalAnswerNode,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user