fix(agent): v2.2.62 — 출력 degeneration 방어
- 재시작 감지: auto-continuation이 "이어쓰기" 대신 답변을 처음부터 재생성하면 버림 → 분석이 두 번 나오던 문제 제거 - degeneration 정리 패스(cleanDegeneratedOutput): 문자 벽(같은 기호 8개+), (Note:…) 메타 노트, Candidate records 내부 지시문 누출, (질문 의도:…)/[핵심 확인 질문] 누출, 연속 중복 문단 제거 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+56
-1
@@ -269,7 +269,7 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
private sanitizeAssistantContent(text: string): string {
|
||||
return text
|
||||
const stripped = text
|
||||
.replace(/<rationale>[\s\S]*?<\/rationale>/gi, '')
|
||||
.replace(/^\s*\[PROBLEM\][\s\S]*?\[GOAL\][\s\S]*?\[REASONING\][^\n]*(?:\n+|$)/i, '')
|
||||
.replace(/^\s*\[PROBLEM\][\s\S]*?(?:\n\s*\n|$)/i, '')
|
||||
@@ -283,6 +283,54 @@ export class AgentExecutor {
|
||||
.replace(/<\|?channel\|?>\s*final\b\s*(?:<\|?message\|?>)?/gi, '')
|
||||
.replace(/<\|?(?:end|return|start|message)\|?>/gi, '')
|
||||
.trim();
|
||||
return this.cleanDegeneratedOutput(stripped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain the blast radius when a small local model degenerates on a long /
|
||||
* near-full context: runaway character walls, leaked English meta-notes,
|
||||
* leaked internal directives, stuttered duplicate paragraphs. This does not
|
||||
* fix the model failure — it just keeps a bad answer readable instead of a
|
||||
* wall of noise.
|
||||
*/
|
||||
private cleanDegeneratedOutput(text: string): string {
|
||||
let s = text;
|
||||
// Runaway character walls — 8+ of the same separator. A real markdown rule
|
||||
// is exactly 3 (`---`/`***`/`___`); nothing legitimate repeats a char 8+ times.
|
||||
s = s.replace(/([_=~.*])\1{7,}/g, '');
|
||||
// Leaked English meta-commentary the model narrates about itself.
|
||||
s = s.replace(/\(Note:[^)]*\)/gi, '');
|
||||
// Leaked project-chronicle directive (inline form — the `## ` heading form is stripped elsewhere).
|
||||
s = s.replace(/\n?Candidate records for this discussion[^\n]*/gi, '');
|
||||
// Leaked follow-up-question scaffolding the system prompt forbids.
|
||||
s = s.replace(/\(\s*질문\s*의도\s*[::][^)]*\)/g, '');
|
||||
s = s.replace(/\[\s*핵심\s*확인\s*질문\s*\]\s*/g, '');
|
||||
// Collapse consecutive duplicate paragraphs (model stutter).
|
||||
const paras = s.split(/\n{2,}/);
|
||||
const deduped: string[] = [];
|
||||
for (const p of paras) {
|
||||
const norm = p.trim().replace(/\s+/g, ' ');
|
||||
const prevNorm = deduped.length ? deduped[deduped.length - 1].trim().replace(/\s+/g, ' ') : '';
|
||||
if (norm && norm === prevNorm) { continue; }
|
||||
deduped.push(p);
|
||||
}
|
||||
return deduped.join('\n\n').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a continuation round restarted the whole answer from the top
|
||||
* instead of continuing it — a small model commonly ignores "continue from
|
||||
* here". Detected by a shared leading prefix with the answer so far.
|
||||
*/
|
||||
private isRestartedAnswer(soFar: string, cont: string): boolean {
|
||||
const norm = (x: string) => x.trim().replace(/\s+/g, ' ').toLowerCase();
|
||||
const a = norm(soFar);
|
||||
const b = norm(cont);
|
||||
if (a.length < 40 || b.length < 40) { return false; }
|
||||
let i = 0;
|
||||
const max = Math.min(a.length, b.length, 80);
|
||||
while (i < max && a[i] === b[i]) { i++; }
|
||||
return i >= 12;
|
||||
}
|
||||
|
||||
private async restoreLastSession() {
|
||||
@@ -1103,6 +1151,13 @@ export class AgentExecutor {
|
||||
logInfo('Continuation produced no visible text — stopping.', { model: actualModel, round: continuationCount });
|
||||
break;
|
||||
}
|
||||
// A weak model often ignores "continue from here" and re-generates the
|
||||
// whole answer from the top. Discard such a restart instead of merging
|
||||
// it — otherwise the user gets the entire analysis twice.
|
||||
if (this.isRestartedAnswer(cleaned.visible, ccl.visible)) {
|
||||
logInfo('Continuation restarted the answer instead of continuing — discarding it.', { model: actualModel, round: continuationCount });
|
||||
break;
|
||||
}
|
||||
const before = cleaned.visible;
|
||||
cleaned = { ...cleaned, visible: mergeContinuationParts(cleaned.visible, ccl.visible), wasThoughtOnly: false };
|
||||
lastOutputTokens = estimateTokens(ccl.visible);
|
||||
|
||||
Reference in New Issue
Block a user