Files
smartmate/backend/newAgent/prompt/execute_context.go
Losita 21b864390b Version: 0.9.9.dev.260408
后端:
1. 粗排后分流与顺序守卫落地,支持“无明确微调偏好时粗排后直接收口”,并新增 allow_reorder / needs_refine_after_rough_build 语义,打通 chat→rough_build→execute/order_guard→deliver 路由。
2. execute 工具执行链路修复:清理乱码坏块与重复分支;新增 min_context_switch 未授权拦截;补齐 suggested 顺序基线初始化与顺序守卫联动。
3. 新增复合写工具 min_context_switch(减少上下文切换)并接入注册、参数解析、写工具白名单、提示词与文档;仅在用户明确允许打乱顺序时可用。
4. 工具口径升级:find_first_free 支持 day/day_start/day_end 范围参数并统一文案;移除 find_free 兼容别名;读写工具输出统一到“第N天(星期X)”格式。
5. prompt 同步升级:chat/execute/execute_context 增加粗排后是否继续微调、顺序授权、min_context_switch 使用边界与返回示例约束。
6. handoff 文档重命名并重写下班交接重点:下一步聚焦“工具收敛能力研究 + 运行态必要参数重置(不丢运行态)”。
7. 同步更新调试日志文件。
前端:无
仓库:无
2026-04-08 23:55:09 +08:00

597 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagentprompt
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
"github.com/cloudwego/eino/schema"
)
const (
executeHistoryKindKey = "newagent_history_kind"
executeHistoryKindCorrectionUser = "llm_correction_prompt"
// executeLoopWindowLimit 控制“当轮 ReAct Loop 窗口”最多保留多少条记录。
// 采用固定窗口能避免上下文无上限增长,且可保持“最近行为”可追踪。
executeLoopWindowLimit = 8
// executeTrimmedObservationText 是重复工具压缩后的 observation 占位文案。
// 当同工具在窗口内出现多次时,只保留最新一条真实结果,其余旧结果统一替换为该文案。
executeTrimmedObservationText = "当前工具调用结果过于久远,已经被删除。"
)
type executeToolSchemaDoc struct {
Name string `json:"name"`
Parameters map[string]any `json:"parameters"`
}
type executeLoopRecord struct {
Thought string
ToolName string
ToolArgs string
Observation string
}
// buildExecuteStageMessages 组装 execute 阶段 4 条消息骨架。
//
// 消息结构(固定):
// 1. message[0] 固定 prompt规则 + 微调硬引导 + 输出约束 + 工具简表)
// 2. message[1] 历史上下文(聊天摘要 + 早期 ReAct 摘要)
// 3. message[2] 当轮 ReAct Loop 窗口thought/reason + tool_call + observation 绑定展示)
// 4. message[3] 当前执行状态(含初始目标、结束判断原则、非目标)
func buildExecuteStageMessages(
stageSystemPrompt string,
state *newagentmodel.CommonState,
ctx *newagentmodel.ConversationContext,
runtimeUserPrompt string,
) []*schema.Message {
msg0 := buildExecuteMessage0(stageSystemPrompt, ctx)
msg1 := buildExecuteMessage1(ctx)
msg2 := buildExecuteMessage2(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 生成固定规则消息,并附带工具简表。
func buildExecuteMessage0(stageSystemPrompt string, ctx *newagentmodel.ConversationContext) string {
base := strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
if base == "" {
base = "你是 SmartFlow NewAgent 执行器,请继续 execute 阶段。"
}
toolCatalog := renderExecuteToolCatalogCompact(ctx)
if toolCatalog == "" {
return base
}
return base + "\n\n" + toolCatalog
}
// buildExecuteMessage1 生成历史上下文短摘要。
func buildExecuteMessage1(ctx *newagentmodel.ConversationContext) string {
lines := []string{"历史上下文(仅供参考):"}
if ctx == nil {
lines = append(lines,
"- 用户目标:暂无可用历史输入。",
"- 阶段锚点:按当前工具事实推进执行。",
"- 早期 ReAct 摘要:暂无。",
)
return strings.Join(lines, "\n")
}
history := ctx.HistorySnapshot()
firstUser, lastUser := pickExecuteUserInputs(history)
switch {
case firstUser == "":
lines = append(lines, "- 用户目标:暂无可用历史输入。")
case lastUser != "" && lastUser != firstUser:
lines = append(lines, "- 用户目标:"+firstUser+";最近补充:"+lastUser)
default:
lines = append(lines, "- 用户目标:"+firstUser)
}
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段锚点:粗排已完成,本轮仅做微调,不重新 place。")
} else {
lines = append(lines, "- 阶段锚点:按当前工具事实推进,不做无依据操作。")
}
allLoops := collectExecuteLoopRecords(history)
lines = append(lines, "- 早期 ReAct 摘要:"+buildEarlyExecuteReactSummary(allLoops, executeLoopWindowLimit))
return strings.Join(lines, "\n")
}
// buildExecuteMessage2 生成当轮 ReAct Loop 窗口。
//
// 规则:
// 1. 每条记录都展示 thought/reason + tool_call + observation
// 2. 对窗口内重复工具应用压缩:同工具只保留最新一条真实 observation
// 3. 被压缩的旧 observation 统一替换为占位文案,避免语义断裂。
func buildExecuteMessage2(ctx *newagentmodel.ConversationContext) string {
lines := []string{"当轮 ReAct Loop 记录(窗口):"}
if ctx == nil {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
allLoops := collectExecuteLoopRecords(ctx.HistorySnapshot())
if len(allLoops) == 0 {
lines = append(lines, "- 暂无可用 ReAct 记录。")
return strings.Join(lines, "\n")
}
windowLoops := tailExecuteLoops(allLoops, executeLoopWindowLimit)
windowLoops = compressExecuteLoopObservationsByTool(windowLoops)
for i, loop := range windowLoops {
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 生成当前执行状态与执行锚点。
func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.ConversationContext, runtimeUserPrompt string) string {
lines := []string{"当前执行状态:"}
roundUsed, maxRounds := 0, newagentmodel.DefaultMaxRounds
modeText := "自由执行(无预定义步骤)"
if state != nil {
roundUsed = state.RoundUsed
if state.MaxRounds > 0 {
maxRounds = state.MaxRounds
}
if state.HasPlan() {
modeText = "计划执行(有预定义步骤)"
}
}
lines = append(lines,
fmt.Sprintf("- 当前轮次:%d/%d", roundUsed, maxRounds),
"- 当前模式:"+modeText,
)
goal := extractExecuteInitialGoal(ctx)
if goal == "" {
goal = "暂无可用目标描述,请按当前上下文稳步推进。"
}
lines = append(lines, "执行锚点:")
lines = append(lines, "- 初始用户目标:"+goal)
if taskClassText := renderExecuteTaskClassIDs(state); taskClassText != "" {
lines = append(lines, "- 目标任务类:"+taskClassText)
}
lines = append(lines, "- 啥时候结束Loop你可以根据工具调用记录自行判断。")
lines = append(lines, "- 非目标:不重新粗排、不修改无关任务类。")
if hasExecuteRoughBuildDone(ctx) {
lines = append(lines, "- 阶段约束:粗排已完成,本轮只微调 suggestedexisting 仅作已安排事实参考,不做 move/batch_move。")
}
if state != nil {
if state.AllowReorder {
lines = append(lines, "- 顺序策略:用户已明确允许打乱顺序,可在必要时使用 min_context_switch。")
} else {
lines = append(lines, "- 顺序策略:默认保持 suggested 相对顺序,禁止调用 min_context_switch。")
}
}
// 兼容上层传入的执行指令;若为空则使用固定收口指令。
instruction := strings.TrimSpace(runtimeUserPrompt)
if instruction == "" {
instruction = "请继续当前任务执行阶段,严格输出 JSON。"
} else {
instruction = firstExecuteLine(instruction)
}
lines = append(lines, "本轮指令:"+instruction)
return strings.Join(lines, "\n")
}
// renderExecuteToolCatalogCompact 将工具 schema 渲染成简表,避免大段 JSON 示例占用上下文。
func renderExecuteToolCatalogCompact(ctx *newagentmodel.ConversationContext) string {
if ctx == nil {
return ""
}
schemas := ctx.ToolSchemasSnapshot()
if len(schemas) == 0 {
return ""
}
lines := []string{"可用工具(简表):"}
for i, schemaItem := range schemas {
name := strings.TrimSpace(schemaItem.Name)
desc := strings.TrimSpace(schemaItem.Desc)
if name == "" {
continue
}
if desc == "" {
desc = "无描述"
}
lines = append(lines, fmt.Sprintf("%d. %s%s", i+1, name, desc))
doc := parseExecuteToolSchema(schemaItem.SchemaText)
paramSummary := renderExecuteToolParamSummary(doc.Parameters)
lines = append(lines, " 参数:"+paramSummary)
returnType, returnSample := renderExecuteToolReturnHint(name)
lines = append(lines, " 返回类型:"+returnType)
lines = append(lines, " 返回示例:"+returnSample)
}
return strings.Join(lines, "\n")
}
// renderExecuteToolReturnHint 返回工具的“返回类型 + 最小示例”。
//
// 说明:
// 1. 所有工具当前都返回 string自然语言这里主要补“内容形态示例”减少模型盲猜
// 2. 示例只保留最小片段,避免工具说明过长挤占上下文窗口。
func renderExecuteToolReturnHint(toolName string) (returnType string, sample string) {
returnType = "string自然语言文本"
switch strings.ToLower(strings.TrimSpace(toolName)) {
case "get_overview":
return returnType, "规划窗口共27天...课程占位条目34个...任务清单(全量,已过滤课程)..."
case "list_tasks":
return returnType, "已预排任务共24个 [35]第一章随机事件与概率 — 已预排至 第3天第5-6节..."
case "get_task_info":
return returnType, "[35]第一章随机事件与概率 | 状态:已预排(suggested) | 占用时段第3天第5-6节"
case "find_first_free":
return returnType, "首个可用位置第5天第1-2节可直接放置| 当日负载总占6/12..."
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 "min_context_switch":
return returnType, "最少上下文切换重排完成:共处理 6 个任务,上下文切换次数 5 -> 2。"
case "unplace":
return returnType, "已将 [35]... 移除,恢复为待安排状态。"
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 从历史中提取 ReAct 记录。
//
// 提取策略:
// 1. 以 assistant tool_call 消息为主键;
// 2. 关联同 ToolCallID 的 tool result 作为 observation
// 3. 向前回溯最近一条 assistant 文本消息作为 thought/reason。
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 == "" {
continue
}
return content
}
return "(未记录)"
}
func tailExecuteLoops(records []executeLoopRecord, limit int) []executeLoopRecord {
if len(records) == 0 {
return nil
}
if limit <= 0 || len(records) <= limit {
result := make([]executeLoopRecord, len(records))
copy(result, records)
return result
}
result := make([]executeLoopRecord, limit)
copy(result, records[len(records)-limit:])
return result
}
// compressExecuteLoopObservationsByTool 对窗口内重复工具做 observation 压缩。
//
// 规则:
// 1. 以“工具名”作为压缩键;
// 2. 同工具仅保留最新一条 observation 原文;
// 3. 旧记录保持 thought/tool_call不丢记录仅替换 observation。
func compressExecuteLoopObservationsByTool(records []executeLoopRecord) []executeLoopRecord {
if len(records) == 0 {
return records
}
latestIndexByTool := make(map[string]int, len(records))
for i := len(records) - 1; i >= 0; i-- {
key := strings.ToLower(strings.TrimSpace(records[i].ToolName))
if key == "" {
key = "unknown_tool"
}
if _, exists := latestIndexByTool[key]; !exists {
latestIndexByTool[key] = i
}
}
result := make([]executeLoopRecord, len(records))
copy(result, records)
for i := range result {
key := strings.ToLower(strings.TrimSpace(result[i].ToolName))
if key == "" {
key = "unknown_tool"
}
if latestIndexByTool[key] != i {
result[i].Observation = executeTrimmedObservationText
}
}
return result
}
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 buildEarlyExecuteReactSummary(records []executeLoopRecord, windowLimit int) string {
if len(records) == 0 {
return "暂无。"
}
if len(records) <= windowLimit {
return "无(当前窗口已覆盖全部 ReAct 记录)。"
}
early := records[:len(records)-windowLimit]
toolCounts := make(map[string]int, len(early))
for _, record := range early {
key := strings.TrimSpace(record.ToolName)
if key == "" {
key = "unknown_tool"
}
toolCounts[key]++
}
names := make([]string, 0, len(toolCounts))
for name := range toolCounts {
names = append(names, name)
}
sort.Strings(names)
parts := make([]string, 0, len(names))
for _, name := range names {
parts = append(parts, fmt.Sprintf("%s×%d", name, toolCounts[name]))
}
return fmt.Sprintf("已折叠 %d 条旧记录,涉及:%s。", len(early), strings.Join(parts, "、"))
}
func extractExecuteInitialGoal(ctx *newagentmodel.ConversationContext) string {
if ctx == nil {
return ""
}
history := ctx.HistorySnapshot()
firstUser, _ := pickExecuteUserInputs(history)
return firstUser
}
func hasExecuteRoughBuildDone(ctx *newagentmodel.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 pickExecuteUserInputs(history []*schema.Message) (first string, last string) {
realUsers := make([]string, 0, 2)
for _, msg := range history {
if msg == nil || msg.Role != schema.User {
continue
}
if isExecuteCorrectionPrompt(msg) {
continue
}
text := compactExecuteText(msg.Content, 120)
if text == "" {
continue
}
realUsers = append(realUsers, text)
}
if len(realUsers) == 0 {
return "", ""
}
return realUsers[0], realUsers[len(realUsers)-1]
}
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 *newagentmodel.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, ","))
}