Version: 0.7.3.dev.260322
♻️ refactor(schedule-refine): [WIP] 重构 Plan-and-Execute ReAct 链路,并增强 JSON 解析兜底能力 - 🧩 重构 `schedulerefine` 主流程,引入 `Planner` / `Replan` 机制,以及执行预算与轮次状态管理 - 🧠 扩展状态与观察上下文,补充工具结果、失败签名、连续失败计数与后置反思策略等信息 - 🔧 增强工具层能力与参数兼容性,补齐 `Query` / `Move` / `Swap` / `BatchMove` / `Verify` 等行为及约束校验 - 🛡️ 提升解析鲁棒性,支持从代码块或混杂文本中提取首个 JSON 对象,并增加单次解析重试机制 - 👀 增强可观测性,补充 `debug raw` 阶段输出与分片透传能力 - ✍️ 优化提示词近端约束,将严格 JSON 输出协议追加到各节点 `userPrompt` 末尾 - 🚧 备注:当前链路仍处于持续调优阶段,稳定性与可用性仍需进一步验证
This commit is contained in:
93
backend/agent/schedulerefine/graph.go
Normal file
93
backend/agent/schedulerefine/graph.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package schedulerefine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
)
|
||||
|
||||
const (
|
||||
graphNodeContract = "schedule_refine_contract"
|
||||
graphNodeReact = "schedule_refine_react"
|
||||
graphNodeHardCheck = "schedule_refine_hard_check"
|
||||
graphNodeSummary = "schedule_refine_summary"
|
||||
)
|
||||
|
||||
// ScheduleRefineGraphRunInput 是“连续微调图”运行参数。
|
||||
//
|
||||
// 字段语义:
|
||||
// 1. Model:本轮图运行使用的聊天模型。
|
||||
// 2. State:预先注入的微调状态(通常来自上一版预览快照)。
|
||||
// 3. EmitStage:SSE 阶段回调,允许服务层把阶段进度透传给前端。
|
||||
type ScheduleRefineGraphRunInput struct {
|
||||
Model *ark.ChatModel
|
||||
State *ScheduleRefineState
|
||||
EmitStage func(stage, detail string)
|
||||
}
|
||||
|
||||
// RunScheduleRefineGraph 执行“连续微调”独立图链路。
|
||||
//
|
||||
// 链路顺序:
|
||||
// START -> contract -> react -> hard_check -> summary -> END
|
||||
//
|
||||
// 设计说明:
|
||||
// 1. 当前链路采用线性图,确保可读性优先;
|
||||
// 2. “终审失败后单次修复”在 hard_check 节点内部闭环处理,避免图连线分叉过多;
|
||||
// 3. 若后续需要引入多分支策略(例如大改动转重排),可在 contract 后追加 branch 节点。
|
||||
func RunScheduleRefineGraph(ctx context.Context, input ScheduleRefineGraphRunInput) (*ScheduleRefineState, error) {
|
||||
if input.Model == nil {
|
||||
return nil, fmt.Errorf("schedule refine graph: model is nil")
|
||||
}
|
||||
if input.State == nil {
|
||||
return nil, fmt.Errorf("schedule refine graph: state is nil")
|
||||
}
|
||||
|
||||
emitStage := func(stage, detail string) {
|
||||
if input.EmitStage != nil {
|
||||
input.EmitStage(stage, detail)
|
||||
}
|
||||
}
|
||||
runner := newScheduleRefineRunner(input.Model, emitStage)
|
||||
|
||||
graph := compose.NewGraph[*ScheduleRefineState, *ScheduleRefineState]()
|
||||
if err := graph.AddLambdaNode(graphNodeContract, compose.InvokableLambda(runner.contractNode)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.AddLambdaNode(graphNodeReact, compose.InvokableLambda(runner.reactNode)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.AddLambdaNode(graphNodeHardCheck, compose.InvokableLambda(runner.hardCheckNode)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.AddLambdaNode(graphNodeSummary, compose.InvokableLambda(runner.summaryNode)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := graph.AddEdge(compose.START, graphNodeContract); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.AddEdge(graphNodeContract, graphNodeReact); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.AddEdge(graphNodeReact, graphNodeHardCheck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.AddEdge(graphNodeHardCheck, graphNodeSummary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.AddEdge(graphNodeSummary, compose.END); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runnable, err := graph.Compile(ctx,
|
||||
compose.WithGraphName("ScheduleRefineGraph"),
|
||||
compose.WithMaxRunSteps(12),
|
||||
compose.WithNodeTriggerMode(compose.AnyPredecessor),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return runnable.Invoke(ctx, input.State)
|
||||
}
|
||||
1690
backend/agent/schedulerefine/nodes.go
Normal file
1690
backend/agent/schedulerefine/nodes.go
Normal file
File diff suppressed because it is too large
Load Diff
190
backend/agent/schedulerefine/prompt.go
Normal file
190
backend/agent/schedulerefine/prompt.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package schedulerefine
|
||||
|
||||
const (
|
||||
// contractPrompt 用于“微调契约抽取”节点。
|
||||
//
|
||||
// 目标:
|
||||
// 1. 把用户自然语言微调请求收敛成结构化契约;
|
||||
// 2. 明确是否需要“保持相对顺序不变”;
|
||||
// 3. 严格输出 JSON,降低解析抖动。
|
||||
contractPrompt = `你是 SmartFlow 的排程微调契约分析器。
|
||||
你会收到:当前时间、用户本轮微调请求、已有排程摘要。
|
||||
你的任务是把“用户真正想改什么”转成结构化契约。
|
||||
|
||||
请只输出 JSON,不要 markdown,不要解释,字段如下:
|
||||
{
|
||||
"intent": "一句话概括用户本轮微调目标",
|
||||
"strategy": "local_adjust|keep",
|
||||
"hard_requirements": ["必须满足的硬性要求1","硬性要求2"],
|
||||
"keep_relative_order": true,
|
||||
"order_scope": "global|week",
|
||||
"reason": "简短中文原因,<=40字"
|
||||
}
|
||||
|
||||
规则:
|
||||
1) 当用户表达“保持原顺序/不打乱顺序/按原顺序推进”时,keep_relative_order=true。
|
||||
2) 若用户没有提顺序要求,keep_relative_order=false,order_scope 固定输出 "global"。
|
||||
3) strategy=keep 仅用于“无需改动”的情况;只要要移动任务,就输出 local_adjust。
|
||||
4) hard_requirements 要可验证,避免空话。`
|
||||
|
||||
// plannerPrompt 用于“Plan-and-Execute”的规划阶段。
|
||||
//
|
||||
// 目标:
|
||||
// 1. 让模型按当前请求自动规划“先取证再动作”的执行路径;
|
||||
// 2. 规划结果要求结构化,便于执行阶段直接引用;
|
||||
// 3. 不在 Planner 阶段执行工具,只负责产出计划。
|
||||
plannerPrompt = `你是 SmartFlow 的排程微调规划器(Planner)。
|
||||
你会收到:用户请求、契约、最近动作日志与观察。
|
||||
你的职责是生成“下一阶段的执行计划”,而不是直接执行工具。
|
||||
|
||||
只输出 JSON:
|
||||
{
|
||||
"summary": "本轮计划一句话",
|
||||
"steps": ["步骤1","步骤2","步骤3"],
|
||||
"success_signals": ["满足什么算成功1","成功2"],
|
||||
"fallback": "若连续失败,准备怎么改道"
|
||||
}
|
||||
|
||||
规则:
|
||||
1. steps 请优先采用“先取证后动作”的路径:例如 QueryTargetTasks / QueryAvailableSlots / BatchMove / Move / Swap / Verify。
|
||||
2. steps 保持 3~4 条,单条不超过 26 字。
|
||||
3. summary 不超过 36 字;fallback 不超过 30 字;success_signals 最多 3 条。
|
||||
4. 严禁输出半截 JSON;若信息过多,请精简而不是展开解释。
|
||||
5. 不要输出 markdown,不要输出额外文本。`
|
||||
|
||||
// reactPrompt 用于“强 ReAct 微调循环”节点。
|
||||
//
|
||||
// 目标:
|
||||
// 1. 每轮先输出“计划 -> 缺口 -> 工具动作”(不承担执行后反思);
|
||||
// 2. 每轮最多一个 tool_call,但支持 BatchMove 在一个调用里原子执行多步;
|
||||
// 3. 明确遵守顺序硬约束与 existing 不可改约束。
|
||||
reactPrompt = `你是 SmartFlow 的排程微调执行器,采用“走一步看一步”的 ReAct 风格。
|
||||
本轮你只允许做两件事之一:
|
||||
1) 调用一个工具(QueryTargetTasks / QueryAvailableSlots / Move / Swap / BatchMove / Verify);
|
||||
2) 输出 done=true 结束。
|
||||
|
||||
你将收到 3 个关键输入:
|
||||
1) LAST_TOOL_RESULT:上一轮工具结果(结构化 JSON);
|
||||
2) LAST_TOOL_OBSERVATION:上一轮完整观察(包含 tool_name/tool_params/tool_success/tool_error_code/tool_result);
|
||||
3) LAST_FAILED_CALL_SIGNATURE:上一轮失败动作签名(tool+params)。
|
||||
|
||||
硬约束:
|
||||
1. 每轮最多 1 个 tool_call。
|
||||
2. 只能修改 status="suggested" 的任务,禁止修改 existing。
|
||||
3. 如果合同中 keep_relative_order=true,任何动作都不能打乱任务原始相对顺序。
|
||||
4. 如果当前方案已满足目标,直接 done=true,不要多余动作。
|
||||
5. day_of_week 数值映射必须严格按:1周一,2周二,3周三,4周四,5周五,6周六,7周日。
|
||||
6. 若上一轮 tool_success=false,你必须先根据 tool_error_code 调整策略,再给新动作。
|
||||
7. 禁止重复上一轮失败动作(tool 与 params 完全一致);若重复会被后端拒绝执行并记为失败轮次。
|
||||
|
||||
你必须只输出 JSON,字段如下:
|
||||
{
|
||||
"done": false,
|
||||
"summary": "",
|
||||
"goal_check": "本轮先检查什么",
|
||||
"decision": "本轮为什么这样决策",
|
||||
"missing_info": ["如果缺信息就在这里写;不缺则返回空数组"],
|
||||
"reflect": "本轮计划备注(动作前,不是执行后复盘)",
|
||||
"tool_calls": [
|
||||
{
|
||||
"tool": "QueryTargetTasks|QueryAvailableSlots|Move|Swap|BatchMove|Verify",
|
||||
"params": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
补充规则:
|
||||
1. 若 done=true,则 tool_calls 必须是空数组。
|
||||
2. 若 done=false 且有动作,tool_calls 必须只有一个元素。
|
||||
3. QueryTargetTasks 用于“先定位要改哪些任务”,禁止直接猜。
|
||||
4. QueryAvailableSlots 用于“先看可用空位”,禁止凭直觉盲移。
|
||||
5. Move 参数优先使用标准字段:task_item_id,to_week,to_day,to_section_from,to_section_to。
|
||||
6. BatchMove 参数格式必须是:{"moves":[{Move参数1},{Move参数2},...]},后端会按顺序原子执行;任一步失败则整批回滚。
|
||||
7. Verify 是终止前自检工具:done=true 前建议先执行一次 Verify。
|
||||
8. reflect 只描述“本轮计划备注”,不要把未执行的动作写成已完成事实。
|
||||
9. 为保证 JSON 稳定可解析,请控制长度:goal_check<=50字、decision<=90字、reflect<=80字、summary<=60字、missing_info 最多3条。
|
||||
10. 你必须显式说明“上一轮失败原因如何影响本轮决策”(写在 decision 里)。
|
||||
11. 不要输出代码块,不要输出额外文本。`
|
||||
|
||||
// postReflectPrompt 用于“动作执行后真反思”节点。
|
||||
//
|
||||
// 目标:
|
||||
// 1. 基于后端返回的真实工具结果做复盘,而不是动作前预期;
|
||||
// 2. 输出下一轮可执行的改进策略,驱动真正的 Observe -> Think;
|
||||
// 3. 严格输出 JSON,供后端稳定解析并透传 stage。
|
||||
postReflectPrompt = `你是 SmartFlow 的 ReAct 复盘器。
|
||||
你会收到:本轮工具调用参数、后端真实执行结果、上一轮上下文。
|
||||
请基于“真实结果”复盘,不要把失败说成成功。
|
||||
|
||||
只输出 JSON:
|
||||
{
|
||||
"reflection": "本轮发生了什么(基于真实结果)",
|
||||
"next_strategy": "下一轮建议如何改(具体到换时段/换工具/保持)",
|
||||
"should_stop": false,
|
||||
"stop_reason": "若应结束,给简短原因"
|
||||
}
|
||||
|
||||
规则:
|
||||
1. tool_success=false 时,reflection 必须明确失败原因(优先引用 error_code)。
|
||||
2. 若 error_code=ORDER_VIOLATION/SLOT_CONFLICT/REPEAT_FAILED_ACTION,next_strategy 必须给出“如何避开同类失败”。
|
||||
3. should_stop=true 仅在“目标已满足”或“继续动作收益很低”时使用。
|
||||
4. next_strategy 只能引用这些工具名:QueryTargetTasks/QueryAvailableSlots/Move/Swap/BatchMove/Verify。
|
||||
5. 不要输出 markdown,不要输出额外文本。`
|
||||
|
||||
// reviewPrompt 用于“终审语义校验”节点。
|
||||
//
|
||||
// 目标:
|
||||
// 1. 检查方案是否满足用户本轮请求;
|
||||
// 2. 给出未满足项列表,供一次修复动作使用;
|
||||
// 3. 输出结构化 JSON,避免校验结果歧义。
|
||||
reviewPrompt = `你是 SmartFlow 的终审校验器。
|
||||
请判断“当前排程”是否满足“本轮用户微调请求 + 契约硬要求”。
|
||||
只输出 JSON:
|
||||
{
|
||||
"pass": true,
|
||||
"reason": "中文简短结论",
|
||||
"unmet": ["若不满足,这里列未满足点"]
|
||||
}
|
||||
|
||||
要求:
|
||||
1. pass=true 时,unmet 必须为空数组。
|
||||
2. pass=false 时,reason 必须给出核心差距。`
|
||||
|
||||
// summaryPrompt 用于“最终回复润色”节点。
|
||||
//
|
||||
// 目标:
|
||||
// 1. 给用户返回自然语言总结;
|
||||
// 2. 体现“做了什么调整 + 为什么这样改”;
|
||||
// 3. 若终审仍有缺口,也要诚实说明。
|
||||
summaryPrompt = `你是 SmartFlow 的排程结果解读助手。
|
||||
请基于输入输出 2~4 句自然中文总结:
|
||||
1) 先说本轮改了什么;
|
||||
2) 再说这样改的收益;
|
||||
3) 如果终审未完全通过,要明确说明还差什么。
|
||||
不要输出 JSON。`
|
||||
|
||||
// repairPrompt 用于“终审失败后的单次修复”节点。
|
||||
//
|
||||
// 目标:
|
||||
// 1. 在不重跑全链路的前提下做一次局部补救;
|
||||
// 2. 强制只输出一个工具调用,避免再次拉长思考。
|
||||
repairPrompt = `你是 SmartFlow 的修复执行器。
|
||||
当前方案未通过终审,请根据“未满足点”只做一次修复动作。
|
||||
只允许输出一个 tool_call(Move 或 Swap),不允许 done。
|
||||
|
||||
输出格式(严格 JSON):
|
||||
{
|
||||
"done": false,
|
||||
"summary": "",
|
||||
"goal_check": "本轮修复目标",
|
||||
"decision": "修复决策依据",
|
||||
"missing_info": [],
|
||||
"reflect": "修复动作后的预期",
|
||||
"tool_calls": [
|
||||
{
|
||||
"tool": "Move|Swap",
|
||||
"params": {}
|
||||
}
|
||||
]
|
||||
}`
|
||||
)
|
||||
41
backend/agent/schedulerefine/runner.go
Normal file
41
backend/agent/schedulerefine/runner.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package schedulerefine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
)
|
||||
|
||||
// scheduleRefineRunner 是“单次图运行”的请求级依赖容器。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责收口模型与阶段回调,避免 graph.go 出现大量闭包;
|
||||
// 2. 负责把节点函数适配为统一签名;
|
||||
// 3. 不负责分支决策(当前链路为线性图)。
|
||||
type scheduleRefineRunner struct {
|
||||
chatModel *ark.ChatModel
|
||||
emitStage func(stage, detail string)
|
||||
}
|
||||
|
||||
func newScheduleRefineRunner(chatModel *ark.ChatModel, emitStage func(stage, detail string)) *scheduleRefineRunner {
|
||||
return &scheduleRefineRunner{
|
||||
chatModel: chatModel,
|
||||
emitStage: emitStage,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *scheduleRefineRunner) contractNode(ctx context.Context, st *ScheduleRefineState) (*ScheduleRefineState, error) {
|
||||
return runContractNode(ctx, r.chatModel, st, r.emitStage)
|
||||
}
|
||||
|
||||
func (r *scheduleRefineRunner) reactNode(ctx context.Context, st *ScheduleRefineState) (*ScheduleRefineState, error) {
|
||||
return runReactLoopNode(ctx, r.chatModel, st, r.emitStage)
|
||||
}
|
||||
|
||||
func (r *scheduleRefineRunner) hardCheckNode(ctx context.Context, st *ScheduleRefineState) (*ScheduleRefineState, error) {
|
||||
return runHardCheckNode(ctx, r.chatModel, st, r.emitStage)
|
||||
}
|
||||
|
||||
func (r *scheduleRefineRunner) summaryNode(ctx context.Context, st *ScheduleRefineState) (*ScheduleRefineState, error) {
|
||||
return runSummaryNode(ctx, r.chatModel, st, r.emitStage)
|
||||
}
|
||||
308
backend/agent/schedulerefine/state.go
Normal file
308
backend/agent/schedulerefine/state.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package schedulerefine
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
)
|
||||
|
||||
const (
|
||||
// timezoneName 固定排程链路使用的业务时区,避免容器默认时区导致“明天/今晚”偏移。
|
||||
timezoneName = "Asia/Shanghai"
|
||||
// datetimeLayout 统一使用分钟级时间文本,方便模型理解与日志比对。
|
||||
datetimeLayout = "2006-01-02 15:04"
|
||||
// defaultPlanMax 是 Planner 最大调用次数(包含首次规划 + 重规划)。
|
||||
defaultPlanMax = 2
|
||||
// defaultExecuteMax 是执行阶段最大工具动作轮次。
|
||||
defaultExecuteMax = 16
|
||||
// defaultReplanMax 是执行阶段允许触发的重规划次数上限。
|
||||
defaultReplanMax = 2
|
||||
// defaultRepairReserve 表示为“终审修复”保留的最小动作预算。
|
||||
defaultRepairReserve = 1
|
||||
)
|
||||
|
||||
// RefineContract 表示“微调意图契约”。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责承载“本轮微调到底要满足什么”的结构化目标;
|
||||
// 2. 负责给后续 ReAct 动作与终审硬校验提供统一语义;
|
||||
// 3. 不负责实际排程修改动作执行(动作由工具层负责)。
|
||||
type RefineContract struct {
|
||||
Intent string `json:"intent"`
|
||||
Strategy string `json:"strategy"`
|
||||
HardRequirements []string `json:"hard_requirements"`
|
||||
KeepRelativeOrder bool `json:"keep_relative_order"`
|
||||
OrderScope string `json:"order_scope"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// HardCheckReport 表示“终审硬校验报告”。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 记录规则层(物理冲突)是否通过;
|
||||
// 2. 记录语义层(是否满足用户要求)是否通过;
|
||||
// 3. 记录顺序层(是否保持相对顺序)是否通过;
|
||||
// 4. 记录失败原因与修复尝试信息,便于后续持续优化 prompt;
|
||||
// 5. 不负责直接决定是否落库(落库决策仍由服务层控制)。
|
||||
type HardCheckReport struct {
|
||||
PhysicsPassed bool `json:"physics_passed"`
|
||||
PhysicsIssues []string `json:"physics_issues,omitempty"`
|
||||
|
||||
IntentPassed bool `json:"intent_passed"`
|
||||
IntentReason string `json:"intent_reason,omitempty"`
|
||||
IntentUnmet []string `json:"intent_unmet,omitempty"`
|
||||
|
||||
OrderPassed bool `json:"order_passed"`
|
||||
OrderIssues []string `json:"order_issues,omitempty"`
|
||||
|
||||
RepairTried bool `json:"repair_tried"`
|
||||
}
|
||||
|
||||
// ReactRoundObservation 用于沉淀“每轮 ReAct 的可见观测信息”。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责记录每轮“计划 -> 动作 -> 观察 -> 反思”的关键信息;
|
||||
// 2. 既用于 SSE 透传,也用于下一轮 prompt 的上下文回灌;
|
||||
// 3. 不承担排程真实数据存储职责(真实排程仍在 HybridEntries)。
|
||||
type ReactRoundObservation struct {
|
||||
Round int `json:"round"`
|
||||
GoalCheck string `json:"goal_check,omitempty"`
|
||||
Decision string `json:"decision,omitempty"`
|
||||
MissingInfo []string `json:"missing_info,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
ToolParams map[string]any `json:"tool_params,omitempty"`
|
||||
ToolSuccess bool `json:"tool_success"`
|
||||
ToolErrorCode string `json:"tool_error_code,omitempty"`
|
||||
ToolResult string `json:"tool_result,omitempty"`
|
||||
Reflect string `json:"reflect,omitempty"`
|
||||
}
|
||||
|
||||
// PlannerPlan 表示“本轮执行前的结构化计划”。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责记录模型当前建议的执行路径(先查什么、再做什么);
|
||||
// 2. 负责在失败重规划后替换为新版本,供执行器下一轮参考;
|
||||
// 3. 不直接约束工具执行结果(执行合法性仍由工具层硬校验负责)。
|
||||
type PlannerPlan struct {
|
||||
Summary string `json:"summary"`
|
||||
Steps []string `json:"steps,omitempty"`
|
||||
SuccessSignals []string `json:"success_signals,omitempty"`
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
}
|
||||
|
||||
// ScheduleRefineState 是“连续微调图”的统一状态容器。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责在图节点间传递“上一版排程快照 + 本轮用户微调请求 + 动作日志 + 终审报告”;
|
||||
// 2. 负责承载最终对用户可见的 summary 与结构化 candidate_plans;
|
||||
// 3. 不负责 Redis/MySQL 读写(持久化由 service 层负责)。
|
||||
type ScheduleRefineState struct {
|
||||
// 1. 基础请求上下文。
|
||||
TraceID string
|
||||
UserID int
|
||||
ConversationID string
|
||||
UserMessage string
|
||||
RequestNow time.Time
|
||||
RequestNowText string
|
||||
|
||||
// 2. 继承自上一版预览快照的可调度数据。
|
||||
TaskClassIDs []int
|
||||
Constraints []string
|
||||
HybridEntries []model.HybridScheduleEntry
|
||||
AllocatedItems []model.TaskClassItem
|
||||
CandidatePlans []model.UserWeekSchedule
|
||||
|
||||
// 3. 本轮微调过程状态。
|
||||
UserIntent string
|
||||
Contract RefineContract
|
||||
|
||||
PlanMax int
|
||||
ExecuteMax int
|
||||
ReplanMax int
|
||||
|
||||
PlanUsed int
|
||||
ReplanUsed int
|
||||
|
||||
// MaxRounds 保留“总预算”语义,供终审修复节点继续复用:
|
||||
// MaxRounds = ExecuteMax + RepairReserve
|
||||
MaxRounds int
|
||||
RepairReserve int
|
||||
RoundUsed int
|
||||
ActionLogs []string
|
||||
|
||||
// ConsecutiveFailures 记录执行阶段连续失败次数,用于触发“失败兜底 thinking”。
|
||||
ConsecutiveFailures int
|
||||
// ThinkingBoostArmed 表示“当前失败串已触发过一次 thinking 兜底”。
|
||||
ThinkingBoostArmed bool
|
||||
|
||||
LastToolResult string
|
||||
ObservationHistory []ReactRoundObservation
|
||||
CurrentPlan PlannerPlan
|
||||
LastPostStrategy string
|
||||
// LastFailedCallSignature 记录“上一轮失败动作签名(tool+params)”。用于后端硬拦截重复失败动作。
|
||||
LastFailedCallSignature string
|
||||
OriginOrderMap map[int]int
|
||||
|
||||
// 4. 终审校验状态。
|
||||
HardCheck HardCheckReport
|
||||
|
||||
// 5. 最终输出。
|
||||
FinalSummary string
|
||||
Completed bool
|
||||
}
|
||||
|
||||
// NewScheduleRefineState 基于“上一版排程预览快照”初始化连续微调状态。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 先初始化请求基础字段与默认预算,保证图内每个节点都能读取到稳定上下文。
|
||||
// 2. 再把 preview 的核心排程数据做深拷贝注入,避免跨请求引用污染。
|
||||
// 3. 最后构建 origin_order_map,作为“保持相对顺序”硬约束的判定基线。
|
||||
// 4. 若 preview 为空,仍返回可用 state,由上层决定是报错还是降级。
|
||||
func NewScheduleRefineState(traceID string, userID int, conversationID string, userMessage string, preview *model.SchedulePlanPreviewCache) *ScheduleRefineState {
|
||||
now := nowToMinute()
|
||||
st := &ScheduleRefineState{
|
||||
TraceID: strings.TrimSpace(traceID),
|
||||
UserID: userID,
|
||||
ConversationID: strings.TrimSpace(conversationID),
|
||||
UserMessage: strings.TrimSpace(userMessage),
|
||||
RequestNow: now,
|
||||
RequestNowText: now.In(loadLocation()).Format(datetimeLayout),
|
||||
PlanMax: defaultPlanMax,
|
||||
ExecuteMax: defaultExecuteMax,
|
||||
ReplanMax: defaultReplanMax,
|
||||
RepairReserve: defaultRepairReserve,
|
||||
MaxRounds: defaultExecuteMax + defaultRepairReserve,
|
||||
ActionLogs: make([]string, 0, 24),
|
||||
ObservationHistory: make([]ReactRoundObservation, 0, 16),
|
||||
OriginOrderMap: make(map[int]int),
|
||||
CurrentPlan: PlannerPlan{
|
||||
Summary: "初始化完成,等待 Planner 生成执行计划。",
|
||||
},
|
||||
}
|
||||
if preview == nil {
|
||||
return st
|
||||
}
|
||||
|
||||
st.TaskClassIDs = append([]int(nil), preview.TaskClassIDs...)
|
||||
st.HybridEntries = cloneHybridEntries(preview.HybridEntries)
|
||||
st.AllocatedItems = cloneTaskClassItems(preview.AllocatedItems)
|
||||
st.CandidatePlans = cloneWeekSchedules(preview.CandidatePlans)
|
||||
st.OriginOrderMap = buildOriginOrderMap(st.HybridEntries)
|
||||
return st
|
||||
}
|
||||
|
||||
// loadLocation 返回排程链路使用的业务时区。
|
||||
func loadLocation() *time.Location {
|
||||
loc, err := time.LoadLocation(timezoneName)
|
||||
if err != nil {
|
||||
return time.Local
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
||||
// nowToMinute 返回当前时刻并截断到分钟级,降低 prompt 中秒级噪声。
|
||||
func nowToMinute() time.Time {
|
||||
return time.Now().In(loadLocation()).Truncate(time.Minute)
|
||||
}
|
||||
|
||||
// cloneHybridEntries 深拷贝混合日程切片。
|
||||
func cloneHybridEntries(src []model.HybridScheduleEntry) []model.HybridScheduleEntry {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make([]model.HybridScheduleEntry, len(src))
|
||||
copy(dst, src)
|
||||
return dst
|
||||
}
|
||||
|
||||
// cloneTaskClassItems 深拷贝任务块切片(包含指针字段)。
|
||||
func cloneTaskClassItems(src []model.TaskClassItem) []model.TaskClassItem {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make([]model.TaskClassItem, 0, len(src))
|
||||
for _, item := range src {
|
||||
copied := item
|
||||
if item.CategoryID != nil {
|
||||
v := *item.CategoryID
|
||||
copied.CategoryID = &v
|
||||
}
|
||||
if item.Order != nil {
|
||||
v := *item.Order
|
||||
copied.Order = &v
|
||||
}
|
||||
if item.Content != nil {
|
||||
v := *item.Content
|
||||
copied.Content = &v
|
||||
}
|
||||
if item.Status != nil {
|
||||
v := *item.Status
|
||||
copied.Status = &v
|
||||
}
|
||||
if item.EmbeddedTime != nil {
|
||||
t := *item.EmbeddedTime
|
||||
copied.EmbeddedTime = &t
|
||||
}
|
||||
dst = append(dst, copied)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// cloneWeekSchedules 深拷贝周视图切片。
|
||||
func cloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make([]model.UserWeekSchedule, 0, len(src))
|
||||
for _, week := range src {
|
||||
eventsCopy := make([]model.WeeklyEventBrief, len(week.Events))
|
||||
copy(eventsCopy, week.Events)
|
||||
dst = append(dst, model.UserWeekSchedule{
|
||||
Week: week.Week,
|
||||
Events: eventsCopy,
|
||||
})
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// buildOriginOrderMap 从当前 suggested 排程位置构建“初始相对顺序映射”。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 先筛出所有可调的 suggested 任务;
|
||||
// 2. 按 week/day/section/task_item_id 稳定排序,得到“时间先后基线”;
|
||||
// 3. 把 task_item_id -> rank 写入 map,后续 Move/Swap 都基于该 rank 做顺序硬校验。
|
||||
func buildOriginOrderMap(entries []model.HybridScheduleEntry) map[int]int {
|
||||
orderMap := make(map[int]int)
|
||||
if len(entries) == 0 {
|
||||
return orderMap
|
||||
}
|
||||
suggested := make([]model.HybridScheduleEntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Status == "suggested" && entry.TaskItemID > 0 {
|
||||
suggested = append(suggested, entry)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(suggested, func(i, j int) bool {
|
||||
left := suggested[i]
|
||||
right := suggested[j]
|
||||
if left.Week != right.Week {
|
||||
return left.Week < right.Week
|
||||
}
|
||||
if left.DayOfWeek != right.DayOfWeek {
|
||||
return left.DayOfWeek < right.DayOfWeek
|
||||
}
|
||||
if left.SectionFrom != right.SectionFrom {
|
||||
return left.SectionFrom < right.SectionFrom
|
||||
}
|
||||
if left.SectionTo != right.SectionTo {
|
||||
return left.SectionTo < right.SectionTo
|
||||
}
|
||||
return left.TaskItemID < right.TaskItemID
|
||||
})
|
||||
for idx, entry := range suggested {
|
||||
orderMap[entry.TaskItemID] = idx + 1
|
||||
}
|
||||
return orderMap
|
||||
}
|
||||
1187
backend/agent/schedulerefine/tool.go
Normal file
1187
backend/agent/schedulerefine/tool.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user