Version: 0.9.9.dev.260408

后端:
1. 粗排后分流与顺序守卫落地,支持“无明确微调偏好时粗排后直接收口”,并新增 allow_reorder / needs_refine_after_rough_build 语义,打通 chat→rough_build→execute/order_guard→deliver 路由。
2. execute 工具执行链路修复:清理乱码坏块与重复分支;新增 min_context_switch 未授权拦截;补齐 suggested 顺序基线初始化与顺序守卫联动。
3. 新增复合写工具 min_context_switch(减少上下文切换)并接入注册、参数解析、写工具白名单、提示词与文档;仅在用户明确允许打乱顺序时可用。
4. 工具口径升级:find_first_free 支持 day/day_start/day_end 范围参数并统一文案;移除 find_free 兼容别名;读写工具输出统一到“第N天(星期X)”格式。
5. prompt 同步升级:chat/execute/execute_context 增加粗排后是否继续微调、顺序授权、min_context_switch 使用边界与返回示例约束。
6. handoff 文档重命名并重写下班交接重点:下一步聚焦“工具收敛能力研究 + 运行态必要参数重置(不丢运行态)”。
7. 同步更新调试日志文件。
前端:无
仓库:无
This commit is contained in:
Losita
2026-04-08 23:55:09 +08:00
parent 4195e65cba
commit 21b864390b
21 changed files with 3546 additions and 1009 deletions

View File

@@ -135,7 +135,7 @@ func QueryRange(state *ScheduleState, day int, slotStart, slotEnd *int) string {
// 输出格式对齐 SCHEDULE_TOOLS.md 4.2 节示例。
func queryRangeFullDay(state *ScheduleState, day int) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("第%d天 全天:\n\n", day))
sb.WriteString(fmt.Sprintf("%s 全天:\n\n", formatDayLabel(state, day)))
// 1. 按 6 个标准段输出1-2, 3-4, 5-6, 7-8, 9-10, 11-12
for start := 1; start <= 11; start += 2 {
@@ -174,7 +174,7 @@ func queryRangeFullDay(state *ScheduleState, day int) string {
// queryRangeSpecific 指定范围查询模式:逐节输出。
func queryRangeSpecific(state *ScheduleState, day, startSlot, endSlot int) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("第%d天 第%s\n\n", day, formatSlotRange(startSlot, endSlot)))
sb.WriteString(fmt.Sprintf("%s第%s\n\n", formatDayLabel(state, day), formatSlotRange(startSlot, endSlot)))
total := endSlot - startSlot + 1
freeCount := 0
@@ -199,16 +199,26 @@ func queryRangeSpecific(state *ScheduleState, day, startSlot, endSlot int) strin
// FindFirstFree 查找首个可用空位,并返回该日详细信息。
//
// 参数说明:
// 1. duration 必填,表示需要的连续时段数;
// 2. day 选填,指定单天搜索;
// 3. dayStart/dayEnd 选填,指定按天范围搜索(闭区间);
// 4. day 与 dayStart/dayEnd 互斥,避免语义冲突。
//
// 说明:
// 1. 参数与旧 find_free 保持一致duration/day
// 2. 返回“首个命中候选位 + 当日负载明细”,供 LLM 直接决策;
// 3. 当前阶段按用户要求全量返回,不做文本截断。
func FindFirstFree(state *ScheduleState, duration int, day *int) string {
// 1. 返回“首个命中候选位 + 当日负载明细”,供 LLM 直接决策
// 2. 当前阶段按用户要求全量返回,不做文本截断。
func FindFirstFree(state *ScheduleState, duration int, day, dayStart, dayEnd *int) string {
if duration <= 0 {
return "查询失败duration 必须大于 0。"
}
// 1. 确定搜索范围。
// 1. 参数互斥校验:单天搜索范围搜索只能二选一
if day != nil && (dayStart != nil || dayEnd != nil) {
return "查询失败day 与 day_start/day_end 不能同时传入。"
}
// 2. 确定搜索范围。
days := make([]int, 0)
if day != nil {
if *day < 1 || *day > state.Window.TotalDays {
@@ -216,12 +226,30 @@ func FindFirstFree(state *ScheduleState, duration int, day *int) string {
}
days = append(days, *day)
} else {
for d := 1; d <= state.Window.TotalDays; d++ {
startDay := 1
endDay := state.Window.TotalDays
if dayStart != nil {
startDay = *dayStart
}
if dayEnd != nil {
endDay = *dayEnd
}
if startDay < 1 || startDay > state.Window.TotalDays {
return fmt.Sprintf("查询失败day_start=%d 不在规划窗口范围内1-%d。", startDay, state.Window.TotalDays)
}
if endDay < 1 || endDay > state.Window.TotalDays {
return fmt.Sprintf("查询失败day_end=%d 不在规划窗口范围内1-%d。", endDay, state.Window.TotalDays)
}
if startDay > endDay {
return fmt.Sprintf("查询失败day_start=%d 不能大于 day_end=%d。", startDay, endDay)
}
for d := startDay; d <= endDay; d++ {
days = append(days, d)
}
}
// 2. 按天从前往后寻找“首个可直接放置”的空位。
// 3. 按天从前往后寻找“首个可直接放置”的空位。
for _, d := range days {
freeRanges := findFreeRangesOnDay(state, d)
for _, r := range freeRanges {
@@ -235,7 +263,7 @@ func FindFirstFree(state *ScheduleState, duration int, day *int) string {
}
}
// 3. 若没有纯空位,再尝试首个可嵌入宿主时段。
// 4. 若没有纯空位,再尝试首个可嵌入宿主时段。
for _, d := range days {
host, slotStart, slotEnd := findFirstEmbeddablePosition(state, d, duration)
if host != nil {
@@ -243,7 +271,7 @@ func FindFirstFree(state *ScheduleState, duration int, day *int) string {
}
}
// 4. 无可用位置时返回摘要,辅助 LLM 判断是否需要换天或降时长。
// 5. 无可用位置时返回摘要,辅助 LLM 判断是否需要换天或降时长。
var sb strings.Builder
sb.WriteString(fmt.Sprintf("未找到满足%d个连续时段的可用位置。\n", duration))
sb.WriteString("各天最大连续空闲区前10天\n")
@@ -261,17 +289,11 @@ func FindFirstFree(state *ScheduleState, duration int, day *int) string {
maxDur = dur
}
}
sb.WriteString(fmt.Sprintf("第%d天:最大连续空闲%d节\n", d, maxDur))
sb.WriteString(fmt.Sprintf("%s:最大连续空闲%d节\n", formatDayLabel(state, d), maxDur))
}
return sb.String()
}
// FindFree 是 find_first_free 的兼容别名。
// 保留该入口可避免旧提示词和历史轨迹中的工具名失效。
func FindFree(state *ScheduleState, duration int, day *int) string {
return FindFirstFree(state, duration, day)
}
// buildFindFirstFreeReport 构造首个可用位的详细报告。
func buildFindFirstFreeReport(
state *ScheduleState,
@@ -284,10 +306,10 @@ func buildFindFirstFreeReport(
) string {
var sb strings.Builder
if isEmbedded && host != nil {
sb.WriteString(fmt.Sprintf("首个可用位置:第%d天第%s可嵌入宿主 [%d]%s。\n",
day, formatSlotRange(slotStart, slotEnd), host.StateID, host.Name))
sb.WriteString(fmt.Sprintf("首个可用位置:%s可嵌入宿主 [%d]%s。\n",
formatDaySlotLabel(state, day, slotStart, slotEnd), host.StateID, host.Name))
} else {
sb.WriteString(fmt.Sprintf("首个可用位置:第%d天第%s可直接放置。\n", day, formatSlotRange(slotStart, slotEnd)))
sb.WriteString(fmt.Sprintf("首个可用位置:%s可直接放置。\n", formatDaySlotLabel(state, day, slotStart, slotEnd)))
}
sb.WriteString(fmt.Sprintf("匹配条件:需要%d个连续时段。\n", duration))
@@ -313,7 +335,7 @@ func buildFindFirstFreeReport(
sb.WriteString(" 无连续空闲区。\n")
} else {
for _, r := range freeRanges {
sb.WriteString(" - " + buildFreeRangeLine(r) + "\n")
sb.WriteString(" - " + buildFreeRangeLine(state, r) + "\n")
}
}
return sb.String()
@@ -385,9 +407,10 @@ func buildTaskOnlyOverviewDayLine(state *ScheduleState, day int) string {
taskOccupied := countDayTaskOccupied(state, day)
courseOccupied := totalOccupied - taskOccupied
taskEntries := collectTaskEntriesOnDay(state, day)
dayLabel := formatDayLabel(state, day)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("第%d天:总占%d/12课程占%d/12任务占%d/12", day, totalOccupied, courseOccupied, taskOccupied))
sb.WriteString(fmt.Sprintf("%s:总占%d/12课程占%d/12任务占%d/12", dayLabel, totalOccupied, courseOccupied, taskOccupied))
if len(taskEntries) == 0 {
sb.WriteString(" — 任务:无")
return sb.String()
@@ -435,7 +458,7 @@ func buildTaskOnlyOverviewList(state *ScheduleState) string {
continue
}
sb.WriteString(fmt.Sprintf("[%d]%s | 状态:%s | 类别:%s%s | 时段:%s\n",
t.StateID, t.Name, taskStatusLabel(t), t.Category, classID, formatTaskSlotsBrief(t.Slots)))
t.StateID, t.Name, taskStatusLabel(t), t.Category, classID, formatTaskSlotsBriefWithState(state, t.Slots)))
}
return sb.String()
}
@@ -545,7 +568,7 @@ func ListTasks(state *ScheduleState, category, status *string) string {
if len(suggestedTasks) == 0 {
return formatListTasksEmptyResult(statusFilter, categoryFilter)
}
return formatSuggestedList(suggestedTasks)
return formatSuggestedList(state, suggestedTasks)
}
// 6. 纯已安排模式:只输出已安排任务。
@@ -553,7 +576,7 @@ func ListTasks(state *ScheduleState, category, status *string) string {
if len(existingTasks) == 0 {
return formatListTasksEmptyResult(statusFilter, categoryFilter)
}
return formatExistingList(existingTasks)
return formatExistingList(state, existingTasks)
}
// 7. 全部模式:统计 + 分组输出。
@@ -563,11 +586,11 @@ func ListTasks(state *ScheduleState, category, status *string) string {
if len(existingTasks) > 0 {
sb.WriteString("\n已安排(existing)\n")
sb.WriteString(formatExistingList(existingTasks))
sb.WriteString(formatExistingList(state, existingTasks))
}
if len(suggestedTasks) > 0 {
sb.WriteString("\n已预排(suggested)\n")
sb.WriteString(formatSuggestedList(suggestedTasks))
sb.WriteString(formatSuggestedList(state, suggestedTasks))
}
if len(pendingTasks) > 0 {
sb.WriteString("\n待安排(pending)\n")
@@ -680,7 +703,7 @@ func GetTaskInfo(state *ScheduleState, taskID int) string {
if len(task.Slots) > 0 {
sb.WriteString("占用时段:\n")
for _, slot := range task.Slots {
sb.WriteString(fmt.Sprintf(" 第%d天 第%s\n", slot.Day, formatSlotRange(slot.SlotStart, slot.SlotEnd)))
sb.WriteString(fmt.Sprintf(" %s\n", formatDaySlotLabel(state, slot.Day, slot.SlotStart, slot.SlotEnd)))
}
}
@@ -778,14 +801,14 @@ func formatEmbedInfoForDay(state *ScheduleState, day int) string {
// formatExistingList 格式化已安排任务列表。
// 格式如: [1]高等数学(课程,固定) — 第1天(1-2节) 第4天(1-2节)
func formatExistingList(tasks []ScheduleTask) string {
func formatExistingList(state *ScheduleState, tasks []ScheduleTask) string {
var sb strings.Builder
for _, t := range tasks {
label := formatTaskLabelWithCategory(t)
// 格式化所有时段位置。
slotParts := make([]string, 0, len(t.Slots))
for _, slot := range t.Slots {
slotParts = append(slotParts, fmt.Sprintf("第%d天(%s)", slot.Day, formatSlotRange(slot.SlotStart, slot.SlotEnd)))
slotParts = append(slotParts, fmt.Sprintf("%s(%s)", formatDayLabel(state, slot.Day), formatSlotRange(slot.SlotStart, slot.SlotEnd)))
}
sb.WriteString(fmt.Sprintf(" %s — %s\n", label, strings.Join(slotParts, " ")))
}
@@ -794,13 +817,13 @@ func formatExistingList(tasks []ScheduleTask) string {
// formatSuggestedList 格式化已预排任务列表。
// 格式如:[3]复习线代 — 已预排至 第2天第3-4节类别学习
func formatSuggestedList(tasks []ScheduleTask) string {
func formatSuggestedList(state *ScheduleState, tasks []ScheduleTask) string {
var sb strings.Builder
if len(tasks) > 0 {
sb.WriteString(fmt.Sprintf("已预排任务共%d个\n\n", len(tasks)))
}
for _, t := range tasks {
sb.WriteString(fmt.Sprintf("[%d]%s — 已预排至 %s类别%s\n", t.StateID, t.Name, formatTaskSlotsBrief(t.Slots), t.Category))
sb.WriteString(fmt.Sprintf("[%d]%s — 已预排至 %s类别%s\n", t.StateID, t.Name, formatTaskSlotsBriefWithState(state, t.Slots), t.Category))
}
return sb.String()
}