Files
smartmate/backend/newAgent/model/plan_contract.go
Losita 66c06eed0a 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永远只适合做选择题、判断题,不适合做开放创新题。
2026-04-27 01:09:37 +08:00

246 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import (
"fmt"
"strings"
)
// PlanComplexity 表示规划阶段评估的任务复杂度。
type PlanComplexity string
const (
// PlanComplexitySimple 表示简单明确的操作,步骤之间无复杂依赖。
PlanComplexitySimple PlanComplexity = "simple"
// PlanComplexityModerate 表示多步操作,需要一定推理但不涉及深度分析。
PlanComplexityModerate PlanComplexity = "moderate"
// PlanComplexityComplex 表示需要深度推理、多方案比较或复杂依赖关系的任务。
PlanComplexityComplex PlanComplexity = "complex"
)
// PlanAction 表示规划阶段单轮决策的动作类型。
//
// 设计原则:
// 1. 规划阶段只关心“继续规划 / 追问用户 / 规划完成”这三类动作;
// 2. 这里先不把工具调用塞进 contract避免过早把 plan loop 复杂化;
// 3. 规划层产出的是“自然语言计划”,不是执行层的工具动作。
type PlanAction string
const (
// PlanActionContinue 表示当前信息已足够,继续规划下一轮。
PlanActionContinue PlanAction = "continue"
// PlanActionAskUser 表示当前规划缺少关键信息,需要中断并追问用户。
PlanActionAskUser PlanAction = "ask_user"
// PlanActionDone 表示规划已经完成,可以进入 confirm 或下一阶段。
PlanActionDone PlanAction = "plan_done"
)
// PlanDecision 是 plan prompt 单轮产出的统一决策结构。
//
// 职责边界:
// 1. Speak 是本轮先对用户说的话;若 action=ask_user通常这里会承载要追问的问题
// 2. Action 是规划阶段的下一步动作类型;
// 3. Reason 是给后端和日志看的简短解释;
// 4. PlanSteps 只在 plan_done 时要求返回,表示本轮最终确认下来的完整自然语言计划;
// 5. NeedsRoughBuild 为 true 时Confirm 后自动触发粗排节点,不需要 LLM 在 plan_steps 里手动描述放置步骤;
// 6. TaskClassIDs 是本次粗排涉及的任务类 ID 列表,与 CommonState.TaskClassIDs 保持一致。
type PlanDecision struct {
Speak string `json:"speak,omitempty"`
Action PlanAction `json:"action"`
Reason string `json:"reason,omitempty"`
Complexity PlanComplexity `json:"complexity"`
PlanSteps []PlanStep `json:"plan_steps,omitempty"`
NeedsRoughBuild bool `json:"needs_rough_build,omitempty"`
TaskClassIDs []int `json:"task_class_ids,omitempty"`
ContextHook *ContextHook `json:"context_hook,omitempty"`
}
// ContextHook 表示 plan 阶段给 execute 阶段的上下文注入建议。
//
// 职责边界:
// 1. 仅承载“建议激活哪个 domain/packs”不负责真正执行 context_tools_add/remove
// 2. domain 仅允许 schedule/taskclasspacks 仅允许 schedule 的可选包;
// 3. 该结构会在 execute 首轮被消费一次,消费后由后端清空。
type ContextHook struct {
Domain string `json:"domain,omitempty"`
Packs []string `json:"packs,omitempty"`
Reason string `json:"reason,omitempty"`
}
// Normalize 统一清洗规划决策中的字符串字段。
func (d *PlanDecision) Normalize() {
if d == nil {
return
}
d.Speak = strings.TrimSpace(d.Speak)
d.Action = PlanAction(strings.TrimSpace(string(d.Action)))
d.Reason = strings.TrimSpace(d.Reason)
d.Complexity = PlanComplexity(strings.TrimSpace(string(d.Complexity)))
for i := range d.PlanSteps {
d.PlanSteps[i].Normalize()
}
if d.ContextHook != nil {
d.ContextHook.Normalize()
}
}
// Validate 校验规划决策的最小合法性。
//
// 校验原则:
// 1. 这里只校验“协议是否自洽”,不校验规划内容是否聪明、是否足够好;
// 2. 只有 plan_done 允许返回完整 plan_steps
// 3. 真正的规划质量判断仍留给后续 node 层和用户确认环节。
func (d *PlanDecision) Validate() error {
if d == nil {
return fmt.Errorf("plan decision 不能为空")
}
d.Normalize()
if d.Action == "" {
return fmt.Errorf("plan decision.action 不能为空")
}
// 复杂度兜底:未填写时默认 moderate不因此拒绝整个决策。
switch d.Complexity {
case PlanComplexitySimple, PlanComplexityModerate, PlanComplexityComplex:
// ok
case "":
d.Complexity = PlanComplexityModerate
default:
return fmt.Errorf("未知 complexity: %s", d.Complexity)
}
switch d.Action {
case PlanActionContinue, PlanActionAskUser:
if len(d.PlanSteps) > 0 {
return fmt.Errorf("%s 动作不应携带 plan_steps", d.Action)
}
if d.ContextHook != nil {
return fmt.Errorf("%s 动作不应携带 context_hook", d.Action)
}
return nil
case PlanActionDone:
if len(d.PlanSteps) == 0 {
return fmt.Errorf("plan_done 动作必须携带完整 plan_steps")
}
for i := range d.PlanSteps {
if err := d.PlanSteps[i].Validate(); err != nil {
return fmt.Errorf("plan_steps[%d] 非法: %w", i, err)
}
}
if d.ContextHook != nil {
if err := d.ContextHook.Validate(); err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("未知 plan action: %s", d.Action)
}
}
// PlanStep 表示规划阶段产出的一条自然语言步骤。
//
// 设计说明:
// 1. Content 是步骤正文,后续可直接落到 CommonState.PlanSteps
// 2. DoneWhen 是可选的完成判定描述,用来给 execute 阶段提供最小退出条件;
// 3. 这里仍然保持“自然语言优先”,不把 plan step 过度结构化。
type PlanStep struct {
Content string `json:"content"`
DoneWhen string `json:"done_when,omitempty"`
}
// Normalize 统一清洗 plan step 中的字符串字段。
func (s *PlanStep) Normalize() {
if s == nil {
return
}
s.Content = strings.TrimSpace(s.Content)
s.DoneWhen = strings.TrimSpace(s.DoneWhen)
}
// Validate 校验单条 plan step 的最小合法性。
func (s *PlanStep) Validate() error {
if s == nil {
return fmt.Errorf("plan step 不能为空")
}
s.Normalize()
if s.Content == "" {
return fmt.Errorf("plan step.content 不能为空")
}
return nil
}
// Normalize 统一清洗 context hook 字段。
func (h *ContextHook) Normalize() {
if h == nil {
return
}
h.Domain = normalizeContextHookDomain(h.Domain)
h.Reason = strings.TrimSpace(h.Reason)
h.Packs = normalizeContextHookPacks(h.Domain, h.Packs)
}
// Validate 校验 context hook 最小合法性。
func (h *ContextHook) Validate() error {
if h == nil {
return nil
}
h.Normalize()
if h.Domain == "" {
return fmt.Errorf("context_hook.domain 非法,仅支持 schedule/taskclass")
}
if h.Domain == "taskclass" && len(h.Packs) > 0 {
return fmt.Errorf("context_hook.taskclass 暂不支持 packs")
}
return nil
}
func normalizeContextHookDomain(domain string) string {
switch strings.ToLower(strings.TrimSpace(domain)) {
case "schedule":
return "schedule"
case "taskclass":
return "taskclass"
default:
return ""
}
}
func normalizeContextHookPacks(domain string, packs []string) []string {
if domain != "schedule" || len(packs) == 0 {
return nil
}
allowed := map[string]struct{}{
"queue": {},
"mutation": {},
"analyze": {},
"detail_read": {},
"deep_analyze": {},
"web": {},
}
seen := make(map[string]struct{}, len(packs))
result := make([]string, 0, len(packs))
for _, raw := range packs {
pack := strings.ToLower(strings.TrimSpace(raw))
if pack == "" || pack == "core" {
continue
}
if _, ok := allowed[pack]; !ok {
continue
}
if _, exists := seen[pack]; exists {
continue
}
seen[pack] = struct{}{}
result = append(result, pack)
}
if len(result) == 0 {
return nil
}
return result
}