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:
Losita
2026-03-22 22:38:51 +08:00
parent e5b27df80d
commit 525a8b32cb
12 changed files with 3809 additions and 100 deletions

View File

@@ -16,45 +16,47 @@ import (
)
const (
// ControlTimeout 是“模型控制码分流”这一步的额外超时预算。
//
// 约束说明:
// 1. 设为 0 代表完全继承父 ctx 的 deadline不额外截断
// 2. 若后续线上观测到分流偶发超时,可再加一个小预算(例如 2s做隔离。
// ControlTimeout 表示“路由控制码”阶段的额外超时预算。
// 说明:
// 1. 设为 0 表示完全继承父 ctx 的 deadline不额外截断。
// 2. 若后续观察到路由阶段偶发超时,可按需配置一个小预算(例如 2s
ControlTimeout = 0 * time.Second
)
var (
// routeHeaderRegex 用于解析控制码头部。
//
// 支持动作:
// 1. quick_note_create新增随口记任务
// 2. task_query查询任务;
// 3. schedule_plan:智能排程(生成/微调排程计划);
// 4. chat普通聊天
// 5. quick_note:历史兼容别名,解析后映射到 quick_note_create。
routeHeaderRegex = regexp.MustCompile(`(?is)<\s*smartflow_route\b[^>]*\bnonce\s*=\s*["']?([a-zA-Z0-9\-]+)["']?[^>]*\baction\s*=\s*["']?(quick_note_create|task_query|schedule_plan|quick_note|chat)["']?[^>]*>`)
// routeReasonRegex 用于提取可选的理由块,方便日志排障
// 1. quick_note_create新增随口记任务
// 2. task_query任务查询。
// 3. schedule_plan_create新建排程。
// 4. schedule_plan_refine连续对话微调排程。
// 5. schedule_plan:历史兼容动作(解析后映射到 schedule_plan_create
// 6. quick_note历史兼容动作解析后映射到 quick_note_create
// 7. chat普通聊天
routeHeaderRegex = regexp.MustCompile(`(?is)<\s*smartflow_route\b[^>]*\bnonce\s*=\s*["']?([a-zA-Z0-9\-]+)["']?[^>]*\baction\s*=\s*["']?(quick_note_create|task_query|schedule_plan_create|schedule_plan_refine|schedule_plan|quick_note|chat)["']?[^>]*>`)
// routeReasonRegex 用于提取可选 reason便于日志排障。
routeReasonRegex = regexp.MustCompile(`(?is)<\s*smartflow_reason\s*>(.*?)<\s*/\s*smartflow_reason\s*>`)
)
const routeControlPrompt = `你是 SmartFlow 的请求分流控制器。
你的唯一任务是给后端返回可机读控制码”,不要做用户可见回复,不要解释。
你的唯一任务是给后端返回可机读控制码”,不要做用户可见回复,不要解释。
动作定义:
1) quick_note_create用户明确希望”记录/安排/提醒某件未来要做的事”。
2) task_query用户想”查看/筛选/排序/获取”已有任务如最紧急、按DDL、某象限、关键词
3) schedule_plan:用户想”生成/调整/微调日程排程”(如”帮我排个学习计划”、”把早八的课调走”、”我不想周末学习”)
4) chat其余全部普通对话包括闲聊、知识问答、纯讨论”怎么安排任务”但未要求你真的去操作)。
1) quick_note_create用户明确要“帮我记一下/安排一个未来要做的事/提醒我”。
2) task_query用户要“查任务、筛任务、按条件列任务”
3) schedule_plan_create用户要“新建/生成一份排程方案”
4) schedule_plan_refine用户要“基于已有排程做连续微调”如挪动某天、限制某时段、局部改动)。
5) chat其余普通聊天与讨论。
判定优先级(冲突时按顺序):
1) 若句子核心诉求是”帮我记一件事”,选 quick_note_create
2) 若核心诉求是”帮我查任务列表/某类任务”,选 task_query
3) 若核心诉求是”帮我排日程/调整日程/生成学习计划/修改排程”,选 schedule_plan
4) 其他情况选 chat
优先级(冲突时按顺序):
1) quick_note_create
2) task_query
3) schedule_plan_refine
4) schedule_plan_create
5) chat
输出格式必须严格如下(两行):
<SMARTFLOW_ROUTE nonce=给定nonce action=quick_note_create|task_query|schedule_plan|chat></SMARTFLOW_ROUTE>
<SMARTFLOW_ROUTE nonce="给定nonce" action="quick_note_create|task_query|schedule_plan_create|schedule_plan_refine|chat"></SMARTFLOW_ROUTE>
<SMARTFLOW_REASON>一句不超过30字的中文理由</SMARTFLOW_REASON>
禁止输出任何其他内容。`
@@ -63,16 +65,20 @@ const routeControlPrompt = `你是 SmartFlow 的请求分流控制器。
type Action string
const (
ActionChat Action = "chat"
ActionQuickNoteCreate Action = "quick_note_create"
ActionTaskQuery Action = "task_query"
ActionSchedulePlan Action = "schedule_plan"
ActionChat Action = "chat"
ActionQuickNoteCreate Action = "quick_note_create"
ActionTaskQuery Action = "task_query"
ActionSchedulePlanCreate Action = "schedule_plan_create"
ActionSchedulePlanRefine Action = "schedule_plan_refine"
// ActionQuickNote 是历史兼容别名,只用于解析旧 action 值。
// ActionSchedulePlan 是历史兼容动作值。
// 说明:旧模型可能返回 schedule_plan解析后统一映射到 schedule_plan_create。
ActionSchedulePlan Action = "schedule_plan"
// ActionQuickNote 是历史兼容动作值,解析后统一映射到 quick_note_create。
ActionQuickNote Action = "quick_note"
)
// ControlDecision “模型控制码解析结果”。
// ControlDecision 表示“模型控制码解析结果”。
type ControlDecision struct {
Action Action
Reason string
@@ -80,34 +86,26 @@ type ControlDecision struct {
}
// RoutingDecision 是服务层使用的统一分流结果。
//
// 职责边界:
// 1. Action最终动作chat/quick_note_create/task_query
// 2. TrustRoute是否允许下游跳过二次意图判定
// 1. Action最终动作chat/quick_note_create/task_query/schedule_plan_create/schedule_plan_refine
// 2. TrustRoute是否允许下游跳过二次意图判定
// 3. Detail可选说明用于阶段提示或日志。
// 4. RouteFailed标记“控制码路由是否失败”供上层决定是否直接报错。
type RoutingDecision struct {
Action Action
TrustRoute bool
Detail string
// RouteFailed 标记“控制码路由是否失败”。
//
// 语义:
// 1. true路由阶段发生异常模型调用失败、控制码解析失败等
// 2. false路由阶段正常完成无论最终 action 是 chat 还是其它分支)。
//
// 说明:
// 1. 该字段用于让上层决定“是否直接报错而不是回落聊天”;
// 2. 历史行为是失败回落 chat本字段用于支持新的“失败即报错”策略。
Action Action
TrustRoute bool
Detail string
RouteFailed bool
}
// DecideActionRouting 通过“模型控制码”决定本次请求走向。
//
// 返回语义:
// 1. Action=quick_note_create进入随口记写入图;
// 2. Action=task_query进入任务查询 tool-calling
// 3. Action=chat进入普通聊天流;
// 4. 路由失败时会标记 RouteFailed=true由上层直接返回内部错误
// 1. Action=quick_note_create进入随口记链路。
// 2. Action=task_query进入任务查询链路。
// 3. Action=schedule_plan_create:进入新建排程链路。
// 4. Action=schedule_plan_refine进入连续微调链路
// 5. Action=chat进入普通聊天链路。
// 6. 路由失败时标记 RouteFailed=true由上层统一处理。
func DecideActionRouting(ctx context.Context, selectedModel *ark.ChatModel, userMessage string) RoutingDecision {
decision, err := routeByModelControlTag(ctx, selectedModel, userMessage)
if err != nil {
@@ -132,50 +130,30 @@ func DecideActionRouting(ctx context.Context, selectedModel *ark.ChatModel, user
if reason == "" {
reason = "识别到新增任务请求,准备执行随口记流程。"
}
return RoutingDecision{
Action: ActionQuickNoteCreate,
TrustRoute: true,
Detail: reason,
RouteFailed: false,
}
return RoutingDecision{Action: ActionQuickNoteCreate, TrustRoute: true, Detail: reason, RouteFailed: false}
case ActionTaskQuery:
reason := strings.TrimSpace(decision.Reason)
if reason == "" {
reason = "识别到任务查询请求,准备调用任务查询工具。"
reason = "识别到任务查询请求,准备执行任务查询流程。"
}
return RoutingDecision{
Action: ActionTaskQuery,
TrustRoute: true,
Detail: reason,
RouteFailed: false,
}
case ActionSchedulePlan:
return RoutingDecision{Action: ActionTaskQuery, TrustRoute: true, Detail: reason, RouteFailed: false}
case ActionSchedulePlanCreate:
reason := strings.TrimSpace(decision.Reason)
if reason == "" {
reason = "识别到排程请求,准备执行智能排程流程。"
reason = "识别到新建排程请求,准备执行智能排程流程。"
}
return RoutingDecision{
Action: ActionSchedulePlan,
TrustRoute: true,
Detail: reason,
RouteFailed: false,
return RoutingDecision{Action: ActionSchedulePlanCreate, TrustRoute: true, Detail: reason, RouteFailed: false}
case ActionSchedulePlanRefine:
reason := strings.TrimSpace(decision.Reason)
if reason == "" {
reason = "识别到排程微调请求,准备执行连续微调流程。"
}
return RoutingDecision{Action: ActionSchedulePlanRefine, TrustRoute: true, Detail: reason, RouteFailed: false}
case ActionChat:
return RoutingDecision{
Action: ActionChat,
TrustRoute: false,
Detail: "",
RouteFailed: false,
}
return RoutingDecision{Action: ActionChat, TrustRoute: false, Detail: "", RouteFailed: false}
default:
// 兜底:未知动作视为路由异常,标记 RouteFailed 让上层统一报错。
log.Printf("通用分流出现未知动作,标记路由失败并等待上层报错: action=%s raw=%s", decision.Action, decision.Raw)
return RoutingDecision{
Action: ActionChat,
TrustRoute: false,
Detail: "",
RouteFailed: true,
}
return RoutingDecision{Action: ActionChat, TrustRoute: false, Detail: "", RouteFailed: true}
}
}
@@ -215,9 +193,8 @@ func routeByModelControlTag(ctx context.Context, selectedModel *ark.ChatModel, u
}
// deriveRouteControlContext 为“控制码路由”创建子上下文。
//
// 设计要点:
// 1. timeout<=0 时不加额外 deadline仅继承父上下文
// 1. timeout<=0 时不加额外 deadline仅继承父上下文
// 2. 父 ctx deadline 更紧时,沿用父上下文,避免过早超时误判。
func deriveRouteControlContext(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if timeout <= 0 {
@@ -232,11 +209,10 @@ func deriveRouteControlContext(parent context.Context, timeout time.Duration) (c
}
// ParseRouteControlTag 解析通用控制码返回。
//
// 容错策略:
// 1. 允许大小写、属性顺序、额外属性差异;
// 2. nonce 必须精确匹配;
// 3. action 仅允许 quick_note_create/task_query/chat兼容 quick_note
// 3. 兼容旧 action schedule_plan/quick_note
func ParseRouteControlTag(raw, expectedNonce string) (*ControlDecision, error) {
text := strings.TrimSpace(raw)
if text == "" {
@@ -256,11 +232,12 @@ func ParseRouteControlTag(raw, expectedNonce string) (*ControlDecision, error) {
actionText := strings.ToLower(strings.TrimSpace(header[2]))
action := Action(actionText)
switch action {
case ActionQuickNoteCreate, ActionTaskQuery, ActionSchedulePlan, ActionChat:
case ActionQuickNoteCreate, ActionTaskQuery, ActionSchedulePlanCreate, ActionSchedulePlanRefine, ActionChat:
// 合法动作直接通过。
case ActionQuickNote:
// 兼容旧动作值:统一映射到 quick_note_create。
action = ActionQuickNoteCreate
case ActionSchedulePlan:
action = ActionSchedulePlanCreate
default:
return nil, fmt.Errorf("invalid route action: %s", actionText)
}
@@ -279,10 +256,9 @@ func ParseRouteControlTag(raw, expectedNonce string) (*ControlDecision, error) {
}
// DecideQuickNoteRouting 是历史兼容入口。
//
// 说明:
// 1. 旧代码只区分“进不进 quick_note”
// 2. 新分流 task_query 不应进入 quick_note因此这里会映射为 false。
// 1. 旧代码只区分“是否进入 quick_note”
// 2. 新分流 task_query/schedule_plan_* 都不应进入 quick_note。
func DecideQuickNoteRouting(ctx context.Context, selectedModel *ark.ChatModel, userMessage string) RoutingDecision {
decision := DecideActionRouting(ctx, selectedModel, userMessage)
if decision.Action == ActionQuickNoteCreate {
@@ -297,10 +273,7 @@ func DecideQuickNoteRouting(ctx context.Context, selectedModel *ark.ChatModel, u
}
// ParseQuickNoteRouteControlTag 是历史兼容解析入口。
//
// 说明:
// 1. 旧测试仍调用该函数名;
// 2. 新实现统一委托给 ParseRouteControlTag。
// 说明:旧测试仍使用该方法名,内部统一委托 ParseRouteControlTag。
func ParseQuickNoteRouteControlTag(raw, expectedNonce string) (*ControlDecision, error) {
return ParseRouteControlTag(raw, expectedNonce)
}

View File

@@ -0,0 +1,45 @@
package route
import "testing"
func TestParseRouteControlTag_SchedulePlanCreate(t *testing.T) {
nonce := "nonce-create"
raw := `<SMARTFLOW_ROUTE nonce="nonce-create" action="schedule_plan_create"></SMARTFLOW_ROUTE>
<SMARTFLOW_REASON>新建排程</SMARTFLOW_REASON>`
decision, err := ParseRouteControlTag(raw, nonce)
if err != nil {
t.Fatalf("解析失败: %v", err)
}
if decision.Action != ActionSchedulePlanCreate {
t.Fatalf("action 不匹配,期望=%s 实际=%s", ActionSchedulePlanCreate, decision.Action)
}
}
func TestParseRouteControlTag_SchedulePlanRefine(t *testing.T) {
nonce := "nonce-refine"
raw := `<SMARTFLOW_ROUTE nonce="nonce-refine" action="schedule_plan_refine"></SMARTFLOW_ROUTE>
<SMARTFLOW_REASON>微调排程</SMARTFLOW_REASON>`
decision, err := ParseRouteControlTag(raw, nonce)
if err != nil {
t.Fatalf("解析失败: %v", err)
}
if decision.Action != ActionSchedulePlanRefine {
t.Fatalf("action 不匹配,期望=%s 实际=%s", ActionSchedulePlanRefine, decision.Action)
}
}
func TestParseRouteControlTag_LegacySchedulePlan(t *testing.T) {
nonce := "nonce-legacy"
raw := `<SMARTFLOW_ROUTE nonce="nonce-legacy" action="schedule_plan"></SMARTFLOW_ROUTE>
<SMARTFLOW_REASON>兼容旧动作</SMARTFLOW_REASON>`
decision, err := ParseRouteControlTag(raw, nonce)
if err != nil {
t.Fatalf("解析失败: %v", err)
}
if decision.Action != ActionSchedulePlanCreate {
t.Fatalf("旧动作映射错误,期望=%s 实际=%s", ActionSchedulePlanCreate, decision.Action)
}
}