// 重复调用的识别(对齐 ZCode 的 model-anomaly:只提醒,不强停)。 // // 已有的 [STUCK] 数的是**连续失败**(按工具名,因为模型重试时通常会微调参数)。 // 它盖不住另一类空转:调用**成功**了,却是一模一样的东西再来一遍——反复 read 同一个 // 文件、反复跑同一条 grep。每一步都是一次完整的模型往返 + 一次工具执行,token 与 // 时间都实打实花掉,而屏幕上的进展为零。实测里这类空转往往出现在"模型不确定下一步 // 做什么"的时候,此时它需要的正是一句"换个动作,或者把卡在哪讲出来"。 package agent import ( "encoding/json" "fmt" "strings" ) // repeatWarnAfter 是同一签名在本回合内第几次调用时提醒(含第 N 次)。 // 2 次是合理重试(第一次结果太长没看到、或想确认改动后的状态),3 次就是原地打转。 const repeatWarnAfter = 3 // anomalyNoteMax 是本回合最多注入几条重复提醒。提醒本身也占 token, // 而且模型如果无视两次就不会突然开始听——第 3 条之后不再啰嗦。 const anomalyNoteMax = 3 // toolSignature 是一次调用的归一指纹:工具名 + 参数。 // // 参数要归一而不是用原文:模型两次生成同一个调用时,JSON 的键序与空白可能不同 // (编码/json.Marshal 对 map 按键排序),照原文比就会把"其实一模一样"判成不同, // 而这个检测器一旦漏判就永远不会响。解析失败时退回原文(宁可漏判不误判)。 func toolSignature(name, input string) string { var v any if json.Unmarshal([]byte(strings.TrimSpace(input)), &v) == nil { if b, err := json.Marshal(v); err == nil { input = string(b) } } return name + "\x00" + strings.TrimSpace(input) } // noteToolRepeat 记下一次调用,返回"这次该提醒"的次数(0 = 不提醒)。 // 只在**恰好第 N 次**以及之后每多一次时提醒一次,且每回合总量封顶。 func (a *Agent) noteToolRepeat(name, input string) int { key := toolSignature(name, input) a.repeatMu.Lock() defer a.repeatMu.Unlock() if a.repeatCounts == nil { a.repeatCounts = map[string]int{} } a.repeatCounts[key]++ n := a.repeatCounts[key] if n < repeatWarnAfter { return 0 } if a.anomalyNotes >= anomalyNoteMax { return 0 } a.anomalyNotes++ return n } // repeatNote 是给模型看的那一行。写进工具结果而不是单独一条消息:结果本来就要 // 回灌给模型,多塞一条假 user 消息会污染历史(也与 wrapUp/续写的选择一致)。 func repeatNote(tool string, n int) string { return fmt.Sprintf("[REPEAT] This is call #%d to `%s` with **identical arguments** in this turn. "+ "Nothing has changed since the previous one, so the result above is the same. Do not run it again: "+ "either change something real (different arguments, a different tool), use what you already have, "+ "or tell the user what you are blocked on.", n, tool) } // resetAnomalyCounters 在回合开始时清零(与提问账、续写账同一个时机): // "本回合空转"管的是这一次任务,下一条消息重新开始算。 func (a *Agent) resetAnomalyCounters() { a.repeatMu.Lock() defer a.repeatMu.Unlock() a.repeatCounts = map[string]int{} a.anomalyNotes = 0 }