diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java index d8c1f144..80d880be 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java @@ -103,20 +103,40 @@ public class AgentService { } public Flux chatStructuredStream(Long agentId, String message, String conversationId) { - return chatStructuredStream(agentId, message, conversationId, ""); + return chatStructuredStream(agentId, message, conversationId, "", null); } public Flux chatStructuredStream(Long agentId, String message, String conversationId, String requesterId) { + return chatStructuredStream(agentId, message, conversationId, requesterId, null); + } + + public Flux chatStructuredStream(Long agentId, String message, String conversationId, + String requesterId, String thinkingLevel) { memoryRecallTracker.trackRecalls(agentId, message); BaseAgent agent = getOrBuildAgent(agentId); + // 设置请求级思考深度(通过 ThreadLocal 传递到 StateGraph 执行) + if (thinkingLevel != null && !thinkingLevel.isBlank()) { + ThinkingLevelHolder.set(thinkingLevel); + } else { + // 尝试从 Agent 默认配置读取 + AgentEntity entity = getAgent(agentId); + if (entity != null && entity.getDefaultThinkingLevel() != null) { + ThinkingLevelHolder.set(entity.getDefaultThinkingLevel()); + } else { + ThinkingLevelHolder.clear(); + } + } + if (agent instanceof StructuredStreamCapable capable) { return capable.chatStructuredStream(message, conversationId, - requesterId != null ? requesterId : ""); + requesterId != null ? requesterId : "") + .doFinally(signal -> ThinkingLevelHolder.clear()); } // 降级:不支持结构化流的 Agent,包装为纯内容流 + ThinkingLevelHolder.clear(); return agent.chatStream(message, conversationId) .map(chunk -> new StreamDelta(chunk, null)); } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/ThinkingLevelHolder.java b/mateclaw-server/src/main/java/vip/mate/agent/ThinkingLevelHolder.java new file mode 100644 index 00000000..635957c0 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/agent/ThinkingLevelHolder.java @@ -0,0 +1,33 @@ +package vip.mate.agent; + +/** + * 请求级思考深度的 ThreadLocal 持有器。 + *

+ * 用于将前端选择的思考级别从 AgentService 传递到 ReasoningNode, + * 避免修改 Agent 缓存实例或 StructuredStreamCapable 接口。 + *

+ * 支持的值:off / low / medium / high / max,null 表示跟随模型默认。 + * + * @author MateClaw Team + */ +public final class ThinkingLevelHolder { + + private static final ThreadLocal HOLDER = new ThreadLocal<>(); + + private ThinkingLevelHolder() {} + + public static void set(String level) { + HOLDER.set(level); + } + + /** + * 获取当前请求的思考级别,null 表示未设置(跟随模型默认) + */ + public static String get() { + return HOLDER.get(); + } + + public static void clear() { + HOLDER.remove(); + } +} 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 4e80804c..bccc2b9b 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 @@ -320,7 +320,10 @@ public class NodeStreamingChatHelper { return; } thinkingAccum.append(thinkingDelta); - if (broadcast) { + // thinkingLevel=off 时不广播 thinking(模型仍可能产生,但前端不展示) + boolean suppressThinking = "off".equalsIgnoreCase( + vip.mate.agent.ThinkingLevelHolder.get()); + if (broadcast && !suppressThinking) { broadcastDelta(conversationId, "thinking_delta", thinkingDelta); } } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java index f9298b56..3e15c0c0 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java @@ -17,6 +17,7 @@ import org.springframework.ai.tool.ToolCallback; import org.springframework.util.StringUtils; import vip.mate.agent.AgentToolSet; import vip.mate.agent.GraphEventPublisher; +import vip.mate.agent.ThinkingLevelHolder; import vip.mate.agent.graph.NodeStreamingChatHelper; import vip.mate.agent.context.ConversationWindowManager; import vip.mate.agent.context.RuntimeContextInjector; @@ -177,11 +178,16 @@ public class ReasoningNode implements NodeAction { promptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage(workspaceBasePath))); promptMessages.addAll(messages); + // 请求级思考深度覆盖(ThinkingLevelHolder 由 AgentService 设置) + String effectiveReasoning = resolveEffectiveReasoningEffort(); + log.info("[ReasoningNode] thinkingLevel={}, effectiveReasoningEffort={}, nodeDefault={}", + ThinkingLevelHolder.get(), effectiveReasoning, this.reasoningEffort); + ChatOptions options; - if (StringUtils.hasText(reasoningEffort)) { + if (StringUtils.hasText(effectiveReasoning)) { OpenAiChatOptions oaiOpts = OpenAiChatOptions.builder() .toolCallbacks(toolCallbacks) - .reasoningEffort(reasoningEffort) + .reasoningEffort(effectiveReasoning) .maxTokens(maxOutputTokens) .build(); oaiOpts.setInternalToolExecutionEnabled(false); @@ -373,4 +379,28 @@ public class ReasoningNode implements NodeAction { streamTracker.updatePhase(conversationId, phase); streamTracker.broadcastObject(conversationId, "phase", GraphEventPublisher.phase(phase, extra).data()); } + + /** + * 解析有效的 reasoningEffort。 + * 优先级:ThinkingLevelHolder(请求级) > 构造时的 reasoningEffort(Agent/模型默认)。 + * "off" 会清除 reasoningEffort(返回 null)。 + */ + private String resolveEffectiveReasoningEffort() { + String requestLevel = ThinkingLevelHolder.get(); + if (requestLevel != null) { + if ("off".equalsIgnoreCase(requestLevel)) { + return null; + } + // thinkingLevel → reasoningEffort 映射 + return switch (requestLevel.toLowerCase()) { + case "low" -> "low"; + case "medium" -> "medium"; + case "high" -> "high"; + case "max" -> "high"; // OpenAI 最高支持 high + default -> requestLevel; // 透传未知值 + }; + } + // 无请求级覆盖,使用构造时的默认值 + return this.reasoningEffort; + } } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java b/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java index 3c1ce8f5..bdc0ed4e 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java @@ -52,6 +52,9 @@ public class AgentEntity { /** 所属工作区 ID(默认 1 = default) */ private Long workspaceId; + /** 默认思考深度:off / low / medium / high / max,null 表示跟随模型默认 */ + private String defaultThinkingLevel; + @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; diff --git a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java index bcf86d5b..aa06f9bb 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java @@ -418,7 +418,7 @@ public class ChatController { )); streamTracker.incrementFlux(conversationId); - Disposable disposable = agentService.chatStructuredStream(agentId, promptText, conversationId, username) + Disposable disposable = agentService.chatStructuredStream(agentId, promptText, conversationId, username, request.getThinkingLevel()) .doOnNext(delta -> { if (emitterDone.get()) return; try { @@ -922,6 +922,8 @@ public class ChatController { private List contentParts; /** true 表示断线重连,不发送新消息,只附着到已有的流 */ private Boolean reconnect; + /** 思考深度:off / low / medium / high / max,null 表示跟随 Agent 默认 */ + private String thinkingLevel; } /** diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V4__agent_thinking_level.sql b/mateclaw-server/src/main/resources/db/migration/h2/V4__agent_thinking_level.sql new file mode 100644 index 00000000..4b98c14e --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V4__agent_thinking_level.sql @@ -0,0 +1,3 @@ +-- V4: Add default_thinking_level to mate_agent +-- Supports: off / low / medium / high / max (null = follow model default) +ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS default_thinking_level VARCHAR(32) DEFAULT NULL; diff --git a/mateclaw-server/src/main/resources/db/schema.sql b/mateclaw-server/src/main/resources/db/schema.sql index 6d1cb0c8..4c9aa35d 100644 --- a/mateclaw-server/src/main/resources/db/schema.sql +++ b/mateclaw-server/src/main/resources/db/schema.sql @@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS mate_agent ( icon VARCHAR(256), tags VARCHAR(256), workspace_id BIGINT NOT NULL DEFAULT 1, + default_thinking_level VARCHAR(32) DEFAULT NULL, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, deleted INT NOT NULL DEFAULT 0 diff --git a/mateclaw-ui/src/components/chat/ChatInput.vue b/mateclaw-ui/src/components/chat/ChatInput.vue index 3684012c..c4515808 100644 --- a/mateclaw-ui/src/components/chat/ChatInput.vue +++ b/mateclaw-ui/src/components/chat/ChatInput.vue @@ -136,6 +136,18 @@ + + +