From ec7ea038e78a4bcc6c9daaf60a9fefc5a07211b1 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 10 Apr 2026 00:52:26 +0800 Subject: [PATCH] feat(agent): context compression upgrade, 429 retry, iteration limit & repetition fixes --- .../vip/mate/agent/AgentGraphBuilder.java | 4 +- .../main/java/vip/mate/agent/BaseAgent.java | 2 +- .../context/ConversationWindowManager.java | 452 +++++++++++++----- .../agent/graph/NodeStreamingChatHelper.java | 15 +- .../mate/agent/graph/RepetitionDetector.java | 45 ++ .../agent/graph/StateGraphReActAgent.java | 5 +- .../graph/edge/ObservationDispatcher.java | 4 +- .../agent/graph/edge/ReasoningDispatcher.java | 5 +- .../agent/graph/node/ObservationNode.java | 18 + .../plan/StateGraphPlanExecuteAgent.java | 5 +- .../graph/state/MateClawStateAccessor.java | 5 +- .../config/ConversationWindowProperties.java | 16 +- .../mate/memory/nudge/MemoryNudgeService.java | 41 +- .../service/MemorySummarizationService.java | 43 +- .../src/main/resources/application.yml | 8 + .../src/main/resources/db/data-en.sql | 4 +- .../src/main/resources/db/data-mysql-en.sql | 4 +- .../src/main/resources/db/data-mysql-zh.sql | 4 +- .../src/main/resources/db/data-zh.sql | 4 +- .../context/structured-summary-system.txt | 32 ++ .../context/structured-summary-update.txt | 23 + .../context/structured-summary-user.txt | 5 + 22 files changed, 594 insertions(+), 150 deletions(-) create mode 100644 mateclaw-server/src/main/resources/prompts/context/structured-summary-system.txt create mode 100644 mateclaw-server/src/main/resources/prompts/context/structured-summary-update.txt create mode 100644 mateclaw-server/src/main/resources/prompts/context/structured-summary-user.txt diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 48aa2378..8231bd48 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -315,7 +315,7 @@ public class AgentGraphBuilder { .addEdge(PlanStateKeys.DIRECT_ANSWER_NODE, StateGraph.END); return graph.compile(CompileConfig.builder() - .recursionLimit(maxIterations * 3 + 10) + .recursionLimit(maxIterations > 0 ? maxIterations * 3 + 10 : 300) .build()); } catch (Exception e) { throw new MateClawException("Plan-Execute StateGraph 编译失败: " + e.getMessage()); @@ -424,7 +424,7 @@ public class AgentGraphBuilder { .addEdge(MateClawStateKeys.FINAL_ANSWER_NODE, StateGraph.END); return graph.compile(CompileConfig.builder() - .recursionLimit(maxIterations * 3 + 10) + .recursionLimit(maxIterations > 0 ? maxIterations * 3 + 10 : 300) .withLifecycleListener(new ReActLifecycleListener()) .build()); } catch (Exception e) { diff --git a/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java b/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java index 06b6e3fe..84a75bd9 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java @@ -44,7 +44,7 @@ public abstract class BaseAgent { protected String systemPrompt; /** 最大工具调用迭代次数 */ - protected int maxIterations = 10; + protected int maxIterations = 25; /** 模型名称 */ protected String modelName; diff --git a/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java b/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java index 35bdb91f..251e11b3 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java @@ -15,19 +15,33 @@ import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.springframework.stereotype.Component; import vip.mate.agent.prompt.PromptLoader; import vip.mate.config.ConversationWindowProperties; +import vip.mate.memory.spi.MemoryManager; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** - * 会话历史上下文窗口管理器 + * 会话历史上下文窗口管理器(Hermes 风格升级版) *

- * 在消息注入 StateGraph 之前,检测 token 是否超出模型上下文窗口, - * 若超出则将较早的消息通过 LLM 压缩为摘要,保留最近 N 轮原始消息。 + * 四阶段压缩策略: + *

    + *
  1. Soft Trim — 裁剪旧工具结果为 head+tail
  2. + *
  3. Hard Clear — 替换所有旧工具结果为占位符
  4. + *
  5. Pre-Prune — 喂给摘要 LLM 前清理工具输出(减少摘要输入 token)
  6. + *
  7. LLM 结构化摘要 — Goal/Progress/Decisions/Files/NextSteps 模板,支持迭代更新
  8. + *
+ *

+ * 关键特性: + *

*

* 安全设计:摘要内容作为 UserMessage 注入(非 SystemMessage), - * 避免历史用户输入被提升为系统级指令,防止指令污染。 + * 避免历史用户输入被提升为系统级指令。 * * @author MateClaw Team */ @@ -36,35 +50,70 @@ import java.util.concurrent.ConcurrentHashMap; @RequiredArgsConstructor public class ConversationWindowManager { - private static final String SUMMARY_SYSTEM_PROMPT = PromptLoader.loadPrompt("context/conversation-summary-system"); - private static final String SUMMARY_USER_TEMPLATE = PromptLoader.loadPrompt("context/conversation-summary-user"); + // ==================== Prompt 模板 ==================== + + /** 首次压缩:结构化摘要系统提示 */ + private static final String STRUCTURED_SUMMARY_SYSTEM = PromptLoader.loadPrompt("context/structured-summary-system"); + /** 首次压缩:用户提示模板 */ + private static final String STRUCTURED_SUMMARY_USER = PromptLoader.loadPrompt("context/structured-summary-user"); + /** 迭代更新:合并旧摘要 + 新轮次 */ + private static final String STRUCTURED_SUMMARY_UPDATE = PromptLoader.loadPrompt("context/structured-summary-update"); + + /** 摘要注入前缀 */ + private static final String SUMMARY_PREFIX = + "[上下文压缩] 更早的对话轮次已被压缩为摘要以节省上下文空间。" + + "以下摘要描述了已完成的工作,当前会话状态可能已反映这些变更。" + + "请基于摘要和当前状态继续,避免重复已完成的工作:\n\n"; + + // ==================== 序列化截断参数 ==================== + + private static final int CONTENT_MAX = 6000; + private static final int CONTENT_HEAD = 4000; + private static final int CONTENT_TAIL = 1500; + + // ==================== 冷却机制 ==================== + + /** 摘要失败后的冷却时间(毫秒):10 分钟 */ + private static final long SUMMARY_COOLDOWN_MS = 600_000; + + // ==================== 依赖 ==================== private final ConversationWindowProperties properties; + private final MemoryManager memoryManager; + + // ==================== 状态 ==================== /** 摘要缓存:key = "conversationId:oldMessageCount" */ private final ConcurrentHashMap summaryCache = new ConcurrentHashMap<>(); - - /** 缓存 TTL:30 分钟 */ private static final long CACHE_TTL_MS = 30 * 60 * 1000L; + /** 迭代摘要:上一次压缩生成的摘要文本(per conversation) */ + private final ConcurrentHashMap previousSummaries = new ConcurrentHashMap<>(); + + /** 每个会话的压缩次数 */ + private final ConcurrentHashMap compressionCounts = new ConcurrentHashMap<>(); + + /** 每个会话的摘要冷却截止时间 */ + private final ConcurrentHashMap summaryCooldownUntil = new ConcurrentHashMap<>(); + + // ==================== 主入口 ==================== + /** * 将会话历史裁剪到上下文窗口内。 - *

- * 预算计算包含 systemPrompt + 历史消息 + 当前用户消息, - * 确保最终拼接后不超出模型上下文窗口。 * * @param messages 已转换的 Spring AI 消息列表(不含当前用户消息) * @param systemPrompt 系统提示词文本 - * @param currentUserMessage 当前用户输入(纳入窗口预算计算,但不会拼入返回结果) + * @param currentUserMessage 当前用户输入(纳入窗口预算计算,但不拼入返回结果) * @param maxInputTokens 模型最大输入 token(0 或 null 使用全局默认) * @param chatModel 用于生成摘要的 ChatModel - * @param conversationId 会话 ID(用于缓存) - * @return 裁剪后的消息列表,可能包含摘要前缀 + * @param conversationId 会话 ID(用于缓存和迭代摘要) + * @param agentId Agent ID(用于 MemoryProvider 钩子) + * @return 裁剪后的消息列表 */ public List fitToWindow(List messages, String systemPrompt, String currentUserMessage, Integer maxInputTokens, ChatModel chatModel, - String conversationId) { + String conversationId, Long agentId) { if (messages == null || messages.isEmpty()) { return messages; } @@ -82,47 +131,57 @@ public class ConversationWindowManager { return messages; } - log.info("[ConversationWindow] 超阈值: {} tokens (system={}, current={}, history={}) > {} 触发阈值 (max={}), conversationId={}", + log.info("[ConversationWindow] 超阈值: {} tokens (system={}, current={}, history={}) > {} 触发阈值 (max={}), conv={}", totalTokens, systemTokens, currentMsgTokens, historyTokens, triggerThreshold, effectiveMax, conversationId); - // 清理过期缓存 evictExpiredEntries(); // 可用于历史的 token 预算 = max - system - currentMsg - 安全余量 int reservedTokens = systemTokens + currentMsgTokens + (int) (effectiveMax * 0.05); int historyBudget = effectiveMax - reservedTokens; - return compactMessages(messages, historyBudget, chatModel, conversationId); + // 尾部保护 token 预算:阈值的 20%(与 Hermes 一致) + int tailTokenBudget = (int) (triggerThreshold * 0.20); + + return compactMessages(messages, historyBudget, tailTokenBudget, chatModel, conversationId, agentId); } - private List compactMessages(List messages, int historyBudget, - ChatModel chatModel, String conversationId) { - // 计算保留多少条最近消息 - int preserveCount = calculatePreserveCount(messages); + /** + * 向后兼容:不传 agentId 的旧签名(agentId = null,不触发 Memory 钩子) + */ + public List fitToWindow(List messages, String systemPrompt, + String currentUserMessage, + Integer maxInputTokens, ChatModel chatModel, + String conversationId) { + return fitToWindow(messages, systemPrompt, currentUserMessage, + maxInputTokens, chatModel, conversationId, null); + } - // 如果消息总数不够拆分,尝试逐步减少保留数 - if (preserveCount >= messages.size()) { - // 消息太少无法拆分,尝试保留最少 2 条 - preserveCount = Math.min(2, messages.size()); - if (preserveCount >= messages.size()) { - log.debug("[ConversationWindow] 消息数 {} 无法拆分,跳过压缩", messages.size()); - return messages; - } + // ==================== 核心压缩逻辑 ==================== + + private List compactMessages(List messages, int historyBudget, + int tailTokenBudget, ChatModel chatModel, + String conversationId, Long agentId) { + // 动态计算尾部保护边界(替代固定 preserveRecentPairs) + int headEnd = 0; // 头部保护:暂不保护(system prompt 已在外部计算) + int tailStart = findTailBoundary(messages, headEnd, tailTokenBudget); + + if (tailStart <= headEnd) { + log.debug("[ConversationWindow] 消息数不足以拆分,跳过压缩"); + return messages; } - int splitPoint = messages.size() - preserveCount; - List oldMessages = new ArrayList<>(messages.subList(0, splitPoint)); // 可变副本 - List recentMessages = messages.subList(splitPoint, messages.size()); + List oldMessages = new ArrayList<>(messages.subList(headEnd, tailStart)); + List recentMessages = messages.subList(tailStart, messages.size()); - // ═══ Phase 1: Soft Trim — 裁剪工具结果(head+tail),避免不必要的 LLM 摘要 ═══ + // ═══ Phase 1: Soft Trim — 裁剪旧工具结果 ═══ int softTrimmed = softTrimToolResults(oldMessages); if (softTrimmed > 0) { int afterTrimTokens = TokenEstimator.estimateTokens(oldMessages) + TokenEstimator.estimateTokens(recentMessages); - log.info("[ConversationWindow] Soft trim: {} tool results trimmed, tokens now={}, budget={}", + log.info("[ConversationWindow] Phase 1 Soft trim: {} tool results trimmed, tokens={}, budget={}", softTrimmed, afterTrimTokens, historyBudget); if (afterTrimTokens <= historyBudget) { - // Soft trim 够了,跳过 LLM 摘要 List result = new ArrayList<>(oldMessages); result.addAll(recentMessages); return result; @@ -133,7 +192,7 @@ public class ConversationWindowManager { int hardCleared = hardClearToolResults(oldMessages); if (hardCleared > 0) { int afterClearTokens = TokenEstimator.estimateTokens(oldMessages) + TokenEstimator.estimateTokens(recentMessages); - log.info("[ConversationWindow] Hard clear: {} tool results replaced with placeholder, tokens now={}, budget={}", + log.info("[ConversationWindow] Phase 2 Hard clear: {} replaced, tokens={}, budget={}", hardCleared, afterClearTokens, historyBudget); if (afterClearTokens <= historyBudget) { List result = new ArrayList<>(oldMessages); @@ -142,7 +201,31 @@ public class ConversationWindowManager { } } - // ═══ Phase 3: LLM 摘要(原有逻辑,仅在 Phase 1+2 不够时执行) ═══ + // ═══ Phase 2.5: MemoryProvider 钩子 — 压缩前提取关键信息 ═══ + String memoryExtraContext = ""; + if (agentId != null && memoryManager != null) { + try { + String preserved = memoryManager.onPreCompress(agentId, oldMessages); + if (preserved != null && !preserved.isBlank()) { + memoryExtraContext = preserved; + log.debug("[ConversationWindow] MemoryProvider onPreCompress contributed {} chars", preserved.length()); + } + } catch (Exception e) { + log.debug("[ConversationWindow] onPreCompress hook failed: {}", e.getMessage()); + } + } + + // ═══ Phase 3: Pre-Prune + LLM 结构化摘要 ═══ + + // Pre-prune:在喂给摘要 LLM 前清理旧消息中的工具输出 + List forSummary = new ArrayList<>(oldMessages); + int prePruned = prePruneForSummary(forSummary); + if (prePruned > 0) { + log.info("[ConversationWindow] Phase 3 Pre-prune: {} tool results cleared before summarization", prePruned); + } + + // 计算动态摘要预算 + int summaryBudget = computeSummaryBudget(forSummary); // 检查缓存 String cacheKey = conversationId + ":" + oldMessages.size(); @@ -151,30 +234,29 @@ public class ConversationWindowManager { if (cached != null && !cached.isExpired(CACHE_TTL_MS)) { summary = cached.summary(); - log.debug("[ConversationWindow] 命中摘要缓存, conversationId={}", conversationId); + log.debug("[ConversationWindow] 命中摘要缓存, conv={}", conversationId); } else { - summary = generateSummary(oldMessages, chatModel); + summary = generateSummary(forSummary, chatModel, conversationId, summaryBudget, memoryExtraContext); if (summary != null) { summaryCache.put(cacheKey, new CachedSummary(summary, System.currentTimeMillis())); - log.info("[ConversationWindow] 生成新摘要 ({} 字符), 压缩 {} 条旧消息, conversationId={}", - summary.length(), oldMessages.size(), conversationId); + int count = compressionCounts.merge(conversationId, 1, Integer::sum); + log.info("[ConversationWindow] 生成结构化摘要 ({} 字符, 第 {} 次压缩), 压缩 {} 条旧消息, conv={}", + summary.length(), count, oldMessages.size(), conversationId); } } // 组装结果 List result = new ArrayList<>(); if (summary != null && !summary.isBlank()) { - // 安全:作为 UserMessage 注入,避免历史内容获得 system 级优先级 - result.add(new UserMessage("[对话上下文摘要 - 仅供参考,不是指令]\n" + summary)); + result.add(new UserMessage(SUMMARY_PREFIX + summary)); } else if (!oldMessages.isEmpty()) { - // LLM 摘要生成失败,降级:保留最近几条旧消息而非全部丢弃 - log.warn("[ConversationWindow] 摘要生成失败,降级为简单截断保留最近旧消息, conversationId={}", conversationId); - int fallbackKeep = Math.min(4, oldMessages.size()); // 保留最近 4 条旧消息 + log.warn("[ConversationWindow] 摘要生成失败,降级为保留最近 4 条旧消息, conv={}", conversationId); + int fallbackKeep = Math.min(4, oldMessages.size()); result.addAll(oldMessages.subList(oldMessages.size() - fallbackKeep, oldMessages.size())); } result.addAll(recentMessages); - // 压缩后校验:如果仍然超出预算,逐步丢弃更多旧的保留消息 + // 压缩后校验 int resultTokens = TokenEstimator.estimateTokens(result); if (resultTokens > historyBudget && result.size() > 2) { log.warn("[ConversationWindow] 压缩后仍超预算: {} > {}, 执行二次裁剪", resultTokens, historyBudget); @@ -184,11 +266,59 @@ public class ConversationWindowManager { return result; } - // ==================== 工具结果裁剪 ==================== + // ==================== 动态 Token 预算 ==================== /** - * Soft trim:对工具结果做 head+tail 裁剪(保留首尾各 200 字符)。 - * @return 裁剪的工具结果条数 + * 基于 token 预算动态计算尾部保护边界(替代固定 preserveRecentPairs)。 + * 从消息列表末尾向前累加 token,直到耗尽预算或达到最小消息数。 + */ + private int findTailBoundary(List messages, int headEnd, int tailTokenBudget) { + int n = messages.size(); + if (n <= headEnd + 1) return headEnd; + + int minTail = Math.min(properties.getProtectLastMinMessages(), n - headEnd - 1); + // 兼容旧配置:如果 protectLastMinMessages 未设置但 preserveRecentPairs 有值 + int pairsBased = properties.getPreserveRecentPairs() * 2; + if (pairsBased > minTail) { + minTail = Math.min(pairsBased, n - headEnd - 1); + } + + int softCeiling = (int) (tailTokenBudget * 1.5); + int accumulated = 0; + int cutIdx = n; + + for (int i = n - 1; i >= headEnd; i--) { + int msgTokens = TokenEstimator.estimateTokens(messages.get(i)); + if (accumulated + msgTokens > softCeiling && (n - i) >= minTail) { + break; + } + accumulated += msgTokens; + cutIdx = i; + } + + // 确保至少保留 minTail 条 + int fallbackCut = n - minTail; + if (cutIdx > fallbackCut) { + cutIdx = fallbackCut; + } + + return Math.max(cutIdx, headEnd + 1); + } + + /** + * 计算摘要字数预算:被压缩内容 token 的 20%,不低于 500、不超过 3000。 + */ + private int computeSummaryBudget(List turnsToSummarize) { + int contentTokens = TokenEstimator.estimateTokens(turnsToSummarize); + int budget = (int) (contentTokens * properties.getSummaryBudgetRatio()); + return Math.max(properties.getSummaryBudgetFloor(), + Math.min(budget, properties.getSummaryBudgetCeiling())); + } + + // ==================== 工具结果处理 ==================== + + /** + * Phase 1 - Soft trim:对工具结果做 head+tail 裁剪(保留首尾各 200 字符)。 */ private int softTrimToolResults(List messages) { int trimmed = 0; @@ -218,8 +348,7 @@ public class ConversationWindowManager { } /** - * Hard clear:将所有工具结果替换为占位符。 - * @return 替换的工具结果条数 + * Phase 2 - Hard clear:将所有旧工具结果替换为占位符。 */ private int hardClearToolResults(List messages) { int cleared = 0; @@ -235,9 +364,137 @@ public class ConversationWindowManager { return cleared; } + /** + * Phase 3 Pre-prune:在 LLM 摘要前,将工具输出替换为占位符(减少摘要输入 token)。 + */ + private int prePruneForSummary(List messages) { + int pruned = 0; + for (int i = 0; i < messages.size(); i++) { + if (messages.get(i) instanceof ToolResponseMessage trm) { + boolean hasSubstantial = trm.getResponses().stream() + .anyMatch(r -> r.responseData() != null && r.responseData().length() > 200); + if (hasSubstantial) { + List placeholders = trm.getResponses().stream() + .map(r -> new ToolResponseMessage.ToolResponse(r.id(), r.name(), + "[旧工具输出已清理以节省上下文空间]")) + .toList(); + messages.set(i, ToolResponseMessage.builder().responses(placeholders).build()); + pruned++; + } + } + } + return pruned; + } + + // ==================== LLM 摘要生成(结构化 + 迭代更新) ==================== + + /** + * 生成结构化摘要。支持首次压缩和迭代更新两种模式。 + * 包含冷却机制:LLM 调用失败后 10 分钟内不重试。 + */ + private String generateSummary(List oldMessages, ChatModel chatModel, + String conversationId, int summaryBudget, + String memoryExtraContext) { + // 冷却检查 + if (isInSummaryCooldown(conversationId)) { + log.info("[ConversationWindow] 摘要在冷却中,跳过 LLM 调用, conv={}", conversationId); + return null; + } + + try { + String conversationText = serializeForSummary(oldMessages); + + // 如果 MemoryProvider 有额外上下文,追加到对话文本中 + if (memoryExtraContext != null && !memoryExtraContext.isBlank()) { + conversationText += "\n\n[Memory Provider 补充上下文]\n" + memoryExtraContext; + } + + String previousSummary = previousSummaries.get(conversationId); + String systemPrompt; + String userPrompt; + + if (previousSummary != null) { + // 迭代更新模式:旧摘要 + 新轮次 + systemPrompt = STRUCTURED_SUMMARY_SYSTEM; + userPrompt = STRUCTURED_SUMMARY_UPDATE + .replace("{previous_summary}", previousSummary) + .replace("{conversation}", conversationText) + .replace("{summary_budget}", String.valueOf(summaryBudget)); + log.debug("[ConversationWindow] 使用迭代更新模式(第 {} 次压缩), conv={}", + compressionCounts.getOrDefault(conversationId, 0) + 1, conversationId); + } else { + // 首次压缩 + systemPrompt = STRUCTURED_SUMMARY_SYSTEM + .replace("{summary_budget}", String.valueOf(summaryBudget)); + userPrompt = STRUCTURED_SUMMARY_USER + .replace("{conversation}", conversationText); + log.debug("[ConversationWindow] 使用首次压缩模式, conv={}", conversationId); + } + + List promptMessages = new ArrayList<>(); + promptMessages.add(new SystemMessage(systemPrompt)); + promptMessages.add(new UserMessage(userPrompt)); + + ChatOptions options = DashScopeChatOptions.builder() + .withMaxToken(properties.getSummaryMaxTokens()) + .build(); + + ChatResponse response = chatModel.call(new Prompt(promptMessages, options)); + if (response != null && response.getResult() != null + && response.getResult().getOutput() != null) { + String summary = response.getResult().getOutput().getText(); + if (summary != null && !summary.isBlank()) { + // 成功:保存摘要供下次迭代更新,清除冷却 + previousSummaries.put(conversationId, summary); + clearSummaryCooldown(conversationId); + return summary; + } + } + log.warn("[ConversationWindow] LLM 摘要返回空结果, conv={}", conversationId); + setSummaryCooldown(conversationId); + return null; + + } catch (Exception e) { + log.warn("[ConversationWindow] LLM 摘要生成失败(进入 {} 秒冷却): {}, conv={}", + SUMMARY_COOLDOWN_MS / 1000, e.getMessage(), conversationId); + setSummaryCooldown(conversationId); + return null; + } + } + + // ==================== 消息序列化(智能截断) ==================== + + /** + * 将消息列表序列化为摘要 LLM 可消化的文本格式。 + * 长内容做 head+tail 截断,比简单截断保留更多信息。 + */ + private String serializeForSummary(List messages) { + StringBuilder sb = new StringBuilder(); + for (Message msg : messages) { + String role = switch (msg) { + case UserMessage ignored -> "[USER]"; + case SystemMessage ignored -> "[SYSTEM]"; + case AssistantMessage ignored -> "[ASSISTANT]"; + case ToolResponseMessage ignored -> "[TOOL RESULT]"; + default -> "[OTHER]"; + }; + + String text = msg.getText(); + if (text != null && text.length() > CONTENT_MAX) { + text = text.substring(0, CONTENT_HEAD) + + "\n...[截断 " + text.length() + " 字符]...\n" + + text.substring(text.length() - CONTENT_TAIL); + } + + sb.append(role).append(": ").append(text != null ? text : "").append("\n\n"); + } + return sb.toString(); + } + + // ==================== 辅助方法 ==================== + /** * 二次裁剪:从前往后移除消息直到 token 预算满足。 - * 至少保留最后 2 条消息(最近一轮对话)。 */ private List trimToFit(List messages, int budget) { int startIndex = 0; @@ -255,77 +512,17 @@ public class ConversationWindowManager { return messages; } - /** - * 计算应保留的最近消息条数。 - * 保留 N 轮对话(每轮 = user + assistant = 2 条),至少保留 2 条。 - */ - private int calculatePreserveCount(List messages) { - int pairCount = properties.getPreserveRecentPairs(); - int preserveCount = pairCount * 2; - return Math.max(2, Math.min(preserveCount, messages.size())); - } - - /** - * 调用 LLM 生成会话摘要,使用 summaryMaxTokens 约束输出长度。 - * 失败时返回 null(降级为朴素截断)。 - */ - private String generateSummary(List oldMessages, ChatModel chatModel) { - try { - StringBuilder conversationText = new StringBuilder(); - for (Message msg : oldMessages) { - String role = switch (msg) { - case UserMessage ignored -> "用户"; - case SystemMessage ignored -> "系统"; - default -> "助手"; - }; - String text = msg.getText(); - // 单条消息截断避免摘要 prompt 本身过长 - if (text != null && text.length() > 2000) { - text = text.substring(0, 2000) + "...[已截断]"; - } - conversationText.append(role).append(": ").append(text).append("\n\n"); - } - - String userPrompt = SUMMARY_USER_TEMPLATE - .replace("{conversation}", conversationText.toString()); - - List promptMessages = new ArrayList<>(); - promptMessages.add(new SystemMessage(SUMMARY_SYSTEM_PROMPT)); - promptMessages.add(new UserMessage(userPrompt)); - - // 使用 summaryMaxTokens 约束摘要输出长度 - ChatOptions options = DashScopeChatOptions.builder() - .withMaxToken(properties.getSummaryMaxTokens()) - .build(); - - ChatResponse response = chatModel.call(new Prompt(promptMessages, options)); - if (response != null && response.getResult() != null - && response.getResult().getOutput() != null) { - return response.getResult().getOutput().getText(); - } - log.warn("[ConversationWindow] LLM 摘要返回空结果"); - return null; - } catch (Exception e) { - log.warn("[ConversationWindow] LLM 摘要生成失败,降级为朴素截断: {}", e.getMessage()); - return null; - } - } + // ==================== PTL 紧急压缩 ==================== /** * PTL (Prompt Too Long) 恢复用的紧急压缩。 - *

- * 当 LLM 返回 context_length_exceeded 错误时,由 Node 层调用此方法 - * 对消息列表做更激进的裁剪(保留最近 2 轮 + 朴素截断,不调用 LLM 摘要)。 - * - * @param messages 原始消息列表 - * @return 压缩后的消息列表,如果无法压缩返回 null + * 不调用 LLM 摘要,直接丢弃较旧消息,只保留最近 4 条。 */ public List compactForRetry(List messages) { if (messages == null || messages.size() <= 2) { return null; } - // 紧急模式:不调用 LLM 摘要,直接丢弃较旧消息,只保留最近 2 对 (4 条) int preserveCount = Math.min(4, messages.size()); int splitPoint = messages.size() - preserveCount; @@ -339,16 +536,27 @@ public class ConversationWindowManager { return recentMessages; } - /** - * 清理过期缓存条目 - */ + // ==================== 冷却机制 ==================== + + private boolean isInSummaryCooldown(String conversationId) { + Long until = summaryCooldownUntil.get(conversationId); + return until != null && System.currentTimeMillis() < until; + } + + private void setSummaryCooldown(String conversationId) { + summaryCooldownUntil.put(conversationId, System.currentTimeMillis() + SUMMARY_COOLDOWN_MS); + } + + private void clearSummaryCooldown(String conversationId) { + summaryCooldownUntil.remove(conversationId); + } + + // ==================== 缓存管理 ==================== + private void evictExpiredEntries() { summaryCache.entrySet().removeIf(entry -> entry.getValue().isExpired(CACHE_TTL_MS)); } - /** - * 缓存条目 - */ record CachedSummary(String summary, long createdAt) { boolean isExpired(long ttlMs) { return System.currentTimeMillis() - createdAt > ttlMs; diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java index 6fe93d15..a71a4676 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -97,9 +98,9 @@ public class NodeStreamingChatHelper { // ==================== 重试配置 ==================== - private static final int MAX_RETRIES = 3; - private static final long BACKOFF_BASE_MS = 1000; - private static final long BACKOFF_CAP_MS = 10_000; + private static final int MAX_RETRIES = 5; + private static final long BACKOFF_BASE_MS = 3000; + private static final long BACKOFF_CAP_MS = 60_000; /** * 判断错误是否可重试(基于状态码/异常类型) @@ -246,8 +247,16 @@ public class NodeStreamingChatHelper { boolean broadcast, int attempt) { if (attempt > 0) { long delay = Math.min(BACKOFF_BASE_MS * (1L << (attempt - 1)), BACKOFF_CAP_MS); + // 加入 jitter 防止雷群效应(Hermes 风格) + delay += ThreadLocalRandom.current().nextLong(0, Math.max(1, delay / 2)); + delay = Math.min(delay, BACKOFF_CAP_MS); log.warn("[{}] Retry attempt {}/{} after {}ms for conversation {}", phase, attempt, MAX_RETRIES, delay, conversationId); + // 广播给前端:用户可见的重试倒计时 + if (broadcast) { + broadcastDelta(conversationId, "warning", + buildDeltaJson("⏱️ 请求频率受限,等待 " + (delay / 1000) + " 秒后重试(第 " + attempt + "/" + MAX_RETRIES + " 次)...")); + } try { Thread.sleep(delay); } catch (InterruptedException ie) { diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/RepetitionDetector.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/RepetitionDetector.java index 68d4fd9c..567090ef 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/RepetitionDetector.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/RepetitionDetector.java @@ -83,6 +83,11 @@ public class RepetitionDetector { } if (count >= MIN_REPEATS) { + // 排除装饰性重复(代码缩进、ASCII 图表、Markdown 分隔线常见) + if (isDecorativePattern(pattern)) { + continue; + } + repetitionDetected = true; log.warn("[RepetitionDetector] Detected degenerate repetition: " + "pattern length={}, repeats={}, pattern preview=\"{}\"", @@ -95,6 +100,46 @@ public class RepetitionDetector { return false; } + /** + * 判断 pattern 是否为装饰性字符(不应判定为退化重复)。 + *

+ * 排除场景: + *

+ */ + private boolean isDecorativePattern(String pattern) { + if (pattern.isBlank()) { + return true; // 纯空白 + } + + // 统计不同的非空白字符种类 + long distinctNonWhitespace = pattern.chars() + .filter(c -> !Character.isWhitespace(c)) + .distinct() + .count(); + + // 只有 1-2 种不同的非空白字符 → 装饰性(如 "────────" 或 "│ │") + if (distinctNonWhitespace <= 2) { + return true; + } + + // 检查是否全部是 Box Drawing / 装饰字符 + boolean allDecorative = pattern.chars().allMatch(c -> + Character.isWhitespace(c) + || isBoxDrawing(c) + || "─━│┃┄┅┆┇┈┉┊┋═║╌╍╎╏╔╗╚╝╠╣╦╩╬├┤┬┴┼┌┐└┘".indexOf(c) >= 0 + || "-=_*+|#~<>".indexOf(c) >= 0); + return allDecorative; + } + + private boolean isBoxDrawing(int codePoint) { + // Unicode Box Drawing block: U+2500 – U+257F + return codePoint >= 0x2500 && codePoint <= 0x257F; + } + /** * 重置检测器状态 */ diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java index 463d1f76..c87194d2 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java @@ -341,13 +341,16 @@ public class StateGraphReActAgent extends BaseAgent implements StructuredStreamC // 上下文窗口管理:裁剪超出模型 context window 的历史(含当前消息预算) if (conversationWindowManager != null) { + Long parsedAgentId = null; + try { parsedAgentId = Long.valueOf(agentId); } catch (Exception ignored) {} historyMessages = conversationWindowManager.fitToWindow( historyMessages, systemPrompt != null ? systemPrompt : "", userMessage, maxInputTokens, chatModel, - conversationId); + conversationId, + parsedAgentId); } List messages = new ArrayList<>(historyMessages); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ObservationDispatcher.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ObservationDispatcher.java index 380eb778..7ab451a4 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ObservationDispatcher.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ObservationDispatcher.java @@ -38,8 +38,8 @@ public class ObservationDispatcher implements EdgeAction { return FINAL_ANSWER_NODE; } - // 1. 迭代超限检查 - if (currentIteration >= maxIterations) { + // 1. 迭代超限检查(maxIterations=0 表示不限制) + if (maxIterations > 0 && currentIteration >= maxIterations) { log.warn("[ObservationDispatcher] Max iterations ({}) reached at iteration {}, " + "routing to limitExceededNode", maxIterations, currentIteration); return LIMIT_EXCEEDED_NODE; diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ReasoningDispatcher.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ReasoningDispatcher.java index 2094be61..c1b34635 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ReasoningDispatcher.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/edge/ReasoningDispatcher.java @@ -52,9 +52,10 @@ public class ReasoningDispatcher implements EdgeAction { return FINAL_ANSWER_NODE; } - // 3. LLM 调用次数超限 — 仅拦截继续循环(工具调用/总结)的路径 + // 3. LLM 调用次数超限 — 仅拦截继续循环(工具调用/总结)的路径(maxIterations=0 不限制) int llmCallCount = accessor.llmCallCount(); - int llmCallLimit = accessor.maxIterations() * LLM_CALL_MULTIPLIER; + int maxIter = accessor.maxIterations(); + int llmCallLimit = maxIter > 0 ? maxIter * LLM_CALL_MULTIPLIER : Integer.MAX_VALUE; if (llmCallCount >= llmCallLimit) { log.warn("[ReasoningDispatcher] LLM call count limit reached ({}/{}), " + "routing to limitExceededNode instead of continuing loop", diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ObservationNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ObservationNode.java index 9b439d8c..169e2cae 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ObservationNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ObservationNode.java @@ -71,6 +71,24 @@ public class ObservationNode implements NodeAction { // 合并为单条观察记录 String combinedObservation = String.join("\n---\n", processedObservations); + // Budget Pressure Warning(Hermes 风格):接近上限时注入警告到工具结果中 + // LLM 下一轮 reasoning 时能看到,从而主动收束,而非被硬性截断 + if (maxIterations > 0) { + int progress = (int) ((double) nextIteration / maxIterations * 100); + if (progress >= 90) { + combinedObservation += "\n\n[⚠️ 预算警告] 当前迭代 " + nextIteration + "/" + maxIterations + + ",仅剩 " + (maxIterations - nextIteration) + " 步。" + + "请立即提供最终回答,不要再调用工具(除非绝对必要)。"; + log.info("[ObservationNode] Budget WARNING injected: {}/{} ({}%)", + nextIteration, maxIterations, progress); + } else if (progress >= 70) { + combinedObservation += "\n\n[📊 预算提示] 当前迭代 " + nextIteration + "/" + maxIterations + + ",剩余 " + (maxIterations - nextIteration) + " 步。请开始整合已有信息,准备给出回答。"; + log.info("[ObservationNode] Budget caution injected: {}/{} ({}%)", + nextIteration, maxIterations, progress); + } + } + // 手动累加观察历史(OBSERVATION_HISTORY 使用 REPLACE 策略,以便 SummarizingNode 可清空) List existingHistory = accessor.observationHistory(); List updatedHistory = new ArrayList<>(existingHistory); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java index 94965af7..5991d1a1 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java @@ -245,13 +245,16 @@ public class StateGraphPlanExecuteAgent extends BaseAgent implements StructuredS // 上下文窗口管理:裁剪超出模型 context window 的历史(含当前消息预算) if (conversationWindowManager != null) { + Long parsedAgentId = null; + try { parsedAgentId = Long.valueOf(agentId); } catch (Exception ignored) {} historyMessages = conversationWindowManager.fitToWindow( historyMessages, systemPrompt != null ? systemPrompt : "", userMessage, maxInputTokens, chatModel, - conversationId); + conversationId, + parsedAgentId); } List messages = new ArrayList<>(historyMessages); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/state/MateClawStateAccessor.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/state/MateClawStateAccessor.java index 777c3a8b..03254e4b 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/state/MateClawStateAccessor.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/state/MateClawStateAccessor.java @@ -57,11 +57,12 @@ public final class MateClawStateAccessor { } public int maxIterations() { - return state.value(MAX_ITERATIONS, 10); + return state.value(MAX_ITERATIONS, 25); } public boolean isLimitReached() { - return iterationCount() >= maxIterations(); + int max = maxIterations(); + return max > 0 && iterationCount() >= max; // max=0 表示不限制 } // ===== 工具调用 ===== diff --git a/mateclaw-server/src/main/java/vip/mate/config/ConversationWindowProperties.java b/mateclaw-server/src/main/java/vip/mate/config/ConversationWindowProperties.java index d3bd6307..32b52514 100644 --- a/mateclaw-server/src/main/java/vip/mate/config/ConversationWindowProperties.java +++ b/mateclaw-server/src/main/java/vip/mate/config/ConversationWindowProperties.java @@ -21,6 +21,20 @@ public class ConversationWindowProperties { /** 压缩后保留最近 N 轮对话(user+assistant 算一轮) */ private int preserveRecentPairs = 5; - /** 摘要自身最大 token 数 */ + /** 摘要自身最大 token 数(仅作 LLM maxToken 参数上限,实际预算由动态计算) */ private int summaryMaxTokens = 800; + + // ==================== 动态压缩配置(Hermes 风格) ==================== + + /** 尾部保护的最小消息数(即使 token 预算用完也至少保留这么多) */ + private int protectLastMinMessages = 10; + + /** 摘要 token 预算占被压缩内容的比例 (0-1)。被压缩内容越多,摘要越长。 */ + private double summaryBudgetRatio = 0.20; + + /** 摘要 token 预算上限(字数,非 token) */ + private int summaryBudgetCeiling = 3000; + + /** 摘要 token 预算下限(字数) */ + private int summaryBudgetFloor = 500; } diff --git a/mateclaw-server/src/main/java/vip/mate/memory/nudge/MemoryNudgeService.java b/mateclaw-server/src/main/java/vip/mate/memory/nudge/MemoryNudgeService.java index dc75b82d..87a5cdc0 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/nudge/MemoryNudgeService.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/nudge/MemoryNudgeService.java @@ -108,7 +108,7 @@ public class MemoryNudgeService { .replace("{transcript}", transcript) .replace("{existing_memories}", existingMemories.isBlank() ? "(none)" : existingMemories); - // 5. Call LLM + // 5. Call LLM (with rate limit retry) String llmResponse; try { ChatModel chatModel = buildChatModel(); @@ -116,8 +116,11 @@ public class MemoryNudgeService { new SystemMessage(systemPrompt), new UserMessage(userPrompt) )); - ChatResponse response = chatModel.call(prompt); - llmResponse = response.getResult().getOutput().getText(); + llmResponse = callLlmWithRetry(chatModel, prompt, 2); + if (llmResponse == null) { + log.warn("[Nudge] LLM returned null after retries for agent={}", agentId); + return; + } } catch (Exception e) { log.warn("[Nudge] LLM call failed for agent={}: {}", agentId, e.getMessage()); return; @@ -193,6 +196,38 @@ public class MemoryNudgeService { } } + private String callLlmWithRetry(ChatModel chatModel, Prompt prompt, int maxRetries) { + for (int attempt = 0; attempt <= maxRetries; attempt++) { + try { + ChatResponse response = chatModel.call(prompt); + if (response != null && response.getResult() != null + && response.getResult().getOutput() != null) { + return response.getResult().getOutput().getText(); + } + return null; + } catch (Exception e) { + if (attempt < maxRetries && isRateLimitError(e)) { + long delay = 5000L * (attempt + 1); + log.info("[Nudge] Rate limited, waiting {}ms before retry ({}/{})", + delay, attempt + 1, maxRetries); + try { Thread.sleep(delay); } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return null; + } + } else { + throw e instanceof RuntimeException re ? re : new RuntimeException(e); + } + } + } + return null; + } + + private boolean isRateLimitError(Exception e) { + String msg = e.getMessage(); + return msg != null && (msg.contains("429") || msg.contains("rate_limit") + || msg.contains("速率限制") || msg.contains("Too Many Requests")); + } + private boolean isInCooldown(Long agentId) { Instant lastRun = lastNudgeTimes.get(agentId); if (lastRun == null) return false; diff --git a/mateclaw-server/src/main/java/vip/mate/memory/service/MemorySummarizationService.java b/mateclaw-server/src/main/java/vip/mate/memory/service/MemorySummarizationService.java index 5a2a972a..a8937796 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/service/MemorySummarizationService.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/service/MemorySummarizationService.java @@ -120,8 +120,11 @@ public class MemorySummarizationService { new SystemMessage(systemPrompt), new UserMessage(userPrompt) )); - ChatResponse response = chatModel.call(prompt); - llmResponse = response.getResult().getOutput().getText(); + llmResponse = callLlmWithRetry(chatModel, prompt, 2); + if (llmResponse == null) { + log.warn("[Memory] LLM returned null after retries for agent={}, conv={}", agentId, conversationId); + return; + } } catch (Exception e) { log.warn("[Memory] LLM call failed for agent={}, conv={}: {}", agentId, conversationId, e.getMessage()); @@ -247,6 +250,42 @@ public class MemorySummarizationService { } } + /** + * 带轻量重试的 LLM 调用:遇到 429 时等待后重试,避免后台任务因限流直接放弃。 + * Spring AI RetryTemplate 已处理第一层重试,此方法作为二次保护。 + */ + private String callLlmWithRetry(ChatModel chatModel, Prompt prompt, int maxRetries) { + for (int attempt = 0; attempt <= maxRetries; attempt++) { + try { + ChatResponse response = chatModel.call(prompt); + if (response != null && response.getResult() != null + && response.getResult().getOutput() != null) { + return response.getResult().getOutput().getText(); + } + return null; + } catch (Exception e) { + if (attempt < maxRetries && isRateLimitError(e)) { + long delay = 5000L * (attempt + 1); + log.info("[Memory] Rate limited, waiting {}ms before retry ({}/{})", + delay, attempt + 1, maxRetries); + try { Thread.sleep(delay); } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return null; + } + } else { + throw e instanceof RuntimeException re ? re : new RuntimeException(e); + } + } + } + return null; + } + + private boolean isRateLimitError(Exception e) { + String msg = e.getMessage(); + return msg != null && (msg.contains("429") || msg.contains("rate_limit") + || msg.contains("速率限制") || msg.contains("Too Many Requests")); + } + private boolean isInCooldown(Long agentId) { Instant lastRun = lastRunTimes.get(agentId); if (lastRun == null) return false; diff --git a/mateclaw-server/src/main/resources/application.yml b/mateclaw-server/src/main/resources/application.yml index 3e30a49b..38edb76e 100644 --- a/mateclaw-server/src/main/resources/application.yml +++ b/mateclaw-server/src/main/resources/application.yml @@ -45,6 +45,14 @@ spring: repository: jdbc: initialize-schema: embedded + # Spring AI Retry 配置:429/503/529 归为可重试(TransientAiException),启用指数退避 + retry: + max-attempts: 5 + on-http-codes: 429, 503, 529 + backoff: + initial-interval: 3000 + multiplier: 3 + max-interval: 60000 # 禁用 Spring AI MCP Client 自动配置(由 McpClientManager 自行管理生命周期) mcp: client: diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql index 40370e30..7e62a9db 100644 --- a/mateclaw-server/src/main/resources/db/data-en.sql +++ b/mateclaw-server/src/main/resources/db/data-en.sql @@ -10,7 +10,7 @@ MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_n KEY (id) VALUES (1000000001, 'MateClaw Assistant', 'Default AI assistant with ReAct mode and tool calling', 'react', 'You are MateClaw, an intelligent AI assistant. You can help users answer questions, analyze data, and execute tasks. Please respond professionally and in a friendly manner.', - NULL, 10, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0); + NULL, 25, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0); -- Default Agent: Task Planner (Plan-Execute mode) MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted) @@ -24,7 +24,7 @@ MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_n KEY (id) VALUES (1000000003, 'StateGraph ReAct', 'StateGraph-based ReAct Agent with explicit reasoning loops and tool calling', 'react', 'You are an intelligent assistant based on the StateGraph architecture. You can use tools to help users solve problems. Please respond professionally and in a friendly manner.', - NULL, 10, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0); + NULL, 25, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0); -- ==================== Local Model Providers (displayed first) ==================== diff --git a/mateclaw-server/src/main/resources/db/data-mysql-en.sql b/mateclaw-server/src/main/resources/db/data-mysql-en.sql index 544c557a..0a4514d2 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql @@ -9,7 +9,7 @@ ON DUPLICATE KEY UPDATE username=VALUES(username), password=VALUES(password), ni INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted) VALUES (1000000001, 'MateClaw Assistant', 'Default AI assistant with ReAct mode and tool calling', 'react', 'You are MateClaw, an intelligent AI assistant. You can help users answer questions, analyze data, and execute tasks. Please respond professionally and in a friendly manner.', - NULL, 10, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0) + NULL, 25, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted); -- Default Agent: Task Planner (Plan-Execute mode) @@ -23,7 +23,7 @@ ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agen INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted) VALUES (1000000003, 'StateGraph ReAct', 'StateGraph-based ReAct Agent with explicit reasoning loops and tool calling', 'react', 'You are an intelligent assistant based on the StateGraph architecture. You can use tools to help users solve problems. Please respond professionally and in a friendly manner.', - NULL, 10, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0) + NULL, 25, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted); -- ==================== Local Model Providers (displayed first) ==================== diff --git a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql index 8e1b2ac5..78dd43d5 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql @@ -9,7 +9,7 @@ ON DUPLICATE KEY UPDATE username=VALUES(username), password=VALUES(password), ni INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted) VALUES (1000000001, 'MateClaw Assistant', '默认 AI 助手,基于 ReAct 模式,支持工具调用', 'react', '你是 MateClaw,一个智能 AI 助手。你可以帮助用户回答问题、分析数据、执行任务。请用中文回复,保持专业、友好的态度。', - NULL, 10, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0) + NULL, 25, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted); -- 默认 Agent:任务规划助手(Plan-Execute 模式) @@ -23,7 +23,7 @@ ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agen INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted) VALUES (1000000003, 'StateGraph ReAct', '基于 StateGraph 的 ReAct Agent,支持显式推理循环和工具调用', 'react', '你是基于 StateGraph 架构的智能助手。你可以使用工具来帮助用户解决问题。请用中文回复,保持专业、友好的态度。', - NULL, 10, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0) + NULL, 25, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted); -- ==================== 本地模型 Provider(优先展示) ==================== diff --git a/mateclaw-server/src/main/resources/db/data-zh.sql b/mateclaw-server/src/main/resources/db/data-zh.sql index abbdb363..68b2dbed 100644 --- a/mateclaw-server/src/main/resources/db/data-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-zh.sql @@ -10,7 +10,7 @@ MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_n KEY (id) VALUES (1000000001, 'MateClaw Assistant', '默认 AI 助手,基于 ReAct 模式,支持工具调用', 'react', '你是 MateClaw,一个智能 AI 助手。你可以帮助用户回答问题、分析数据、执行任务。请用中文回复,保持专业、友好的态度。', - NULL, 10, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0); + NULL, 25, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0); -- 默认 Agent:任务规划助手(Plan-Execute 模式) MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted) @@ -24,7 +24,7 @@ MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_n KEY (id) VALUES (1000000003, 'StateGraph ReAct', '基于 StateGraph 的 ReAct Agent,支持显式推理循环和工具调用', 'react', '你是基于 StateGraph 架构的智能助手。你可以使用工具来帮助用户解决问题。请用中文回复,保持专业、友好的态度。', - NULL, 10, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0); + NULL, 25, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0); -- ==================== 本地模型 Provider(优先展示) ==================== diff --git a/mateclaw-server/src/main/resources/prompts/context/structured-summary-system.txt b/mateclaw-server/src/main/resources/prompts/context/structured-summary-system.txt new file mode 100644 index 00000000..8b326ab3 --- /dev/null +++ b/mateclaw-server/src/main/resources/prompts/context/structured-summary-system.txt @@ -0,0 +1,32 @@ +你是上下文压缩助手。将以下对话轮次压缩为结构化交接摘要,供后续助手继续任务。 + +使用以下结构: + +## 目标 +[用户要完成什么] + +## 约束与偏好 +[用户偏好、编码风格、重要决策] + +## 进展 +### 已完成 +[已完成的工作——包含具体文件路径、执行的命令、获得的结果] +### 进行中 +[当前正在进行的工作] +### 阻塞 +[遇到的阻塞或问题] + +## 关键决策 +[重要的技术决策及原因] + +## 相关文件 +[读取、修改或创建的文件——每个附简要说明] + +## 下一步 +[继续工作需要做什么] + +## 关键上下文 +[不显式保留就会丢失的具体值、错误消息、配置详情] + +目标约 {summary_budget} 字。要具体——包含文件路径、命令输出、错误消息和实际值。 +只输出摘要正文,不要前缀或额外说明。 \ No newline at end of file diff --git a/mateclaw-server/src/main/resources/prompts/context/structured-summary-update.txt b/mateclaw-server/src/main/resources/prompts/context/structured-summary-update.txt new file mode 100644 index 00000000..789e1d28 --- /dev/null +++ b/mateclaw-server/src/main/resources/prompts/context/structured-summary-update.txt @@ -0,0 +1,23 @@ +你正在更新一个上下文压缩摘要。上一次压缩生成了以下摘要,之后发生了新的对话轮次。 + +## 旧摘要 +{previous_summary} + +## 新增轮次 +{conversation} + +请用同样的结构更新摘要。保留所有仍然相关的信息,添加新进展, +将"进行中"的已完成项移到"已完成",仅删除明显过时的信息。 + +使用以下结构: + +## 目标 +## 约束与偏好 +## 进展(已完成 / 进行中 / 阻塞) +## 关键决策 +## 相关文件 +## 下一步 +## 关键上下文 + +目标约 {summary_budget} 字。要具体——包含文件路径、命令输出、错误消息和实际值。 +只输出摘要正文,不要前缀或额外说明。 \ No newline at end of file diff --git a/mateclaw-server/src/main/resources/prompts/context/structured-summary-user.txt b/mateclaw-server/src/main/resources/prompts/context/structured-summary-user.txt new file mode 100644 index 00000000..e40af017 --- /dev/null +++ b/mateclaw-server/src/main/resources/prompts/context/structured-summary-user.txt @@ -0,0 +1,5 @@ +以下是需要压缩的对话轮次: + +{conversation} + +请生成结构化交接摘要。 \ No newline at end of file