Version: 0.9.75.dev.260505

后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,241 @@
package agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
// buildStageMessages 组装某个阶段通用的 messages。
//
// 消息排列策略(利用 LLM 近因效应):
// 1. system prompt角色 + 阶段规则)— 始终最顶部,定义基本身份;
// 2. tool schemas能力边界— 稳定参考信息,放在 history 前即可;
// 3. history对话历史、工具调用、修正反馈— 按时间顺序排列;
// 4. pinned blocks当前计划、当前步骤、粗排结果等最新约束— 紧贴 user prompt
// 利用近因效应让 LLM 优先关注本轮最相关的约束,而非被历史消息分散注意力;
// 5. user prompt阶段性指令— 始终在末尾,是本轮回答的核心触发。
func buildStageMessages(stageSystemPrompt string, ctx *agentmodel.ConversationContext, runtimeUserPrompt string) []*schema.Message {
messages := make([]*schema.Message, 0, 4)
// 1. 合并 system prompt基础角色约束 + 阶段规则,始终在最顶部。
mergedSystemPrompt := mergeSystemPrompts(ctx, stageSystemPrompt)
if mergedSystemPrompt != "" {
messages = append(messages, schema.SystemMessage(mergedSystemPrompt))
}
// 2. 工具摘要:稳定参考信息,放在 history 前即可。
if toolText := renderToolSchemas(ctx); toolText != "" {
messages = append(messages, schema.SystemMessage(toolText))
}
// 3. 对话历史:按时间顺序,包含工具调用结果和修正反馈。
if ctx != nil {
history := ctx.HistorySnapshot()
if len(history) > 0 {
// 兼容旧快照:裸 Tool 消息(无 ToolCallID违反 OpenAI 兼容 API 格式约束,
// 会触发 API 拒绝请求导致连接断开。
// 这里将裸 Tool 消息降级为 User 消息,保证向后兼容。
for i, msg := range history {
if msg.Role == schema.Tool && msg.ToolCallID == "" {
history[i] = &schema.Message{
Role: schema.User,
Content: fmt.Sprintf("[工具执行结果]\n%s", msg.Content),
}
}
}
messages = append(messages, history...)
}
}
// 4. 置顶上下文块:当前计划、当前步骤、粗排结果等最新约束。
// 放在 history 之后、user prompt 之前,利用 LLM 近因效应提升对最新约束的注意力。
if pinnedText := renderPinnedBlocks(ctx); pinnedText != "" {
messages = append(messages, schema.SystemMessage(pinnedText))
}
// 5. 阶段性用户提示词:始终在末尾,是本轮回答的核心触发。
runtimeUserPrompt = strings.TrimSpace(runtimeUserPrompt)
if runtimeUserPrompt != "" {
messages = append(messages, schema.UserMessage(runtimeUserPrompt))
}
return messages
}
// renderStateSummary 把当前流程状态渲染成简洁文本。
func renderStateSummary(state *agentmodel.CommonState) string {
if state == nil {
return "当前状态state 缺失,请先做兜底处理。"
}
var sb strings.Builder
current, total := state.PlanProgress()
sb.WriteString(fmt.Sprintf("当前阶段:%s\n", state.Phase))
sb.WriteString(fmt.Sprintf("当前轮次:%d/%d\n", state.RoundUsed, state.MaxRounds))
if state.HasTerminalOutcome() && state.TerminalOutcome != nil {
sb.WriteString(fmt.Sprintf("终止结果:%s\n", state.TerminalOutcome.Status))
if strings.TrimSpace(state.TerminalOutcome.Stage) != "" {
sb.WriteString(fmt.Sprintf("终止阶段:%s\n", state.TerminalOutcome.Stage))
}
if strings.TrimSpace(state.TerminalOutcome.Code) != "" {
sb.WriteString(fmt.Sprintf("终止代码:%s\n", state.TerminalOutcome.Code))
}
if strings.TrimSpace(state.TerminalOutcome.UserMessage) != "" {
sb.WriteString(fmt.Sprintf("终止说明:%s\n", state.TerminalOutcome.UserMessage))
}
}
if !state.HasPlan() {
sb.WriteString("当前完整 plan暂无。\n")
} else {
sb.WriteString("当前完整 plan\n")
for i, step := range state.PlanSteps {
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, strings.TrimSpace(step.Content)))
if strings.TrimSpace(step.DoneWhen) != "" {
sb.WriteString(fmt.Sprintf(" 完成判定:%s\n", strings.TrimSpace(step.DoneWhen)))
}
}
if step, ok := state.CurrentPlanStep(); ok {
sb.WriteString(fmt.Sprintf("当前步骤进度:%d/%d\n", current, total))
sb.WriteString("当前步骤内容:\n")
sb.WriteString(strings.TrimSpace(step.Content))
sb.WriteString("\n")
if strings.TrimSpace(step.DoneWhen) != "" {
sb.WriteString("当前步骤完成判定:\n")
sb.WriteString(strings.TrimSpace(step.DoneWhen))
sb.WriteString("\n")
}
} else {
sb.WriteString("当前步骤进度:暂时无有效当前步骤。\n")
}
}
// 渲染任务类约束元数据(如有),帮助 LLM 了解排程范围和策略,避免追问已有信息。
if len(state.TaskClasses) > 0 {
sb.WriteString("\n本次排课涉及的任务类约束\n")
for _, tc := range state.TaskClasses {
line := fmt.Sprintf("- [ID=%d] %s策略=%s总时段预算=%d", tc.ID, tc.Name, tc.Strategy, tc.TotalSlots)
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")
}
}
return sb.String()
}
func defaultSemanticValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "未标注"
}
return trimmed
}
// renderPinnedBlocks 把 ConversationContext 中的置顶块渲染成独立的 system 文本。
func renderPinnedBlocks(ctx *agentmodel.ConversationContext) string {
if ctx == nil {
return ""
}
blocks := ctx.PinnedBlocksSnapshot()
if len(blocks) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("以下是后端置顶注入的上下文,请优先遵守:\n")
for _, block := range blocks {
title := strings.TrimSpace(block.Title)
if title == "" {
title = strings.TrimSpace(block.Key)
}
if title != "" {
sb.WriteString("【")
sb.WriteString(title)
sb.WriteString("】\n")
}
sb.WriteString(strings.TrimSpace(block.Content))
sb.WriteString("\n")
}
return strings.TrimSpace(sb.String())
}
// renderToolSchemas 把工具摘要渲染成独立文本块。
func renderToolSchemas(ctx *agentmodel.ConversationContext) string {
if ctx == nil {
return ""
}
schemas := ctx.ToolSchemasSnapshot()
if len(schemas) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("以下是当前可用工具摘要,仅供你在规划时参考能力边界:\n")
for _, item := range schemas {
name := strings.TrimSpace(item.Name)
desc := strings.TrimSpace(item.Desc)
schemaText := strings.TrimSpace(item.SchemaText)
if name != "" {
sb.WriteString("- 工具名:")
sb.WriteString(name)
sb.WriteString("\n")
}
if desc != "" {
sb.WriteString(" 说明:")
sb.WriteString(desc)
sb.WriteString("\n")
}
if schemaText != "" {
sb.WriteString(" 参数摘要:")
sb.WriteString(schemaText)
sb.WriteString("\n")
}
}
return strings.TrimSpace(sb.String())
}
func mergeSystemPrompts(ctx *agentmodel.ConversationContext, stageSystemPrompt string) string {
base := ""
if ctx != nil {
base = strings.TrimSpace(ctx.SystemPrompt)
}
stageSystemPrompt = strings.TrimSpace(stageSystemPrompt)
switch {
case base == "" && stageSystemPrompt == "":
return ""
case base == "":
return stageSystemPrompt
case stageSystemPrompt == "":
return base
default:
return base + "\n\n" + stageSystemPrompt
}
}

View File

@@ -0,0 +1,169 @@
package agentprompt
import (
"fmt"
"strings"
"time"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
const chatRoutingSystemPrompt = `
你是 SmartMate 的聊天路由助手。SmartMate 是时伴SmartMate的中文 AI 排程伙伴,面向大学生提供陪伴式日程管理与日常协助;它擅长日程安排、任务管理与学习规划,但不只会做排程。你的回复必须以路由控制码开头,控制码后紧跟用户可见的内容。
路由规则:
- direct_reply纯闲聊、简单问答、轻量生活建议、打招呼、感谢等不需要工具、也不需要长链路思考的请求。控制码后直接输出完整回复。
- quick_task用户明确想记录/添加一个待办或提醒(如"记一下""提醒我""帮我记"),或查看/筛选任务列表(如"我有什么任务""待办清单""最近急事""今天/明天/本周有什么事要做")。该路由走轻量快捷路径,延迟低、废话少。控制码后不要输出任何内容。
- execute需要用工具处理的日程编排请求明确查课表/日程块、移动课程、排课等),但不需要先制定计划。控制码后输出简短确认。
- deep_answer复杂问题但不需要工具如分析建议、知识解释、方案比较、深度讨论等需要深度思考后回答。控制码后不要输出任何占位过渡语后端会直接进入第二次正式回答。
- plan用户明确要求先制定计划或涉及多阶段复杂规划。控制码后输出简短确认。
quick_task 判别要点:
- 用户明确要"记/添加/提醒"一个待办 → quick_task
- 用户要查看/筛选/列出任务清单 → quick_task
- 用户问"今天/明天/本周有什么待办/任务/事情要做"这类时间窗任务查询 → quick_task
- 用户明确在查课表/日程块、排课、移动安排 → execute
- 但如果用户同时提了日程排布(如"把明天的课调一下,再记一下周五开会"),混合操作走 execute
- 如果信息不足(如"帮我记一下"但没说记什么),走 direct_reply 追问
任务类设计路由要点:
- 普通"创建/修改任务类配置task class"默认走 execute由 execute 负责补字段与写入)。
- 仅当用户明确要"补课程学习资料/学习建议/学习路径(需要外部知识)"时,走 plan后续可使用 web_search
- 考试时间、DDL、课程具体时间安排、个人可用时段等时间信息必须向用户本人确认不能作为 web 搜索补齐目标。
通用回答约束:
- 非日程、非任务类问题,只要不需要工具,也应当正常回答。
- 不要因为用户的问题不涉及排程,就说自己“只能处理日程/任务安排”。
- 不要把普通问答、生活建议、开放式讨论,硬拐成排程请求。
- route=direct_reply 时,控制码后的可见内容应直接回应用户问题,而不是先讲能力边界。
- route=deep_answer 时,只输出控制码即可,不要补“让我想想”“这是个好问题”之类的占位话术。
粗排判断:当用户意图包含"批量安排/排课/把任务类排进日程"等批量调度需求时,可设置 rough_build=true后端会结合真实请求范围决定是否真正进入粗排。
二次粗排约束(强约束):
- 若上下文已出现 rough_build_done且用户未明确要求"重新粗排/从头重排",必须设置 rough_build=false。
- "移动/微调/优化/均匀化/调顺序"等请求默认视为 refine不得再次触发 rough build。
粗排后微调判断:
- 仅当 rough_build=true 时才判断 refine。
- 默认策略首次粗排完成后应进入微调refine=true按中位标准做主动优化。
- 仅当用户明确表达"只要初稿/先排进去别优化/先不微调/排完就收口"时,才设 refine=false。
顺序授权判断:
- reorder 仅在用户明确说明"允许打乱顺序/顺序不重要"时才为 true。
- 用户明确要求"保持顺序/不要打乱"时必须为 false。
- 若用户未明确提及顺序,一律为 false。
深度思考判断:
- thinking 仅在 route=execute 时有效。
- 当用户请求涉及复杂推理、多条件约束、需要深度分析后才能执行的操作时,设 thinking=true。
- 简单查询、单步操作设 thinking=false。
输出格式(严格两段式):
第一段(控制码,用户不可见,后端会截取):
<SMARTFLOW_ROUTE nonce="给定nonce" route="direct_reply|execute|deep_answer|plan|quick_task" rough_build="false" refine="false" reorder="false" thinking="false"/>
第二段(紧接控制码之后,用户可见):
根据路由输出对应内容。
属性说明(仅 route=execute 时有效,其余路由省略这些属性):
- rough_build是否需要粗排
- refine粗排后是否需要微调
- reorder是否允许打乱顺序
- thinking后续执行阶段是否需要深度思考
合法示例:
<SMARTFLOW_ROUTE nonce="给定nonce" route="direct_reply"/>
当然可以,我先直接回答你这个问题。
<SMARTFLOW_ROUTE nonce="给定nonce" route="quick_task"/>
<SMARTFLOW_ROUTE nonce="给定nonce" route="execute"/>
好的,我来帮你看看今天的安排。
<SMARTFLOW_ROUTE nonce="给定nonce" route="execute" rough_build="true" refine="false" reorder="false" thinking="false"/>
好的,我来帮你排课。
<SMARTFLOW_ROUTE nonce="给定nonce" route="execute" rough_build="true" refine="true" reorder="false" thinking="true"/>
好的,我来帮你排课并按你的偏好做微调。
<SMARTFLOW_ROUTE nonce="给定nonce" route="deep_answer"/>
<SMARTFLOW_ROUTE nonce="给定nonce" route="plan"/>
明白,我来帮你制定一个完整的学习计划。
禁止输出任何 JSON、markdown 代码块或额外解释。nonce 必须精确使用给定值。
`
// BuildChatRoutingSystemPrompt 返回路由阶段的系统提示词。
func BuildChatRoutingSystemPrompt() string {
return strings.TrimSpace(chatRoutingSystemPrompt)
}
// BuildChatRoutingMessages 组装路由阶段的 messages。
func BuildChatRoutingMessages(ctx *agentmodel.ConversationContext, userInput string, state *agentmodel.CommonState, nonce string) []*schema.Message {
return buildUnifiedStageMessages(
ctx,
StageMessagesConfig{
SystemPrompt: BuildChatRoutingSystemPrompt(),
Msg1Content: buildChatConversationMessage(ctx),
Msg2Content: buildChatRoutingWorkspace(ctx),
Msg3Suffix: BuildChatRoutingUserPrompt(userInput, nonce),
Msg3Role: schema.User,
},
)
}
// BuildChatRoutingUserPrompt 构造路由阶段的用户提示词。
func BuildChatRoutingUserPrompt(userInput string, nonce string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("nonce=%s\n", nonce))
sb.WriteString(fmt.Sprintf("当前时间=%s\n", time.Now().In(time.Local).Format("2006-01-02 15:04")))
sb.WriteString("\n请基于最近真实对话和本轮输入选择最合适的路由并严格按系统约定输出控制码。\n")
trimmedInput := strings.TrimSpace(userInput)
if trimmedInput != "" {
sb.WriteString("\n用户本轮输入\n")
sb.WriteString(trimmedInput)
sb.WriteString("\n")
}
return strings.TrimSpace(sb.String())
}
// --- 深度回答 prompt ---
const deepAnswerSystemPrompt = `
你是 SmartMate 的深度分析助手。SmartMate 是时伴SmartMate的中文 AI 排程伙伴;即使问题与日程、任务无关,只要不需要工具,你也应当认真分析后给出详细、有价值的回答。
请遵守以下规则:
1. 优先回答用户真实问题,不要把普通问答硬拐回排程、任务或计划制定。
2. 充分利用上下文中已有的信息(历史对话、记忆、任务类约束、日程数据等),但不要无关硬套。
3. 如果缺少关键信息,在回答中说明需要哪些额外信息。
4. 直接输出你的回答,不要输出 JSON。
`
// BuildDeepAnswerSystemPrompt 返回深度回答阶段的系统提示词。
func BuildDeepAnswerSystemPrompt() string {
return strings.TrimSpace(deepAnswerSystemPrompt)
}
// BuildDeepAnswerMessages 组装深度回答阶段的 messages。
func BuildDeepAnswerMessages(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext, userInput string) []*schema.Message {
return buildUnifiedStageMessages(
ctx,
StageMessagesConfig{
SystemPrompt: BuildDeepAnswerSystemPrompt(),
Msg1Content: buildChatConversationMessage(ctx),
Msg2Content: buildDeepAnswerWorkspace(),
Msg3Suffix: buildDeepAnswerUserPrompt(userInput),
Msg3Role: schema.User,
},
)
}
func buildDeepAnswerUserPrompt(userInput string) string {
trimmedInput := strings.TrimSpace(userInput)
if trimmedInput != "" {
return trimmedInput
}
return "请直接回答用户刚才的问题。"
}

View File

@@ -0,0 +1,33 @@
package agentprompt
import (
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
)
// buildChatConversationMessage 生成 chat / deep_answer 共用的真实对话视图。
func buildChatConversationMessage(ctx *agentmodel.ConversationContext) string {
return buildConversationHistoryMessage(ctx, "真实对话记录")
}
// buildChatRoutingWorkspace 渲染 chat 路由节点的轻量补充区。
//
// 设计说明:
// 1. chat 只保留与路由判断直接相关的最小流程标记;
// 2. rough_build_done 仍需显式暴露,否则路由层会丢掉“不要重复粗排”的关键信号;
// 3. 不再展示轮次、阶段锚点、ReAct 摘要等 execute 专属信息。
func buildChatRoutingWorkspace(ctx *agentmodel.ConversationContext) string {
lines := []string{"路由补充:"}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 已存在 rough_build_done除非用户明确要求重新粗排否则不要再次触发 rough_build。")
} else {
lines = append(lines, "- 暂无额外流程标记。")
}
return strings.Join(lines, "\n")
}
// buildDeepAnswerWorkspace 渲染 deep_answer 节点的轻量工作区。
func buildDeepAnswerWorkspace() string {
return "回答补充:请直接延续最近对话,聚焦回答用户本轮问题。"
}

View File

@@ -0,0 +1,62 @@
package agentprompt
import (
"context"
"fmt"
llmservice "github.com/LoveLosita/smartflow/backend/services/llm"
"github.com/cloudwego/eino/schema"
)
const compactMsg1SystemPrompt = `你是一个对话压缩助手。你的任务是将以下多轮对话历史压缩为一段简洁的结构化摘要。
要求:
1. 保留用户的核心诉求和意图(原文关键词不要丢失)
2. 保留所有已确认的约束条件和规则
3. 保留关键操作决策和结果(比如排程相关的调整结果)
4. 保留用户偏好的重要信息
5. 去除冗余和重复信息
6. 按要点列出,每条一行
直接输出压缩后的摘要,不要输出其他内容。`
// CompactMsg1 将 msg1历史对话压缩为摘要。
// existingSummary 不为空时表示已有旧摘要,需要合并压缩。
func CompactMsg1(
ctx context.Context,
client *llmservice.Client,
historyText string,
existingSummary string,
) (string, error) {
var userContent string
if existingSummary != "" {
userContent = fmt.Sprintf(`已有压缩摘要:
%s
新增的对话记录:
%s
请将以上两部分合并为一份更紧凑的摘要。`, existingSummary, historyText)
} else {
userContent = fmt.Sprintf(`对话历史:
%s
请压缩以上对话历史。`, historyText)
}
messages := []*schema.Message{
schema.SystemMessage(compactMsg1SystemPrompt),
schema.UserMessage(userContent),
}
result, err := client.GenerateText(ctx, messages, llmservice.GenerateOptions{
MaxTokens: 4000,
})
if err != nil {
return "", fmt.Errorf("compact msg1 LLM call failed: %w", err)
}
if result == nil || result.Text == "" {
return "", fmt.Errorf("compact msg1 LLM returned empty result")
}
return result.Text, nil
}

View File

@@ -0,0 +1,49 @@
package agentprompt
import (
"context"
"fmt"
llmservice "github.com/LoveLosita/smartflow/backend/services/llm"
"github.com/cloudwego/eino/schema"
)
const compactMsg2SystemPrompt = `你是一个执行记录压缩助手。你的任务是将以下 ReAct 执行循环记录压缩为简洁摘要。
要求:
1. 保留每个工具调用的关键返回值尤其是包含排程数据的JSON
2. 保留执行路径(哪些操作成功了,哪些失败了)
3. 保留当前执行进度(正在做什么,下一步要做什么)
4. 去除重复的工具调用结果
5. 按时间顺序组织,每条一行
直接输出压缩后的摘要,不要输出其他内容。`
// CompactMsg2 将 msg2ReAct Loop 记录)的早期部分压缩为摘要。
// recentText 是保留的近期记录原文,不参与压缩。
func CompactMsg2(
ctx context.Context,
client *llmservice.Client,
earlyLoopText string,
) (string, error) {
userContent := fmt.Sprintf(`早期的 ReAct 执行记录:
%s
请压缩以上执行记录,保留关键信息。`, earlyLoopText)
messages := []*schema.Message{
schema.SystemMessage(compactMsg2SystemPrompt),
schema.UserMessage(userContent),
}
result, err := client.GenerateText(ctx, messages, llmservice.GenerateOptions{
MaxTokens: 4000,
})
if err != nil {
return "", fmt.Errorf("compact msg2 LLM call failed: %w", err)
}
if result == nil || result.Text == "" {
return "", fmt.Errorf("compact msg2 LLM returned empty result")
}
return result.Text, nil
}

View File

@@ -0,0 +1,37 @@
package agentprompt
import (
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
)
// buildConversationHistoryMessage 将“真实对话流”渲染成节点可直接复用的 msg1。
//
// 职责边界:
// 1. 只负责把 user + assistant speak 组织成稳定文本;
// 2. 不拼接 tool_call / tool observation这些不属于“真实对话”
// 3. 不做长度裁剪,长度预算交给统一压缩层处理。
func buildConversationHistoryMessage(ctx *agentmodel.ConversationContext, title string) string {
title = strings.TrimSpace(title)
if title == "" {
title = "真实对话记录"
}
lines := []string{title + ""}
if ctx == nil {
lines = append(lines, "暂无。")
return strings.Join(lines, "\n")
}
turns := CollectConversationTurns(ctx.HistorySnapshot())
if len(turns) == 0 {
lines = append(lines, "暂无。")
return strings.Join(lines, "\n")
}
for _, turn := range turns {
lines = append(lines, turn.Role+": \""+turn.Content+"\"")
}
return strings.Join(lines, "\n")
}

View File

@@ -0,0 +1,78 @@
package agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
const deliverSystemPrompt = `
你是 SmartMate 的交付器。你的职责是基于原始计划和执行历史,生成一份简洁、诚实的任务完成总结。
请遵守以下规则:
1. 只基于已有历史和计划状态生成总结,不要编造未执行的操作。
2. 如果所有步骤都已完成,请自然概括每一步的主要成果。
3. 如果流程因轮次耗尽或主动终止而提前结束,请如实说明当前进度与未完成部分。
4. 使用自然、友好的语气,不要机械罗列工具过程。
5. 如果用户后续还需要继续操作,可以给出一句简短建议。
6. 只输出总结文本,不要输出 JSON也不要输出 markdown 标题。
你会看到:
- 原始计划步骤及完成进度
- 最近真实对话
- 当前流程的收口状态`
// BuildDeliverSystemPrompt 返回交付阶段系统提示词。
func BuildDeliverSystemPrompt() string {
return strings.TrimSpace(deliverSystemPrompt)
}
// BuildDeliverMessages 组装交付阶段 messages。
func BuildDeliverMessages(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext) []*schema.Message {
roughBuildPrefix := buildDeliverRoughBuildPrefix(ctx, state)
return buildUnifiedStageMessages(
ctx,
StageMessagesConfig{
SystemPrompt: BuildDeliverSystemPrompt(),
Msg1Content: buildDeliverConversationMessage(ctx),
Msg2Content: buildDeliverWorkspace(state, ctx),
Msg3Prefix: roughBuildPrefix,
Msg3Suffix: BuildDeliverUserPrompt(state, ctx),
Msg3Role: schema.User,
},
)
}
// BuildDeliverUserPrompt 构造交付阶段的用户提示词。
func BuildDeliverUserPrompt(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext) string {
var sb strings.Builder
sb.WriteString("请基于最近对话和交付工作区,生成一段自然、诚实的完成总结。\n")
if state == nil || !state.HasPlan() {
if hasExecuteRoughBuildDone(ctx) {
sb.WriteString("当前没有正式计划,但本轮已经完成粗排,请结合粗排补充和任务类详情总结粗排结果,不要把它说成正式完结。\n")
} else {
sb.WriteString("当前没有正式计划,请只概括本次互动,不要编造成果。\n")
}
return strings.TrimSpace(sb.String())
}
completed := countCompletedPlanSteps(state)
total := len(state.PlanSteps)
if state.IsExhaustedTerminal() {
sb.WriteString(fmt.Sprintf("注意:任务因轮次耗尽提前结束,当前已完成 %d/%d 步。\n", completed, total))
sb.WriteString("请如实说明已完成与未完成的部分,并给出一句继续建议。\n")
return strings.TrimSpace(sb.String())
}
if state.IsAborted() {
sb.WriteString(fmt.Sprintf("注意:流程已被主动终止,当前已完成 %d/%d 步。\n", completed, total))
sb.WriteString("请如实说明停在何处,以及用户若想继续应如何衔接。\n")
return strings.TrimSpace(sb.String())
}
sb.WriteString("若计划已正常完成,请概括整体成果;若仍有未完成步骤,也必须如实说明。\n")
return strings.TrimSpace(sb.String())
}

View File

@@ -0,0 +1,145 @@
package agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
)
// buildDeliverConversationMessage 生成 deliver 节点看到的轻量历史提示。
//
// 职责边界:
// 1. 这里不再承载完整历史,也不再把旧轮次对话重新灌回 deliver
// 2. 真正可供收口的本轮 execute 窗口放到 msg2由工作区统一呈现
// 3. 这里只给模型一个明确提示:历史已经折叠,请不要主动回顾旧轮次。
func buildDeliverConversationMessage(ctx *agentmodel.ConversationContext) string {
return "历史视图:已折叠到交付工作区的本轮 execute 窗口,请仅依据 msg2 收口,不要回顾旧轮次。"
}
// buildDeliverRoughBuildPrefix 构造 deliver 在“粗排已完成”场景下的专属前缀。
//
// 职责边界:
// 1. 这里只负责把粗排相关的任务类信息补进 msg3 前缀,不改写交付总结本身;
// 2. 只有在上下文里明确存在 rough_build_done 时才注入,避免普通交付场景被额外信息污染;
// 3. 这段前缀用于补齐第一次粗排没有正式计划时的任务类详情,优先让 deliver 看到 task_class_ids 和任务类约束。
func buildDeliverRoughBuildPrefix(ctx *agentmodel.ConversationContext, state *agentmodel.CommonState) string {
if !hasExecuteRoughBuildDone(ctx) {
return ""
}
lines := []string{
"粗排补充信息:",
"- 本轮已经完成粗排,相关任务类已进入 suggested/existing不要把它们说成正式计划。",
}
if taskClassIDs := renderPlanTaskClassIDs(state); taskClassIDs != "" {
lines = append(lines, "- "+taskClassIDs)
}
if taskClassMeta := renderPlanTaskClassMeta(state); taskClassMeta != "" {
lines = append(lines, "任务类详情:")
lines = append(lines, taskClassMeta)
}
if state == nil || !state.HasPlan() {
lines = append(lines, "- 当前没有正式计划,请把这批任务类的粗排结果作为总结重点。")
}
return strings.Join(lines, "\n")
}
// buildDeliverWorkspace 渲染 deliver 节点自己的结果态工作区。
//
// 设计说明:
// 1. 先保留 deliver 原本依赖的结果态信息terminal outcome、计划进度、步骤简表
// 2. 再把基于 execute_loop_closed 切出来的“本轮 execute 窗口”拼到 msg2作为唯一的本轮事实视图
// 3. 没有正式计划时也保留 execute 窗口,保证 deliver 仍能基于当前轮活跃上下文诚实收口。
func buildDeliverWorkspace(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext) string {
lines := []string{"交付工作区:"}
if state == nil {
lines = append(lines, "- 当前缺少流程状态,请仅基于可见结果态与本轮 execute 窗口诚实收口。")
lines = append(lines, "", buildDeliverExecuteWindow(ctx))
return strings.Join(lines, "\n")
}
lines = append(lines, renderDeliverTerminalSummary(state))
if !state.HasPlan() {
lines = append(lines, "- 当前没有正式计划,请只概括本次互动。")
lines = append(lines, "", buildDeliverExecuteWindow(ctx))
return strings.Join(lines, "\n")
}
total := len(state.PlanSteps)
completed := countCompletedPlanSteps(state)
lines = append(lines, fmt.Sprintf("- 计划进度:已完成 %d/%d 步。", completed, total))
lines = append(lines, "计划步骤:")
lines = append(lines, renderDeliverStepOutline(state, completed))
lines = append(lines, "", buildDeliverExecuteWindow(ctx))
return strings.Join(lines, "\n")
}
// renderDeliverTerminalSummary 返回 deliver 节点需要知道的收口状态。
func renderDeliverTerminalSummary(state *agentmodel.CommonState) string {
if state == nil || !state.HasTerminalOutcome() || state.TerminalOutcome == nil {
return "- 当前没有正式终止结果,请按最近对话和计划进度自然总结。"
}
outcome := state.TerminalOutcome
line := fmt.Sprintf("- 收口状态:%s", outcome.Status)
if stage := strings.TrimSpace(outcome.Stage); stage != "" {
line += fmt.Sprintf(";阶段:%s", stage)
}
if msg := strings.TrimSpace(outcome.UserMessage); msg != "" {
line += fmt.Sprintf(";用户提示:%s", msg)
}
return line
}
// renderDeliverStepOutline 生成 deliver 节点使用的步骤简表。
func renderDeliverStepOutline(state *agentmodel.CommonState, completed int) string {
if state == nil || len(state.PlanSteps) == 0 {
return "- 暂无。"
}
lines := make([]string, 0, len(state.PlanSteps))
for i, step := range state.PlanSteps {
status := "未完成"
if i < completed {
status = "已完成"
}
content := strings.TrimSpace(step.Content)
if content == "" {
content = "(步骤正文为空)"
}
line := fmt.Sprintf("%d. [%s] %s", i+1, status, content)
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
line += fmt.Sprintf(" | 完成判定:%s", doneWhen)
}
lines = append(lines, line)
}
return strings.Join(lines, "\n")
}
// countCompletedPlanSteps 统计当前已经完成的计划步骤数。
func countCompletedPlanSteps(state *agentmodel.CommonState) int {
if state == nil {
return 0
}
total := len(state.PlanSteps)
if total == 0 {
return 0
}
if state.CurrentStep <= 0 {
if state.IsCompleted() {
return total
}
return 0
}
if state.CurrentStep >= total {
return total
}
return state.CurrentStep
}

View File

@@ -0,0 +1,103 @@
package agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
const deliverHistoryKindExecuteLoopClosed = "execute_loop_closed"
// sliceHistoryAfterLastExecuteLoopClosed 基于最后一个 execute_loop_closed 标记切出当前活跃窗口。
//
// 步骤化说明:
// 1. 先读取完整 history 快照,避免直接在 ConversationContext 原地切片,减少后续调用方误改底层数组的风险;
// 2. 从后往前找最后一个 execute_loop_closed确保拿到的是“最近一次已正常收口”的边界
// 3. 命中边界后只返回边界之后的消息,这样 deliver 看到的就是当前活跃轮次;
// 4. 若完全没有边界,说明会话尚未形成稳定闭环,此时退回全量 history避免误丢当前活跃上下文。
func sliceHistoryAfterLastExecuteLoopClosed(ctx *agentmodel.ConversationContext) []*schema.Message {
if ctx == nil {
return nil
}
history := ctx.HistorySnapshot()
if len(history) == 0 {
return nil
}
cut := -1
for i := len(history) - 1; i >= 0; i-- {
if isDeliverExecuteLoopClosedMarker(history[i]) {
cut = i
break
}
}
if cut < 0 {
return history
}
if cut+1 >= len(history) {
return nil
}
return history[cut+1:]
}
// isDeliverExecuteLoopClosedMarker 判断一条历史消息是否为 execute loop 正常收口边界。
//
// 职责边界:
// 1. 这里只识别 prompt 层真正关心的 execute_loop_closed 标记;
// 2. 不负责推断其他中断/恢复语义,避免把 confirm/ask_user 等同一轮过程误判成新边界;
// 3. 若消息结构不完整,则统一按“非边界”处理,保证切窗策略保守可回退。
func isDeliverExecuteLoopClosedMarker(msg *schema.Message) bool {
if msg == nil || msg.Extra == nil {
return false
}
kind, ok := msg.Extra[executeHistoryKindKey].(string)
if !ok {
return false
}
return strings.TrimSpace(kind) == deliverHistoryKindExecuteLoopClosed
}
// buildDeliverExecuteWindow 基于当前活跃 history 窗口渲染 deliver 节点要看的执行事实视图。
//
// 步骤化说明:
// 1. 先按 execute_loop_closed 切掉旧轮次,只保留当前仍活跃的执行窗口;
// 2. 再分别抽取“本轮真实对话流”和“本轮 ReAct 工具事实链”,避免 deliver 回看旧 deliver 总结;
// 3. 若本轮还没有工具调用,也要明确告诉模型“当前无工具事实”,避免它擅自脑补;
// 4. 整段文本只服务 deliver.msg2不改变四段式骨架也不回写任何状态。
func buildDeliverExecuteWindow(ctx *agentmodel.ConversationContext) string {
lines := []string{"本轮 execute 窗口:"}
historyWindow := sliceHistoryAfterLastExecuteLoopClosed(ctx)
if len(historyWindow) == 0 {
lines = append(lines, "- 当前没有可用的本轮执行窗口,请仅依据结果态工作区诚实收口。")
return strings.Join(lines, "\n")
}
turns := collectExecuteConversationTurns(historyWindow)
if len(turns) == 0 {
lines = append(lines, "- 本轮对话流:暂无。")
} else {
lines = append(lines, "本轮对话流:")
for _, turn := range turns {
lines = append(lines, fmt.Sprintf("- %s: %q", turn.Role, turn.Content))
}
}
loops := collectExecuteLoopRecords(historyWindow)
if len(loops) == 0 {
lines = append(lines, "- 本轮 ReAct 记录:暂无工具调用。")
return strings.Join(lines, "\n")
}
lines = append(lines, "本轮 ReAct 记录:")
for i, loop := range loops {
lines = append(lines, fmt.Sprintf("%d. thought/reason%s", i+1, loop.Thought))
lines = append(lines, fmt.Sprintf(" tool_call%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
lines = append(lines, fmt.Sprintf(" observation%s", loop.Observation))
}
return strings.Join(lines, "\n")
}

View File

@@ -0,0 +1,137 @@
package agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
// BuildExecuteSystemPrompt 返回执行阶段系统提示词(有 plan 模式)。
func BuildExecuteSystemPrompt() string {
return buildExecutePromptWithFormatGuard(executeSystemPromptBaseWithPlan)
}
// BuildExecuteReActSystemPrompt 返回执行阶段系统提示词(自由执行模式)。
func BuildExecuteReActSystemPrompt() string {
return buildExecutePromptWithFormatGuard(executeSystemPromptBaseReAct)
}
// BuildExecuteMessages 组装执行阶段消息。
func BuildExecuteMessages(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext) []*schema.Message {
if state != nil && state.HasPlan() {
return buildExecuteStageMessages(
BuildExecuteSystemPrompt(),
state,
ctx,
buildExecuteStrictJSONUserPromptWithPlan(state),
)
}
return buildExecuteStageMessages(
BuildExecuteReActSystemPrompt(),
state,
ctx,
buildExecuteStrictJSONUserPrompt(),
)
}
// buildExecuteStrictJSONUserPromptWithPlan 在通用 JSON 约束上补充"当前计划步骤"强约束。
//
// 职责边界:
// 1. 负责把"当前是第几步、当前步骤内容、done_when 判定"明确写进用户指令;
// 2. 不负责替代系统提示词中的工具规则和安全边界;
// 3. 当 state 无法提供有效当前步骤时,仅追加兜底提示,不在此处推进流程状态。
func buildExecuteStrictJSONUserPromptWithPlan(state *agentmodel.CommonState) string {
base := buildExecuteStrictJSONUserPrompt()
if state == nil || !state.HasPlan() {
return base
}
current, total := state.PlanProgress()
step, ok := state.CurrentPlanStep()
if !ok {
return strings.TrimSpace(base + `
计划步骤强约束:
- 当前没有可执行的计划步骤,请先基于已有事实检查是否已完成全部计划。
- 若全部计划已完成:输出 action=done并在 goal_check 总结完成证据。
- goal_check 字段类型必须为 string不要输出对象或数组。
- 若未完成但缺少关键信息:输出 action=ask_user。`)
}
stepContent := strings.TrimSpace(step.Content)
if stepContent == "" {
stepContent = "(当前步骤内容为空,以 done_when 为准)"
}
doneWhen := strings.TrimSpace(step.DoneWhen)
if doneWhen == "" {
doneWhen = "(未提供 done_when需基于当前步骤目标给出可验证完成证据"
}
return strings.TrimSpace(fmt.Sprintf(`%s
计划步骤强约束:
- 你当前只允许推进第 %d/%d 步。
- 当前步骤内容:%s
- 当前步骤完成判定(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))
}
// buildExecutePromptWithFormatGuard 统一补一层更硬的 JSON 输出约束。
func buildExecutePromptWithFormatGuard(base string) string {
base = strings.TrimSpace(base)
guard := strings.TrimSpace(`
输出协议硬约束:
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
}
return base + "\n\n" + guard
}
// buildExecuteStrictJSONUserPrompt 统一构造 execute 阶段面向模型的最终用户指令。
func buildExecuteStrictJSONUserPrompt() string {
return strings.TrimSpace(`
请继续当前任务的执行阶段,严格按 SMARTFLOW_DECISION 标签格式输出。
输出格式:先输出 <SMARTFLOW_DECISION>{JSON 决策}</SMARTFLOW_DECISION>,然后换行输出给用户看的正文。
执行提醒:
- JSON 中不要包含 speak 字段;给用户看的话放在 </SMARTFLOW_DECISION> 标签之后
- 不要在 <SMARTFLOW_DECISION> 标签之前输出任何文字;哪怕只有一句“我先看下”也不行
- 任何写工具都一律走 action=confirm包括 upsert_task_class 与日程写工具place/move/swap/batch_move/unplace哪怕只是“按 validation.issues 重试一次”,也不能输出 continue + 写工具
- 若当前处于粗排后主动优化专用模式,先调 analyze_health再直接从 decision.candidates 里选一个合法候选去执行;不要自行发明新的全窗搜索步骤
- 若读工具结果与已知事实明显冲突,先修正参数并重查一次,再决定是否 ask_user
- 不要连续两轮调用“同一读工具 + 等价 arguments”上一轮已成功返回时下一轮必须换工具、进入 confirm或明确说明阻塞
- 若上下文已明确“当前未收到微调偏好,本轮先收口”,请直接输出 action=done
- web_search 仅用于通用学习资料补充不可用于考试时间、DDL、个人时段等时间字段填充
- 任何写工具在真正输出 action=confirm 前,都必须先做一次“写前检查”:确认参数已齐全、格式合法、业务前提已满足;若尚未通过检查,就先补齐/归一/生成/ask_user不要把 validation 失败当成正常探索路径
- upsert_task_class 若返回 validation.ok=false必须先按 validation.issues 补齐,再重试;禁止直接 done
- 对 upsert_task_class写前至少检查mode=auto 时日期边界是否已满足subject_type / difficulty_level / cognitive_intensity 是否齐difficulty_level 是否已映射到合法枚举items 是否非空且顺序内容已生成config 中已知约束字段是否已落到合法格式
- 若像 items 这种内容本就由当前轮模型负责生成,就应先把内容生成齐、顺序排好,再写入;不要先写一个 items 为空的 taskclass 去让 validation 提醒你补内容
- 处理 validation.issues 时先分类:若是用户关键信息确实缺失,才 action=ask_user若是 schema 字段名、字段位置、内部索引、枚举值、日期格式、工具语义映射等内部表示问题,应静默改参后直接重试,不要把底层表示教学抛给用户
- 像 config.excluded_slots 的半天块索引映射默认属于内部表示修正你应自己把“第1-2节 / 第11-12节”换算成合法块索引不要为此 ask_user不要长篇解释底层表示
- 当前时间锚点只用于解析用户已经明确说出的相对时间(如“今天开始”“两周内”“下周一前”),不能反过来把“现在是今天”当成用户已经同意从今天开始,更不能据此默认生成 start_date / end_date
- 像 auto 模式缺 start_date/end_date 这类问题,先检查当前对话、历史、记忆、已知工具结果里是否已经出现可用日期;若已出现就静默补齐并重试,只有在上下文里确实没有时再 ask_user
- 若当前是首次创建/修正 taskclass且上下文里并没有用户明确给出的开始日期、结束日期、日期范围、完成期限、或可直接换算出的相对时间承诺就不要擅自写 start_date / end_date此时若工具闭环确实要求这些字段必须 ask_user
- 对 taskclass 来说,以下属于必须 ask_user 的关键信息start_date、end_date、明确的日期范围、明确的开始时间承诺、明确的完成期限这些会决定任务类的真实时间边界不能由模型自行拍板
- subject_type / difficulty_level / cognitive_intensity 是任务类语义画像必填;优先静默推断,只有确实无法判断时再 ask_user
- 学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权,这些默认都只是 taskclass 语义;不要因为信息完整就自动切进 schedule
- 只有用户明确要求“排进日程 / 给出具体时间安排 / 现在就排一版”时,才允许 context_tools_add domain="schedule"、触发 rough_build或继续 schedule 链路
- 例:“我要复习离散数学,基础较差,大概学 8 节课,不要早上第 1-2 节和晚上第 11-12 节,周末也不想学,每节课内容你自己来”——应先生成或更新 taskclass而不是主动排进日程
- 仅 upsert_task_class 成功不代表已开始排程;若未触发 rough_build 且未调用任何日程修改工具,禁止承诺“接下来会自动排程”
- 当前轮目标若是创建/修正 taskclass就优先把 taskclass 静默闭环;除非真缺用户关键信息,否则不要把主要篇幅花在解释工具内部约束上
`)
}

View File

@@ -0,0 +1,788 @@
package agentprompt
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
const (
// executeHistoryKindKey 用于在 history 里区分普通用户消息与后端注入的纠错提示。
// 这里负责“识别并过滤”,不负责写入该标记。
executeHistoryKindKey = "newagent_history_kind"
executeHistoryKindCorrectionUser = "llm_correction_prompt"
)
type executeToolSchemaDoc struct {
Name string `json:"name"`
Parameters map[string]any `json:"parameters"`
}
type executeLoopRecord struct {
Thought string
ToolName string
ToolArgs string
Observation string
}
type conversationTurn struct {
Role string
Content string
}
type executeLatestToolRecord struct {
ToolName string
Observation string
}
// buildExecuteStageMessages 组装 execute 阶段的四段式消息。
//
// 1. msg0系统提示 + 动态规则包 + 工具简表。
// 2. msg1真实对话流只保留 user 和 assistant speak。
// 3. msg2当前 ReAct tool loop 记录。
// 4. msg3执行状态、阶段约束、记忆和本轮指令。
func buildExecuteStageMessages(
stageSystemPrompt string,
state *agentmodel.CommonState,
ctx *agentmodel.ConversationContext,
runtimeUserPrompt string,
) []*schema.Message {
msg0 := buildExecuteMessage0(stageSystemPrompt, state, ctx)
msg1 := buildExecuteMessage1V3(ctx)
msg2 := buildExecuteMessage2V3(ctx)
msg3 := buildExecuteMessage3(state, ctx, runtimeUserPrompt)
return []*schema.Message{
schema.SystemMessage(msg0),
{Role: schema.Assistant, Content: msg1},
{Role: schema.Assistant, Content: msg2},
schema.SystemMessage(msg3),
}
}
// buildExecuteMessage0 生成 execute 阶段的固定规则消息。
//
// 1. 先拼基础 system prompt保证身份和输出协议稳定。
// 2. 再按当前 domain / packs 注入动态规则包,让模型先读到边界。
// 3. 最后再附工具简表,避免模型只看到工具不看到纪律。
func buildExecuteMessage0(stageSystemPrompt string, state *agentmodel.CommonState, ctx *agentmodel.ConversationContext) string {
base := strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
if base == "" {
base = "你是 SmartMate 执行器,请继续当前执行阶段。"
}
rulePackSection, _ := renderExecuteRulePackSection(state, ctx)
if rulePackSection != "" {
base += "\n\n" + rulePackSection
}
toolCatalog := renderExecuteToolCatalogCompact(ctx, state)
if toolCatalog != "" {
base += "\n\n" + toolCatalog
}
return base
}
// buildExecuteMessage1V3 只渲染真实对话流,不混入 tool observation。
func buildExecuteMessage1V3(ctx *agentmodel.ConversationContext) string {
lines := []string{"历史上下文:"}
if ctx == nil {
lines = append(lines,
"- 对话历史:暂无。",
"- 阶段锚点:按当前工具事实推进执行。",
)
return strings.Join(lines, "\n")
}
turns := collectExecuteConversationTurns(ctx.HistorySnapshot())
if len(turns) == 0 {
lines = append(lines, "- 对话历史:暂无。")
} else {
turnLines := make([]string, 0, len(turns)+1)
turnLines = append(turnLines, "对话历史:")
for _, turn := range turns {
turnLines = append(turnLines, turn.Role+": \""+turn.Content+"\"")
}
lines = append(lines, strings.Join(turnLines, "\n"))
}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段锚点:粗排已完成,本轮仅做微调,不重新 place。")
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
return strings.Join(lines, "\n")
}
// buildExecuteMessage2V3 只承载当轮 ReAct loop。
//
// 1. 每条记录固定展示 thought / tool_call / observation方便模型做局部闭环。
// 2. 如果当前还没有任何 tool loop明确给“新一轮”占位避免模型误判缺上下文。
func buildExecuteMessage2V3(ctx *agentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录:"}
if ctx == nil {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
loops := collectExecuteLoopRecords(ctx.HistorySnapshot())
if len(loops) == 0 {
lines = append(lines, "- 已清空(新一轮 loop 准备中)。")
return strings.Join(lines, "\n")
}
for i, loop := range loops {
lines = append(lines, fmt.Sprintf("%d) thought/reason%s", i+1, loop.Thought))
lines = append(lines, fmt.Sprintf(" tool_call%s", renderExecuteToolCallText(loop.ToolName, loop.ToolArgs)))
lines = append(lines, fmt.Sprintf(" observation%s", loop.Observation))
}
return strings.Join(lines, "\n")
}
// buildExecuteMessage3 汇总当前执行状态和本轮指令。
//
// 1. 这里只放“当前轮真正会影响决策”的状态,避免 msg3 继续膨胀。
// 2. 读工具最近结果只给最新一条摘要,避免旧 observation 重复占上下文。
// 3. 最后一行固定落到“本轮指令”,保证模型收尾时注意力还在执行目标上。
func buildExecuteMessage3(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext, runtimeUserPrompt string) string {
lines := []string{"当前执行状态:"}
roughBuildDone := hasExecuteRoughBuildDone(ctx)
roundUsed, maxRounds := 0, agentmodel.DefaultMaxRounds
modeText := "自由执行(无预定义步骤)"
activeDomain := ""
activePacks := []string{}
if state != nil {
roundUsed = state.RoundUsed
if state.MaxRounds > 0 {
maxRounds = state.MaxRounds
}
if state.HasPlan() {
modeText = "计划执行(有预定义步骤)"
}
activeDomain = strings.TrimSpace(state.ActiveToolDomain)
activePacks = readExecuteActiveToolPacks(state)
}
lines = append(lines,
fmt.Sprintf("- 当前轮次:%d/%d", roundUsed, maxRounds),
"- 当前模式:"+modeText,
)
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, "计划步骤锚点(强约束):")
if step, ok := state.CurrentPlanStep(); ok {
stepContent := strings.TrimSpace(step.Content)
if stepContent == "" {
stepContent = "(当前步骤内容为空)"
}
doneWhen := strings.TrimSpace(step.DoneWhen)
if doneWhen == "" {
doneWhen = "(未提供 done_when需基于步骤目标给出可验证完成证据"
}
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,
"- 当前计划步骤不可读;请先判断是否已完成全部计划。",
"- 若已完成全部计划,输出 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你可以根据工具调用记录自行判断。",
"- 非目标:不重新粗排、不修改无关任务类。",
)
if roughBuildDone {
lines = append(lines, "- 阶段约束:粗排已完成,本轮只微调 suggestedexisting 仅作已安排事实参考,不作为可移动目标。")
}
lines = append(lines, "- 参数纪律:工具参数必须严格使用 schema 字段;若返回“参数非法”,需先改参再继续。")
if state != nil {
if state.AllowReorder {
lines = append(lines, "- 顺序策略:用户已明确允许打乱顺序,但当前主链不再提供顺序重排工具,请优先使用 move/swap 做局部调整。")
} else {
lines = append(lines, "- 顺序策略:默认保持 suggested 相对顺序,仅做局部 move/swap 调整。")
}
}
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 = "请继续当前任务执行阶段,严格按 SMARTFLOW_DECISION 标签格式输出。"
} else {
instruction = firstExecuteLine(instruction)
}
lines = append(lines, "本轮指令:"+instruction)
return strings.Join(lines, "\n")
}
// renderExecuteToolCatalogCompact 将当前 tool schemas 渲染为紧凑简表。
//
// 1. 这里只给模型最低必要的参数和返回值感知,不重复塞完整 schema JSON。
// 2. 对复杂工具额外给一条调用示例,降低“参数字段写错”的概率。
// 3. 这里只展示当前真实可用工具,避免历史残留能力继续污染工具面。
func renderExecuteToolCatalogCompact(ctx *agentmodel.ConversationContext, state *agentmodel.CommonState) string {
if ctx == nil {
return ""
}
schemas := ctx.ToolSchemasSnapshot()
if len(schemas) == 0 {
return ""
}
lines := []string{"可用工具(简表):"}
index := 0
for _, schemaItem := range schemas {
name := strings.TrimSpace(schemaItem.Name)
if name == "" {
continue
}
index++
desc := strings.TrimSpace(schemaItem.Desc)
if desc == "" {
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)
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")
}
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",
"upsert_task_class":
return true
default:
return false
}
}
func renderExecuteToolCallHint(toolName string) string {
switch strings.ToLower(strings.TrimSpace(toolName)) {
case "upsert_task_class":
return `仅当用户或上下文已明确给出日期范围时,才允许写入 start_date/end_date写前先检查 difficulty_level 已归一为 low/medium/highitems 已非空且内容顺序已生成完成:{"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,6],"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个...任务清单(已过滤课程)..."
case "get_task_info":
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":
return "stringJSON字符串", `{"tool":"query_target_tasks","count":6,"status":"suggested","enqueue":true,"enqueued":6,"queue":{"pending_count":6},"items":[{"task_id":35,"name":"示例任务","status":"suggested","slots":[{"day":3,"week":12,"day_of_week":1,"slot_start":5,"slot_end":6}]}]}`
case "queue_pop_head":
return "stringJSON字符串", `{"tool":"queue_pop_head","has_head":true,"pending_count":5,"current":{"task_id":35,"name":"示例任务","status":"suggested","slots":[{"day":3,"week":12,"day_of_week":1,"slot_start":5,"slot_end":6}]}}`
case "queue_status":
return "stringJSON字符串", `{"tool":"queue_status","pending_count":5,"completed_count":1,"skipped_count":0,"current_task_id":35,"current_attempt":1}`
case "queue_apply_head_move":
return "stringJSON字符串", `{"tool":"queue_apply_head_move","success":true,"task_id":35,"pending_count":4,"completed_count":2,"result":"已将 [35]... 从第3天第5-6节移至第5天第3-4节。"}`
case "queue_skip_head":
return "stringJSON字符串", `{"tool":"queue_skip_head","success":true,"skipped_task_id":35,"pending_count":4,"skipped_count":1}`
case "query_range":
return returnType, "第5天第3-6节第3节空、第4节空..."
case "place":
return returnType, "已将 [35]... 预排到第5天第3-4节。"
case "move":
return returnType, "已将 [35]... 从第3天第5-6节移至第5天第3-4节。"
case "swap":
return returnType, "交换完成:[35]... ↔ [36]..."
case "batch_move":
return returnType, "批量移动完成2 个任务全部成功。"
case "unplace":
return returnType, "已将 [35]... 移除,恢复为待安排状态。"
case "web_search":
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 "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, "自然语言结果(成功/失败原因/关键数据摘要)。"
}
}
func parseExecuteToolSchema(schemaText string) executeToolSchemaDoc {
doc := executeToolSchemaDoc{Parameters: map[string]any{}}
schemaText = strings.TrimSpace(schemaText)
if schemaText == "" {
return doc
}
if err := json.Unmarshal([]byte(schemaText), &doc); err != nil {
return doc
}
if doc.Parameters == nil {
doc.Parameters = map[string]any{}
}
return doc
}
func renderExecuteToolParamSummary(parameters map[string]any) string {
if len(parameters) == 0 {
return "{}"
}
keys := make([]string, 0, len(parameters))
for key := range parameters {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
status := "可选"
typeText := ""
switch typed := parameters[key].(type) {
case string:
status = "必填"
typeText = strings.TrimSpace(typed)
case map[string]any:
if required, ok := typed["required"].(bool); ok && required {
status = "必填"
}
typeText = strings.TrimSpace(asExecuteString(typed["type"]))
if enumRaw, ok := typed["enum"].([]any); ok && len(enumRaw) > 0 {
enumText := make([]string, 0, len(enumRaw))
for _, item := range enumRaw {
enumText = append(enumText, fmt.Sprintf("%v", item))
}
if typeText == "" {
typeText = "enum"
}
typeText += ":" + strings.Join(enumText, "/")
}
}
if typeText == "" {
parts = append(parts, fmt.Sprintf("%s(%s)", key, status))
continue
}
parts = append(parts, fmt.Sprintf("%s(%s,%s)", key, status, typeText))
}
return strings.Join(parts, "")
}
// collectExecuteLoopRecords 从 history 里提取 thought + tool_call + observation 三元组。
//
// 1. 以 assistant tool_call 为主记录。
// 2. 用 ToolCallID 去关联 tool observation保证同轮绑定。
// 3. thought 只向前取最近一条 assistant 纯文本消息,不跨越到更早的工具调用之前做复杂回溯。
func collectExecuteLoopRecords(history []*schema.Message) []executeLoopRecord {
if len(history) == 0 {
return nil
}
toolResultByCallID := make(map[string]*schema.Message, len(history))
for _, msg := range history {
if msg == nil || msg.Role != schema.Tool {
continue
}
callID := strings.TrimSpace(msg.ToolCallID)
if callID == "" {
continue
}
toolResultByCallID[callID] = msg
}
records := make([]executeLoopRecord, 0, len(history))
for i, msg := range history {
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 = "{}"
}
observation := "该工具调用尚未返回结果。"
callID := strings.TrimSpace(call.ID)
if callID != "" {
if resultMsg, ok := toolResultByCallID[callID]; ok && resultMsg != nil {
text := strings.TrimSpace(resultMsg.Content)
if text != "" {
observation = text
}
}
}
records = append(records, executeLoopRecord{
Thought: thought,
ToolName: toolName,
ToolArgs: toolArgs,
Observation: observation,
})
}
}
return records
}
func findExecuteThoughtBefore(history []*schema.Message, index int) string {
for i := index - 1; i >= 0; i-- {
msg := history[i]
if msg == nil || msg.Role != schema.Assistant {
continue
}
if len(msg.ToolCalls) > 0 {
continue
}
content := compactExecuteText(msg.Content, 140)
if content != "" {
return content
}
}
return "(未记录)"
}
func renderExecuteToolCallText(toolName, toolArgs string) string {
toolName = strings.TrimSpace(toolName)
if toolName == "" {
toolName = "unknown_tool"
}
toolArgs = strings.TrimSpace(toolArgs)
if toolArgs == "" {
toolArgs = "{}"
}
return toolName + "(" + toolArgs + ")"
}
func hasExecuteRoughBuildDone(ctx *agentmodel.ConversationContext) bool {
if ctx == nil {
return false
}
for _, block := range ctx.PinnedBlocksSnapshot() {
if strings.TrimSpace(block.Key) == "rough_build_done" {
return true
}
}
return false
}
func renderExecuteLatestAnalyzeSummary(ctx *agentmodel.ConversationContext) string {
record, ok := findExecuteLatestToolRecord(ctx, map[string]struct{}{
"analyze_health": {},
"analyze_rhythm": {},
})
if !ok {
return ""
}
return fmt.Sprintf("%s -> %s", record.ToolName, record.Observation)
}
func renderExecuteLatestMutationSummary(ctx *agentmodel.ConversationContext) string {
record, ok := findExecuteLatestToolRecord(ctx, map[string]struct{}{
"place": {},
"move": {},
"swap": {},
"batch_move": {},
"unplace": {},
"queue_apply_head_move": {},
})
if !ok {
return ""
}
return fmt.Sprintf("%s -> %s", record.ToolName, record.Observation)
}
func findExecuteLatestToolRecord(ctx *agentmodel.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. 过滤 correction prompt避免把后端纠错提示伪装成用户真实意图。
// 2. 过滤 assistant tool_call 消息,避免 msg1 和 msg2 重复。
// 3. 保持原始顺序,不在这里裁剪长度。
func collectExecuteConversationTurns(history []*schema.Message) []conversationTurn {
if len(history) == 0 {
return nil
}
turns := make([]conversationTurn, 0, len(history))
for _, msg := range history {
if msg == nil {
continue
}
text := strings.TrimSpace(msg.Content)
if text == "" {
continue
}
switch msg.Role {
case schema.User:
if isExecuteCorrectionPrompt(msg) {
continue
}
turns = append(turns, conversationTurn{Role: "user", Content: text})
case schema.Assistant:
if len(msg.ToolCalls) > 0 {
continue
}
turns = append(turns, conversationTurn{Role: "assistant", Content: text})
}
}
return turns
}
func isExecuteCorrectionPrompt(msg *schema.Message) bool {
if msg == nil || msg.Role != schema.User {
return false
}
if msg.Extra != nil {
if kind, ok := msg.Extra[executeHistoryKindKey].(string); ok && strings.TrimSpace(kind) == executeHistoryKindCorrectionUser {
return true
}
}
content := strings.TrimSpace(msg.Content)
return strings.Contains(content, "请重新分析当前状态,输出正确的内容。")
}
func compactExecuteText(content string, maxLen int) string {
content = firstExecuteLine(content)
content = strings.TrimSpace(content)
if content == "" {
return ""
}
runes := []rune(content)
if len(runes) <= maxLen {
return content
}
if maxLen <= 3 {
return string(runes[:maxLen])
}
return string(runes[:maxLen-3]) + "..."
}
func firstExecuteLine(content string) string {
content = strings.TrimSpace(content)
if content == "" {
return ""
}
lines := strings.Split(content, "\n")
return strings.TrimSpace(lines[0])
}
func asExecuteString(value any) string {
if text, ok := value.(string); ok {
return text
}
return ""
}
func renderExecuteTaskClassIDs(state *agentmodel.CommonState) string {
if state == nil || len(state.TaskClassIDs) == 0 {
return ""
}
parts := make([]string, len(state.TaskClassIDs))
for i, id := range state.TaskClassIDs {
parts[i] = strconv.Itoa(id)
}
return fmt.Sprintf("task_class_ids=[%s]", strings.Join(parts, ","))
}
// renderExecuteMemoryContext 复用统一记忆入口,避免 execute 私自拼接其他 pinned block。
func renderExecuteMemoryContext(ctx *agentmodel.ConversationContext) string {
return renderUnifiedMemoryContext(ctx)
}
func renderTaskClassUpsertRuntime(state *agentmodel.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, "- 写前最少检查项mode=auto 的 start_date/end_date、subject_type/difficulty_level/cognitive_intensity、difficulty_level 合法枚举、items 非空且内容已生成、config 约束字段合法。")
lines = append(lines, "- 先判断当前 issues 属于哪一类:若是 schema 字段名、字段位置、半天块索引、枚举值、日期格式、工具语义映射等内部表示问题,直接静默改参重试。")
lines = append(lines, "- 若 issue 指向 start_date/end_date 等字段,先检查当前对话、历史、记忆、最近工具结果里是否已出现可用值;只有确实没有时再 ask_user。")
lines = append(lines, "- 若缺的是 start_date/end_date/日期范围/开始日期承诺/完成期限,而这些值并未在上下文中出现,就必须 ask_user不能把当前日期或默认周期当成用户已同意的时间边界。")
lines = append(lines, "- 若 issue 像 difficulty_level 非法、items 为空、约束字段格式不合法,就先在本轮静默归一/补齐/生成,再 confirm 重试;不要把 validation 当试错器。")
lines = append(lines, "- 若再次调用 upsert_task_class动作必须是 confirm不能输出 continue + tool_call。")
lines = append(lines, "- 在 issues 处理完之前,不要用 done 收口。")
}
return strings.Join(lines, "\n")
}

View File

@@ -0,0 +1,33 @@
package agentprompt
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 agentprompt
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 agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
agenttools "github.com/LoveLosita/smartflow/backend/services/agent/tools"
)
// renderExecuteNextStepHintV2 生成 execute.msg3 的轻量方向提示。
//
// 设计目标:
// 1. 主动优化模式下,只强调“先 analyze_health再从 candidates 里选”,不再散发额外搜索暗示。
// 2. 普通链路仍保留必要的业务引导,避免误伤用户明确提出的普通调整请求。
// 3. 提示只给方向,不替模型代填最终写参数。
func renderExecuteNextStepHintV2(
state *agentmodel.CommonState,
latestAnalyze string,
latestMutation string,
roughBuildDone bool,
) string {
if state == nil {
return ""
}
activeDomain := strings.TrimSpace(state.ActiveToolDomain)
activePacks := agenttools.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 激活对应工具。若用户只是在描述学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权,默认先走 taskclass只有用户明确要求“排进日程 / 给出具体时间安排 / 现在就排一版”时,才切 schedule。`
}
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, agenttools.ToolPackQueue) {
return `若诊断已经完成,下一步应转入读事实或写操作,不要重复 analyze_health涉及同类批量任务时优先考虑 packs=["queue"]。`
}
if activeDomain == "taskclass" &&
state.TaskClassUpsertLastTried &&
!state.TaskClassUpsertLastSuccess {
return `先判断 validation.issues 是“用户缺信息”还是“内部表示修正”;能从上下文补的先静默补齐,再用 confirm 重试 upsert_task_class不要继续解释底层约束更不要直接收口。`
}
return ""
}

View File

@@ -0,0 +1,350 @@
package agentprompt
import (
"fmt"
"strings"
"time"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
agenttools "github.com/LoveLosita/smartflow/backend/services/agent/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 *agentmodel.CommonState, ctx *agentmodel.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 *agentmodel.CommonState, ctx *agentmodel.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, agenttools.ToolPackQueue) {
appendPack(buildExecuteQueueMicroPack())
}
if hasExecutePack(activePacks, agenttools.ToolPackMutation) {
appendPack(buildExecuteScheduleMutationPack())
}
if hasExecutePack(activePacks, agenttools.ToolPackAnalyze) {
appendPack(buildExecuteScheduleAnalyzePackV2())
}
if hasExecutePack(activePacks, agenttools.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 *agentmodel.CommonState) string {
if state == nil {
return ""
}
return strings.TrimSpace(state.ActiveToolDomain)
}
func readExecuteActiveToolPacks(state *agentmodel.CommonState) []string {
if state == nil {
return nil
}
return agenttools.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。
- 域切换要克制:用户若只是在描述学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权,这默认仍是 taskclass不要主动切到 schedule。
- 只有用户明确要求“排进日程 / 给出具体时间安排 / 现在就排一版”时,才允许进入 schedule 或触发粗排。
- 先事实后动作:优先读工具补齐事实,再决定下一步。
- 只要决定调用任何写工具,就必须输出 action=confirmcontinue + 写工具无效。这个纪律同样适用于 upsert_task_class 的每一次重试。
- 输出格式固定:先 <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(`
- 严禁伪造工具结果;若新结果与既有事实冲突,先重查一次再决定。
- P1 阶段禁止调用 min_context_switch。
- 工具参数必须严格使用 schema 字段名,禁止自造别名。
- JSON 只保留当前 action 必需字段;不要输出空字符串、空对象、空数组或 null 占位。
- 连续两轮同类读查询后,必须转执行 / 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 前先判断用户是否明确提出排程诉求;若只是描述任务类内容与排程偏好,先留在 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只处理当前目标任务类不重排无关内容。
- 只有用户已明确要求“排进日程 / 给出具体时间安排 / 现在就排一版”时,才应停留或切入 schedule。
- 单纯看到总节数、难度、节次偏好、禁排时段、排除星期,不足以进入 schedule这些默认仍属于 taskclass 约束。
- 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 域只负责生成或修正任务类,不代表已经开始排程。
- 学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权,默认都先落在 taskclass 语义中。
- 例:“我要复习离散数学,基础较差,大概学 8 节课,不要早上第 1-2 节和晚上第 11-12 节,周末也不想学,每节课内容你自己来”——应进入或停留 taskclass而不是主动切 schedule也通常不需要 ask_user。
- 在真正调用 upsert_task_class 前,必须先做一轮写前检查;只有当参数已齐全、格式合法、业务前提已满足时,才允许输出 confirm。
- 不要把 validation 失败当成正常试错器validation 只用于兜底发现漏项,不应成为“先乱写一次看看后端报什么”的主流程。
- upsert_task_class 写前最少检查项:
1. mode=auto 时task_class 顶层 start_date/end_date 是否已经满足。
2. subject_type / difficulty_level / cognitive_intensity 是否齐全。
3. difficulty_level 是否已归一到合法枚举 low/medium/high。
4. items 是否非空,且顺序与内容是否已在当前轮生成完成。
5. config 中已知约束字段是否已是合法格式,例如 excluded_slots 半天块索引、excluded_days_of_week 取值范围、total_slots/strategy 等。
- 若像 items 这种内容本就由当前轮模型负责生成,就应先生成齐再写,不要把空 items 提交给 validation 去提醒你补课表内容。
- upsert_task_class 若返回 validation.ok=false必须先处理 validation.issues再考虑重试或 ask_user。
- 先区分 issue 类型schema 字段名、字段位置、内部索引、枚举值、日期格式、工具语义映射,属于内部表示修正,应静默改参后直接重试;真正缺少用户关键信息时,才 ask_user。
- taskclass 里的“关键信息缺失”要收窄定义:真正必须 ask_user 的,是会决定任务类真实时间边界/时间承诺的字段,而不是内部表示问题。
- 必须 ask_user 的时间参数/条件包括start_date、end_date、明确日期范围、明确开始日期承诺、明确完成期限如果这些信息在当前对话、历史、记忆里都不存在就不能由你自行拍板。
- 当前时间锚点只能用来解析用户已经说出的相对时间;若用户没说“今天开始 / 本周内 / 两周内 / 下周前”这类时间承诺,不能因为“今天是 2026-04-27”就默认 start_date=今天,也不能默认补一个 end_date。
- 禁排时段、排除星期、总节数、难度、内容拆分授权,不等于用户已经给出了日期范围;这些信息再完整,也不能单独推出 start_date/end_date。
- config.excluded_slots 使用 1~6 的半天块索引像“第1-2节”应映射到 1“第11-12节”应映射到 6。这类换算由你内部处理不要把底层表示解释成主要回复内容。
- 若 validation 指出 auto 模式缺 start_date/end_date先检查当前对话、历史、记忆里是否已有日期范围已有就静默补齐并重试只有确实没有时再 ask_user。
- subject_type / difficulty_level / cognitive_intensity 是任务类语义画像必填项;优先静默推断,只有确实无法判断时再 ask_user。
- 只要再次调用 upsert_task_class无论是首次写入还是失败后的重试都必须走 action=confirm。
- 当前轮目标若是创建/修正 taskclass应优先追求静默闭环不要把主要篇幅花在教育用户理解工具内部约束上。
- 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 修补。
- 先回到“写前检查”再决定是否重试:确认 mode=auto 的日期边界、difficulty_level 合法枚举、subject_type/difficulty_level/cognitive_intensity 齐全、items 非空且已生成、config 约束字段合法。
- 先判断 issue 是“用户关键信息缺失”还是“内部表示/工具语义修正”:前者才 ask_user后者直接静默改参重试。
- 如果 issue 最终落到 start_date / end_date / 日期范围 / 开始日期承诺 / 完成期限,而这些值在当前对话、历史、记忆、最近工具结果里都没有出现,就必须 ask_user不要再拿当前时间锚点去替用户补。
- 若用户只给了禁排时段、排除星期、总节数、难度、内容拆分授权,这仍不构成日期范围;不要把这类偏好误判成已经拿到了可写入的 start_date/end_date。
- 如果 issue 像 difficulty_level 非法、items 为空、约束字段格式不合法,这都属于“写前本应整理好”的问题:应先在本轮静默归一/补齐/生成,再 confirm 重试,不要继续拿 validation 探路。
- 若 issue 所需字段已在当前对话、历史、记忆或最近工具结果里出现,优先静默补齐,不要多轮解释后再写。
- 重试 upsert_task_class 时仍然必须输出 action=confirm不要输出 continue + tool_call。
- 问题未解决前,不要用 done 假装收口;要么重试,要么 ask_user 补关键信息。`),
}
}
func shouldInjectExecuteDiagLoopPack(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext) bool {
if state == nil || !hasExecuteRoughBuildDone(ctx) {
return false
}
if normalizeExecuteToolDomain(readExecuteActiveToolDomain(state)) != "schedule" {
return false
}
activePacks := readExecuteActiveToolPacks(state)
return hasExecutePack(activePacks, agenttools.ToolPackAnalyze) &&
hasExecutePack(activePacks, agenttools.ToolPackMutation)
}

View File

@@ -0,0 +1,27 @@
package agentprompt
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

@@ -0,0 +1,120 @@
package agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
const planSystemPromptCore = `
你是 SmartMate 的规划器Planner只负责规划不负责执行。
最高优先级规则:
1. 意图边界:只规划用户当前明确要求,禁止擅自扩展后续动作。
2. 事实边界:禁止伪造工具调用、工具结果、外部事实和执行结论。
3. 规划视角:先判断“最小工具闭环”再写步骤;不要先写抽象语义步骤,再让 execute 自己猜该怎么落工具。
规划规则:
1. 每轮只做一次决策continue / ask_user / plan_done
2. 信息足够时优先 plan_done信息不足时才 ask_user且只问最小必要问题。
3. action=plan_done 时必须返回完整 plan_steps不是增量
4. plan_steps 必须优先按“工具闭环”拆步,而不是按抽象语义拆步。
5. 若一个目标可由单个工具闭环完成,优先生成单步计划;禁止把本可直接执行的工具动作,拆成“先分析、再设计、再确认、再执行”这类抽象多步。
6. 每个 step 的 done_when 都应尽量贴近可观察证据,优先锚定工具回执、校验结果、查询 observation而不是“方案完整”“分析完成”“用户应该满意”这类抽象描述。
7. 只有单工具无法闭环,或当前步骤天然依赖上一步 observation / 用户补充信息时,才允许拆成多步。
8. 先判断为完成目标“首个可执行闭环”最小需要的 domain / packs再围绕这些工具写 steps最后再产出 context_hook。context_hook 不是顺手填空,而是计划的自然推导结果。
9. context_hook 只有一份,供 execute 首轮激活工具域使用;它应对齐“第一个可执行 step”的最小工具需求而不是试图一次覆盖整份计划的所有后续能力。
10. 用户若只是在描述学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权,这默认仍是 taskclass 语义,不等于已经要求排进日程。
11. 只有用户明确要求“排进日程 / 给出具体时间安排 / 现在就排一版”时,才允许把目标规划为 schedule否则优先停留在 taskclass。
12. 若意图满足批量排程识别条件,可在 plan_done 时附加 needs_rough_build 与 task_class_ids但仅当用户明确提出排程请求时才允许这样做。
13. 可在 plan_done 时附加 context_hook执行阶段注入建议若用户尚未明确要求排程则 context_hook.domain 不得写 schedule。规划阶段禁止调用 context_tools_add/remove。
14. 例:“我要复习离散数学,基础较差,大概学 8 节课,不要早上第 1-2 节和晚上第 11-12 节,周末也不想学,每节课内容你自己来”——这应判定为 taskclass 设计planner 应优先理解为 taskclass 域可闭环的请求,通常单步或极少步即可,不应抽象拆成多轮。`
// BuildPlanSystemPrompt 返回规划阶段系统提示词。
func BuildPlanSystemPrompt() string {
parts := []string{
strings.TrimSpace(planSystemPromptCore),
BuildPlanDecisionContractText(),
}
return strings.TrimSpace(strings.Join(parts, "\n\n"))
}
// BuildPlanMessages 组装规划阶段的 messages。
func BuildPlanMessages(state *agentmodel.CommonState, ctx *agentmodel.ConversationContext, userInput string) []*schema.Message {
return buildUnifiedStageMessages(
ctx,
StageMessagesConfig{
SystemPrompt: BuildPlanSystemPrompt(),
Msg1Content: buildPlanConversationMessage(ctx),
Msg2Content: buildPlanWorkspace(state),
Msg3Suffix: BuildPlanUserPrompt(state, userInput),
Msg3Role: schema.User,
SkipBaseSystemPrompt: true,
UseLiteToolCatalogMsg: true,
},
)
}
// BuildPlanUserPrompt 构造规划阶段的用户提示词。
func BuildPlanUserPrompt(state *agentmodel.CommonState, userInput string) string {
var sb strings.Builder
sb.WriteString("请继续当前任务规划,只输出一组 SMARTFLOW_DECISION 决策。\n")
sb.WriteString("请基于最近对话与规划工作区推进,不要重复已有计划内容。\n")
sb.WriteString("请先判断最小工具闭环,再决定是否需要拆步;能单步就单步。\n")
sb.WriteString("若需要 context_hook请先根据第一个可执行 step 所需的最小 domain / packs 推导,再写入 hook。\n")
sb.WriteString("禁止把本可直接落工具的动作,抽象写成“完成设计 / 确认方案 / 整理思路”之类空步骤。\n")
sb.WriteString("输出格式与字段约束严格按 msg0 协议执行。\n")
trimmedInput := strings.TrimSpace(userInput)
if trimmedInput != "" {
sb.WriteString("\n用户本轮输入\n")
sb.WriteString(trimmedInput)
sb.WriteString("\n")
}
return strings.TrimSpace(sb.String())
}
// BuildPlanDecisionContractText 返回规划阶段的输出协议说明。
func BuildPlanDecisionContractText() string {
return strings.TrimSpace(fmt.Sprintf(strings.Join([]string{
"输出协议(唯一口径):",
"1. 先输出:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>",
"2. 再输出:给用户看的自然语言正文",
"",
"JSON 字段:",
"- action只能是 %s / %s / %s",
"- reason给后端和日志看的简短说明",
"- complexity只能是 simple / moderate / complex",
"- plan_steps仅当 action=%s 时允许返回,且必须是完整计划",
"- plan_steps[].content步骤正文必填",
"- plan_steps[].done_when可选若提供必须尽量写成 observation / 工具回执可直接证明的完成判定",
"- 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 字段",
"- 不要在 planning 阶段调用任何工具(包括 context_tools_add/remove",
"- 写 plan_steps 前,先判断当前目标能否由单个工具或单个紧凑工具闭环完成;若能,优先输出单步计划",
"- 禁止把本可直接执行的工具动作,拆成抽象语义步骤,例如“先分析需求”“完成设计”“确认方案完整”",
"- 多步计划只应用于:上一步 observation 决定下一步;或确实需要先问用户补关键事实;或目标天然跨域",
"- context_hook 必须从 plan_steps 自然推导:优先对齐第一个可执行 step 的最小 domain / packs不要脱离步骤单独拍脑袋生成",
"- 若用户只给出学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权,这默认属于 taskclass 设计;不要因此写 needs_rough_build=true也不要把 context_hook.domain 设为 schedule",
"- 只有用户明确要求\"排进日程 / 给出具体时间安排 / 现在就排一版\"时,才允许输出 needs_rough_build=true 或 context_hook.domain=schedule",
"- 若首步本质上是任务类写入或修正context_hook 通常应对齐 taskclass若首步需要 schedule 查询/分析/修改,再按最小 packs 推导 schedule hook",
"- step 的 done_when 应优先锚定查询结果已返回、validation 已通过、写工具已成功回执、粗排标记已产生、分析结论已可直接支撑下一步",
"- 例:\"我要复习离散数学,基础较差,大概学 8 节课,不要早上第 1-2 节和晚上第 11-12 节学习,周末也不想学,每节课内容你自己来\"——应规划为 taskclass而不是 schedule也通常不需要 ask_user",
}, "\n"),
agentmodel.PlanActionContinue,
agentmodel.PlanActionAskUser,
agentmodel.PlanActionDone,
agentmodel.PlanActionDone,
))
}

View File

@@ -0,0 +1,223 @@
package agentprompt
import (
"fmt"
"strconv"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
)
// buildPlanConversationMessage 生成 plan 节点看到的真实对话视图。
func buildPlanConversationMessage(ctx *agentmodel.ConversationContext) string {
return buildConversationHistoryMessage(ctx, "规划参考对话")
}
// buildPlanWorkspace 渲染 plan 节点自己的工作区。
//
// 设计说明:
// 1. 这里既保留“当前已有计划/任务类约束”,也显式补充“规划视角的工具摘要”;
// 2. planner 需要先理解工具边界,才能把步骤收敛到最小闭环,而不是按抽象语义乱拆;
// 3. 工具摘要不展开全量 schema只提供规划真正需要的负责什么、不负责什么、常见闭环、完成证据、域切换条件。
func buildPlanWorkspace(state *agentmodel.CommonState) string {
lines := []string{"规划工作区:"}
if state == nil {
lines = append(lines, "- 当前缺少流程状态,请主要依据最近对话与本轮输入继续规划。")
lines = append(lines, buildPlanToolPlanningSummary())
return strings.Join(lines, "\n")
}
if !state.HasPlan() {
lines = append(lines, "- 当前还没有正式计划。")
} else {
lines = append(lines, fmt.Sprintf("- 已有计划:共 %d 步。", len(state.PlanSteps)))
lines = append(lines, renderPlanCurrentStepSummary(state))
lines = append(lines, "计划简表:")
lines = append(lines, renderPlanStepOutline(state.PlanSteps))
}
if taskClassIDs := renderPlanTaskClassIDs(state); taskClassIDs != "" {
lines = append(lines, "- "+taskClassIDs)
}
if taskClassMeta := renderPlanTaskClassMeta(state); taskClassMeta != "" {
lines = append(lines, "任务类约束:")
lines = append(lines, taskClassMeta)
}
lines = append(lines, buildPlanToolPlanningSummary())
return strings.Join(lines, "\n")
}
// renderPlanCurrentStepSummary 返回 plan 节点需要知道的当前步骤进度。
func renderPlanCurrentStepSummary(state *agentmodel.CommonState) string {
if state == nil || !state.HasPlan() {
return "- 当前步骤:暂无。"
}
current, total := state.PlanProgress()
step, ok := state.CurrentPlanStep()
if !ok {
return fmt.Sprintf("- 当前步骤:计划共 %d 步,当前没有可继续沿用的有效步骤。", total)
}
content := strings.TrimSpace(step.Content)
if content == "" {
content = "(当前步骤正文为空)"
}
summary := fmt.Sprintf("- 当前步骤:第 %d/%d 步,%s", current, total, content)
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
summary += fmt.Sprintf(";完成判定:%s", doneWhen)
}
return summary
}
// renderPlanStepOutline 将完整计划压成 plan 节点可读的简表。
func renderPlanStepOutline(steps []agentmodel.PlanStep) string {
if len(steps) == 0 {
return "- 暂无。"
}
lines := make([]string, 0, len(steps))
for i, step := range steps {
content := strings.TrimSpace(step.Content)
if content == "" {
content = "(步骤正文为空)"
}
line := fmt.Sprintf("%d. %s", i+1, content)
if doneWhen := strings.TrimSpace(step.DoneWhen); doneWhen != "" {
line += fmt.Sprintf(" | 完成判定:%s", doneWhen)
}
lines = append(lines, line)
}
return strings.Join(lines, "\n")
}
// renderPlanTaskClassIDs 返回批量排课场景下的 task_class_ids 简表。
func renderPlanTaskClassIDs(state *agentmodel.CommonState) string {
if state == nil || len(state.TaskClassIDs) == 0 {
return ""
}
parts := make([]string, len(state.TaskClassIDs))
for i, id := range state.TaskClassIDs {
parts[i] = strconv.Itoa(id)
}
return fmt.Sprintf("task_class_ids=[%s]", strings.Join(parts, ", "))
}
// renderPlanTaskClassMeta 返回 plan 节点真正需要看的任务类边界。
//
// 说明:
// 1. 这里只保留名称、策略、总时段、日期范围这类规划相关信息;
// 2. 不再把所有字段原样平铺,避免工作区过胖;
// 3. 若某项字段为空,则直接省略,不制造噪声。
func renderPlanTaskClassMeta(state *agentmodel.CommonState) string {
if state == nil || len(state.TaskClasses) == 0 {
return ""
}
lines := make([]string, 0, len(state.TaskClasses))
for _, tc := range state.TaskClasses {
line := fmt.Sprintf("- [ID=%d] %s", tc.ID, strings.TrimSpace(tc.Name))
if strategy := strings.TrimSpace(tc.Strategy); strategy != "" {
line += fmt.Sprintf(";策略:%s", strategy)
}
if tc.TotalSlots > 0 {
line += fmt.Sprintf(";总时段预算:%d", tc.TotalSlots)
}
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")
}
// buildPlanToolPlanningSummary 生成“规划视角的工具摘要”。
//
// 步骤化说明:
// 1. 先讲 domain让 planner 先判断目标应该停留在哪个业务域;
// 2. 再讲 schedule packs让 planner 知道若进入 schedule该选最小哪组能力
// 3. 最后讲 hook 推导规则:因为 context_hook 只有一份,必须和“首个可执行闭环”对齐。
func buildPlanToolPlanningSummary() string {
sections := []string{
"规划视角的工具摘要:",
buildPlanToolDomainTaskClassSummary(),
buildPlanToolDomainScheduleSummary(),
buildPlanToolPackSummary(),
buildPlanContextHookSummary(),
}
return strings.Join(sections, "\n")
}
func buildPlanToolDomainTaskClassSummary() string {
lines := []string{
"1. taskclass 域:",
"- 负责什么:创建 / 更新任务类,沉淀学习目标、总节数、难度、节次偏好、禁排时段、排除星期、内容拆分授权、任务项结构。",
"- 不负责什么:不给出具体日期/节次落位,不负责把任务真正排进日程。",
"- 常见一步闭环:任务类设计或修正通常可由 taskclass 域单步闭环,核心写入动作为 upsert_task_class。",
"- 何时停留在本域:用户仍在描述目标、偏好、约束、拆分方式,而不是要求现在排进日程。",
"- 何时切到下一个域:只有用户明确要求“排进日程 / 给出具体时间安排 / 现在就排一版”,或当前目标本身已变成排程执行。",
"- done_when 证据偏好:优先锚定 upsert_task_class 成功回执、validation.ok=true、validation.issues 已清空。",
}
return strings.Join(lines, "\n")
}
func buildPlanToolDomainScheduleSummary() string {
lines := []string{
"2. schedule 域:",
"- 负责什么:查询日程现状、粗排、具体落位、局部移动/交换、批量同规则调整、排程健康分析。",
"- 不负责什么不凭空补考试时间、DDL、个人空闲、外部时间事实这类信息拿不到时应 ask_user。",
"- 常见一步闭环:单次查询通常一个读工具即可闭环;单次移动/交换/放置通常一个写工具即可闭环;局部分析通常一个 analyze 工具即可闭环。",
"- 何时停留在本域:用户明确要求查询、安排、调整、优化当前日程。",
"- 何时先回 taskclass如果用户还在定义“学什么、学多少、怎么拆、哪些时段不要学”而不是要求立刻排程应先停留在 taskclass。",
"- done_when 证据偏好:优先锚定查询 observation、写工具成功回执、rough_build_done 标记、analyze observation 已能直接支撑下一步。",
}
return strings.Join(lines, "\n")
}
func buildPlanToolPackSummary() string {
lines := []string{
"3. schedule packs 选择参考:",
"- detail_read查看总览、查询区间、看任务详情适合“先读事实再决定”的首步。",
"- mutationplace / move / swap / batch_move / unplace适合真正落日程或调日程。",
"- analyzeanalyze_health / analyze_rhythm适合先判断是否还有优化空间、该往哪里动。",
"- queue适合“按同一规则逐个处理一批任务”的计划不必把整批任务细节都堆进 steps。",
"- web仅补通用学习资料或通识信息不用于补个人时间事实。",
"- deep_analyze适合确实需要更深一层 schedule 分析时再加,默认不要为了“看起来完整”就提前注入。",
"- 选 pack 原则:只选首个可执行 step 真的需要的最小 packs不要为了保险一次全带上。",
}
return strings.Join(lines, "\n")
}
func buildPlanContextHookSummary() string {
lines := []string{
"4. context_hook 推导规则:",
"- 先确定 steps再看第一个可执行 step 需要哪个 domain / 哪组最小 packs最后才写 hook。",
"- 若第一个可执行 step 本质上是任务类写入或修正hook 通常应为 taskclass且一般不需要 packs。",
"- 若第一个可执行 step 是 schedule 查询hook 应为 schedule并优先只带 detail_read。",
"- 若第一个可执行 step 是 schedule 分析hook 应为 schedule并优先带 analyze若分析后立刻要落写再补 mutation。",
"- 若第一个可执行 step 是批量同规则处理hook 应在 schedule 基础上按需加 queue。",
"- hook 只有一份不要求提前覆盖整份计划的所有后续能力execute 可以在后续按计划再切域或补 packs。",
}
return strings.Join(lines, "\n")
}
func planSemanticValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "未标注"
}
return trimmed
}

View File

@@ -0,0 +1,106 @@
package agentprompt
import (
"fmt"
"strings"
"time"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
const quickTaskSystemPrompt = `
你是 SmartMate 的快捷任务助手。用户想记录或查看待办任务。你需要从用户消息中提取操作意图和参数。
你能做的操作:
- create记录一条新任务/提醒
- query查看/筛选任务列表
输出格式(两阶段):
先输出一行决策标签,标签内是 JSON标签之后换行输出给用户看的自然语言正文。
决策标签格式:<SMARTFLOW_DECISION>{JSON}</SMARTFLOW_DECISION>
JSON 字段说明:
- action只能是 create / query / ask
- create 时title 必填deadline_at 必填priority_group 必填,范围 1-4estimated_sections 必填,范围 1-4不确定默认 1urgency_threshold_at 满足条件时填写,条件在下面
- query 时quadrant 可选 1-4keyword 可选limit 可选deadline_after/deadline_before 可选(用于截止时间窗口筛选)
- ask 时question 必填
规则:
1. 优先级1=重要且紧急2=重要不紧急3=简单不重要4=复杂不重要;紧急判定:截止时间距今不超过 48 小时为紧急(1),超过 48 小时为不紧急(2),无截止时间默认 3
2. 信息不足时(如缺少 title输出 {"action":"ask","question":"追问内容"}
3. deadline_at 支持「明天下午3点」「下周一」「2026-04-20 18:00」等格式
4. 未提供的可选字段直接省略,不要填 null 或空字符串
5. JSON 中不要包含 speak 字段,给用户看的话放在 </SMARTFLOW_DECISION> 标签之后
6. 紧急分界时间,即任务从"重要不紧急"自动轮换到"重要且紧急"的时间点;格式同 deadline_at ——当 priority_group=2 时必填,你必须根据 deadline 自动推算一个合理的紧急分界时间(通常为 deadline 前 24-48 小时不要等用户提供priority_group 为 1、3、4 或无截止时间时不要输出此字段
7. query 里出现相对日期窗口(如"明天有什么事要做"优先输出明确边界deadline_after="明天 00:00"deadline_before="后天 00:00",按 [after, before) 语义筛选
示例:
<SMARTFLOW_DECISION>{"action":"create","title":"明天开会","deadline_at":"明天下午3点","estimated_sections":1}</SMARTFLOW_DECISION>
好的,我来帮你记一下。
<SMARTFLOW_DECISION>{"action":"create","title":"下周交报告","deadline_at":"下周五 18:00","priority_group":2,"estimated_sections":2,"urgency_threshold_at":"下周四 09:00"}</SMARTFLOW_DECISION>
好的,我也帮你记一下。
<SMARTFLOW_DECISION>{"action":"query","limit":5}</SMARTFLOW_DECISION>
我帮你查一下当前的任务。
<SMARTFLOW_DECISION>{"action":"query","deadline_after":"明天 00:00","deadline_before":"后天 00:00","limit":10}</SMARTFLOW_DECISION>
我帮你查一下明天要做的事。
<SMARTFLOW_DECISION>{"action":"ask","question":"你想记录什么呢?告诉我具体内容吧。"}</SMARTFLOW_DECISION>
你想记录什么呢?告诉我具体内容吧。`
// BuildQuickTaskSystemPrompt 返回快捷任务阶段的系统提示词。
func BuildQuickTaskSystemPrompt() string {
return strings.TrimSpace(quickTaskSystemPrompt)
}
// BuildQuickTaskMessagesSimple 组装快捷任务阶段的最简 messages无对话历史
func BuildQuickTaskMessagesSimple(userInput string) []*schema.Message {
systemMsg := schema.SystemMessage(BuildQuickTaskSystemPrompt())
userMsg := schema.UserMessage(buildQuickTaskUserPrompt(userInput))
return []*schema.Message{systemMsg, userMsg}
}
func buildQuickTaskUserPrompt(userInput string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("当前时间=%s\n", time.Now().In(time.Local).Format("2006-01-02 15:04")))
sb.WriteString("\n请从用户消息中提取操作意图和参数严格按 SMARTFLOW_DECISION 标签格式输出。\n\n")
trimmedInput := strings.TrimSpace(userInput)
if trimmedInput != "" {
sb.WriteString("用户输入:\n")
sb.WriteString(trimmedInput)
sb.WriteString("\n")
}
return sb.String()
}
// BuildQuickTaskMessages 组装快捷任务阶段的完整 messages含对话历史
func BuildQuickTaskMessages(
ctx *agentmodel.ConversationContext,
userInput string,
toolSchemas []agentmodel.ToolSchemaContext,
) []*schema.Message {
return buildUnifiedStageMessages(
ctx,
StageMessagesConfig{
SystemPrompt: BuildQuickTaskSystemPrompt(),
Msg1Content: buildChatConversationMessage(ctx),
Msg2Content: buildQuickTaskWorkspace(toolSchemas),
Msg3Suffix: buildQuickTaskUserPrompt(userInput),
Msg3Role: schema.User,
},
)
}
func buildQuickTaskWorkspace(toolSchemas []agentmodel.ToolSchemaContext) string {
var sb strings.Builder
sb.WriteString("可用工具:\n")
for _, ts := range toolSchemas {
sb.WriteString(fmt.Sprintf("- %s: %s\n 参数: %s\n", ts.Name, ts.Desc, ts.SchemaText))
}
return sb.String()
}

View File

@@ -0,0 +1,136 @@
package agentprompt
import (
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
"github.com/cloudwego/eino/schema"
)
const (
reasoningSummaryMaxFullRunes = 6000
reasoningSummaryMaxDeltaRunes = 1800
)
// ReasoningSummaryPromptInput 描述一次“思考摘要”模型调用所需的最小输入。
//
// 职责边界:
// 1. 只承载摘要模型需要看的文本与运行态,不绑定 stream 包的 DTO避免 prompt 层反向依赖输出协议;
// 2. FullReasoning 会在构造 prompt 时只保留尾部,避免长时间思考把便宜模型上下文撑爆;
// 3. PreviousSummary 只作为连续摘要的参考,不要求模型逐字继承。
type ReasoningSummaryPromptInput struct {
FullReasoning string
DeltaReasoning string
PreviousSummary string
CandidateSeq int
Final bool
DurationSeconds float64
}
type reasoningSummaryPromptPayload struct {
CandidateSeq int `json:"candidate_seq"`
Final bool `json:"final"`
DurationSeconds float64 `json:"duration_seconds"`
PreviousSummary string `json:"previous_summary,omitempty"`
RecentReasoning string `json:"recent_reasoning,omitempty"`
DeltaReasoning string `json:"delta_reasoning,omitempty"`
SourceTextRunes int `json:"source_text_runes,omitempty"`
MaxDetailSummaryRunes int `json:"max_detail_summary_runes,omitempty"`
}
// BuildReasoningSummaryMessages 构造思考摘要模型调用的 messages。
//
// 步骤说明:
// 1. system prompt 明确“只做用户可见摘要”,禁止复述原始思考链和内部推理细节;
// 2. user prompt 使用 JSON 承载输入,便于后续扩展字段且减少模型误读;
// 3. 长文本只保留尾部窗口,保证异步摘要请求稳定、便宜、可控。
func BuildReasoningSummaryMessages(input ReasoningSummaryPromptInput) []*schema.Message {
recentReasoning := trimRunesFromEnd(input.FullReasoning, reasoningSummaryMaxFullRunes)
deltaReasoning := trimRunesFromEnd(input.DeltaReasoning, reasoningSummaryMaxDeltaRunes)
payload := reasoningSummaryPromptPayload{
CandidateSeq: input.CandidateSeq,
Final: input.Final,
DurationSeconds: input.DurationSeconds,
PreviousSummary: strings.TrimSpace(input.PreviousSummary),
RecentReasoning: recentReasoning,
DeltaReasoning: deltaReasoning,
SourceTextRunes: reasoningSummarySourceRunes(recentReasoning, deltaReasoning),
MaxDetailSummaryRunes: ReasoningSummaryDetailRuneLimit(input.FullReasoning, input.DeltaReasoning),
}
raw, err := json.MarshalIndent(payload, "", " ")
if err != nil {
raw = []byte(fmt.Sprintf(`{"recent_reasoning":%q}`, trimRunesFromEnd(input.FullReasoning, reasoningSummaryMaxFullRunes)))
}
return []*schema.Message{
schema.SystemMessage(buildReasoningSummarySystemPrompt()),
schema.UserMessage("请基于 delta_reasoning 生成本轮新增的用户可见阶段摘要recent_reasoning 仅作上下文previous_summary 仅作去重参考。\n输入\n" + string(raw)),
}
}
func buildReasoningSummarySystemPrompt() string {
return strings.TrimSpace(`你是 SmartMate 的“思考摘要器”。你的任务是把模型内部 reasoning 整理成用户可见的进度摘要。
输出必须是严格 JSON 对象:
{
"short_summary": "8到18个汉字的短摘要",
"detail_summary": "不超过 max_detail_summary_runes 个字的展开摘要"
}
字段语义:
- previous_summary上一条已经展示给用户的摘要只用于判断哪些内容已经说过。
- delta_reasoning本轮新增 reasoning是生成 detail_summary 的主要依据。
- recent_reasoning全量尾部上下文只用于补齐题目、变量名、阶段背景不要按它重写一遍完整摘要。
规则:
1. 不输出 markdown不输出代码块不解释 JSON 以外的内容。
2. 摘要要像“阶段更新”,不是流水账;优先写新增结论、阶段变化、卡点、修正、下一步动作。
3. detail_summary 以 delta_reasoning 为主previous_summary 已覆盖的信息不要大段重复,除非本轮对它有修正或推进。
4. short_summary 用 8 到 18 个汉字,偏结果或动作短语,例如“补齐边界条件”“转入代码实现”“优化滚动数组”。
5. detail_summary 用自然的一到两句话表达,优先以具体对象、动作或结果开头,不要把“正在”“当前”“已确定”“已完成”作为默认句首模板;若 previous_summary 已使用类似开头,本轮必须换一种表达。
6. final=false 时不要用“已完成”概括整体任务;只有 delta_reasoning 明确完成某个局部步骤时,才可描述该局部已经完成。
7. detail_summary 字数必须小于等于 max_detail_summary_runes不需要凑满上限信息密度优先。
8. 不暴露原始思考链、隐含假设链、逐步演算,只保留用户可见的进展。
9. 若本轮没有实质新增信息,输出保守但不重复的摘要,例如“沿用上一轮判断,暂无新的可展示进展。”
10. final=true 时,用完成态语气,说明思考已经收拢到下一步答复或动作。`)
}
// ReasoningSummaryDetailRuneLimit 返回 detail_summary 的最大字数。
//
// 职责边界:
// 1. 与 BuildReasoningSummaryMessages 使用同一套输入窗口,避免 prompt 提示和服务端兜底口径不一致;
// 2. 上限取“提供给摘要模型的主要文本段”的一半,并向上取整,适配极短文本;
// 3. 返回 0 表示没有有效输入文本,调用方不应做硬裁剪。
func ReasoningSummaryDetailRuneLimit(fullReasoning, deltaReasoning string) int {
recentReasoning := trimRunesFromEnd(fullReasoning, reasoningSummaryMaxFullRunes)
delta := trimRunesFromEnd(deltaReasoning, reasoningSummaryMaxDeltaRunes)
sourceRunes := reasoningSummarySourceRunes(recentReasoning, delta)
if sourceRunes <= 0 {
return 0
}
return (sourceRunes + 1) / 2
}
func reasoningSummarySourceRunes(recentReasoning, deltaReasoning string) int {
recentReasoning = strings.TrimSpace(recentReasoning)
if recentReasoning != "" {
return utf8.RuneCountInString(recentReasoning)
}
return utf8.RuneCountInString(strings.TrimSpace(deltaReasoning))
}
func trimRunesFromEnd(text string, maxRunes int) string {
text = strings.TrimSpace(text)
if text == "" || maxRunes <= 0 {
return ""
}
runes := []rune(text)
if len(runes) <= maxRunes {
return text
}
return string(runes[len(runes)-maxRunes:])
}

View File

@@ -0,0 +1,11 @@
package agentprompt
const (
// SystemPrompt 全局系统人设:定义 SmartMate 的基本调性
SystemPrompt = `你叫 SmartMate是时伴SmartMate的中文 AI 排程伙伴,面向大学生提供陪伴式日程管理与日常协助。
你擅长课表与任务安排、任务管理、学习规划和随口记,也可以正常回答日常问答、生活建议、信息整理、分析讨论等非排程问题。
你的目标是像一个越用越懂用户的伙伴一样,结合历史对话、长期记忆和当前上下文,给出贴心、清晰、可信的帮助。
你的回复应当专业、自然、有陪伴感,偶尔可以带一点轻松幽默。
如果用户的问题与日程无关,不要因为"不属于排程"就拒绝、回避或强行转到任务安排;只要不需要工具且你有把握,就直接回答。
重要约束:你无法直接写入数据库。除非系统明确告知"任务已落库成功",否则禁止使用"已安排/已记录/已帮你记下"等完成态表述。`
)

View File

@@ -0,0 +1,273 @@
package agentprompt
import (
"fmt"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
"github.com/cloudwego/eino/schema"
)
// ConversationTurn 表示对话历史中的一轮自然语言交互。
//
// 职责边界:
// 1. 这里只承载 user 与 assistant speak不承载 tool_call 和 tool observation
// 2. 供 chat / plan / deliver 等节点复用,避免各节点重复写一套提取逻辑;
// 3. 不负责裁剪长度,长度预算统一交给压缩层处理。
type ConversationTurn struct {
Role string
Content string
}
// StageMessagesConfig 描述统一四段式骨架下,各节点自行提供的内容块。
//
// 设计目标:
// 1. 统一层只负责“四条消息怎么拼”,不再替节点决定“每条消息里该放什么”;
// 2. Msg1 / Msg2 / Msg3Prefix / Msg3Suffix 都由节点自己渲染,避免 chat / plan / deliver 继续套 execute 的内容模板;
// 3. memory_context 仍由统一层单入口注入到 msg3避免多处重复注入。
type StageMessagesConfig struct {
// SystemPrompt 是节点自己的系统提示词。
SystemPrompt string
// Msg1Content 是第 2 条 assistant 消息,通常放“节点想看的历史视图”。
Msg1Content string
// Msg2Content 是第 3 条 assistant 消息,通常放“节点自己的工作区/补充约束”。
Msg2Content string
// Msg3Prefix 是第 4 条消息中位于 memory_context 之前的内容。
// 常见放法:阶段状态、规划工作区摘要、交付收口约束等。
Msg3Prefix string
// Msg3Suffix 是第 4 条消息中位于 memory_context 之后的内容。
// 对 user-role 节点来说,这里通常放最终用户指令,保证“用户输入收尾”。
Msg3Suffix string
// Msg3Role 指定第 4 条消息的角色。
// Execute 继续使用 system其余节点一般使用 user。
Msg3Role schema.RoleType
// SkipBaseSystemPrompt 为 true 时msg0 只使用节点自己的 SystemPrompt
// 不再拼接 ConversationContext.SystemPrompt。
SkipBaseSystemPrompt bool
// UseLiteToolCatalogMsg 为 true 时msg0 工具目录采用轻量模式(仅名称与职责)。
UseLiteToolCatalogMsg bool
}
// buildUnifiedStageMessages 组装统一 4 段式消息骨架。
//
// 固定布局:
// 1. msg0(system):系统规则 + 阶段规则 + 工具简表;
// 2. msg1(assistant):节点自定义的历史视图;
// 3. msg2(assistant):节点自定义的工作区;
// 4. msg3(user/system):节点自定义前后缀 + 统一 memory_context。
func buildUnifiedStageMessages(
ctx *agentmodel.ConversationContext,
config StageMessagesConfig,
) []*schema.Message {
msg0 := buildUnifiedMsg0(config.SystemPrompt, ctx, config.SkipBaseSystemPrompt, config.UseLiteToolCatalogMsg)
msg1 := buildUnifiedMsg1(config.Msg1Content)
msg2 := buildUnifiedMsg2(config.Msg2Content)
msg3 := buildUnifiedMsg3(ctx, config)
return []*schema.Message{
schema.SystemMessage(msg0),
{Role: schema.Assistant, Content: msg1},
{Role: schema.Assistant, Content: msg2},
buildUnifiedMsg3Message(msg3, config.Msg3Role),
}
}
// buildUnifiedMsg3Message 根据配置决定第 4 条消息的角色。
func buildUnifiedMsg3Message(content string, role schema.RoleType) *schema.Message {
if role == schema.User {
return schema.UserMessage(content)
}
return schema.SystemMessage(content)
}
// buildUnifiedMsg0 合并系统提示 + 工具简表,生成 msg0。
//
// 步骤化说明:
// 1. 先合并基础系统提示与节点系统提示,保证模型身份稳定;
// 2. 若当前节点注入了工具 schema则附加紧凑工具目录
// 3. 若两部分都为空,则回退到最小兜底提示,避免出现空消息。
func buildUnifiedMsg0(stageSystemPrompt string, ctx *agentmodel.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, 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 *agentmodel.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 返回节点自行提供的历史视图。
//
// 说明:
// 1. 统一层不再内置 execute 风格的 ReAct 摘要;
// 2. 节点若未传入内容,则回退到最小占位,保证四段结构稳定;
// 3. 压缩层仍会统一统计和压缩这条消息。
func buildUnifiedMsg1(content string) string {
content = strings.TrimSpace(content)
if content != "" {
return content
}
return "历史上下文:暂无。"
}
// buildUnifiedMsg2 返回节点自行提供的工作区。
//
// 说明:
// 1. 非 execute 节点也允许有自己的 msg2不再被统一层硬塞“暂无”语义
// 2. 若节点暂时没有额外工作区,则回退到最小占位,保证结构稳定。
func buildUnifiedMsg2(content string) string {
content = strings.TrimSpace(content)
if content != "" {
return content
}
return "阶段工作区:暂无。"
}
// buildUnifiedMsg3 统一拼装 msg3前缀 + memory_context + 后缀。
//
// 步骤化说明:
// 1. 前缀由节点决定,适合放轻量状态或阶段约束;
// 2. memory_context 只在这里注入一次,避免 pinned block 多入口重复出现;
// 3. 后缀由节点决定。对于 user-role 节点,通常把最终用户指令放在这里,保证消息末尾仍是用户输入。
func buildUnifiedMsg3(ctx *agentmodel.ConversationContext, config StageMessagesConfig) string {
var sections []string
if prefix := strings.TrimSpace(config.Msg3Prefix); prefix != "" {
sections = append(sections, prefix)
}
if memoryText := renderUnifiedMemoryContext(ctx); memoryText != "" {
sections = append(sections, "相关记忆(仅在确有帮助时参考,不要机械复述):\n"+memoryText)
}
if suffix := strings.TrimSpace(config.Msg3Suffix); suffix != "" {
sections = append(sections, suffix)
}
if len(sections) == 0 {
return "请继续当前阶段。"
}
return strings.Join(sections, "\n\n")
}
// renderUnifiedMemoryContext 提取需要补充到 msg3 的记忆文本。
//
// 步骤化说明:
// 1. 只消费 memory_context避免把 execution_context / current_step 等阶段专属块混回 prompt
// 2. block 不存在或正文为空时直接返回空串;
// 3. 这里只读取 agent/sv 已经产出的最终文本,不在这里重新拼装记忆。
func renderUnifiedMemoryContext(ctx *agentmodel.ConversationContext) string {
if ctx == nil {
return ""
}
block, ok := ctx.PinnedBlockByKey("memory_context")
if !ok {
return ""
}
content := strings.TrimSpace(block.Content)
if content == "" {
return ""
}
return content
}
// CollectConversationTurns 从历史消息中提取 user + assistant speak 对话流。
//
// 提取规则:
// 1. 只保留 user 消息(排除 correction prompt和 assistant 纯文本消息;
// 2. assistant tool_call 消息与 tool observation 消息不纳入“真实对话”;
// 3. 返回顺序保持与原始 history 一致。
func CollectConversationTurns(history []*schema.Message) []ConversationTurn {
if len(history) == 0 {
return nil
}
turns := make([]ConversationTurn, 0, len(history))
for _, msg := range history {
if msg == nil {
continue
}
text := strings.TrimSpace(msg.Content)
if text == "" {
continue
}
switch msg.Role {
case schema.User:
// 1. 跳过后端注入的 correction prompt避免把纠错文案误判为用户真实意图。
if isExecuteCorrectionPrompt(msg) {
continue
}
turns = append(turns, ConversationTurn{Role: "user", Content: text})
case schema.Assistant:
// 2. 跳过工具调用消息,只保留真正面向用户的 speak/答复。
if len(msg.ToolCalls) > 0 {
continue
}
turns = append(turns, ConversationTurn{Role: "assistant", Content: text})
}
}
return turns
}