后端: 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. 同步更新调试日志文件。 前端:无 仓库:无
208 lines
6.0 KiB
Go
208 lines
6.0 KiB
Go
package newagentnode
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
|
||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||
)
|
||
|
||
const (
|
||
orderGuardStageName = "order_guard"
|
||
orderGuardStatusBlock = "order_guard.status"
|
||
)
|
||
|
||
type suggestedOrderItem struct {
|
||
StateID int
|
||
Day int
|
||
SlotStart int
|
||
SlotEnd int
|
||
}
|
||
|
||
// RunOrderGuardNode 负责在收口前校验 suggested 任务相对顺序是否被打乱。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只做“相对顺序守卫”这一件事,不负责执行调度工具,也不负责写库;
|
||
// 2. 仅当 AllowReorder=false 时生效,用户明确授权可打乱顺序时直接放行;
|
||
// 3. 校验失败只写入统一终止结果(Abort),由 Deliver 节点统一收口文案。
|
||
func RunOrderGuardNode(ctx context.Context, st *newagentmodel.AgentGraphState) error {
|
||
if st == nil {
|
||
return fmt.Errorf("order_guard node: state is nil")
|
||
}
|
||
|
||
flowState := st.EnsureFlowState()
|
||
if flowState == nil {
|
||
return fmt.Errorf("order_guard node: flow state is nil")
|
||
}
|
||
// 1. 用户明确授权可打乱顺序时,顺序守卫节点直接放行。
|
||
if flowState.AllowReorder {
|
||
return nil
|
||
}
|
||
|
||
// 2. 读取当前 ScheduleState,提取 suggested 任务的“时间顺序快照”。
|
||
scheduleState, err := st.EnsureScheduleState(ctx)
|
||
if err != nil {
|
||
return fmt.Errorf("order_guard node: load schedule state failed: %w", err)
|
||
}
|
||
if scheduleState == nil {
|
||
return nil
|
||
}
|
||
currentOrder := buildSuggestedOrderSnapshot(scheduleState)
|
||
|
||
// 3. 基线为空时,仅初始化基线并放行,避免第一次进入守卫就误判。
|
||
if len(flowState.SuggestedOrderBaseline) == 0 {
|
||
flowState.SuggestedOrderBaseline = append([]int(nil), currentOrder...)
|
||
_ = st.EnsureChunkEmitter().EmitStatus(
|
||
orderGuardStatusBlock,
|
||
orderGuardStageName,
|
||
"order_guard_initialized",
|
||
"已记录本轮建议任务顺序基线,继续交付当前结果。",
|
||
false,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// 4. 基线存在时做逆序检测;一旦发现逆序,立即终止本轮自动微调。
|
||
violated, detail := detectRelativeOrderViolation(flowState.SuggestedOrderBaseline, currentOrder)
|
||
if !violated {
|
||
_ = st.EnsureChunkEmitter().EmitStatus(
|
||
orderGuardStatusBlock,
|
||
orderGuardStageName,
|
||
"order_guard_passed",
|
||
"顺序守卫校验通过,保持原有相对顺序。",
|
||
false,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
userMessage := "检测到当前方案打乱了原有建议任务顺序,本轮先停止自动微调。若你确认可以打乱顺序,请明确说明“允许打乱顺序”。"
|
||
flowState.Abort(
|
||
orderGuardStageName,
|
||
"relative_order_violation",
|
||
userMessage,
|
||
fmt.Sprintf("baseline=%v current=%v detail=%s", flowState.SuggestedOrderBaseline, currentOrder, detail),
|
||
)
|
||
_ = st.EnsureChunkEmitter().EmitStatus(
|
||
orderGuardStatusBlock,
|
||
orderGuardStageName,
|
||
"order_guard_failed",
|
||
userMessage,
|
||
true,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// buildSuggestedOrderSnapshot 生成 suggested 任务的相对顺序快照(按时间坐标排序)。
|
||
//
|
||
// 说明:
|
||
// 1. 这里只关心 suggested 任务,因为顺序守卫目标是约束“本轮建议层”的相对次序;
|
||
// 2. 多 slot 任务取“最早 slot”作为排序锚点,保证排序键稳定;
|
||
// 3. 返回值是 state_id 列表,便于写入 CommonState 做跨节点持久化。
|
||
func buildSuggestedOrderSnapshot(state *newagenttools.ScheduleState) []int {
|
||
if state == nil || len(state.Tasks) == 0 {
|
||
return nil
|
||
}
|
||
|
||
items := make([]suggestedOrderItem, 0, len(state.Tasks))
|
||
for i := range state.Tasks {
|
||
task := state.Tasks[i]
|
||
if !newagenttools.IsSuggestedTask(task) || len(task.Slots) == 0 {
|
||
continue
|
||
}
|
||
day, slotStart, slotEnd := earliestTaskSlot(task.Slots)
|
||
items = append(items, suggestedOrderItem{
|
||
StateID: task.StateID,
|
||
Day: day,
|
||
SlotStart: slotStart,
|
||
SlotEnd: slotEnd,
|
||
})
|
||
}
|
||
|
||
sort.SliceStable(items, func(i, j int) bool {
|
||
if items[i].Day != items[j].Day {
|
||
return items[i].Day < items[j].Day
|
||
}
|
||
if items[i].SlotStart != items[j].SlotStart {
|
||
return items[i].SlotStart < items[j].SlotStart
|
||
}
|
||
if items[i].SlotEnd != items[j].SlotEnd {
|
||
return items[i].SlotEnd < items[j].SlotEnd
|
||
}
|
||
return items[i].StateID < items[j].StateID
|
||
})
|
||
|
||
order := make([]int, 0, len(items))
|
||
for _, item := range items {
|
||
order = append(order, item.StateID)
|
||
}
|
||
return order
|
||
}
|
||
|
||
func earliestTaskSlot(slots []newagenttools.TaskSlot) (day int, slotStart int, slotEnd int) {
|
||
if len(slots) == 0 {
|
||
return 0, 0, 0
|
||
}
|
||
best := slots[0]
|
||
for i := 1; i < len(slots); i++ {
|
||
current := slots[i]
|
||
if current.Day < best.Day {
|
||
best = current
|
||
continue
|
||
}
|
||
if current.Day == best.Day && current.SlotStart < best.SlotStart {
|
||
best = current
|
||
continue
|
||
}
|
||
if current.Day == best.Day && current.SlotStart == best.SlotStart && current.SlotEnd < best.SlotEnd {
|
||
best = current
|
||
}
|
||
}
|
||
return best.Day, best.SlotStart, best.SlotEnd
|
||
}
|
||
|
||
// detectRelativeOrderViolation 检查 current 是否破坏 baseline 的相对顺序。
|
||
//
|
||
// 规则:
|
||
// 1. 仅比较 baseline 与 current 的交集任务,避免新增/删除任务引发误报;
|
||
// 2. 一旦出现 rank 逆序即判定为 violation;
|
||
// 3. detail 只用于内部排查,不直接给用户。
|
||
func detectRelativeOrderViolation(baseline []int, current []int) (bool, string) {
|
||
if len(baseline) == 0 || len(current) == 0 {
|
||
return false, ""
|
||
}
|
||
|
||
rankByID := make(map[int]int, len(baseline))
|
||
for idx, id := range baseline {
|
||
rankByID[id] = idx
|
||
}
|
||
|
||
filtered := make([]int, 0, len(current))
|
||
for _, id := range current {
|
||
if _, ok := rankByID[id]; ok {
|
||
filtered = append(filtered, id)
|
||
}
|
||
}
|
||
if len(filtered) < 2 {
|
||
return false, ""
|
||
}
|
||
|
||
prevID := filtered[0]
|
||
prevRank := rankByID[prevID]
|
||
for i := 1; i < len(filtered); i++ {
|
||
id := filtered[i]
|
||
rank := rankByID[id]
|
||
if rank < prevRank {
|
||
return true, strings.TrimSpace(fmt.Sprintf(
|
||
"reverse pair detected: prev_id=%d prev_rank=%d current_id=%d current_rank=%d",
|
||
prevID, prevRank, id, rank,
|
||
))
|
||
}
|
||
prevID = id
|
||
prevRank = rank
|
||
}
|
||
return false, ""
|
||
}
|