package agent import ( "context" "fmt" "strings" "testing" "git.gopher.ink/mirrors2/AutoZero/llm" "git.gopher.ink/mirrors2/AutoZero/tools" ) // TestMaxTurnsWrapUp:模型一直请求工具、始终不收敛时,到步数上限**不再抛错**, // 而是禁掉工具再要一轮收尾说明。 // // 回归:此前到顶直接 return reached max turns,用户只拿到一句错误——不知道做到 // 哪、还剩什么,也看不到任何可以行动的结论。现在上限从「中断」降级成「体面收尾」。 func TestMaxTurnsWrapUp(t *testing.T) { var ( wrapCalls int wrapToolCount = -1 wrapSystem string wrapDisabled bool normalCalls int ) var prov *scriptedProvider prov = &scriptedProvider{fn: func(call int, system string, msgs []llm.Message, tl []llm.ToolDef) (llm.Message, error) { if !prov.lastOpts().DisableTools { // 正常轮:永远请求工具,永不收敛 normalCalls++ return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{ {ID: fmt.Sprintf("t%d", call), Name: "noop", Input: "{}"}, }}, nil } // 收尾轮:工具定义保留(缓存前缀不变),但 tool_choice=none 禁止调用 wrapCalls++ wrapToolCount = len(tl) wrapSystem = system wrapDisabled = prov.lastOpts().DisableTools return llm.Message{Role: llm.RoleAssistant, Content: "已完成 A;剩余 B。"}, nil }} noop := &tools.Tool{ Name: "noop", Description: "no-op", Run: func(context.Context, string) (tools.Result, error) { return tools.TextResult("ok"), nil }, } a, sess := newTestAgent(t, noop) a.Provider = prov a.MaxTurns = 3 if err := a.Run(context.Background(), "do it"); err != nil { t.Fatalf("到上限应体面收尾而不是报错: %v", err) } if normalCalls != 3 { t.Errorf("正常轮次数 = %d, want 3(正好用完预算)", normalCalls) } if wrapCalls != 1 { t.Errorf("收尾轮应恰好一次,实际 %d", wrapCalls) } if wrapToolCount <= 0 { t.Errorf("收尾轮应保留工具定义(只禁用调用),实际 %d 个", wrapToolCount) } if !wrapDisabled { t.Error("收尾轮没有禁用工具调用(DisableTools)") } if !strings.Contains(wrapSystem, "步数已用尽") { t.Errorf("收尾轮 system 未带收尾指令: %q", wrapSystem) } last := sess.Messages[len(sess.Messages)-1] if last.Role != llm.RoleAssistant { t.Fatalf("最后一条应是 assistant,实际 %s", last.Role) } if !strings.Contains(last.Content, "已达本轮步数上限(3)") { t.Errorf("收尾正文缺少上限标记: %q", last.Content) } if !strings.Contains(last.Content, "已完成 A") { t.Errorf("收尾正文缺少模型给出的结论: %q", last.Content) } if len(last.ToolCalls) != 0 { t.Errorf("收尾消息不该带 tool_use(会破坏 tool_use/tool_result 配对): %+v", last.ToolCalls) } // 上限用完但工作没丢:前面几轮的 tool_use/tool_result 都在会话里, // 用户回一句「继续」就能带着完整上下文接着跑。 if !strings.Contains(sess.Messages[len(sess.Messages)-2].Content, "ok") { t.Errorf("倒数第二条应是最后一个工具结果: %+v", sess.Messages[len(sess.Messages)-2]) } } // TestApplyDefaultsMaxTurns:未配置时回落默认值。 func TestApplyDefaultsMaxTurns(t *testing.T) { a, _ := newTestAgent(t) a.applyDefaults() if a.MaxTurns != DefaultMaxTurns { t.Errorf("MaxTurns = %d, want %d", a.MaxTurns, DefaultMaxTurns) } if DefaultMaxTurns < 40 { t.Errorf("默认上限 %d 不该低于旧硬编码值 40", DefaultMaxTurns) } }