fix: 检测模型退化循环输出并自动中止流 (#66)

当 Cursor 后端模型陷入退化循环时(不断重复输出 </s>、</br> 等
短文本标记),会导致内容输出一点就停止、后台日志充斥大量
重复标记的现象。

修复方案:在 cursor-client.ts SSE 解析层加入退化重复检测器:
- 跟踪连续相同的 text-delta(仅检测 ≤20 字符的短 token)
- 同一 delta 连续出现 ≥8 次时判定为退化循环
- 立即中止流(reader.cancel),保留已收到的有效内容
- 外层 sendCursorRequest 识别 DEGENERATE_LOOP_ABORTED 后
  不再重试(重试也会重蹈覆辙)

所有流式路径(Anthropic handler、OpenAI handler、续写)
自动受保护,无需逐个修改。
This commit is contained in:
小海
2026-03-19 09:14:29 +08:00
parent 53335aeeab
commit 4bd6066dda

View File

@@ -58,6 +58,8 @@ export async function sendCursorRequest(
} catch (err) {
// 外部主动中止不重试
if (externalSignal?.aborted) throw err;
// ★ 退化循环中止不重试 — 已有的内容是有效的,重试也会重蹈覆辙
if (err instanceof Error && err.message === 'DEGENERATE_LOOP_ABORTED') return;
const msg = err instanceof Error ? err.message : String(err);
console.error(`[Cursor] 请求失败 (${attempt}/${maxRetries}): ${msg.substring(0, 100)}`);
if (attempt < maxRetries) {
@@ -126,6 +128,14 @@ async function sendCursorRequestInner(
const decoder = new TextDecoder();
let buffer = '';
// ★ 退化重复检测器 (#66)
// 模型有时会陷入循环,不断输出 </s>、</br> 等无意义标记
// 检测原理:跟踪最近的连续相同 delta超过阈值则中止流
let lastDelta = '';
let repeatCount = 0;
const REPEAT_THRESHOLD = 8; // 同一 delta 连续出现 8 次 → 退化
let degenerateAborted = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
@@ -144,11 +154,44 @@ async function sendCursorRequestInner(
try {
const event: CursorSSEEvent = JSON.parse(data);
// ★ 退化重复检测:当模型重复输出同一短文本片段时中止
if (event.type === 'text-delta' && event.delta) {
const trimmedDelta = event.delta.trim();
// 只检测短 token长文本重复是正常的比如重复的代码行
if (trimmedDelta.length > 0 && trimmedDelta.length <= 20) {
if (trimmedDelta === lastDelta) {
repeatCount++;
if (repeatCount >= REPEAT_THRESHOLD) {
console.warn(`[Cursor] ⚠️ 检测到退化循环: "${trimmedDelta}" 已连续重复 ${repeatCount} 次,中止流`);
degenerateAborted = true;
// 不再转发此 delta直接中止
reader.cancel();
break;
}
} else {
lastDelta = trimmedDelta;
repeatCount = 1;
}
} else {
// 长文本或空白 → 重置计数
lastDelta = '';
repeatCount = 0;
}
}
onChunk(event);
} catch {
// 非 JSON 数据,忽略
}
}
if (degenerateAborted) break;
}
// ★ 退化循环中止后,抛出特殊错误让外层 sendCursorRequest 不再重试
if (degenerateAborted) {
throw new Error('DEGENERATE_LOOP_ABORTED');
}
// 处理剩余 buffer