Version: 0.9.45.dev.260427

后端:
1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜
2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写
3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态
4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分
5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定

前端:
6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作

仓库:
7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件

PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
This commit is contained in:
Losita
2026-04-27 01:09:37 +08:00
parent 04b5836b39
commit 66c06eed0a
60 changed files with 9163 additions and 1819 deletions

View File

@@ -337,6 +337,17 @@ func buildTimeGrid(schedules []model.Schedule, taskClass *model.TaskClass) *grid
}
}
}
// 标记整天屏蔽:
// 1. excluded_days_of_week 表示“这些星期几整天都不允许粗排”;
// 2. 与 excluded_slots 一样属于硬约束,因此直接写入 Blocked
// 3. 一旦工作日容量不足,粗排应直接失败,而不是偷偷排到被排除的星期里。
for _, blockedDay := range taskClass.ExcludedDaysOfWeek {
for w := startW; w <= endW; w++ {
for s := 1; s <= 12; s++ {
g.setNode(w, blockedDay, s, slotNode{Status: Blocked})
}
}
}
// 映射日程 (尊重 Blocked 且只处理范围内的数据)
for _, s := range schedules {
@@ -450,6 +461,146 @@ type planningSlotCandidate struct {
sectionTo int
}
// countDayAvailable 统计某一天当前还可用于粗排的节次数。
//
// 职责边界:
// 1. 只把 Free/Filler 视为“仍可消费”的资源;
// 2. 不区分其来源是纯空位还是可嵌入课程,因为对粗排而言二者都代表后续还能放任务;
// 3. 仅用于候选打分,不直接参与最终合法性判断。
func (g *grid) countDayAvailable(week, day int) int {
if g == nil {
return 0
}
count := 0
for section := 1; section <= 12; section++ {
node := g.getNode(week, day, section)
if node.Status == Free || node.Status == Filler {
count++
}
}
return count
}
// countDayOccupied 统计某一天当前已被 existing/virtual/task 占住的节次数。
func (g *grid) countDayOccupied(week, day int) int {
if g == nil {
return 0
}
count := 0
for section := 1; section <= 12; section++ {
if g.getNode(week, day, section).Status == Occupied {
count++
}
}
return count
}
// collectPlanningCandidatesFromCursor 收集从给定游标开始仍然合法的候选落位。
//
// 设计说明:
// 1. 这里复用现有 findNextCandidateFromCursor 的合法性规则,避免复制一套“什么叫合法双节”的判断;
// 2. 通过跳过已命中候选的跨度,减少同一课程块被重复返回;
// 3. 保留快照上的 coordIndex供 steady 策略计算“距离目标位置有多远”。
func (g *grid) collectPlanningCandidatesFromCursor(coords []slotCoord, startCursor int) []planningSlotCandidate {
if g == nil || startCursor >= len(coords) {
return nil
}
candidates := make([]planningSlotCandidate, 0, 16)
seen := make(map[string]struct{})
for cursor := startCursor; cursor < len(coords); {
candidate, found := g.findNextCandidateFromCursor(coords, cursor)
if !found {
break
}
key := fmt.Sprintf("%d-%d-%d-%d", candidate.week, candidate.dayOfWeek, candidate.sectionFrom, candidate.sectionTo)
if _, exists := seen[key]; !exists {
seen[key] = struct{}{}
candidates = append(candidates, candidate)
}
nextCursor := candidate.coordIndex + (candidate.sectionTo - candidate.sectionFrom + 1)
if nextCursor <= cursor {
nextCursor = cursor + 1
}
cursor = nextCursor
}
return candidates
}
func computeSteadyTargetCursor(totalAvailable, totalItems, itemIndex int) int {
if totalAvailable <= 1 || totalItems <= 1 {
return 0
}
target := ((itemIndex + 1) * totalAvailable) / (totalItems + 1)
if target < 0 {
return 0
}
if target >= totalAvailable {
return totalAvailable - 1
}
return target
}
func planningDayOrdinal(week, day int) int {
return week*7 + day
}
func absInt(value int) int {
if value < 0 {
return -value
}
return value
}
// chooseSteadyCandidate 为 steady 策略挑选“更均衡、更分散、更留余地”的候选位。
//
// 评分原则:
// 1. 先尽量接近本任务在窗口中的目标分布位置;
// 2. 再偏好当前已占用更少的天,避免单日继续堆高;
// 3. 再惩罚与同任务类既有落位过近或同日重复,降低同科过度集中;
// 4. 最后惩罚吃掉当天最后一小段缓冲,给后续调整保留容错空间。
func (g *grid) chooseSteadyCandidate(
coords []slotCoord,
targetCursor int,
placedDayOrdinals []int,
) (planningSlotCandidate, bool) {
candidates := g.collectPlanningCandidatesFromCursor(coords, 0)
if len(candidates) == 0 {
return planningSlotCandidate{}, false
}
best := candidates[0]
bestScore := int(^uint(0) >> 1)
for _, candidate := range candidates {
slotSpan := candidate.sectionTo - candidate.sectionFrom + 1
distancePenalty := absInt(candidate.coordIndex-targetCursor) * 10
dayOccupiedPenalty := g.countDayOccupied(candidate.week, candidate.dayOfWeek) * 25
remainingAvailable := g.countDayAvailable(candidate.week, candidate.dayOfWeek) - slotSpan
bufferPenalty := 0
if remainingAvailable < 2 {
bufferPenalty = 80
}
dayOrdinal := planningDayOrdinal(candidate.week, candidate.dayOfWeek)
rhythmPenalty := 0
for _, placed := range placedDayOrdinals {
diff := absInt(dayOrdinal - placed)
switch {
case diff == 0:
rhythmPenalty += 180
case diff == 1:
rhythmPenalty += 60
}
}
score := distancePenalty + dayOccupiedPenalty + bufferPenalty + rhythmPenalty + candidate.coordIndex
if score < bestScore {
bestScore = score
best = candidate
}
}
return best, true
}
// getAllAvailable 获取窗口内所有可用的原子节次坐标(逻辑一维化)。
//
// 设计说明:
@@ -604,8 +755,8 @@ func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([
}
// 2. 计算间隔策略:
// 2.1 rapidgap=0尽快塞满
// 2.2 steady按剩余可用位均匀留白
// 2.1 rapid沿用“尽快塞满”的线性前进
// 2.2 steady不再只靠 gap 跳格子,而是结合目标位置、单日负载、同科分散和缓冲保留做候选打分
gap := 0
if strategy == "steady" {
gap = (totalAvailable - totalRequired) / (len(items) + 1)
@@ -617,16 +768,22 @@ func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([
// 3.3 若当前位置不满足约束(例如后继节被占),继续向后扫描,不降级为 1 节。
cursor := gap
lastPlacedIndex := -1
placedDayOrdinals := make([]int, 0, len(items))
for i := range items {
if cursor >= totalAvailable {
break
var (
candidate planningSlotCandidate
found bool
)
if strategy == "steady" {
targetCursor := computeSteadyTargetCursor(totalAvailable, len(items), i)
candidate, found = g.chooseSteadyCandidate(coords, targetCursor, placedDayOrdinals)
} else {
if cursor >= totalAvailable {
break
}
candidate, found = g.findNextCandidateFromCursor(coords, cursor)
}
// 4. 先找候选,不立即写入:
// 4.1 找不到候选时提前结束;
// 4.2 最终统一通过 lastPlacedIndex 判断是否完整排完。
candidate, found := g.findNextCandidateFromCursor(coords, cursor)
if !found {
break
}
@@ -648,7 +805,10 @@ func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([
// 7. 推进游标并记录成功位置。
slotLen := candidate.sectionTo - candidate.sectionFrom + 1
cursor = candidate.coordIndex + slotLen + gap
if strategy != "steady" {
cursor = candidate.coordIndex + slotLen + gap
}
placedDayOrdinals = append(placedDayOrdinals, planningDayOrdinal(candidate.week, candidate.dayOfWeek))
lastPlacedIndex = i
}