mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(agent): complete RFC-001 — Anthropic thinking, iteration budget, UI fixes
This commit is contained in:
parent
2a8b90365b
commit
46f77c281b
@ -37,6 +37,7 @@ import org.springframework.web.client.RestClient;
|
|||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import vip.mate.agent.ThinkingLevelHolder;
|
||||||
import vip.mate.agent.graph.StateGraphReActAgent;
|
import vip.mate.agent.graph.StateGraphReActAgent;
|
||||||
import vip.mate.agent.graph.NodeStreamingChatHelper;
|
import vip.mate.agent.graph.NodeStreamingChatHelper;
|
||||||
import vip.mate.agent.graph.executor.ToolExecutionExecutor;
|
import vip.mate.agent.graph.executor.ToolExecutionExecutor;
|
||||||
@ -940,18 +941,40 @@ public class AgentGraphBuilder {
|
|||||||
if (StringUtils.hasText(runtimeModel.getModelName())) {
|
if (StringUtils.hasText(runtimeModel.getModelName())) {
|
||||||
builder.model(runtimeModel.getModelName());
|
builder.model(runtimeModel.getModelName());
|
||||||
}
|
}
|
||||||
// Anthropic API does not allow temperature and top_p to be specified simultaneously.
|
|
||||||
// Prefer temperature; only fall back to top_p when temperature is absent.
|
// Extended thinking: 通过 ThinkingLevelHolder 获取请求级思考深度
|
||||||
if (runtimeModel.getTemperature() != null) {
|
String thinkingLevel = ThinkingLevelHolder.get();
|
||||||
builder.temperature(runtimeModel.getTemperature());
|
boolean thinkingEnabled = thinkingLevel != null && !"off".equalsIgnoreCase(thinkingLevel);
|
||||||
} else if (runtimeModel.getTopP() != null) {
|
|
||||||
builder.topP(runtimeModel.getTopP());
|
if (thinkingEnabled) {
|
||||||
}
|
// Anthropic thinking 模式下:temperature 必须为 1,不能设 top_p
|
||||||
if (runtimeModel.getMaxTokens() != null) {
|
// budget_tokens 根据级别映射
|
||||||
builder.maxTokens(runtimeModel.getMaxTokens());
|
int budgetTokens = switch (thinkingLevel.toLowerCase()) {
|
||||||
|
case "low" -> 4096;
|
||||||
|
case "medium" -> 8192;
|
||||||
|
case "high" -> 16384;
|
||||||
|
case "max" -> 32768;
|
||||||
|
default -> 16384;
|
||||||
|
};
|
||||||
|
builder.thinking(org.springframework.ai.anthropic.api.AnthropicApi.ThinkingType.ENABLED, budgetTokens);
|
||||||
|
// Thinking 模式要求 max_tokens 足够大(含 thinking tokens)
|
||||||
|
builder.maxTokens(Math.max(budgetTokens + 4096,
|
||||||
|
runtimeModel.getMaxTokens() != null ? runtimeModel.getMaxTokens() : 8192));
|
||||||
|
// Anthropic thinking 模式要求 temperature=1
|
||||||
|
builder.temperature(1.0);
|
||||||
} else {
|
} else {
|
||||||
// Anthropic requires max_tokens; set a safe default
|
// 非 thinking 模式:正常设置参数
|
||||||
builder.maxTokens(4096);
|
// Anthropic API does not allow temperature and top_p to be specified simultaneously.
|
||||||
|
if (runtimeModel.getTemperature() != null) {
|
||||||
|
builder.temperature(runtimeModel.getTemperature());
|
||||||
|
} else if (runtimeModel.getTopP() != null) {
|
||||||
|
builder.topP(runtimeModel.getTopP());
|
||||||
|
}
|
||||||
|
if (runtimeModel.getMaxTokens() != null) {
|
||||||
|
builder.maxTokens(runtimeModel.getMaxTokens());
|
||||||
|
} else {
|
||||||
|
builder.maxTokens(4096);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return builder.internalToolExecutionEnabled(false).build();
|
return builder.internalToolExecutionEnabled(false).build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -365,8 +365,11 @@ public class StateGraphReActAgent extends BaseAgent implements StructuredStreamC
|
|||||||
inputs.put(WORKSPACE_BASE_PATH, workspaceBasePath != null ? workspaceBasePath : "");
|
inputs.put(WORKSPACE_BASE_PATH, workspaceBasePath != null ? workspaceBasePath : "");
|
||||||
inputs.put(SYSTEM_PROMPT, systemPrompt != null ? systemPrompt : "你是一个有帮助的AI助手。");
|
inputs.put(SYSTEM_PROMPT, systemPrompt != null ? systemPrompt : "你是一个有帮助的AI助手。");
|
||||||
inputs.put(MESSAGES, messages);
|
inputs.put(MESSAGES, messages);
|
||||||
// 迭代控制
|
// 迭代控制:深度思考模式允许更多迭代(思考需要更多轮工具调用)
|
||||||
inputs.put(MAX_ITERATIONS, maxIterations);
|
String thinkingLevel = vip.mate.agent.ThinkingLevelHolder.get();
|
||||||
|
boolean thinkingOn = thinkingLevel != null && !"off".equalsIgnoreCase(thinkingLevel);
|
||||||
|
int effectiveMaxIterations = thinkingOn ? maxIterations + 5 : maxIterations;
|
||||||
|
inputs.put(MAX_ITERATIONS, effectiveMaxIterations);
|
||||||
inputs.put(CURRENT_ITERATION, 0);
|
inputs.put(CURRENT_ITERATION, 0);
|
||||||
// 初始化新字段
|
// 初始化新字段
|
||||||
inputs.put(TOOL_CALL_COUNT, 0);
|
inputs.put(TOOL_CALL_COUNT, 0);
|
||||||
|
|||||||
@ -183,22 +183,7 @@ public class ReasoningNode implements NodeAction {
|
|||||||
log.info("[ReasoningNode] thinkingLevel={}, effectiveReasoningEffort={}, nodeDefault={}",
|
log.info("[ReasoningNode] thinkingLevel={}, effectiveReasoningEffort={}, nodeDefault={}",
|
||||||
ThinkingLevelHolder.get(), effectiveReasoning, this.reasoningEffort);
|
ThinkingLevelHolder.get(), effectiveReasoning, this.reasoningEffort);
|
||||||
|
|
||||||
ChatOptions options;
|
ChatOptions options = buildChatOptions(effectiveReasoning);
|
||||||
if (StringUtils.hasText(effectiveReasoning)) {
|
|
||||||
OpenAiChatOptions oaiOpts = OpenAiChatOptions.builder()
|
|
||||||
.toolCallbacks(toolCallbacks)
|
|
||||||
.reasoningEffort(effectiveReasoning)
|
|
||||||
.maxTokens(maxOutputTokens)
|
|
||||||
.build();
|
|
||||||
oaiOpts.setInternalToolExecutionEnabled(false);
|
|
||||||
options = oaiOpts;
|
|
||||||
} else {
|
|
||||||
options = ToolCallingChatOptions.builder()
|
|
||||||
.toolCallbacks(toolCallbacks)
|
|
||||||
.internalToolExecutionEnabled(false)
|
|
||||||
.maxTokens(maxOutputTokens)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Prompt prompt = new Prompt(promptMessages, options);
|
Prompt prompt = new Prompt(promptMessages, options);
|
||||||
|
|
||||||
@ -380,6 +365,62 @@ public class ReasoningNode implements NodeAction {
|
|||||||
streamTracker.broadcastObject(conversationId, "phase", GraphEventPublisher.phase(phase, extra).data());
|
streamTracker.broadcastObject(conversationId, "phase", GraphEventPublisher.phase(phase, extra).data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ChatModel 类型构建合适的 ChatOptions。
|
||||||
|
* - AnthropicChatModel → AnthropicChatOptions(支持 extended thinking)
|
||||||
|
* - 其他(OpenAI/DashScope)→ OpenAiChatOptions(支持 reasoningEffort)
|
||||||
|
*/
|
||||||
|
private ChatOptions buildChatOptions(String effectiveReasoning) {
|
||||||
|
// Anthropic 协议模型(AnthropicChatModel):MiniMax 也用此协议但不支持 thinking
|
||||||
|
if (chatModel instanceof org.springframework.ai.anthropic.AnthropicChatModel anthropicModel) {
|
||||||
|
org.springframework.ai.anthropic.AnthropicChatOptions.Builder builder =
|
||||||
|
org.springframework.ai.anthropic.AnthropicChatOptions.builder()
|
||||||
|
.toolCallbacks(toolCallbacks)
|
||||||
|
.internalToolExecutionEnabled(false);
|
||||||
|
|
||||||
|
// 仅对真正的 Claude 模型启用 extended thinking(MiniMax 等走 Anthropic 协议但不支持)
|
||||||
|
String thinkingLevel = ThinkingLevelHolder.get();
|
||||||
|
boolean thinkingOn = thinkingLevel != null && !"off".equalsIgnoreCase(thinkingLevel);
|
||||||
|
String currentModel = getAnthropicModelName(anthropicModel);
|
||||||
|
boolean isClaudeModel = currentModel != null && currentModel.toLowerCase().contains("claude");
|
||||||
|
|
||||||
|
if (thinkingOn && isClaudeModel) {
|
||||||
|
int budgetTokens = switch (thinkingLevel.toLowerCase()) {
|
||||||
|
case "low" -> 4096;
|
||||||
|
case "medium" -> 8192;
|
||||||
|
case "high" -> 16384;
|
||||||
|
case "max" -> 32768;
|
||||||
|
default -> 16384;
|
||||||
|
};
|
||||||
|
builder.thinking(org.springframework.ai.anthropic.api.AnthropicApi.ThinkingType.ENABLED, budgetTokens);
|
||||||
|
builder.maxTokens(budgetTokens + maxOutputTokens);
|
||||||
|
builder.temperature(1.0);
|
||||||
|
log.info("[ReasoningNode] Anthropic extended thinking enabled: model={}, budget={}", currentModel, budgetTokens);
|
||||||
|
} else {
|
||||||
|
builder.maxTokens(maxOutputTokens);
|
||||||
|
if (thinkingOn && !isClaudeModel) {
|
||||||
|
log.debug("[ReasoningNode] Anthropic protocol model {} does not support thinking, skipping", currentModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI / DashScope / 其他
|
||||||
|
// 始终使用 OpenAiChatOptions(而非 ToolCallingChatOptions),
|
||||||
|
// 因为 ToolCallingChatOptions 会丢失 OpenAI 特有参数(streamUsage 等),
|
||||||
|
// 导致 Kimi 等 OpenAI 兼容 API 响应异常或提前截断。
|
||||||
|
OpenAiChatOptions.Builder oaiBuilder = OpenAiChatOptions.builder()
|
||||||
|
.toolCallbacks(toolCallbacks)
|
||||||
|
.maxTokens(maxOutputTokens);
|
||||||
|
if (StringUtils.hasText(effectiveReasoning)) {
|
||||||
|
oaiBuilder.reasoningEffort(effectiveReasoning);
|
||||||
|
}
|
||||||
|
OpenAiChatOptions oaiOpts = oaiBuilder.build();
|
||||||
|
oaiOpts.setInternalToolExecutionEnabled(false);
|
||||||
|
oaiOpts.setStreamUsage(true);
|
||||||
|
return oaiOpts;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析有效的 reasoningEffort。
|
* 解析有效的 reasoningEffort。
|
||||||
* 优先级:ThinkingLevelHolder(请求级) > 构造时的 reasoningEffort(Agent/模型默认)。
|
* 优先级:ThinkingLevelHolder(请求级) > 构造时的 reasoningEffort(Agent/模型默认)。
|
||||||
@ -403,4 +444,20 @@ public class ReasoningNode implements NodeAction {
|
|||||||
// 无请求级覆盖,使用构造时的默认值
|
// 无请求级覆盖,使用构造时的默认值
|
||||||
return this.reasoningEffort;
|
return this.reasoningEffort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 AnthropicChatModel 的 defaultOptions 中提取模型名称。
|
||||||
|
* 用于判断是否为真正的 Claude 模型(vs MiniMax 等走 Anthropic 协议的非 Claude 模型)。
|
||||||
|
*/
|
||||||
|
private String getAnthropicModelName(org.springframework.ai.anthropic.AnthropicChatModel model) {
|
||||||
|
try {
|
||||||
|
var options = model.getDefaultOptions();
|
||||||
|
if (options instanceof org.springframework.ai.anthropic.AnthropicChatOptions aOpts) {
|
||||||
|
return aOpts.getModel();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[ReasoningNode] Failed to extract Anthropic model name: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -362,9 +362,18 @@ const showThinkingPanel = computed(() => !!thinkingContent.value)
|
|||||||
// 思考耗时(生成结束后显示)
|
// 思考耗时(生成结束后显示)
|
||||||
const thinkingDuration = computed(() => {
|
const thinkingDuration = computed(() => {
|
||||||
if (isGenerating.value) return ''
|
if (isGenerating.value) return ''
|
||||||
|
if (!thinkingContent.value) return ''
|
||||||
|
// 优先使用 segment 真实时间戳
|
||||||
|
const segs = (props.message as any).segments || []
|
||||||
|
const thinkSeg = segs.find((s: any) => s.type === 'thinking')
|
||||||
|
const contentSeg = segs.find((s: any) => s.type === 'content')
|
||||||
|
if (thinkSeg?.timestamp && contentSeg?.timestamp) {
|
||||||
|
const sec = Math.max(1, Math.round((contentSeg.timestamp - thinkSeg.timestamp) / 1000))
|
||||||
|
return sec >= 60 ? `${Math.floor(sec / 60)}m ${sec % 60}s` : `${sec}s`
|
||||||
|
}
|
||||||
|
// 回退:从内容长度估算
|
||||||
const len = thinkingContent.value.length
|
const len = thinkingContent.value.length
|
||||||
if (len < 50) return ''
|
if (len < 50) return ''
|
||||||
// 粗略估计:每 100 字符约 1 秒
|
|
||||||
const sec = Math.max(1, Math.round(len / 100))
|
const sec = Math.max(1, Math.round(len / 100))
|
||||||
return sec >= 60 ? `${Math.floor(sec / 60)}m ${sec % 60}s` : `${sec}s`
|
return sec >= 60 ? `${Math.floor(sec / 60)}m ${sec % 60}s` : `${sec}s`
|
||||||
})
|
})
|
||||||
|
|||||||
@ -286,21 +286,18 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
if (streamPhase.value !== 'summarizing_observations') {
|
if (streamPhase.value !== 'summarizing_observations') {
|
||||||
streamPhase.value = options.thinkingLevel?.value === 'off' ? 'streaming' : 'thinking'
|
streamPhase.value = options.thinkingLevel?.value === 'off' ? 'streaming' : 'thinking'
|
||||||
}
|
}
|
||||||
// 分段:追加到当前 thinking segment 或创建新的
|
// 分段:所有 thinking 合并到一个 segment(不因 tool_call 中断而创建多个)
|
||||||
const segs = currentSegments.value
|
const segs = currentSegments.value
|
||||||
let thinkSeg = segs.findLast((s: MessageSegment) => s.type === 'thinking' && s.status === 'running')
|
// 优先复用已有的 thinking segment(无论 running 还是 completed)
|
||||||
|
let thinkSeg = segs.find((s: MessageSegment) => s.type === 'thinking')
|
||||||
if (!thinkSeg) {
|
if (!thinkSeg) {
|
||||||
thinkSeg = { id: genSegId(), type: 'thinking', status: 'running', thinkingText: '', timestamp: Date.now() }
|
thinkSeg = { id: genSegId(), type: 'thinking', status: 'running', thinkingText: '', timestamp: Date.now() }
|
||||||
// 修复:如果前面已有 content segment(模型先发 content 后发 thinking),
|
// 插入到开头(thinking 始终在最上方)
|
||||||
// 把 thinking 插入到第一个 content 之前,确保 thinking 在上方显示
|
segs.unshift(thinkSeg)
|
||||||
const firstContentIdx = segs.findIndex((s: MessageSegment) => s.type === 'content')
|
|
||||||
if (firstContentIdx >= 0 && !segs.some((s: MessageSegment) => s.type === 'tool_call')) {
|
|
||||||
segs.splice(firstContentIdx, 0, thinkSeg)
|
|
||||||
} else {
|
|
||||||
segs.push(thinkSeg)
|
|
||||||
}
|
|
||||||
flushSegmentsToMessage()
|
flushSegmentsToMessage()
|
||||||
}
|
}
|
||||||
|
// 新的 thinking 到来,重新设为 running
|
||||||
|
thinkSeg.status = 'running'
|
||||||
thinkSeg.thinkingText = (thinkSeg.thinkingText || '') + (data.delta || '')
|
thinkSeg.thinkingText = (thinkSeg.thinkingText || '') + (data.delta || '')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user