Files
smartmate/backend/logic/refine_compound_ops_test.go
LoveLosita f4ef6fb256 Version: 0.7.5.dev.260324
🐛 fix(agent/schedulerefine): 修复复合微调分支链路问题,并将 MinContextSwitch 重构为固定坑位重排语义

- 🔧 修复 `schedulerefine` 复合路由中参数透传不完整、缺少 deterministic objective 时错误降级,以及“复合工具执行成功”与“终审通过”语义混淆的问题
-  保证新的独立复合分支能够正确执行、正确出站,并统一交由 `hard_check` 裁决最终结果
- 🔍 排查时发现 `MinContextSwitch` 上游 `context_tag` 存在整体退化为 `General` 的风险,影响MinContextSwitch
- 🛡️ 为 `MinContextSwitch` 增加兜底策略:当标签整体退化时,按任务名关键词推断学科分组,避免分组能力失效
- ♻️ 将 `MinContextSwitch` 从“整周重新寻找新坑位”调整为“坑位不变,任务顺序改变”
- 🎯 将落地方式从顺序 `BatchMove` 改为固定坑位原子重写,避免出现远距离跳位、跨天错迁、异常嵌入课位及循环换位冲突
- 🧹 修复 `hard_check` 在 `MinContextSwitch` 成功后仍执行 `origin_rank` 顺序归位、并导致逆序终审误判的问题
- 🚦 命中该分支后跳过顺序归位与顺序硬校验,避免 `summary` / `hard_check` 将有效重排结果误判为失败

📈 当前连续微调规划涉及的全部功能已可以稳定运行;下一步将继续扩展能力边界,并进一步优化 `schedule_plan` 流程

♻️ refactor: 重整 agent2 架构,并迁移 quicknote/chat 新链路,目前还剩3个模块未迁移,后续迁移完成后会删除原agent并将此目录命名为agent

- 🏗️ 明确 `agent2` 采用“统一分层目录 + 文件分层 + 依赖注入”的重构方案,不再沿用模块目录多层嵌套结构
- 🧩 完善 `agent2` 基础骨架,统一收口 `entrance` / `router` / `llm` / `stream` / `shared` / `model` / `prompt` / `node` / `graph` 等层级职责
- 🚚 将通用路由能力迁移至 `agent2/router`,沉淀统一的 `Action`、`RoutingDecision`、控制码解析,以及 `Dispatcher` / `Resolver` 抽象
- 💬 将普通聊天链路迁移至 `agent2/chat`,复用 `stream` 的 OpenAI 兼容输出协议与 LLM usage 聚合能力
- 📝 将 `quicknote` 链路迁移到 `agent2` 新结构,拆分为 `model` / `prompt` / `llm` / `node` / `graph` 多层实现,替换对旧 `agent/quicknote` 的直接依赖
- 🔌 调整 `agentsvc` 对 `agent2` 的引用,普通聊天、通用分流与 `quicknote` 全部切换到新链路
- ✂️ 去除 graph 内部 `runner` 转接层,改为由 node 层直接持有请求级依赖,并向 graph 暴露节点方法
- 🧹 合并 `graph/quicknote` 与 `graph/quicknote_run`,删除冗余骨架文件,收敛为单一 `quicknote graph` 文件
- 📚 新增 `agent2`《通用能力接入文档》,明确公共能力边界、接入方式以及 graph/node 协作约定
- 📝 更新 `AGENTS.md`,要求后续扩展 `agent2` 通用能力时必须同步维护接入文档

♻️ refactor: 删除了现Agent目录内Chat模块的两条冗余Prompt
2026-03-24 21:35:22 +08:00

132 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"sort"
"testing"
)
func TestPlanEvenSpreadMovesPrefersLowerLoadDay(t *testing.T) {
tasks := []RefineTaskCandidate{
{TaskItemID: 101, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2, OriginRank: 1},
{TaskItemID: 102, Week: 16, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4, OriginRank: 2},
}
slots := []RefineSlotCandidate{
{Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2},
{Week: 12, DayOfWeek: 2, SectionFrom: 1, SectionTo: 2},
{Week: 12, DayOfWeek: 3, SectionFrom: 1, SectionTo: 2},
}
moves, err := PlanEvenSpreadMoves(tasks, slots, RefineCompositePlanOptions{
ExistingDayLoad: map[string]int{
composeDayKey(12, 1): 5,
composeDayKey(12, 2): 1,
composeDayKey(12, 3): 0,
},
})
if err != nil {
t.Fatalf("PlanEvenSpreadMoves 返回错误: %v", err)
}
if len(moves) != 2 {
t.Fatalf("期望移动 2 条,实际=%d", len(moves))
}
// 1. 低负载日(周三)应优先被填充;
// 2. 第二条应落在次低负载日(周二),而不是高负载日(周一)。
weekDayByID := make(map[int][2]int, len(moves))
for _, move := range moves {
weekDayByID[move.TaskItemID] = [2]int{move.ToWeek, move.ToDay}
}
if got := weekDayByID[101]; got != [2]int{12, 3} {
t.Fatalf("任务101应优先落到 W12D3实际=%v", got)
}
if got := weekDayByID[102]; got != [2]int{12, 2} {
t.Fatalf("任务102应落到 W12D2实际=%v", got)
}
}
func TestPlanMinContextSwitchMovesGroupsSameContext(t *testing.T) {
tasks := []RefineTaskCandidate{
{TaskItemID: 201, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2, ContextTag: "数学", OriginRank: 1},
{TaskItemID: 202, Week: 16, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4, ContextTag: "算法", OriginRank: 2},
{TaskItemID: 203, Week: 16, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6, ContextTag: "数学", OriginRank: 3},
}
slots := []RefineSlotCandidate{
{Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2},
{Week: 12, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4},
{Week: 12, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6},
}
moves, err := PlanMinContextSwitchMoves(tasks, slots, RefineCompositePlanOptions{})
if err != nil {
t.Fatalf("PlanMinContextSwitchMoves 返回错误: %v", err)
}
if len(moves) != 3 {
t.Fatalf("期望移动 3 条,实际=%d", len(moves))
}
// 1. “数学”有 2 条,分组后应先连续落在最早两个坑位;
// 2. 因此 201 与 203 对应的目标节次应是 1-2 与 3-4顺序由 origin_rank 决定)。
sort.SliceStable(moves, func(i, j int) bool {
if moves[i].ToWeek != moves[j].ToWeek {
return moves[i].ToWeek < moves[j].ToWeek
}
if moves[i].ToDay != moves[j].ToDay {
return moves[i].ToDay < moves[j].ToDay
}
return moves[i].ToSectionFrom < moves[j].ToSectionFrom
})
if moves[0].TaskItemID != 201 || moves[1].TaskItemID != 203 {
t.Fatalf("期望前两个坑位由同上下文任务占据,实际=%+v", moves)
}
if moves[2].TaskItemID != 202 {
t.Fatalf("期望最后一个坑位为算法任务,实际=%+v", moves[2])
}
}
func TestPlanMinContextSwitchMovesFallsBackToTaskNameWhenAllGeneral(t *testing.T) {
tasks := []RefineTaskCandidate{
{TaskItemID: 301, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2, Name: "随机事件与概率基础概念复习", ContextTag: "General", OriginRank: 1},
{TaskItemID: 302, Week: 16, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4, Name: "数制、码制与逻辑代数基础", ContextTag: "General", OriginRank: 2},
{TaskItemID: 303, Week: 16, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6, Name: "第二章 条件概率与全概率公式", ContextTag: "General", OriginRank: 3},
}
slots := []RefineSlotCandidate{
{Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2},
{Week: 12, DayOfWeek: 1, SectionFrom: 3, SectionTo: 4},
{Week: 12, DayOfWeek: 1, SectionFrom: 5, SectionTo: 6},
}
moves, err := PlanMinContextSwitchMoves(tasks, slots, RefineCompositePlanOptions{})
if err != nil {
t.Fatalf("PlanMinContextSwitchMoves 返回错误: %v", err)
}
if len(moves) != 3 {
t.Fatalf("期望移动 3 条,实际=%d", len(moves))
}
sort.SliceStable(moves, func(i, j int) bool {
if moves[i].ToWeek != moves[j].ToWeek {
return moves[i].ToWeek < moves[j].ToWeek
}
if moves[i].ToDay != moves[j].ToDay {
return moves[i].ToDay < moves[j].ToDay
}
return moves[i].ToSectionFrom < moves[j].ToSectionFrom
})
if moves[0].TaskItemID != 301 || moves[1].TaskItemID != 303 {
t.Fatalf("期望概率任务通过名称兜底连续聚类,实际=%+v", moves)
}
if moves[2].TaskItemID != 302 {
t.Fatalf("期望数电任务落在最后一个坑位,实际=%+v", moves[2])
}
}
func TestPlanEvenSpreadMovesReturnsErrorWhenSpanNotMatched(t *testing.T) {
tasks := []RefineTaskCandidate{
{TaskItemID: 301, Week: 16, DayOfWeek: 1, SectionFrom: 1, SectionTo: 3, OriginRank: 1}, // span=3
}
slots := []RefineSlotCandidate{
{Week: 12, DayOfWeek: 1, SectionFrom: 1, SectionTo: 2}, // span=2
}
_, err := PlanEvenSpreadMoves(tasks, slots, RefineCompositePlanOptions{})
if err == nil {
t.Fatalf("期望 span 不匹配时报错,实际 err=nil")
}
}