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。
77 lines
3.3 KiB
Go
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
|
|
}
|