Files
AutoZero/agent/anomaly.go
T
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

77 lines
3.3 KiB
Go

// 重复调用的识别(对齐 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
}