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

@@ -123,12 +123,22 @@ func renderStateSummary(state *newagentmodel.CommonState) string {
if tc.StartDate != "" || tc.EndDate != "" {
line += fmt.Sprintf(",日期范围=%s ~ %s", tc.StartDate, tc.EndDate)
}
if tc.SubjectType != "" || tc.DifficultyLevel != "" || tc.CognitiveIntensity != "" {
line += fmt.Sprintf(",语义画像=%s/%s/%s",
defaultSemanticValue(tc.SubjectType),
defaultSemanticValue(tc.DifficultyLevel),
defaultSemanticValue(tc.CognitiveIntensity),
)
}
if tc.AllowFillerCourse {
line += ",允许嵌入水课"
}
if len(tc.ExcludedSlots) > 0 {
line += fmt.Sprintf(",排除时段=%v", tc.ExcludedSlots)
}
if len(tc.ExcludedDaysOfWeek) > 0 {
line += fmt.Sprintf(",排除星期=%v", tc.ExcludedDaysOfWeek)
}
sb.WriteString(line + "\n")
}
}
@@ -136,6 +146,14 @@ func renderStateSummary(state *newagentmodel.CommonState) string {
return sb.String()
}
func defaultSemanticValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "未标注"
}
return trimmed
}
// renderPinnedBlocks 把 ConversationContext 中的置顶块渲染成独立的 system 文本。
func renderPinnedBlocks(ctx *newagentmodel.ConversationContext) string {
if ctx == nil {

View File

@@ -26,6 +26,11 @@ quick_task 判别要点:
- 但如果用户同时提了日程排布(如"把明天的课调一下,再记一下周五开会"),混合操作走 execute
- 如果信息不足(如"帮我记一下"但没说记什么),走 direct_reply 追问
任务类设计路由要点:
- 普通"创建/修改任务类"默认走 execute由 execute 负责补字段与写入)。
- 仅当用户明确要"补课程学习资料/学习建议/学习路径(需要外部知识)"时,走 plan后续可使用 web_search
- 考试时间、DDL、课程具体时间安排、个人可用时段等时间信息必须向用户本人确认不能作为 web 搜索补齐目标。
通用回答约束:
- 非日程、非任务类问题,只要不需要工具,也应当正常回答。
- 不要因为用户的问题不涉及排程,就说自己“只能处理日程/任务安排”。
@@ -39,8 +44,8 @@ quick_task 判别要点:
- "移动/微调/优化/均匀化/调顺序"等请求默认视为 refine不得再次触发 rough build。
粗排后微调判断:
- 仅当 rough_build=true 时才判断 refine。
- 若用户明确提出优化目标/偏好(如"尽量均衡""周三别太满""某门课往后挪"),设 refine=true
- 若用户只要求"先排进去/给初稿",未提出微调目标,设 refine=false。
- 默认策略首次粗排完成后应进入微调refine=true按中位标准做主动优化
- 仅当用户明确表达"只要初稿/先排进去别优化/先不微调/排完就收口"时,才设 refine=false。
顺序授权判断:
- reorder 仅在用户明确说明"允许打乱顺序/顺序不重要"时才为 true。
- 用户明确要求"保持顺序/不要打乱"时必须为 false。

View File

@@ -8,261 +8,14 @@ import (
"github.com/cloudwego/eino/schema"
)
const executeSystemPromptWithPlan = `
你是 SmartMate 的执行器。你需要在"当前 plan 步骤"约束下推进任务。
你可以做什么:
1. 只围绕当前步骤推进,先读后写,逐步完成当前步骤。
2. 可调用读工具补充事实,再决定下一步。
3. 日程写操作时输出 action=confirm 并附带 tool_call等待用户确认。
4. 若用户给出了"二次微调方向"(如负载均衡、某天减负、某类任务后移),优先围绕该方向推进,并在 goal_check 说明满足情况。
5. 只有在用户明确允许打乱顺序时,才可使用 min_context_switch 做重排。
6. 多任务微调时默认走队列链路query_target_tasks(enqueue=true) → queue_pop_head → query_available_slots → queue_apply_head_move / queue_skip_head。
你不要做什么:
1. 不要跳到其他 plan 步骤,不要越级执行。
2. 不要伪造工具结果。
3. 如果上下文明确"粗排已完成/rough_build_done",不要把任务当成未排入,不要重新逐个手动 place。
4. 如果上下文明确"当前未收到明确微调偏好/本轮先收口",不要继续微调,直接输出 action=done。
5. 不要连续重复同类查询而没有推进连续两轮同类读查询后必须转入执行、ask_user或明确阻塞原因。
6. 若工具结果与已知事实明显冲突(如无写操作却从"有任务"变成"0任务"),先自我纠错并重查一次,不要直接 ask_user。
7. 不要连续两轮调用"同一读工具 + 等价 arguments";若上一轮已成功返回,下一轮必须换工具或进入 confirm。
8. 不要忽略用户最新补充的微调方向;若与旧目标冲突,以最新用户要求为准。
9. 若当前顺序策略是"默认保持顺序",禁止调用 min_context_switch。
10. 不要把超过 2 条任务打包到 batch_move大批量调整请改走队列逐项处理。
11. 不要在未获取队首queue_pop_head时直接调用 queue_apply_head_move。
12. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。
13. web_search 仅在"制定学习计划需要查外部资料"时使用如考试日期、课程信息、校历政策等日程排布本身place/move/swap不需要搜索。
14. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。
执行规则:
1. 输出格式:先输出一行 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>然后换行输出给用户看的自然语言正文。JSON 中不要包含 speak 字段——用户可见的话放在标签之后。
2. 读操作action=continue + tool_call。
3. 写操作(日程变更,如 place/move/swap/batch_move/unplace/spread_even/min_context_switchaction=confirm + tool_call。
4. 缺关键上下文且无法通过工具补齐action=ask_user。
5. 仅当当前步骤完成时输出 action=next_plan并在 goal_check 对照 done_when 给出证据。
6. 仅当整体任务完成时输出 action=done并在 goal_check 总结完成证据。
7. 流程应正式终止时输出 action=abort。`
const executeSystemPromptReAct = `
你是 SmartMate 的执行器,当前处于自由执行模式(无预定义 plan 步骤)。
阶段事实(强约束):
1. 若上下文给出"粗排已完成/rough_build_done",表示目标任务类已经进入 suggested/existing不是待排入状态。
2. 当前阶段目标是"微调",不是"重新粗排"。
3. 若上下文明确"当前未收到明确微调偏好/本轮先收口",应直接结束而不是继续优化循环。
4. 若用户提出了二次微调方向,本轮优先目标就是满足该方向。
你可以做什么:
1. 你可以基于用户给定的二次微调方向,对 suggested 做定向微调。
2. existing 属于已安排事实层,可用于冲突判断和参考,不作为 move/batch_move/spread_even 的目标。
3. 你可以先调用读工具补充必要事实(例如 get_overview/query_target_tasks/query_available_slots/get_task_info
4. 你可以在需要日程写操作时提出 confirmmove/swap/unplace/batch_move/spread_even
5. 只有用户明确允许打乱顺序时,才可使用 min_context_switch。
6. 多任务处理默认使用队列链路:先 query_target_tasks(enqueue=true) 入队,再 queue_pop_head 逐项处理。
你不要做什么:
1. 不要假设任务还没排进去,然后改成逐个手动 place。
2. 不要伪造工具结果。
3. 不要重复做同类查询而没有新增结论连续两轮同类读查询后必须转入执行、ask_user或明确阻塞原因。
4. 若工具结果与已知事实明显冲突(如无写操作却从"有任务"变成"0任务"),先自我纠错并重查一次,不要直接 ask_user。
5. 不要连续两轮调用"同一读工具 + 等价 arguments";若上一轮已成功返回,下一轮必须换工具或进入 confirm。
6. 若已明确"本轮先收口",不要继续调用 query_available_slots/move 做无目标微调。
7. 若用户明确了微调方向,不要只做"局部看起来更空"的随机调整;每次改动都要能对应到该方向。
8. 若顺序策略为"保持顺序",禁止调用 min_context_switch。
9. 不要在同一轮构造大规模 batch_movebatch_move 最多 2 条,超过请走队列逐项处理。
10. 未调用 queue_pop_head 获取 current 前,不要调用 queue_apply_head_move。
11. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。
12. web_search 仅在"制定学习计划需要查外部资料"时使用如考试日期、课程信息、校历政策等日程排布本身place/move/swap不需要搜索。
13. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。
执行规则:
1. 输出格式:先输出一行 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>然后换行输出给用户看的自然语言正文。JSON 中不要包含 speak 字段——用户可见的话放在标签之后。
2. 读操作action=continue + tool_call。
3. 写操作(日程变更,如 place/move/swap/batch_move/unplace/spread_even/min_context_switchaction=confirm + tool_call。
4. 缺关键上下文且无法通过工具补齐action=ask_user。
5. 任务完成action=done并在 goal_check 总结完成证据。
6. 流程应正式终止action=abort。`
// BuildExecuteSystemPrompt 返回执行阶段系统提示词(有 plan 模式)。
func BuildExecuteSystemPrompt() string {
return buildExecutePromptWithFormatGuard(executeSystemPromptWithPlan)
return buildExecutePromptWithFormatGuard(executeSystemPromptBaseWithPlan)
}
// BuildExecuteReActSystemPrompt 返回执行阶段系统提示词(自由执行模式)。
func BuildExecuteReActSystemPrompt() string {
return buildExecutePromptWithFormatGuard(executeSystemPromptReAct)
}
// BuildExecuteDecisionContractText 返回执行阶段输出协议(有 plan 模式)。
func BuildExecuteDecisionContractText() string {
return strings.TrimSpace(fmt.Sprintf(`
输出协议(两阶段格式):
先输出一行决策标签,标签内是 JSON标签之后换行输出给用户看的自然语言正文。
决策标签格式:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
JSON 字段说明:
- action只能是 %s / %s / %s / %s / %s
- reason给后端和日志看的简短说明
- goal_check输出 %s 或 %s 时必填,对照 done_when 逐条验证
- tool_call输出 %s写操作需 confirm或 %s读操作时可附带格式 {"name":"工具名","arguments":{...}}
注意JSON 中不要包含 speak 字段。给用户看的话放在 </SMARTFLOW_DECISION> 标签之后。
示例:
<SMARTFLOW_DECISION>{"action":"%s","reason":"需要先调用 get_overview 获取事实","tool_call":{"name":"get_overview","arguments":{}}}</SMARTFLOW_DECISION>
我先查看当前整体安排。
<SMARTFLOW_DECISION>{"action":"%s","reason":"已完成当前步骤所需查询与校验","goal_check":"已满足当前步骤 done_when 条件"}</SMARTFLOW_DECISION>
当前步骤已完成。
<SMARTFLOW_DECISION>{"action":"%s","reason":"整体任务已完成"}</SMARTFLOW_DECISION>
`,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionAskUser,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionNextPlan,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionNextPlan,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionNextPlan,
newagentmodel.ExecuteActionDone,
))
}
// BuildExecuteReActContractText 返回自由执行模式输出协议。
func BuildExecuteReActContractText() string {
return strings.TrimSpace(fmt.Sprintf(`
输出协议(两阶段格式):
先输出一行决策标签,标签内是 JSON标签之后换行输出给用户看的自然语言正文。
决策标签格式:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
JSON 字段说明:
- action只能是 %s / %s / %s / %s
- reason给后端和日志看的简短说明
- goal_check输出 %s 时必填,总结任务完成证据
- tool_call输出 %s写操作需 confirm或 %s读操作时可附带格式 {"name":"工具名","arguments":{...}}
注意JSON 中不要包含 speak 字段。给用户看的话放在 </SMARTFLOW_DECISION> 标签之后。
示例:
<SMARTFLOW_DECISION>{"action":"%s","reason":"先读取概览再决定微调方向","tool_call":{"name":"get_overview","arguments":{}}}</SMARTFLOW_DECISION>
我先看一下现在的安排分布。
<SMARTFLOW_DECISION>{"action":"%s","reason":"写操作需要确认","tool_call":{"name":"swap","arguments":{"task_a":1,"task_b":2}}}</SMARTFLOW_DECISION>
我准备把两项任务对调位置,你确认后执行。
<SMARTFLOW_DECISION>{"action":"%s","reason":"微调执行完毕并已校验结果","goal_check":"目标任务类已完成微调,且关键约束满足"}</SMARTFLOW_DECISION>
已完成你的请求。
`,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionAskUser,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionDone,
))
}
// BuildExecuteDecisionContractTextV2 返回补齐 abort 协议后的执行输出契约(有 plan 模式)。
func BuildExecuteDecisionContractTextV2() string {
return strings.TrimSpace(fmt.Sprintf(`
输出协议(两阶段格式):
先输出一行决策标签,标签内是 JSON标签之后换行输出给用户看的自然语言正文。
决策标签格式:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
JSON 字段说明:
- action只能是 %s / %s / %s / %s / %s / %s
- reason给后端和日志看的简短说明
- goal_check输出 %s 或 %s 时必填,对照 done_when 逐条验证
- tool_call输出 %s写操作需 confirm或 %s读操作时可附带格式 {"name":"工具名","arguments":{...}}
- abort仅在 action=%s 时必填,格式为 {"code":"...","user_message":"...","internal_reason":"..."}
- tool_call 与 abort 互斥,禁止同时出现
注意JSON 中不要包含 speak 字段。给用户看的话放在 </SMARTFLOW_DECISION> 标签之后。若 action=%s标签后通常留空。
示例:
<SMARTFLOW_DECISION>{"action":"%s","reason":"先读取事实再决策","tool_call":{"name":"get_overview","arguments":{}}}</SMARTFLOW_DECISION>
我先查看当前安排。
<SMARTFLOW_DECISION>{"action":"%s","reason":"步骤完成条件满足","goal_check":"已满足当前步骤 done_when"}</SMARTFLOW_DECISION>
当前步骤完成。
<SMARTFLOW_DECISION>{"action":"%s","reason":"流程不应继续执行","abort":{"code":"execute_abort","user_message":"当前流程无法继续执行,本轮先终止。","internal_reason":"execute declared abort"}}</SMARTFLOW_DECISION>
`,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionAskUser,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionNextPlan,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionAbort,
newagentmodel.ExecuteActionNextPlan,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionAbort,
newagentmodel.ExecuteActionAbort,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionNextPlan,
newagentmodel.ExecuteActionAbort,
))
}
// BuildExecuteReActContractTextV2 返回补齐 abort 协议后的自由执行输出契约。
func BuildExecuteReActContractTextV2() string {
return strings.TrimSpace(fmt.Sprintf(`
输出协议(两阶段格式):
先输出一行决策标签,标签内是 JSON标签之后换行输出给用户看的自然语言正文。
决策标签格式:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
JSON 字段说明:
- action只能是 %s / %s / %s / %s / %s
- reason给后端和日志看的简短说明
- goal_check输出 %s 时必填,总结任务完成证据
- tool_call输出 %s写操作需 confirm或 %s读操作时可附带格式 {"name":"工具名","arguments":{...}}
- abort仅在 action=%s 时必填,格式为 {"code":"...","user_message":"...","internal_reason":"..."}
- tool_call 与 abort 互斥,禁止同时出现
注意JSON 中不要包含 speak 字段。给用户看的话放在 </SMARTFLOW_DECISION> 标签之后。若 action=%s标签后通常留空。
示例:
<SMARTFLOW_DECISION>{"action":"%s","reason":"先获取事实再决策","tool_call":{"name":"get_overview","arguments":{}}}</SMARTFLOW_DECISION>
我先读取当前安排。
<SMARTFLOW_DECISION>{"action":"%s","reason":"写操作需要确认","tool_call":{"name":"move","arguments":{"task_id":5,"new_day":3,"new_slot_start":1}}}</SMARTFLOW_DECISION>
我准备执行写操作,等待你确认。
<SMARTFLOW_DECISION>{"action":"%s","reason":"当前流程不应继续执行","abort":{"code":"domain_abort","user_message":"当前流程无法继续执行,本轮先终止。","internal_reason":"execute declared abort"}}</SMARTFLOW_DECISION>
`,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionAskUser,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionAbort,
newagentmodel.ExecuteActionDone,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionAbort,
newagentmodel.ExecuteActionAbort,
newagentmodel.ExecuteActionContinue,
newagentmodel.ExecuteActionConfirm,
newagentmodel.ExecuteActionAbort,
))
return buildExecutePromptWithFormatGuard(executeSystemPromptBaseReAct)
}
// BuildExecuteMessages 组装执行阶段消息。
@@ -304,6 +57,7 @@ func buildExecuteStrictJSONUserPromptWithPlan(state *newagentmodel.CommonState)
计划步骤强约束:
- 当前没有可执行的计划步骤,请先基于已有事实检查是否已完成全部计划。
- 若全部计划已完成:输出 action=done并在 goal_check 总结完成证据。
- goal_check 字段类型必须为 string不要输出对象或数组。
- 若未完成但缺少关键信息:输出 action=ask_user。`)
}
@@ -324,6 +78,7 @@ func buildExecuteStrictJSONUserPromptWithPlan(state *newagentmodel.CommonState)
- 当前步骤完成判定(done_when)%s
- 未满足 done_when 时:只能输出 continue / confirm / ask_user禁止输出 next_plan。
- 满足 done_when 时:优先输出 action=next_plan并在 goal_check 逐条对照 done_when 给出证据。
- goal_check 字段类型固定为 string示例"已满足 done_when...;证据:..."),禁止输出 {"done_when":"...","evidence":"..."}。
- 禁止跳步:不要提前执行后续步骤。`,
base, current, total, stepContent, doneWhen))
}
@@ -332,13 +87,15 @@ func buildExecuteStrictJSONUserPromptWithPlan(state *newagentmodel.CommonState)
func buildExecutePromptWithFormatGuard(base string) string {
base = strings.TrimSpace(base)
guard := strings.TrimSpace(`
补充 JSON 约束:
1. 只输出当前 action 真正需要的字段;无关字段直接省略,不要用 ""、{}、[]、null 占位。
2. 若输出 tool_call,参数字段名只能是 arguments禁止写成 parameters
3. tool_call 只能是单个对象:{"name":"工具名","arguments":{...}},不能输出数组
4. 只有 action=abort 时才允许输出 abort 字段;非 abort 动作不要输出 abort
5. action=continue / ask_user / confirm 时,标签后的正文必须是非空自然语言
6. <SMARTFLOW_DECISION> 标签内只放 JSON不要放自然语言。`)
输出协议硬约束:
1. 只输出当前 action 真正需要的字段;不要输出空字符串、空对象、空数组或 null 占位。
2. tool_call 只能是 {"name":"工具名","arguments":{...}};不能写 parameters也不能一次输出多个 tool_call
3. action=ask_user / confirm 时标签后必须有自然语言正文action=continue 可为空
4. action=done 时不要携带 tool_callaction=next_plan / done 时goal_check 必须是字符串
5. 只有 action=abort 时才允许输出 abort 字段
6. <SMARTFLOW_DECISION> 标签内只放 JSON不要放自然语言。
7. 不要在 <SMARTFLOW_DECISION> 标签前输出任何前言、寒暄、解释或铺垫;给用户看的正文只能放在 </SMARTFLOW_DECISION> 之后。
8. 任何动作都不得擅自超出用户当前明确意图;用户没让你做的下一步,不要自作主张推进。`)
if base == "" {
return guard
}
@@ -351,37 +108,17 @@ func buildExecuteStrictJSONUserPrompt() string {
请继续当前任务的执行阶段,严格按 SMARTFLOW_DECISION 标签格式输出。
输出格式:先输出 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>,然后换行输出给用户看的正文。
补充格式要求
- JSON 中不要包含 speak 字段给用户看的话放在 </SMARTFLOW_DECISION> 标签之后
- 与当前 action 无关的字段直接省略,不要输出空字符串、空对象、空数组或 null 占位
- tool_call 只能写 {"name":"工具名","arguments":{...}},且每轮最多一个
- 不要写 {"tool_call":{"name":"工具名","parameters":{...}}}
- 非 abort 动作不要输出 abort 字段
- action 为 continue / ask_user / confirm 时,标签后必须输出非空正文
执行提醒
- JSON 中不要包含 speak 字段给用户看的话放在 </SMARTFLOW_DECISION> 标签之后
- 不要在 <SMARTFLOW_DECISION> 标签之前输出任何文字;哪怕只有一句“我先看下”也不行
- 日程写工具place/move/swap/batch_move/unplace一律走 action=confirm
- 若当前处于粗排后主动优化专用模式,先调 analyze_health再直接从 decision.candidates 里选一个合法候选去执行;不要自行发明新的全窗搜索步骤
- 若读工具结果与已知事实明显冲突,先修正参数并重查一次,再决定是否 ask_user
- 不要连续两轮调用"同一读工具 + 等价 arguments";若上一轮已成功返回,下一轮必须换工具进入 confirm
- 若用户本轮给了二次微调方向,优先满足该方向,再考虑通用均衡优化
- 若上下文已明确"当前未收到微调偏好,本轮先收口",请直接输出 action=done
- 仅当顺序策略明确允许打乱顺序时,才可以调用 min_context_switch
- spread_even 用于"范围内均匀化",必须先用 query_target_tasks 明确目标任务集合
- 多任务调整默认先调用 query_target_tasks(enqueue=true),再用 queue_pop_head 逐项处理
- queue_apply_head_move 只能用于 current 任务;若当前任务无法落位,调用 queue_skip_head 后继续
- batch_move 一次最多 2 条;超过 2 条必须改走队列逐项处理
`)
}
// BuildExecuteUserPrompt 构造有 plan 模式的用户提示词。
func BuildExecuteUserPrompt(_ *newagentmodel.CommonState) string {
return strings.TrimSpace(`
请继续当前任务的执行阶段,严格按 SMARTFLOW_DECISION 标签格式输出。
输出格式:先输出 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>,然后换行输出给用户看的正文。
`)
}
// BuildExecuteReActUserPrompt 构造自由执行模式的用户提示词。
func BuildExecuteReActUserPrompt(_ *newagentmodel.CommonState) string {
return strings.TrimSpace(`
请继续当前任务的执行阶段,严格按 SMARTFLOW_DECISION 标签格式输出。
输出格式:先输出 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>,然后换行输出给用户看的正文。
- 不要连续两轮调用同一读工具 + 等价 arguments”;上一轮已成功返回,下一轮必须换工具进入 confirm,或明确说明阻塞
- 若上下文已明确“当前未收到微调偏好,本轮先收口”,请直接输出 action=done
- web_search 仅用于通用学习资料补充不可用于考试时间、DDL、个人时段等时间字段填充
- upsert_task_class 若返回 validation.ok=false必须先按 validation.issues 补齐,再重试;禁止直接 done
- subject_type / difficulty_level / cognitive_intensity 是任务类语义画像必填;优先静默推断,只有确实无法判断时再 ask_user
- 仅 upsert_task_class 成功不代表已开始排程;若未触发 rough_build 且未调用任何日程修改工具,禁止承诺“接下来会自动排程”
`)
}

View File

@@ -12,9 +12,8 @@ import (
)
const (
// executeHistoryKindKey 用于在 history 中打运行态标记,供 prompt 分层识别
// 说明loop_closed / step_advanced 等边界标记仍由节点层写入,但 prompt 层已不再消费它们——
// 因为 msg1/msg2 已经按"真实对话流 + 当前活跃 ReAct 记录"重构,不再做 msg2→msg1 的归档搬运。
// executeHistoryKindKey 用于在 history 里区分普通用户消息与后端注入的纠错提示
// 这里负责“识别并过滤”,不负责写入该标记。
executeHistoryKindKey = "newagent_history_kind"
executeHistoryKindCorrectionUser = "llm_correction_prompt"
)
@@ -31,20 +30,29 @@ type executeLoopRecord struct {
Observation string
}
// buildExecuteStageMessages 组装 execute 阶段 4 条消息骨架。
type conversationTurn struct {
Role string
Content string
}
type executeLatestToolRecord struct {
ToolName string
Observation string
}
// buildExecuteStageMessages 组装 execute 阶段的四段式消息。
//
// 消息结构(固定):
// 1. message[0] 固定 prompt规则 + 微调硬引导 + 输出约束 + 工具简表)
// 2. message[1] 历史上下文(真实对话流 + 早期 ReAct 摘要)
// 3. message[2] 当轮 ReAct Loop 窗口thought/reason + tool_call + observation 绑定展示)
// 4. message[3] 当前执行状态轮次、模式、plan 步骤、任务类、相关记忆等)
// 1. msg0系统提示 + 动态规则包 + 工具简表。
// 2. msg1真实对话流只保留 user 和 assistant speak。
// 3. msg2当前 ReAct tool loop 记录。
// 4. msg3执行状态、阶段约束、记忆和本轮指令。
func buildExecuteStageMessages(
stageSystemPrompt string,
state *newagentmodel.CommonState,
ctx *newagentmodel.ConversationContext,
runtimeUserPrompt string,
) []*schema.Message {
msg0 := buildExecuteMessage0(stageSystemPrompt, ctx)
msg0 := buildExecuteMessage0(stageSystemPrompt, state, ctx)
msg1 := buildExecuteMessage1V3(ctx)
msg2 := buildExecuteMessage2V3(ctx)
msg3 := buildExecuteMessage3(state, ctx, runtimeUserPrompt)
@@ -57,27 +65,30 @@ func buildExecuteStageMessages(
}
}
// buildExecuteMessage0 生成固定规则消息,并附带工具简表
func buildExecuteMessage0(stageSystemPrompt string, ctx *newagentmodel.ConversationContext) string {
// buildExecuteMessage0 生成 execute 阶段的固定规则消息。
//
// 1. 先拼基础 system prompt保证身份和输出协议稳定。
// 2. 再按当前 domain / packs 注入动态规则包,让模型先读到边界。
// 3. 最后再附工具简表,避免模型只看到工具不看到纪律。
func buildExecuteMessage0(stageSystemPrompt string, state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) string {
base := strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
if base == "" {
base = "你是 SmartMate 执行器,请继续 execute 阶段。"
base = "你是 SmartMate 执行器,请继续当前执行阶段。"
}
toolCatalog := renderExecuteToolCatalogCompact(ctx)
if toolCatalog == "" {
return base
rulePackSection, _ := renderExecuteRulePackSection(state, ctx)
if rulePackSection != "" {
base += "\n\n" + rulePackSection
}
return base + "\n\n" + toolCatalog
toolCatalog := renderExecuteToolCatalogCompact(ctx, state)
if toolCatalog != "" {
base += "\n\n" + toolCatalog
}
return base
}
// buildExecuteMessage1V3 只渲染"真实对话流 + 阶段锚点"
//
// 改造说明:
// 1. msg1 只保留 user + assistant speak 组成的真实对话历史,全量注入;
// 2. tool_call / observation 一律由 msg2 承载,这里不再重复;
// 3. 不再从历史中"归档"上一轮 ReAct 结果到 msg1——归档搬运逻辑已随 splitExecuteLoopRecordsByBoundary 一并移除;
// 4. token 预算由统一压缩层兜底prompt 层不做提前裁剪。
// buildExecuteMessage1V3 只渲染真实对话流,不混入 tool observation
func buildExecuteMessage1V3(ctx *newagentmodel.ConversationContext) string {
lines := []string{"历史上下文:"}
if ctx == nil {
@@ -105,16 +116,13 @@ func buildExecuteMessage1V3(ctx *newagentmodel.ConversationContext) string {
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
return strings.Join(lines, "\n")
}
// buildExecuteMessage2V3 承载当前会话中全部 ReAct Loop 记录
// buildExecuteMessage2V3 承载当 ReAct loop。
//
// 改造说明:
// 1. 不再按 execute_loop_closed / execute_step_advanced 边界切分"归档/活跃"两段;
// 2. 直接从 history 提取全部 assistant tool_call + 对应 observation 作为当前 Loop 视图;
// 3. 新一轮刚开始(尚未产生 tool_call时返回明确占位方便模型识别"干净起点"。
// 1. 每条记录固定展示 thought / tool_call / observation方便模型做局部闭环。
// 2. 如果当前还没有任何 tool loop明确给“新一轮”占位避免模型误判缺上下文。
func buildExecuteMessage2V3(ctx *newagentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录:"}
if ctx == nil {
@@ -136,11 +144,19 @@ func buildExecuteMessage2V3(ctx *newagentmodel.ConversationContext) string {
return strings.Join(lines, "\n")
}
// buildExecuteMessage3 汇总当前执行状态和本轮指令。
//
// 1. 这里只放“当前轮真正会影响决策”的状态,避免 msg3 继续膨胀。
// 2. 读工具最近结果只给最新一条摘要,避免旧 observation 重复占上下文。
// 3. 最后一行固定落到“本轮指令”,保证模型收尾时注意力还在执行目标上。
func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) string {
lines := []string{"当前执行状态:"}
roughBuildDone := hasExecuteRoughBuildDone(ctx)
roundUsed, maxRounds := 0, newagentmodel.DefaultMaxRounds
modeText := "自由执行(无预定义步骤)"
activeDomain := ""
activePacks := []string{}
if state != nil {
roundUsed = state.RoundUsed
if state.MaxRounds > 0 {
@@ -149,15 +165,23 @@ func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.C
if state.HasPlan() {
modeText = "计划执行(有预定义步骤)"
}
activeDomain = strings.TrimSpace(state.ActiveToolDomain)
activePacks = readExecuteActiveToolPacks(state)
}
lines = append(lines,
fmt.Sprintf("- 当前轮次:%d/%d", roundUsed, maxRounds),
"- 当前模式:"+modeText,
)
// 1. 有 plan 时,把当前步骤与完成判定强制写入 msg3。
// 2. 该锚点用于约束模型只推进当前步骤,避免退化成泛化 ReAct。
// 3. 当前步骤不可读时给出兜底指引,避免引用旧步骤。
if activeDomain == "" {
lines = append(lines, "- 动态工具区:当前仅激活 context 管理工具。")
} else if len(activePacks) == 0 {
lines = append(lines, fmt.Sprintf("- 动态工具区domain=%s未显式激活 packs。", activeDomain))
} else {
lines = append(lines, fmt.Sprintf("- 动态工具区domain=%spacks=[%s]。", activeDomain, strings.Join(activePacks, ",")))
}
if state != nil && state.HasPlan() {
current, total := state.PlanProgress()
lines = append(lines, "计划步骤锚点(强约束):")
@@ -170,26 +194,41 @@ func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.C
if doneWhen == "" {
doneWhen = "(未提供 done_when需基于步骤目标给出可验证完成证据"
}
lines = append(lines, fmt.Sprintf("- 当前步骤:第 %d/%d 步", current, total))
lines = append(lines, "- 当前步骤内容:"+stepContent)
lines = append(lines, "- 当前步骤完成判定(done_when)"+doneWhen)
lines = append(lines, "- 动作纪律1未满足 done_when 时,只能 continue / confirm / ask_user禁止 next_plan")
lines = append(lines, "- 动作纪律2满足 done_when 时,优先 next_plan并在 goal_check 对照 done_when 给证据")
lines = append(lines, "- 动作纪律3禁止跳到后续步骤执行")
lines = append(lines,
fmt.Sprintf("- 当前步骤:第 %d/%d 步", current, total),
"- 当前步骤内容:"+stepContent,
"- 当前步骤完成判定(done_when)"+doneWhen,
"- 动作纪律1满足 done_when 时,只能 continue / confirm / ask_user禁止 next_plan。",
"- 动作纪律2满足 done_when 时,优先 next_plan并在 goal_check 对照 done_when 给证据。",
"- 动作纪律3禁止跳到后续步骤执行。",
)
} else {
lines = append(lines, "- 当前计划步骤不可读;请先判断是否已完成全部计划")
lines = append(lines, "- 若已完成全部计划,输出 done 并给出 goal_check 证据")
lines = append(lines,
"- 当前计划步骤不可读;请先判断是否已完成全部计划。",
"- 若已完成全部计划,输出 done 并给出 goal_check 证据。",
)
}
}
if latestAnalyze := renderExecuteLatestAnalyzeSummary(ctx); latestAnalyze != "" {
lines = append(lines, "- 最近一次诊断:"+latestAnalyze)
}
if latestMutation := renderExecuteLatestMutationSummary(ctx); latestMutation != "" {
lines = append(lines, "- 最近一次写操作:"+latestMutation)
}
if taskClassText := renderExecuteTaskClassIDs(state); taskClassText != "" {
lines = append(lines, "- 目标任务类:"+taskClassText)
}
lines = append(lines, "- 啥时候结束Loop你可以根据工具调用记录自行判断。")
lines = append(lines, "- 非目标:不重新粗排、不修改无关任务类。")
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines,
"- 啥时候结束Loop你可以根据工具调用记录自行判断。",
"- 非目标:不重新粗排、不修改无关任务类。",
)
if roughBuildDone {
lines = append(lines, "- 阶段约束:粗排已完成,本轮只微调 suggestedexisting 仅作已安排事实参考,不作为可移动目标。")
}
lines = append(lines, "- 参数纪律:工具参数必须严格使用 schema 字段;若返回'参数非法',需先改参再继续。")
lines = append(lines, "- 参数纪律:工具参数必须严格使用 schema 字段;若返回参数非法,需先改参再继续。")
if state != nil {
if state.AllowReorder {
lines = append(lines, "- 顺序策略:用户已明确允许打乱顺序,可在必要时使用 min_context_switch。")
@@ -197,15 +236,27 @@ func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.C
lines = append(lines, "- 顺序策略:默认保持 suggested 相对顺序,禁止调用 min_context_switch。")
}
}
if upsertRuntime := renderTaskClassUpsertRuntime(state); upsertRuntime != "" {
lines = append(lines, "任务类写入运行态:")
lines = append(lines, upsertRuntime)
}
if memoryText := renderExecuteMemoryContext(ctx); memoryText != "" {
lines = append(lines, "相关记忆(仅在确有帮助时参考,不要机械复述):")
lines = append(lines, memoryText)
}
// 兼容上层传入的执行指令;若为空则使用固定收口指令。
latestAnalyze := renderExecuteLatestAnalyzeSummary(ctx)
latestMutation := renderExecuteLatestMutationSummary(ctx)
if nextStep := renderExecuteNextStepHintV2(state, latestAnalyze, latestMutation, roughBuildDone); nextStep != "" {
lines = append(lines, "下一步提示:")
lines = append(lines, "- "+nextStep)
}
instruction := strings.TrimSpace(runtimeUserPrompt)
if instruction == "" {
instruction = "请继续当前任务执行阶段,严格输出 JSON。"
instruction = "请继续当前任务执行阶段,严格按 SMARTFLOW_DECISION 标签格式输出。"
} else {
instruction = firstExecuteLine(instruction)
}
@@ -214,8 +265,12 @@ func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.C
return strings.Join(lines, "\n")
}
// renderExecuteToolCatalogCompact 将工具 schema 渲染成简表,避免大段 JSON 示例占用上下文
func renderExecuteToolCatalogCompact(ctx *newagentmodel.ConversationContext) string {
// renderExecuteToolCatalogCompact 将当前 tool schemas 渲染为紧凑简表
//
// 1. 这里只给模型最低必要的参数和返回值感知,不重复塞完整 schema JSON。
// 2. 对复杂工具额外给一条调用示例,降低“参数字段写错”的概率。
// 3. P1 阶段隐藏 min_context_switch避免模型误用已禁能力。
func renderExecuteToolCatalogCompact(ctx *newagentmodel.ConversationContext, state *newagentmodel.CommonState) string {
if ctx == nil {
return ""
}
@@ -225,36 +280,79 @@ func renderExecuteToolCatalogCompact(ctx *newagentmodel.ConversationContext) str
}
lines := []string{"可用工具(简表):"}
for i, schemaItem := range schemas {
index := 0
for _, schemaItem := range schemas {
name := strings.TrimSpace(schemaItem.Name)
desc := strings.TrimSpace(schemaItem.Desc)
if name == "" {
continue
}
if shouldHideMinContextSwitchForP1(state, name) {
continue
}
index++
desc := strings.TrimSpace(schemaItem.Desc)
if desc == "" {
desc = "无描述"
}
lines = append(lines, fmt.Sprintf("%d. %s%s", i+1, name, desc))
lines = append(lines, fmt.Sprintf("%d. %s%s", index, name, desc))
doc := parseExecuteToolSchema(schemaItem.SchemaText)
paramSummary := renderExecuteToolParamSummary(doc.Parameters)
lines = append(lines, " 参数:"+paramSummary)
returnType, returnSample := renderExecuteToolReturnHint(name)
lines = append(lines, " 返回类型:"+returnType)
lines = append(lines, " 返回示例:"+returnSample)
if shouldRenderExecuteToolReturnSample(name) {
lines = append(lines, " 返回示例:"+returnSample)
}
if callSample := renderExecuteToolCallHint(name); strings.TrimSpace(callSample) != "" {
lines = append(lines, " 调用示例:"+callSample)
}
}
if index == 0 {
return ""
}
return strings.Join(lines, "\n")
}
// renderExecuteToolReturnHint 返回工具的返回类型 + 最小示例。
func shouldRenderExecuteToolReturnSample(toolName string) bool {
switch strings.ToLower(strings.TrimSpace(toolName)) {
case "query_available_slots",
"query_target_tasks",
"queue_pop_head",
"queue_status",
"queue_apply_head_move",
"queue_skip_head",
"web_search",
"web_fetch",
"analyze_health",
"analyze_rhythm",
"analyze_tolerance",
"upsert_task_class":
return true
default:
return false
}
}
func renderExecuteToolCallHint(toolName string) string {
switch strings.ToLower(strings.TrimSpace(toolName)) {
case "upsert_task_class":
return `{"name":"upsert_task_class","arguments":{"task_class":{"name":"线性代数复习","mode":"auto","start_date":"2026-06-01","end_date":"2026-06-20","subject_type":"quantitative","difficulty_level":"high","cognitive_intensity":"high","config":{"total_slots":8,"strategy":"steady","allow_filler_course":false,"excluded_slots":[1,11],"excluded_days_of_week":[6,7]},"items":[{"order":1,"content":"行列式定义与基础计算"},{"order":2,"content":"矩阵及其运算规则"},{"order":3,"content":"逆矩阵与矩阵的秩"}]}}}`
default:
return ""
}
}
func renderExecuteToolReturnHint(toolName string) (returnType string, sample string) {
returnType = "string自然语言文本"
switch strings.ToLower(strings.TrimSpace(toolName)) {
case "get_overview":
return returnType, "规划窗口共27天...课程占位条目34个...任务清单(全量,已过滤课程)..."
return returnType, "规划窗口共27天...课程占位条目34个...任务清单(已过滤课程)..."
case "get_task_info":
return returnType, "[35]第一章随机事件与概率 | 状态:已预排(suggested) | 占用时段第3天第5-6节"
return returnType, "[35] 第一章随机事件与概率 | 状态:已预排(suggested) | 占用时段第3天第5-6节"
case "query_available_slots":
return "stringJSON字符串", `{"tool":"query_available_slots","count":12,"strict_count":8,"embedded_count":4,"slots":[{"day":5,"week":12,"day_of_week":3,"slot_start":1,"slot_end":2,"slot_type":"empty"}]}`
case "query_target_tasks":
@@ -276,7 +374,7 @@ func renderExecuteToolReturnHint(toolName string) (returnType string, sample str
case "swap":
return returnType, "交换完成:[35]... ↔ [36]..."
case "batch_move":
return returnType, "批量移动完成2个任务全部成功。单次最多2条"
return returnType, "批量移动完成2 个任务全部成功。"
case "spread_even":
return returnType, "均匀化调整完成:共处理 6 个任务,候选坑位 24 个。"
case "min_context_switch":
@@ -287,6 +385,14 @@ func renderExecuteToolReturnHint(toolName string) (returnType string, sample str
return "stringJSON字符串", `{"tool":"web_search","query":"检索关键词","count":2,"items":[{"title":"搜索结果标题","url":"https://example.com/page","snippet":"摘要片段...","domain":"example.com","published_at":"2025-04-10"}]}`
case "web_fetch":
return "stringJSON字符串", `{"tool":"web_fetch","url":"https://example.com/page","title":"页面标题","content":"正文内容...","truncated":false}`
case "analyze_health":
return "stringJSON字符串", `{"tool":"analyze_health","success":true,"metrics":{"rhythm":{"avg_switches_per_day":1.1,"max_switch_count":4,"heavy_adjacent_days":2,"same_type_transition_ratio":0.58,"block_balance":0,"fragmented_count":0,"compressed_run_count":0},"tightness":{"locally_movable_task_count":3,"avg_local_alternative_slots":1.7,"cross_class_swap_options":1,"forced_heavy_adjacent_days":0,"tightness_level":"tight"},"can_close":false},"decision":{"should_continue_optimize":true,"recommended_operation":"swap","primary_problem":"第4天存在高认知背靠背","candidates":[{"candidate_id":"swap_35_44","tool":"swap","arguments":{"task_a":35,"task_b":44}}]}}`
case "analyze_rhythm":
return "stringJSON字符串", `{"tool":"analyze_rhythm","success":true,"metrics":{"overview":{"avg_switches_per_day":3.4,"max_switch_day":4,"max_switch_count":5,"heavy_adjacent_days":2,"long_high_intensity_days":1,"same_type_transition_ratio":0.42}}}`
case "analyze_tolerance":
return "stringJSON字符串", `{"tool":"analyze_tolerance","success":true,"metrics":{"overall":{"fragmentation_rate":0.52,"days_without_buffer":1}}}`
case "upsert_task_class":
return "stringJSON字符串", `{"tool":"upsert_task_class","success":true,"task_class_id":123,"created":true,"validation":{"ok":true,"issues":[]},"error":"","error_code":""}`
default:
return returnType, "自然语言结果(成功/失败原因/关键数据摘要)。"
}
@@ -353,12 +459,11 @@ func renderExecuteToolParamSummary(parameters map[string]any) string {
return strings.Join(parts, "")
}
// collectExecuteLoopRecords 从历史中提取 ReAct 记录
// collectExecuteLoopRecords 从 history 里提取 thought + tool_call + observation 三元组
//
// 提取策略:
// 1. 以 assistant tool_call 消息为主键;
// 2. 关联同 ToolCallID 的 tool result 作为 observation
// 3. 向前回溯最近一条 assistant 文本消息作为 thought/reason。
// 1. 以 assistant tool_call 为主记录。
// 2. 用 ToolCallID 去关联 tool observation保证同轮绑定。
// 3. thought 只向前取最近一条 assistant 纯文本消息,不跨越到更早的工具调用之前做复杂回溯。
func collectExecuteLoopRecords(history []*schema.Message) []executeLoopRecord {
if len(history) == 0 {
return nil
@@ -381,12 +486,14 @@ func collectExecuteLoopRecords(history []*schema.Message) []executeLoopRecord {
if msg == nil || msg.Role != schema.Assistant || len(msg.ToolCalls) == 0 {
continue
}
thought := findExecuteThoughtBefore(history, i)
for _, call := range msg.ToolCalls {
toolName := strings.TrimSpace(call.Function.Name)
if toolName == "" {
toolName = "unknown_tool"
}
toolArgs := compactExecuteText(call.Function.Arguments, 160)
if toolArgs == "" {
toolArgs = "{}"
@@ -424,10 +531,9 @@ func findExecuteThoughtBefore(history []*schema.Message, index int) string {
continue
}
content := compactExecuteText(msg.Content, 140)
if content == "" {
continue
if content != "" {
return content
}
return content
}
return "(未记录)"
}
@@ -456,18 +562,116 @@ func hasExecuteRoughBuildDone(ctx *newagentmodel.ConversationContext) bool {
return false
}
// conversationTurn 表示对话历史中的一轮交互user 或 assistant speak
type conversationTurn struct {
Role string
Content string
func renderExecuteLatestAnalyzeSummary(ctx *newagentmodel.ConversationContext) string {
record, ok := findExecuteLatestToolRecord(ctx, map[string]struct{}{
"analyze_health": {},
"analyze_rhythm": {},
"analyze_tolerance": {},
})
if !ok {
return ""
}
return fmt.Sprintf("%s -> %s", record.ToolName, record.Observation)
}
// collectExecuteConversationTurns 从历史消息中提取 user + assistant speak 对话流。
func renderExecuteLatestMutationSummary(ctx *newagentmodel.ConversationContext) string {
record, ok := findExecuteLatestToolRecord(ctx, map[string]struct{}{
"place": {},
"move": {},
"swap": {},
"batch_move": {},
"unplace": {},
"queue_apply_head_move": {},
"spread_even": {},
"min_context_switch": {},
})
if !ok {
return ""
}
return fmt.Sprintf("%s -> %s", record.ToolName, record.Observation)
}
func findExecuteLatestToolRecord(ctx *newagentmodel.ConversationContext, allowSet map[string]struct{}) (executeLatestToolRecord, bool) {
if ctx == nil || len(allowSet) == 0 {
return executeLatestToolRecord{}, false
}
history := ctx.HistorySnapshot()
if len(history) == 0 {
return executeLatestToolRecord{}, false
}
toolNameByCallID := make(map[string]string, len(history))
for _, msg := range history {
if msg == nil || msg.Role != schema.Assistant || len(msg.ToolCalls) == 0 {
continue
}
for _, call := range msg.ToolCalls {
callID := strings.TrimSpace(call.ID)
toolName := strings.TrimSpace(call.Function.Name)
if callID == "" || toolName == "" {
continue
}
toolNameByCallID[callID] = toolName
}
}
for i := len(history) - 1; i >= 0; i-- {
msg := history[i]
if msg == nil || msg.Role != schema.Tool {
continue
}
callID := strings.TrimSpace(msg.ToolCallID)
if callID == "" {
continue
}
toolName := strings.TrimSpace(toolNameByCallID[callID])
if toolName == "" {
continue
}
if _, ok := allowSet[toolName]; !ok {
continue
}
return executeLatestToolRecord{
ToolName: toolName,
Observation: summarizeExecuteToolObservation(msg.Content),
}, true
}
return executeLatestToolRecord{}, false
}
func summarizeExecuteToolObservation(raw string) string {
content := strings.TrimSpace(raw)
if content == "" {
return "无返回内容。"
}
var payload map[string]any
if err := json.Unmarshal([]byte(content), &payload); err == nil && len(payload) > 0 {
if toolName := strings.TrimSpace(asExecuteString(payload["tool"])); toolName == "analyze_health" {
return summarizeExecuteAnalyzeHealthObservationV2(payload)
}
for _, key := range []string{"result", "message", "reason", "error"} {
if text := strings.TrimSpace(asExecuteString(payload[key])); text != "" {
return compactExecuteText(text, 120)
}
}
if success, ok := payload["success"].(bool); ok {
if success {
return "执行成功。"
}
return "执行失败。"
}
}
return compactExecuteText(content, 120)
}
// collectExecuteConversationTurns 只提取 user 和 assistant speak。
//
// 提取规则:
// 1. 只保留 user 消息(排除 correction prompt和 assistant speak 消息(非空 Content 且无 ToolCalls
// 2. 全量保留不再限制轮数和单条长度token 预算由 execute 层统一管理);
// 3. 返回的条目按原始时间顺序排列。
// 1. 过滤 correction prompt避免把后端纠错提示伪装成用户真实意图。
// 2. 过滤 assistant tool_call 消息,避免 msg1 和 msg2 重复。
// 3. 保持原始顺序,不在这里裁剪长度。
func collectExecuteConversationTurns(history []*schema.Message) []conversationTurn {
if len(history) == 0 {
return nil
@@ -556,11 +760,44 @@ func renderExecuteTaskClassIDs(state *newagentmodel.CommonState) string {
return fmt.Sprintf("task_class_ids=[%s]", strings.Join(parts, ","))
}
// renderExecuteMemoryContext 提取 execute 阶段要注入 msg3 的记忆文本
//
// 1. 只读取统一的 memory_context避免把其他 pinned block 误塞进 prompt。
// 2. 为空时直接返回空串,保持 msg3 干净。
// 3. 复用统一记忆渲染逻辑,保证各阶段记忆入口一致。
// renderExecuteMemoryContext 复用统一记忆入口,避免 execute 私自拼接其他 pinned block
func renderExecuteMemoryContext(ctx *newagentmodel.ConversationContext) string {
return renderUnifiedMemoryContext(ctx)
}
func renderTaskClassUpsertRuntime(state *newagentmodel.CommonState) string {
if state == nil || !state.TaskClassUpsertLastTried {
return ""
}
lines := make([]string, 0, 4)
if state.TaskClassUpsertLastSuccess {
lines = append(lines, "- 最近一次 upsert_task_class 成功。")
} else {
lines = append(lines, "- 最近一次 upsert_task_class 失败。")
}
if state.TaskClassUpsertConsecutiveFailures > 0 {
lines = append(lines, fmt.Sprintf("- 连续失败次数:%d", state.TaskClassUpsertConsecutiveFailures))
}
if len(state.TaskClassUpsertLastIssues) > 0 {
lines = append(lines, "- 需要优先处理 validation.issues")
for _, issue := range state.TaskClassUpsertLastIssues {
trimmed := strings.TrimSpace(issue)
if trimmed == "" {
continue
}
lines = append(lines, " - "+trimmed)
}
}
if !state.TaskClassUpsertLastSuccess {
lines = append(lines, "- 在 issues 处理完之前,不要用 done 收口。")
}
return strings.Join(lines, "\n")
}
func shouldHideMinContextSwitchForP1(state *newagentmodel.CommonState, toolName string) bool {
if strings.TrimSpace(toolName) != "min_context_switch" {
return false
}
return true
}

View File

@@ -0,0 +1,33 @@
package newagentprompt
import (
"fmt"
"strings"
)
func fallbackExecuteText(value string, fallback string) string {
if text := strings.TrimSpace(value); text != "" {
return text
}
return fallback
}
func compactHealthAny(value any) string {
if value == nil {
return ""
}
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
case bool:
if typed {
return "true"
}
return "false"
case int:
return fmt.Sprintf("%d", typed)
case float64:
return fmt.Sprintf("%.0f", typed)
}
return strings.TrimSpace(fmt.Sprintf("%v", value))
}

View File

@@ -0,0 +1,106 @@
package newagentprompt
import (
"fmt"
"strings"
)
// summarizeExecuteAnalyzeHealthObservationV2 把 analyze_health 结果压成更短的单行摘要。
//
// 职责边界:
// 1. 只保留 execute 下一步真正需要消费的裁决字段,不重复展开整份 metrics。
// 2. 若存在候选,会优先展示“候选数量 + 前两个候选工具”,帮助模型迅速进入选择题。
// 3. 这里只做摘要,不负责改变决策含义;真实判定仍以 analyze_health 原始 JSON 为准。
func summarizeExecuteAnalyzeHealthObservationV2(payload map[string]any) string {
decision, _ := payload["decision"].(map[string]any)
metrics, _ := payload["metrics"].(map[string]any)
rhythmMetrics, _ := metrics["rhythm"].(map[string]any)
tightnessMetrics, _ := metrics["tightness"].(map[string]any)
candidates, _ := decision["candidates"].([]any)
parts := make([]string, 0, 7)
if text := compactHealthAny(decision["should_continue_optimize"]); text != "" {
parts = append(parts, "continue="+text)
}
if text := strings.TrimSpace(asExecuteString(decision["recommended_operation"])); text != "" {
parts = append(parts, "recommended="+text)
}
if text := strings.TrimSpace(asExecuteString(tightnessMetrics["tightness_level"])); text != "" {
parts = append(parts, "tightness="+text)
}
if text := buildBlockBalanceSummary(rhythmMetrics); text != "" {
parts = append(parts, text)
}
if text := compactHealthAny(decision["is_forced_imperfection"]); text != "" {
parts = append(parts, "forced="+text)
}
if len(candidates) > 0 {
parts = append(parts, fmt.Sprintf("candidates=%d", len(candidates)))
if preview := compactHealthCandidatePreview(candidates); preview != "" {
parts = append(parts, "options="+preview)
}
}
if text := strings.TrimSpace(asExecuteString(decision["primary_problem"])); text != "" {
parts = append(parts, "problem="+compactExecuteText(text, 36))
}
if len(parts) == 0 {
return "返回了健康裁决结果。"
}
return strings.Join(parts, " | ")
}
// buildBlockBalanceSummary 把 block_balance 连同正负来源一起压成单段摘要。
//
// 职责边界:
// 1. 这里只做 execute 摘要层的可读性补充,避免 LLM 只看到 balance=0 却看不到来源。
// 2. 不改变 analyze_health 原始 JSON 结构;原始结构仍由 metrics.rhythm 提供完整字段。
// 3. 若三个字段都缺失,则直接留空,避免构造误导性的默认值。
func buildBlockBalanceSummary(rhythmMetrics map[string]any) string {
if len(rhythmMetrics) == 0 {
return ""
}
blockBalance := compactHealthAny(rhythmMetrics["block_balance"])
fragmentedCount := compactHealthAny(rhythmMetrics["fragmented_count"])
compressedCount := compactHealthAny(rhythmMetrics["compressed_run_count"])
if blockBalance == "" && fragmentedCount == "" && compressedCount == "" {
return ""
}
return fmt.Sprintf(
"block_balance=%s(fragmented=%s,compressed=%s)",
fallbackExecuteText(blockBalance, "?"),
fallbackExecuteText(fragmentedCount, "?"),
fallbackExecuteText(compressedCount, "?"),
)
}
func compactHealthCandidatePreview(candidates []any) string {
if len(candidates) == 0 {
return ""
}
preview := make([]string, 0, 2)
for _, raw := range candidates {
item, _ := raw.(map[string]any)
if len(item) == 0 {
continue
}
id := strings.TrimSpace(asExecuteString(item["candidate_id"]))
tool := strings.TrimSpace(asExecuteString(item["tool"]))
if id == "" && tool == "" {
continue
}
switch {
case id != "" && tool != "":
preview = append(preview, id+":"+tool)
case id != "":
preview = append(preview, id)
default:
preview = append(preview, tool)
}
if len(preview) >= 2 {
break
}
}
return strings.Join(preview, ",")
}

View File

@@ -0,0 +1,104 @@
package newagentprompt
import (
"fmt"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
)
// renderExecuteNextStepHintV2 生成 execute.msg3 的轻量方向提示。
//
// 设计目标:
// 1. 主动优化模式下,只强调“先 analyze_health再从 candidates 里选”,不再散发额外搜索暗示。
// 2. 普通链路仍保留必要的业务引导,避免误伤用户明确提出的普通调整请求。
// 3. 提示只给方向,不替模型代填最终写参数。
func renderExecuteNextStepHintV2(
state *newagentmodel.CommonState,
latestAnalyze string,
latestMutation string,
roughBuildDone bool,
) string {
if state == nil {
return ""
}
activeDomain := strings.TrimSpace(state.ActiveToolDomain)
activePacks := newagenttools.ResolveEffectiveToolPacks(state.ActiveToolDomain, state.ActiveToolPacks)
if state.ActiveOptimizeOnly {
switch {
case activeDomain == "" && roughBuildDone:
return "当前是粗排后主动优化专用模式;先激活 schedule并只围绕 analyze_health -> move/swap 候选闭环推进。"
case !state.HealthCheckDone:
return "当前是粗排后主动优化专用模式;先调 analyze_health等待后端给出 candidates再做选择。"
case !state.HealthIsFeasible || strings.EqualFold(strings.TrimSpace(state.HealthRecommendedOperation), "ask_user"):
return "analyze_health 已判定当前更像时间窗或信息约束问题;不要继续挪动,先把冲突或缺失点明确告诉用户。"
case !state.HealthShouldContinueOptimize:
return "analyze_health 已判定当前无需继续主动优化;若用户没有新增要求,直接收口。"
default:
return "当前是粗排后主动优化专用模式;直接从 analyze_health 的 decision.candidates 里选一个合法 move/swap 执行,不要再自己搜索读工具。"
}
}
if activeDomain == "schedule" && state.HealthCheckDone {
switch {
case !state.HealthShouldContinueOptimize && state.HealthIsForcedImperfection:
return fmt.Sprintf(
"analyze_health 已判定当前更像约束代价tightness=%s主问题=%s。优先考虑收口。",
fallbackExecuteText(state.HealthTightnessLevel, "unknown"),
fallbackExecuteText(state.HealthPrimaryProblem, "无"),
)
case !state.HealthShouldContinueOptimize:
return fmt.Sprintf(
"analyze_health 已判定当前没有更值得继续处理的局部问题:%s。若用户未追加新要求优先收口。",
fallbackExecuteText(state.HealthPrimaryProblem, "当前可直接收口"),
)
case state.HealthStagnationCount > 0:
return fmt.Sprintf(
"最近诊断已连续 %d 次无明显改善;若本轮仍不能让主问题变轻,优先收口。当前主问题:%s。",
state.HealthStagnationCount,
fallbackExecuteText(state.HealthPrimaryProblem, "无"),
)
case strings.EqualFold(strings.TrimSpace(state.HealthRecommendedOperation), "swap"):
return fmt.Sprintf(
"当前主问题:%s。优先在已有落位之间做局部 swap别把问题扩散到更远的天数。",
fallbackExecuteText(state.HealthPrimaryProblem, "无"),
)
case strings.EqualFold(strings.TrimSpace(state.HealthRecommendedOperation), "move"):
return fmt.Sprintf(
"当前主问题:%s。若要 move只在近范围合法落点里小修不要做全窗口搜索。",
fallbackExecuteText(state.HealthPrimaryProblem, "无"),
)
}
}
if activeDomain == "" {
if roughBuildDone {
return `先激活 schedule 业务域;当前是粗排后的微调场景,通常至少需要 mutation+analyze。若要按统一条件逐个处理一批任务再加 packs=["queue"]。`
}
return `先判断当前任务属于哪个业务域,再用 context_tools_add 激活对应工具。`
}
if activeDomain == "schedule" &&
strings.Contains(latestMutation, "batch_move") &&
(strings.Contains(latestMutation, "缺少") || strings.Contains(latestMutation, "无效")) {
return `当前 batch_move 路径受参数约束;若要处理一批符合同一条件的任务,优先加 packs=["queue"] 逐个处理。`
}
if activeDomain == "schedule" &&
latestAnalyze != "" &&
strings.Contains(latestAnalyze, "metrics") &&
!containsExecutePack(activePacks, newagenttools.ToolPackQueue) {
return `若诊断已经完成,下一步应转入读事实或写操作,不要重复 analyze_health涉及同类批量任务时优先考虑 packs=["queue"]。`
}
if activeDomain == "taskclass" &&
state.TaskClassUpsertLastTried &&
!state.TaskClassUpsertLastSuccess {
return `先根据 validation.issues 补齐缺失字段,再重试 upsert_task_class不要直接收口。`
}
return ""
}

View File

@@ -0,0 +1,318 @@
package newagentprompt
import (
"fmt"
"strings"
"time"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
)
const (
executeRulePackCoreMin = "core_min"
executeRulePackSafetyHard = "safety_hard"
executeRulePackContextProtocol = "context_protocol"
executeRulePackModePlan = "mode_plan"
executeRulePackModeReAct = "mode_react"
executeRulePackDomainSchedule = "domain_schedule"
executeRulePackDomainTaskClass = "domain_taskclass"
executeRulePackScheduleMutation = "schedule_mutation"
executeRulePackScheduleAnalyze = "schedule_analyze"
executeRulePackScheduleWeb = "schedule_web"
executeRulePackMicroRoughDone = "micro_rough_build_done"
executeRulePackMicroDiagLoop = "micro_diag_tune_loop"
executeRulePackMicroQueue = "micro_queue_chain"
executeRulePackMicroTaskRetry = "micro_taskclass_retry"
)
const executeSystemPromptBaseWithPlan = `
你叫 SmartMate是时伴SmartMate的中文 AI 排程伙伴,面向大学生提供陪伴式日程管理与日常协助。
你擅长课表与任务安排、任务管理、学习规划和随口记,也可以正常回答日常问答、生活建议、信息整理、分析讨论等非排程问题。
你的目标是像一个越用越懂用户的伙伴一样,结合历史对话、长期记忆和当前上下文,给出贴心、清晰、可信的帮助。
你当前处于“计划执行”模式。你必须围绕当前计划步骤推进,并通过 SMARTFLOW_DECISION 输出结构化动作。`
const executeSystemPromptBaseReAct = `
你叫 SmartMate是时伴SmartMate的中文 AI 排程伙伴,面向大学生提供陪伴式日程管理与日常协助。
你擅长课表与任务安排、任务管理、学习规划和随口记,也可以正常回答日常问答、生活建议、信息整理、分析讨论等非排程问题。
你的目标是像一个越用越懂用户的伙伴一样,结合历史对话、长期记忆和当前上下文,给出贴心、清晰、可信的帮助。
你当前处于“自由执行ReAct”模式。你需要根据当前目标自主推进、按需调用工具并通过 SMARTFLOW_DECISION 输出结构化动作。`
type executeRulePack struct {
Name string
Content string
}
// renderExecuteRulePackSection 渲染 execute.msg0 的动态规则包区域。
//
// 1. 这里负责“选哪些包 + 以什么顺序展示”,不负责工具目录本身。
// 2. 固定先放通用硬约束,再放 mode/domain/micro 包,保证模型先读边界后读特例。
// 3. 如果没有任何可展示规则包,则直接返回空串,避免无意义占位。
func renderExecuteRulePackSection(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) (string, []string) {
packs := selectExecuteRulePacks(state, ctx)
if len(packs) == 0 {
return "", nil
}
lines := []string{"执行规则包msg0 动态注入):"}
names := make([]string, 0, len(packs))
for _, pack := range packs {
content := strings.TrimSpace(pack.Content)
if content == "" {
continue
}
lines = append(lines, fmt.Sprintf("[%s]", pack.Name))
lines = append(lines, content)
names = append(names, pack.Name)
}
if len(names) == 0 {
return "", nil
}
return strings.Join(lines, "\n"), names
}
func selectExecuteRulePacks(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) []executeRulePack {
selected := make([]executeRulePack, 0, 8)
seen := map[string]bool{}
appendPack := func(pack executeRulePack) {
name := strings.TrimSpace(pack.Name)
if name == "" || seen[name] {
return
}
seen[name] = true
selected = append(selected, pack)
}
appendPack(buildExecuteCoreMinPack())
appendPack(buildExecuteSafetyHardPack())
appendPack(buildExecuteContextProtocolPack())
if state != nil && state.HasPlan() {
appendPack(buildExecuteModePlanPack())
} else {
appendPack(buildExecuteModeReActPack())
}
switch normalizeExecuteToolDomain(readExecuteActiveToolDomain(state)) {
case "schedule":
activePacks := readExecuteActiveToolPacks(state)
appendPack(buildExecuteSchedulePack())
if hasExecutePack(activePacks, newagenttools.ToolPackQueue) {
appendPack(buildExecuteQueueMicroPack())
}
if hasExecutePack(activePacks, newagenttools.ToolPackMutation) {
appendPack(buildExecuteScheduleMutationPack())
}
if hasExecutePack(activePacks, newagenttools.ToolPackAnalyze) {
appendPack(buildExecuteScheduleAnalyzePackV2())
}
if hasExecutePack(activePacks, newagenttools.ToolPackWeb) {
appendPack(buildExecuteScheduleWebPack())
}
case "taskclass":
appendPack(buildExecuteTaskClassPack())
}
if hasExecuteRoughBuildDone(ctx) {
appendPack(buildExecuteRoughDoneMicroPack())
}
if shouldInjectExecuteDiagLoopPack(state, ctx) {
appendPack(buildExecuteDiagLoopMicroPackV2())
}
if state != nil && state.TaskClassUpsertLastTried && !state.TaskClassUpsertLastSuccess {
appendPack(buildExecuteTaskClassRetryMicroPack())
}
return selected
}
func readExecuteActiveToolDomain(state *newagentmodel.CommonState) string {
if state == nil {
return ""
}
return strings.TrimSpace(state.ActiveToolDomain)
}
func readExecuteActiveToolPacks(state *newagentmodel.CommonState) []string {
if state == nil {
return nil
}
return newagenttools.ResolveEffectiveToolPacks(state.ActiveToolDomain, state.ActiveToolPacks)
}
func hasExecutePack(packs []string, target string) bool {
target = strings.ToLower(strings.TrimSpace(target))
if target == "" {
return false
}
for _, pack := range packs {
if strings.ToLower(strings.TrimSpace(pack)) == target {
return true
}
}
return false
}
// containsExecutePack 兼容旧调用点。
//
// 1. 这里只做别名转发,不引入第二套判断口径。
// 2. 保留它是为了避免下一轮再因为历史调用点而误删。
func containsExecutePack(packs []string, target string) bool {
return hasExecutePack(packs, target)
}
func normalizeExecuteToolDomain(domain string) string {
switch strings.ToLower(strings.TrimSpace(domain)) {
case "schedule":
return "schedule"
case "taskclass":
return "taskclass"
default:
return ""
}
}
func buildExecuteCoreMinPack() executeRulePack {
return executeRulePack{
Name: executeRulePackCoreMin,
Content: strings.TrimSpace(fmt.Sprintf(`
- 当前时间锚点:%s。涉及“今天/明天/本周”等相对时间时,先按该锚点换算。
- 用户意图优先:只推进用户当前明确要求;未明确部分优先 ask_user。
- 先事实后动作:优先读工具补齐事实,再决定下一步。
- 只要决定调用 place/move/swap/batch_move/unplace 这类写工具,就必须输出 action=confirmcontinue + 写工具无效。
- 输出格式固定:先 <SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>,再输出用户可见正文。`,
buildExecuteNowAnchorLine())),
}
}
func buildExecuteNowAnchorLine() string {
now := time.Now()
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
return fmt.Sprintf("%s%s%s", now.Format("2006-01-02 15:04:05 -07:00"), weekdays[int(now.Weekday())], now.Format("MST"))
}
func buildExecuteSafetyHardPack() executeRulePack {
return executeRulePack{
Name: executeRulePackSafetyHard,
Content: strings.TrimSpace(`
- 严禁伪造工具结果;若新结果与既有事实冲突,先重查一次再决定。
- 工具参数必须严格使用 schema 字段名,禁止自造别名。
- JSON 只保留当前 action 必需字段;不要输出空字符串、空对象、空数组或 null 占位。
- P1 阶段禁止调用 min_context_switch。
- 连续两轮同类读查询后,必须转执行 / ask_user / 明确说明阻塞,不能无限空转。`),
}
}
func buildExecuteContextProtocolPack() executeRulePack {
return executeRulePack{
Name: executeRulePackContextProtocol,
Content: strings.TrimSpace(`
- msg0 动态区初始仅保留 context_tools_add / context_tools_remove。
- 需要业务工具前先 context_tools_add排程用 domain="schedule",任务类写入用 domain="taskclass"。
- schedule 可选 packs=["mutation","analyze","detail_read","deep_analyze","queue","web"]core 固定注入,不要显式传 core。
- 只在业务方向切换时再 removedone 后的动态区清理由系统自动完成,不必手动 remove。
- 如果目标工具当前不在可用列表,先 add 对应 domain / packs再继续执行。`),
}
}
func buildExecuteModePlanPack() executeRulePack {
return executeRulePack{
Name: executeRulePackModePlan,
Content: strings.TrimSpace(`
- 当前为计划执行模式:必须围绕当前计划步骤推进。
- 未满足 done_when 时,只能 continue / confirm / ask_user禁止 next_plan。
- next_plan / done 时goal_check 必须是字符串,并对照 done_when 给出完成证据。
- 禁止跳步执行后续计划。`),
}
}
func buildExecuteModeReActPack() executeRulePack {
return executeRulePack{
Name: executeRulePackModeReAct,
Content: strings.TrimSpace(`
- 当前为自由执行ReAct模式可自主决定 continue / confirm / ask_user / done / abort。
- 如果关键事实无法通过工具补齐,优先 ask_user不做猜测落库。
- 自主推进时要小步快跑,优先闭合当前局部问题,不要发散成大范围开放搜索。`),
}
}
func buildExecuteSchedulePack() executeRulePack {
return executeRulePack{
Name: executeRulePackDomainSchedule,
Content: strings.TrimSpace(`
- 当前业务域为 schedule只处理当前目标任务类不重排无关内容。
- existing 只作事实参考;真正可调对象优先看 suggested。
- 同任务类内部顺序必须保持,任何越过前驱/后继边界的移动都会被写工具拒绝。`),
}
}
func buildExecuteScheduleMutationPack() executeRulePack {
return executeRulePack{
Name: executeRulePackScheduleMutation,
Content: strings.TrimSpace(`
- mutation 包负责真正落日程写操作place / move / swap / batch_move / unplace。
- 写操作必须走 action=confirm不要在 continue 里偷跑写工具。
- 若是主动优化链路,优先在后端给出的合法候选中选择,不要自己再全窗搜索新坑位。`),
}
}
func buildExecuteQueueMicroPack() executeRulePack {
return executeRulePack{
Name: executeRulePackMicroQueue,
Content: strings.TrimSpace(`
- queue 包适合“按同一条件逐个处理一批任务”的场景,例如把所有早八任务依次挪走。
- query_target_tasks 可结合 enqueue=true 先把候选任务入队,再用 queue_pop_head / queue_apply_head_move / queue_skip_head 顺序处理。
- 当你需要连续处理多条相似任务时,优先走 queue避免把整批任务细节长期堆在上下文里。`),
}
}
func buildExecuteScheduleWebPack() executeRulePack {
return executeRulePack{
Name: executeRulePackScheduleWeb,
Content: strings.TrimSpace(`
- web 包只用于补充通用学习资料或通识信息不用于捏造个人时间、考试时间、DDL 或排程事实。
- web_search 先粗搜web_fetch 再抓正文;不确定时宁可不用,也不要把网页结果当成排程事实直接写入。`),
}
}
func buildExecuteTaskClassPack() executeRulePack {
return executeRulePack{
Name: executeRulePackDomainTaskClass,
Content: strings.TrimSpace(`
- taskclass 域只负责生成或修正任务类,不代表已经开始排程。
- upsert_task_class 若返回 validation.ok=false必须先处理 validation.issues再考虑重试或 ask_user。
- subject_type / difficulty_level / cognitive_intensity 是任务类语义画像必填项;优先静默推断,只有确实无法判断时再 ask_user。
- excluded_slots 取值应与系统节次定义一致excluded_days_of_week 使用 1~7 表示周一到周日。`),
}
}
func buildExecuteRoughDoneMicroPack() executeRulePack {
return executeRulePack{
Name: executeRulePackMicroRoughDone,
Content: strings.TrimSpace(`
- 已有 rough_build_done本轮以微调为主不要把任务重新当成“未排入”再全量 place。
- 若当前问题已经可接受,应优先收口,不要为了追求完美继续反复局部打磨。`),
}
}
func buildExecuteTaskClassRetryMicroPack() executeRulePack {
return executeRulePack{
Name: executeRulePackMicroTaskRetry,
Content: strings.TrimSpace(`
- 最近一次 upsert_task_class 失败时,优先围绕 validation.issues 修补。
- 问题未解决前,不要用 done 假装收口;要么重试,要么 ask_user 补关键信息。`),
}
}
func shouldInjectExecuteDiagLoopPack(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext) bool {
if state == nil || !hasExecuteRoughBuildDone(ctx) {
return false
}
if normalizeExecuteToolDomain(readExecuteActiveToolDomain(state)) != "schedule" {
return false
}
activePacks := readExecuteActiveToolPacks(state)
return hasExecutePack(activePacks, newagenttools.ToolPackAnalyze) &&
hasExecutePack(activePacks, newagenttools.ToolPackMutation)
}

View File

@@ -0,0 +1,27 @@
package newagentprompt
import "strings"
func buildExecuteScheduleAnalyzePackV2() executeRulePack {
return executeRulePack{
Name: executeRulePackScheduleAnalyze,
Content: strings.TrimSpace(`
- analyze 包已激活:优先使用 analyze_health 判断“现在还值不值得继续主动优化”,不要把它当成全能体检表。
- 若需要维度级细诊断(如 rhythm再 add packs=["deep_analyze"],不要默认把所有分析都铺开。
- 在主动优化专用模式里analyze_health 会直接返回 decision.candidates这些就是后端已经验证合法、并且复诊后确实变好的 move/swap 候选。
- 一旦 decision.candidates 已经给出,下一步应直接从候选里选一个去执行;不要再自己搜索 query_target_tasks / query_available_slots。
- 若 analyze_health 显示 should_continue_optimize=false优先收口不要因为“理论上还还能动”就继续局部修补。`),
}
}
func buildExecuteDiagLoopMicroPackV2() executeRulePack {
return executeRulePack{
Name: executeRulePackMicroDiagLoop,
Content: strings.TrimSpace(`
- 粗排后的主动优化允许多轮 execute但每一轮都必须围绕“当前主问题”做局部、小范围、可解释的调整。
- 在主动优化专用模式里analyze_health 负责“出候选题”,你只负责在 decision.candidates 里做选择,不负责重新全窗搜点。
- 若当前问题主要来自时间窗过紧,或所有合法候选都只是平移没有变轻,应接受局部不完美并收口。
- 若连续两轮诊断没有明显改善,或当前 recommended_operation 已经是 close应优先收口。
- 主动优化优先在已有落位之间做选择swap 优先move 次之;不要做全窗口搜索。`),
}
}

View File

@@ -8,58 +8,46 @@ import (
"github.com/cloudwego/eino/schema"
)
const planSystemPrompt = `
你是 SmartMate 的规划器。
你的职责不是直接执行任务,而是先把用户意图拆成一组清晰、稳定、可逐步执行的自然语言计划,并严格按后端约定的 JSON 协议输出。
const planSystemPromptCore = `
你是 SmartMate 的规划器Planner只负责规划不负责执行
请遵守以下规则:
1. 只负责规划,不要假装已经调用了工具,也不要伪造执行结果
2. 每一轮只推进一步规划;如果信息不足,应明确转成 ask_user而不是继续硬猜
3. 若当前计划仍不完整,就继续围绕当前任务补全计划,不要跳去执行细节。
4. 若你认为计划已经完整可执行,请返回 action=plan_done并附带完整 plan_steps。
5. plan_steps 必须使用自然语言,便于后端将完整 plan 重新注入到后续上下文顶部。
6. 输出格式:先输出一行 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>然后换行输出给用户看的自然语言正文。JSON 中不要包含 speak 字段——用户可见的话放在标签之后。
7. 每次输出前先评估任务复杂度simple简单明确无复杂依赖、moderate多步操作需要一定推理、complex需要深度推理、多方案比较或复杂依赖关系
8. 粗排识别规则:若满足以下两个条件,在 action=plan_done 时附加 needs_rough_build=true 和 task_class_ids
条件1用户输入中存在"任务类 ID"字段(见上下文"任务类 ID"部分);
条件2用户意图明确是"批量安排/帮我排课/把任务类排进日程"等批量调度需求。
满足时:后端会在用户确认计划后自动运行粗排算法(硬性约束已由算法保证,无需 LLM 校验)。
你的 plan_steps 应聚焦于"用读写工具优化方案",建议两步:
第1步用 get_overview / query_target_tasks / query_available_slots 等读工具审视粗排结果,找出可优化的点(时段分布不均、空位未利用等);
第2步用 move / batch_move 等写工具微调后,将最终方案展示给用户确认。
禁止安排任何"校验/验证约束"步骤——硬性约束由算法兜底LLM 不需要操心。
最高优先级规则:
1. 意图边界:只规划用户当前明确要求,禁止擅自扩展后续动作
2. 事实边界:禁止伪造工具调用和执行结果
你会看到
- 当前阶段与轮次信息
- 已有完整 plan如果之前已经规划过
- 当前步骤(如果已存在)
- 置顶上下文块
- 可用工具摘要
- 历史对话
请基于这些输入继续规划,而不是重复忽略既有 plan。
`
规划规则
1. 每轮只做一次决策continue / ask_user / plan_done
2. 信息足够时优先 plan_done信息不足时才 ask_user且只问最小必要问题。
3. action=plan_done 时必须返回完整 plan_steps不是增量
4. plan_steps 使用自然语言描述目标与完成判定,不写执行结果。
5. 若意图满足批量排程识别条件,可在 plan_done 时附加 needs_rough_build 与 task_class_ids。
6. 可在 plan_done 时附加 context_hook执行阶段注入建议规划阶段禁止调用 context_tools_add/remove。`
// BuildPlanSystemPrompt 返回规划阶段系统提示词。
func BuildPlanSystemPrompt() string {
return strings.TrimSpace(planSystemPrompt)
parts := []string{
strings.TrimSpace(planSystemPromptCore),
BuildPlanDecisionContractText(),
}
return strings.TrimSpace(strings.Join(parts, "\n\n"))
}
// BuildPlanMessages 组装规划阶段的 messages。
//
// 职责边界:
// 1. 负责把 state + context 收敛成统一 4 段式规划阶段模型输入
// 2. 不负责解析模型输出,也不负责判断规划质量;
// 3. msg3 中的状态文本由本函数显式传入,确保统一骨架下仍能看到完整计划与阶段信息。
// 1. 规划阶段只保留 Planner 专用规则,跳过通用人格底座,避免角色指令冲突。
// 2. msg1 展示真实对话msg2 展示规划工作区msg3 仅给最小执行指令与用户本轮输入
// 3. 工具目录使用轻量版,仅提供“有什么工具”,不注入执行态大段参数示例。
func BuildPlanMessages(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, userInput string) []*schema.Message {
return buildUnifiedStageMessages(
ctx,
StageMessagesConfig{
SystemPrompt: BuildPlanSystemPrompt(),
Msg1Content: buildPlanConversationMessage(ctx),
Msg2Content: buildPlanWorkspace(state),
Msg3Suffix: BuildPlanUserPrompt(state, userInput),
Msg3Role: schema.User,
SystemPrompt: BuildPlanSystemPrompt(),
Msg1Content: buildPlanConversationMessage(ctx),
Msg2Content: buildPlanWorkspace(state),
Msg3Suffix: BuildPlanUserPrompt(state, userInput),
Msg3Role: schema.User,
SkipBaseSystemPrompt: true,
UseLiteToolCatalogMsg: true,
},
)
}
@@ -68,9 +56,9 @@ func BuildPlanMessages(state *newagentmodel.CommonState, ctx *newagentmodel.Conv
func BuildPlanUserPrompt(state *newagentmodel.CommonState, userInput string) string {
var sb strings.Builder
sb.WriteString("请继续当前任务规划阶段,严格按 SMARTFLOW_DECISION 标签格式输出。\n")
sb.WriteString("目标:围绕最近对话规划工作区信息,产出一份稳定、可执行的自然语言计划;若关键信息不足,请明确 ask_user。\n\n")
sb.WriteString(BuildPlanDecisionContractText())
sb.WriteString("请继续当前任务规划,只输出一组 SMARTFLOW_DECISION 决策。\n")
sb.WriteString("请基于最近对话规划工作区推进,不要重复已有计划内容。\n")
sb.WriteString("输出格式与字段约束严格按 msg0 协议执行。\n")
trimmedInput := strings.TrimSpace(userInput)
if trimmedInput != "" {
@@ -85,40 +73,30 @@ func BuildPlanUserPrompt(state *newagentmodel.CommonState, userInput string) str
// BuildPlanDecisionContractText 返回规划阶段的输出协议说明。
func BuildPlanDecisionContractText() string {
return strings.TrimSpace(fmt.Sprintf(`
输出协议(两阶段格式
输出协议(唯一口径
1. 先输出:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
2. 再输出:给用户看的自然语言正文
先输出一行决策标签,标签内是 JSON标签之后换行输出给用户看的自然语言正文。
决策标签格式:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
JSON 字段说明:
JSON 字段:
- action只能是 %s / %s / %s
- reason给后端和日志看的简短说明
- complexity任务复杂度,只能是 simple / moderate / complex
- plan_steps仅当 action=%s 时允许返回;返回时必须是完整计划,不是增量
- complexity只能是 simple / moderate / complex
- plan_steps仅当 action=%s 时允许返回,且必须是完整计划
- plan_steps[].content步骤正文必填
- plan_steps[].done_when可选建议写"什么情况下算这一步做完"
- needs_rough_build满足粗排识别规则时为 true否则省略;为 true 时后端自动运行粗排算法
- task_class_idsneeds_rough_build=true 时必填,从上下文"任务类 ID"字段读取
- plan_steps[].done_when可选建议写完成判定
- needs_rough_build仅满足粗排识别条件时为 true否则省略
- task_class_idsneeds_rough_build=true 时必填,从上下文读取
- context_hook可选仅用于给 execute 阶段提供注入建议
- context_hook.domainschedule / taskclass
- context_hook.packsstring 数组可选core 固定注入,不要填写 core
- context_hook.reason可选说明为何建议该注入
注意:JSON 中不要包含 speak 字段。给用户看的话放在 </SMARTFLOW_DECISION> 标签之后。
合法示例:
<SMARTFLOW_DECISION>{"action":"%s","reason":"当前信息已足够继续规划","complexity":"moderate"}</SMARTFLOW_DECISION>
我先把计划再收束一下。
<SMARTFLOW_DECISION>{"action":"%s","reason":"当前时间范围仍不明确","complexity":"simple"}</SMARTFLOW_DECISION>
你更希望我优先安排今天,还是按整周来规划?
<SMARTFLOW_DECISION>{"action":"%s","reason":"当前计划已具备执行条件","complexity":"simple","plan_steps":[{"content":"先确认本周可用时间范围","done_when":"拿到明确的可用时间段列表"},{"content":"基于可用时间生成执行安排","done_when":"得到一份用户可确认的安排方案"}]}</SMARTFLOW_DECISION>
计划已经整理好了,我先给你确认一下。
`,
注意:
- JSON 中不要包含 speak 字段
- 不要在 planning 阶段调用任何工具(包括 context_tools_add/remove`,
newagentmodel.PlanActionContinue,
newagentmodel.PlanActionAskUser,
newagentmodel.PlanActionDone,
newagentmodel.PlanActionDone,
newagentmodel.PlanActionContinue,
newagentmodel.PlanActionAskUser,
newagentmodel.PlanActionDone,
))
}

View File

@@ -127,7 +127,25 @@ func renderPlanTaskClassMeta(state *newagentmodel.CommonState) string {
if tc.StartDate != "" || tc.EndDate != "" {
line += fmt.Sprintf(";日期范围:%s ~ %s", tc.StartDate, tc.EndDate)
}
if len(tc.ExcludedDaysOfWeek) > 0 {
line += fmt.Sprintf(";排除星期:%v", tc.ExcludedDaysOfWeek)
}
if tc.SubjectType != "" || tc.DifficultyLevel != "" || tc.CognitiveIntensity != "" {
line += fmt.Sprintf(";语义画像:%s/%s/%s",
planSemanticValue(tc.SubjectType),
planSemanticValue(tc.DifficultyLevel),
planSemanticValue(tc.CognitiveIntensity),
)
}
lines = append(lines, line)
}
return strings.Join(lines, "\n")
}
func planSemanticValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "未标注"
}
return trimmed
}

View File

@@ -1,6 +1,7 @@
package newagentprompt
import (
"fmt"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
@@ -45,6 +46,13 @@ type StageMessagesConfig struct {
// Msg3Role 指定第 4 条消息的角色。
// Execute 继续使用 system其余节点一般使用 user。
Msg3Role schema.RoleType
// SkipBaseSystemPrompt 为 true 时msg0 只使用节点自己的 SystemPrompt
// 不再拼接 ConversationContext.SystemPrompt。
SkipBaseSystemPrompt bool
// UseLiteToolCatalogMsg 为 true 时msg0 工具目录采用轻量模式(仅名称与职责)。
UseLiteToolCatalogMsg bool
}
// buildUnifiedStageMessages 组装统一 4 段式消息骨架。
@@ -58,7 +66,7 @@ func buildUnifiedStageMessages(
ctx *newagentmodel.ConversationContext,
config StageMessagesConfig,
) []*schema.Message {
msg0 := buildUnifiedMsg0(config.SystemPrompt, ctx)
msg0 := buildUnifiedMsg0(config.SystemPrompt, ctx, config.SkipBaseSystemPrompt, config.UseLiteToolCatalogMsg)
msg1 := buildUnifiedMsg1(config.Msg1Content)
msg2 := buildUnifiedMsg2(config.Msg2Content)
msg3 := buildUnifiedMsg3(ctx, config)
@@ -85,19 +93,72 @@ func buildUnifiedMsg3Message(content string, role schema.RoleType) *schema.Messa
// 1. 先合并基础系统提示与节点系统提示,保证模型身份稳定;
// 2. 若当前节点注入了工具 schema则附加紧凑工具目录
// 3. 若两部分都为空,则回退到最小兜底提示,避免出现空消息。
func buildUnifiedMsg0(stageSystemPrompt string, ctx *newagentmodel.ConversationContext) string {
base := strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
func buildUnifiedMsg0(stageSystemPrompt string, ctx *newagentmodel.ConversationContext, skipBaseSystemPrompt bool, useLiteToolCatalog bool) string {
base := ""
if skipBaseSystemPrompt {
base = strings.TrimSpace(stageSystemPrompt)
} else {
base = strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
}
if base == "" {
base = "你是 SmartMate 助手,请继续当前阶段。"
}
toolCatalog := renderExecuteToolCatalogCompact(ctx)
toolCatalog := renderExecuteToolCatalogCompact(ctx, nil)
if useLiteToolCatalog {
toolCatalog = renderUnifiedToolCatalogLite(ctx)
}
if toolCatalog == "" {
return base
}
return base + "\n\n" + toolCatalog
}
// renderUnifiedToolCatalogLite 渲染统一阶段可用工具的轻量目录。
//
// 1. 只展示工具名和一句话职责,避免把 execute 的参数/返回示例污染到 plan/chat/deliver。
// 2. 目录信息仅用于“能力边界感知”,不承担具体参数指导。
// 3. 当工具数量过多时保留前若干项并给出省略提示,控制 msg0 体积。
func renderUnifiedToolCatalogLite(ctx *newagentmodel.ConversationContext) string {
if ctx == nil {
return ""
}
schemas := ctx.ToolSchemasSnapshot()
if len(schemas) == 0 {
return ""
}
const maxItems = 18
lines := []string{"当前可用工具(轻量目录):"}
added := 0
for _, item := range schemas {
name := strings.TrimSpace(item.Name)
if name == "" {
continue
}
desc := strings.TrimSpace(item.Desc)
if desc == "" {
lines = append(lines, fmt.Sprintf("- %s", name))
} else {
lines = append(lines, fmt.Sprintf("- %s%s", name, desc))
}
added++
if added >= maxItems {
break
}
}
if added == 0 {
return ""
}
if len(schemas) > added {
lines = append(lines, fmt.Sprintf("- 其余 %d 个工具已省略(按需再看)。", len(schemas)-added))
}
return strings.Join(lines, "\n")
}
// buildUnifiedMsg1 返回节点自行提供的历史视图。
//
// 说明: