mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +08:00
fix(agent): 推理节点尊重模型配置的 maxTokens 输出上限,并按真实窗口钳制,修复严格本地服务端的 max_tokens 预检拒绝
This commit is contained in:
parent
727adcd24b
commit
35142508db
@ -926,9 +926,18 @@ public class AgentGraphBuilder {
|
|||||||
// capability from reasoningEffort == null.
|
// capability from reasoningEffort == null.
|
||||||
boolean supportsReasoningEffort = primaryModelConfig != null
|
boolean supportsReasoningEffort = primaryModelConfig != null
|
||||||
&& ModelFamily.detect(primaryModelConfig.getModelName()).supportsReasoningEffort();
|
&& ModelFamily.detect(primaryModelConfig.getModelName()).supportsReasoningEffort();
|
||||||
|
// Honor the model's configured output cap. Passing 0 here made the
|
||||||
|
// node fall back to its 16384 default, so the user-configured
|
||||||
|
// maxTokens never took effect and strict local servers (vLLM's
|
||||||
|
// max_model_len pre-check) rejected the request outright.
|
||||||
|
int configuredMaxOutputTokens = (primaryModelConfig != null
|
||||||
|
&& primaryModelConfig.getMaxTokens() != null
|
||||||
|
&& primaryModelConfig.getMaxTokens() > 0)
|
||||||
|
? primaryModelConfig.getMaxTokens() : 0;
|
||||||
ReasoningNode reasoningNode = new ReasoningNode(chatModel, toolSet, reasoningEffort,
|
ReasoningNode reasoningNode = new ReasoningNode(chatModel, toolSet, reasoningEffort,
|
||||||
supportsReasoningEffort,
|
supportsReasoningEffort,
|
||||||
streamingHelper, conversationWindowManager, streamTracker, 0, wikiContextService,
|
streamingHelper, conversationWindowManager, streamTracker,
|
||||||
|
configuredMaxOutputTokens, wikiContextService,
|
||||||
skillCatalogRenderer, toolDisclosureService, progressLedgerService);
|
skillCatalogRenderer, toolDisclosureService, progressLedgerService);
|
||||||
reasoningNode.setPrefixBudgetPlan(prefixBudgetPlan);
|
reasoningNode.setPrefixBudgetPlan(prefixBudgetPlan);
|
||||||
reasoningNode.setAutoDemotedTools(autoDemotedTools);
|
reasoningNode.setAutoDemotedTools(autoDemotedTools);
|
||||||
|
|||||||
@ -376,6 +376,27 @@ public class ReasoningNode implements NodeAction {
|
|||||||
this.autoDemotedTools = autoDemotedTools == null ? Set.of() : autoDemotedTools;
|
this.autoDemotedTools = autoDemotedTools == null ? Set.of() : autoDemotedTools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Floor for the window-aware output clamp — an answer needs at least this much room. */
|
||||||
|
private static final int MIN_CLAMPED_OUTPUT_TOKENS = 512;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output cap actually sent to the provider. Strict local servers (vLLM)
|
||||||
|
* statically reject {@code max_tokens >= max_model_len}, so when the
|
||||||
|
* effective context window is known and smaller than the configured /
|
||||||
|
* default output cap, clamp to half the window (leaving the other half
|
||||||
|
* for the prompt). No-op when the window is unknown or already larger.
|
||||||
|
*/
|
||||||
|
int effectiveMaxOutputTokens() {
|
||||||
|
int window = (prefixBudgetPlan != null) ? prefixBudgetPlan.effectiveMaxTokens() : 0;
|
||||||
|
if (window > 0 && maxOutputTokens >= window) {
|
||||||
|
int clamped = Math.max(MIN_CLAMPED_OUTPUT_TOKENS, window / 2);
|
||||||
|
log.info("[ReasoningNode] max_tokens {} ≥ 模型窗口 {},钳制为 {}(窗口一半)以避免服务端拒绝",
|
||||||
|
maxOutputTokens, window, clamped);
|
||||||
|
return clamped;
|
||||||
|
}
|
||||||
|
return maxOutputTokens;
|
||||||
|
}
|
||||||
|
|
||||||
public ReasoningNode(ChatModel chatModel, AgentToolSet toolSet, String reasoningEffort,
|
public ReasoningNode(ChatModel chatModel, AgentToolSet toolSet, String reasoningEffort,
|
||||||
NodeStreamingChatHelper streamingHelper,
|
NodeStreamingChatHelper streamingHelper,
|
||||||
ConversationWindowManager conversationWindowManager,
|
ConversationWindowManager conversationWindowManager,
|
||||||
@ -1234,11 +1255,11 @@ public class ReasoningNode implements NodeAction {
|
|||||||
default -> 16384;
|
default -> 16384;
|
||||||
};
|
};
|
||||||
builder.thinking(org.springframework.ai.anthropic.api.AnthropicApi.ThinkingType.ENABLED, budgetTokens);
|
builder.thinking(org.springframework.ai.anthropic.api.AnthropicApi.ThinkingType.ENABLED, budgetTokens);
|
||||||
builder.maxTokens(budgetTokens + maxOutputTokens);
|
builder.maxTokens(budgetTokens + effectiveMaxOutputTokens());
|
||||||
builder.temperature(1.0);
|
builder.temperature(1.0);
|
||||||
log.info("[ReasoningNode] Anthropic extended thinking enabled: model={}, budget={}", currentModel, budgetTokens);
|
log.info("[ReasoningNode] Anthropic extended thinking enabled: model={}, budget={}", currentModel, budgetTokens);
|
||||||
} else {
|
} else {
|
||||||
builder.maxTokens(maxOutputTokens);
|
builder.maxTokens(effectiveMaxOutputTokens());
|
||||||
if (thinkingOn && !isClaudeModel) {
|
if (thinkingOn && !isClaudeModel) {
|
||||||
log.debug("[ReasoningNode] Anthropic protocol model {} does not support thinking, skipping", currentModel);
|
log.debug("[ReasoningNode] Anthropic protocol model {} does not support thinking, skipping", currentModel);
|
||||||
}
|
}
|
||||||
@ -1253,7 +1274,7 @@ public class ReasoningNode implements NodeAction {
|
|||||||
// DashScope rejects max_tokens above its 8192 ceiling with a 400 that
|
// DashScope rejects max_tokens above its 8192 ceiling with a 400 that
|
||||||
// the failover layer misreads as "model not found"; clamp so a
|
// the failover layer misreads as "model not found"; clamp so a
|
||||||
// DashScope-backed model never overflows the provider limit.
|
// DashScope-backed model never overflows the provider limit.
|
||||||
int effectiveMaxTokens = maxOutputTokens;
|
int effectiveMaxTokens = effectiveMaxOutputTokens();
|
||||||
if (chatModel instanceof com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel
|
if (chatModel instanceof com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel
|
||||||
&& effectiveMaxTokens > DASHSCOPE_MAX_OUTPUT_TOKENS) {
|
&& effectiveMaxTokens > DASHSCOPE_MAX_OUTPUT_TOKENS) {
|
||||||
log.debug("[ReasoningNode] Clamping max_tokens {} -> {} for DashScope-backed model",
|
log.debug("[ReasoningNode] Clamping max_tokens {} -> {} for DashScope-backed model",
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
package vip.mate.agent.graph.node;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import vip.mate.agent.AgentToolSet;
|
||||||
|
import vip.mate.agent.context.PrefixBudgetPlan;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window-aware output-cap clamp: strict local servers (vLLM) statically
|
||||||
|
* reject {@code max_tokens >= max_model_len}, so the cap sent to the
|
||||||
|
* provider must shrink when the known context window is smaller than the
|
||||||
|
* configured / default output cap.
|
||||||
|
*/
|
||||||
|
class ReasoningNodeOutputClampTest {
|
||||||
|
|
||||||
|
private static ReasoningNode nodeWithWindow(Integer windowTokens) {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
ReasoningNode node = new ReasoningNode(null,
|
||||||
|
AgentToolSet.fromCallbacks(List.of(), List.of()), null);
|
||||||
|
if (windowTokens != null) {
|
||||||
|
node.setPrefixBudgetPlan(new PrefixBudgetPlan(
|
||||||
|
true, windowTokens, PrefixBudgetPlan.Profile.COMPACT,
|
||||||
|
0, 0, 0, 0, 0, 0, windowTokens / 4));
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("default 16384 cap on an 8k window clamps to half the window")
|
||||||
|
void defaultCapClampsOnSmallWindow() {
|
||||||
|
// Deprecated ctor leaves maxOutputTokens at the 16384 default.
|
||||||
|
assertEquals(4096, nodeWithWindow(8192).effectiveMaxOutputTokens());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("large window leaves the cap untouched")
|
||||||
|
void largeWindowKeepsCap() {
|
||||||
|
assertEquals(16384, nodeWithWindow(128000).effectiveMaxOutputTokens());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("no budget plan (tests / legacy graphs) keeps previous behavior")
|
||||||
|
void noPlanKeepsCap() {
|
||||||
|
assertEquals(16384, nodeWithWindow(null).effectiveMaxOutputTokens());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("tiny window clamps no lower than the 512-token floor")
|
||||||
|
void tinyWindowRespectsFloor() {
|
||||||
|
assertEquals(512, nodeWithWindow(600).effectiveMaxOutputTokens());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user