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" executeHistoryKindLoopClosed = "execute_loop_closed" executeHistoryKindStepAdvanced = "execute_step_advanced" // 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 } const executeMessage1MaxRunes = 1400 // 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 := 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 生成固定规则消息,并附带工具简表。 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 生成当前执行状态与执行锚点。 // buildExecuteMessage1V2 生成历史摘要: // 1. 已收口的 loop 归档到 msg1; // 2. 当前活跃 loop 只保留“早期摘要”; // 3. 最终对 msg1 做统一长度裁剪,控制 token 开销。 func buildExecuteMessage1V2(ctx *newagentmodel.ConversationContext) string { lines := []string{"历史上下文(仅供参考):"} if ctx == nil { lines = append(lines, "- 用户目标:暂无可用历史输入。", "- 阶段锚点:按当前工具事实推进执行。", "- 历史归档 ReAct 摘要:暂无。", "- 当前循环早期摘要:暂无。", ) return trimExecuteMessage1ByBudget(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, "- 阶段锚点:按当前工具事实推进,不做无依据操作。") } archivedLoops, activeLoops := splitExecuteLoopRecordsByBoundary(history) lines = append(lines, "- 历史归档 ReAct 摘要:"+buildEarlyExecuteReactSummary(archivedLoops, 0)) lines = append(lines, "- 当前循环早期摘要:"+buildEarlyExecuteReactSummary(activeLoops, executeLoopWindowLimit)) return trimExecuteMessage1ByBudget(strings.Join(lines, "\n")) } // buildExecuteMessage2V2 仅展示“当前活跃 loop”的窗口记录。 func buildExecuteMessage2V2(ctx *newagentmodel.ConversationContext) string { lines := []string{"当轮 ReAct Loop 记录(窗口):"} if ctx == nil { lines = append(lines, "- 暂无可用 ReAct 记录。") return strings.Join(lines, "\n") } _, activeLoops := splitExecuteLoopRecordsByBoundary(ctx.HistorySnapshot()) if len(activeLoops) == 0 { lines = append(lines, "- 暂无可用 ReAct 记录。") return strings.Join(lines, "\n") } windowLoops := tailExecuteLoops(activeLoops, 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") } // splitExecuteLoopRecordsByBoundary 按“已收口标记”拆分归档/活跃 ReAct 记录。 // // 规则: // 1. 标记之前的记录归档到 msg1; // 2. 标记之后的记录作为活跃 loop 进入 msg2; // 3. 若没有标记,则全部视为活跃记录(兼容旧会话快照)。 func splitExecuteLoopRecordsByBoundary(history []*schema.Message) (archived []executeLoopRecord, active []executeLoopRecord) { if len(history) == 0 { return nil, nil } boundary := findLatestExecuteBoundaryMarker(history) if boundary < 0 { return nil, collectExecuteLoopRecords(history) } if boundary > 0 { archived = collectExecuteLoopRecords(history[:boundary]) } if boundary+1 < len(history) { active = collectExecuteLoopRecords(history[boundary+1:]) } return archived, active } func findLatestExecuteBoundaryMarker(history []*schema.Message) int { for i := len(history) - 1; i >= 0; i-- { msg := history[i] if msg == nil || msg.Extra == nil { continue } kind, ok := msg.Extra[executeHistoryKindKey].(string) if !ok { continue } switch strings.TrimSpace(kind) { case executeHistoryKindLoopClosed, executeHistoryKindStepAdvanced: return i } } return -1 } func trimExecuteMessage1ByBudget(content string) string { content = strings.TrimSpace(content) if content == "" { return "" } runes := []rune(content) if len(runes) <= executeMessage1MaxRunes { return content } if executeMessage1MaxRunes <= 3 { return string(runes[:executeMessage1MaxRunes]) } return string(runes[:executeMessage1MaxRunes-3]) + "..." } // buildExecuteMessage1V3 负责把“上一轮 loop 归档”并入 msg1,并统一做长度裁剪。 func buildExecuteMessage1V3(ctx *newagentmodel.ConversationContext) string { lines := []string{"历史上下文(仅供参考):"} if ctx == nil { lines = append(lines, "- 用户目标:暂无可用历史输入。", "- 阶段锚点:按当前工具事实推进执行。", "- 历史归档 ReAct 摘要:暂无。", "- 历史归档 ReAct 窗口:暂无。", "- 当前循环早期摘要:暂无。", ) return trimExecuteMessage1ByBudget(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, "- 阶段锚点:按当前工具事实推进,不做无依据操作。") } // 1. 通过收口标记拆分“归档 loop / 当前活跃 loop”。 // 2. 归档 loop 的窗口条目直接并入 msg1,满足“上一轮 msg2 挪入 msg1”。 // 3. 当前活跃 loop 在 msg1 只保留早期摘要,详细窗口交给 msg2。 archivedLoops, activeLoops := splitExecuteLoopRecordsByBoundary(history) lines = append(lines, "- 历史归档 ReAct 摘要:"+buildEarlyExecuteReactSummary(archivedLoops, executeLoopWindowLimit)) lines = append(lines, renderArchivedExecuteLoopWindowForMessage1V3(archivedLoops)) lines = append(lines, "- 当前循环早期摘要:"+buildEarlyExecuteReactSummary(activeLoops, executeLoopWindowLimit)) return trimExecuteMessage1ByBudget(strings.Join(lines, "\n")) } // buildExecuteMessage2V3 仅承载“当前活跃 loop”的窗口。 // 若是新一轮刚开始(活跃 loop 为空),明确返回“已清空”状态。 func buildExecuteMessage2V3(ctx *newagentmodel.ConversationContext) string { lines := []string{"当轮 ReAct Loop 记录(窗口):"} if ctx == nil { lines = append(lines, "- 暂无可用 ReAct 记录。") return strings.Join(lines, "\n") } _, activeLoops := splitExecuteLoopRecordsByBoundary(ctx.HistorySnapshot()) if len(activeLoops) == 0 { lines = append(lines, "- 已清空(新一轮 loop 准备中)。") return strings.Join(lines, "\n") } windowLoops := tailExecuteLoops(activeLoops, 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") } func renderArchivedExecuteLoopWindowForMessage1V3(records []executeLoopRecord) string { if len(records) == 0 { return "- 历史归档 ReAct 窗口:暂无。" } windowLoops := tailExecuteLoops(records, executeLoopWindowLimit) windowLoops = compressExecuteLoopObservationsByTool(windowLoops) lines := []string{"历史归档 ReAct 窗口(由上一轮 msg2 并入):"} 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") } 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, ) initialGoal, currentGoal := extractExecuteGoalAnchors(ctx) if currentGoal == "" { currentGoal = "暂无可用目标描述,请按当前上下文稳步推进。" } lines = append(lines, "执行锚点:") lines = append(lines, "- 当前用户诉求:"+currentGoal) if initialGoal != "" && initialGoal != currentGoal { lines = append(lines, "- 首轮目标来源:"+initialGoal) } // 1. 有 plan 时,把当前步骤与完成判定强制写入 msg3。 // 2. 该锚点用于约束模型“只推进当前步骤”,避免退化成泛化 ReAct。 // 3. 当前步骤不可读时给出兜底指引,避免引用旧步骤。 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)) lines = append(lines, "- 当前步骤内容:"+stepContent) lines = append(lines, "- 当前步骤完成判定(done_when):"+doneWhen) lines = append(lines, "- 动作纪律1:未满足 done_when 时,只能 continue / confirm / ask_user,禁止 next_plan") lines = append(lines, "- 动作纪律2:满足 done_when 时,优先 next_plan,并在 goal_check 对照 done_when 给证据") lines = append(lines, "- 动作纪律3:禁止跳到后续步骤执行") } else { lines = append(lines, "- 当前计划步骤不可读;请先判断是否已完成全部计划") lines = append(lines, "- 若已完成全部计划,输出 done 并给出 goal_check 证据") } } if taskClassText := renderExecuteTaskClassIDs(state); taskClassText != "" { lines = append(lines, "- 目标任务类:"+taskClassText) } lines = append(lines, "- 啥时候结束Loop:你可以根据工具调用记录自行判断。") lines = append(lines, "- 非目标:不重新粗排、不修改无关任务类。") if hasExecuteRoughBuildDone(ctx) { lines = append(lines, "- 阶段约束:粗排已完成,本轮只微调 suggested;existing 仅作已安排事实参考,不作为可移动目标。") } lines = append(lines, "- 参数纪律:工具参数必须严格使用 schema 字段;若返回“参数非法”,需先改参再继续。") 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,但部分是“JSON 字符串”,这里补齐内容形态示例减少模型盲猜; // 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 "query_available_slots": return "string(JSON字符串)", `{"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 "string(JSON字符串)", `{"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 "string(JSON字符串)", `{"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 "string(JSON字符串)", `{"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 "string(JSON字符串)", `{"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 "string(JSON字符串)", `{"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个任务全部成功。(单次最多2条)" case "spread_even": return returnType, "均匀化调整完成:共处理 6 个任务,候选坑位 24 个。" case "min_context_switch": return returnType, "最少上下文切换重排完成:共处理 6 个任务,上下文切换次数 5 -> 2。" case "unplace": return returnType, "已将 [35]... 移除,恢复为待安排状态。" case "web_search": return "string(JSON字符串)", `{"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 "string(JSON字符串)", `{"tool":"web_fetch","url":"https://example.com/page","title":"页面标题","content":"正文内容...","truncated":false}` 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 extractExecuteGoalAnchors(ctx *newagentmodel.ConversationContext) (initial string, current string) { if ctx == nil { return "", "" } history := ctx.HistorySnapshot() firstUser, lastUser := pickExecuteUserInputs(history) return firstUser, lastUser } 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, ",")) }