mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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.WebClientResponseException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import vip.mate.agent.ThinkingLevelHolder;
|
||||
import vip.mate.agent.graph.StateGraphReActAgent;
|
||||
import vip.mate.agent.graph.NodeStreamingChatHelper;
|
||||
import vip.mate.agent.graph.executor.ToolExecutionExecutor;
|
||||
@ -940,18 +941,40 @@ public class AgentGraphBuilder {
|
||||
if (StringUtils.hasText(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.
|
||||
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());
|
||||
|
||||
// Extended thinking: 通过 ThinkingLevelHolder 获取请求级思考深度
|
||||
String thinkingLevel = ThinkingLevelHolder.get();
|
||||
boolean thinkingEnabled = thinkingLevel != null && !"off".equalsIgnoreCase(thinkingLevel);
|
||||
|
||||
if (thinkingEnabled) {
|
||||
// Anthropic thinking 模式下:temperature 必须为 1,不能设 top_p
|
||||
// budget_tokens 根据级别映射
|
||||
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 {
|
||||
// Anthropic requires max_tokens; set a safe default
|
||||
builder.maxTokens(4096);
|
||||
// 非 thinking 模式:正常设置参数
|
||||
// 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();
|
||||
}
|
||||
|
||||
@ -365,8 +365,11 @@ public class StateGraphReActAgent extends BaseAgent implements StructuredStreamC
|
||||
inputs.put(WORKSPACE_BASE_PATH, workspaceBasePath != null ? workspaceBasePath : "");
|
||||
inputs.put(SYSTEM_PROMPT, systemPrompt != null ? systemPrompt : "你是一个有帮助的AI助手。");
|
||||
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(TOOL_CALL_COUNT, 0);
|
||||
|
||||
@ -183,22 +183,7 @@ public class ReasoningNode implements NodeAction {
|
||||
log.info("[ReasoningNode] thinkingLevel={}, effectiveReasoningEffort={}, nodeDefault={}",
|
||||
ThinkingLevelHolder.get(), effectiveReasoning, this.reasoningEffort);
|
||||
|
||||
ChatOptions options;
|
||||
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();
|
||||
}
|
||||
ChatOptions options = buildChatOptions(effectiveReasoning);
|
||||
|
||||
Prompt prompt = new Prompt(promptMessages, options);
|
||||
|
||||
@ -380,6 +365,62 @@ public class ReasoningNode implements NodeAction {
|
||||
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。
|
||||
* 优先级:ThinkingLevelHolder(请求级) > 构造时的 reasoningEffort(Agent/模型默认)。
|
||||
@ -403,4 +444,20 @@ public class ReasoningNode implements NodeAction {
|
||||
// 无请求级覆盖,使用构造时的默认值
|
||||
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(() => {
|
||||
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
|
||||
if (len < 50) return ''
|
||||
// 粗略估计:每 100 字符约 1 秒
|
||||
const sec = Math.max(1, Math.round(len / 100))
|
||||
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') {
|
||||
streamPhase.value = options.thinkingLevel?.value === 'off' ? 'streaming' : 'thinking'
|
||||
}
|
||||
// 分段:追加到当前 thinking segment 或创建新的
|
||||
// 分段:所有 thinking 合并到一个 segment(不因 tool_call 中断而创建多个)
|
||||
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) {
|
||||
thinkSeg = { id: genSegId(), type: 'thinking', status: 'running', thinkingText: '', timestamp: Date.now() }
|
||||
// 修复:如果前面已有 content segment(模型先发 content 后发 thinking),
|
||||
// 把 thinking 插入到第一个 content 之前,确保 thinking 在上方显示
|
||||
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)
|
||||
}
|
||||
// 插入到开头(thinking 始终在最上方)
|
||||
segs.unshift(thinkSeg)
|
||||
flushSegmentsToMessage()
|
||||
}
|
||||
// 新的 thinking 到来,重新设为 running
|
||||
thinkSeg.status = 'running'
|
||||
thinkSeg.thinkingText = (thinkSeg.thinkingText || '') + (data.delta || '')
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user