mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +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.
|
||||
boolean supportsReasoningEffort = primaryModelConfig != null
|
||||
&& 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,
|
||||
supportsReasoningEffort,
|
||||
streamingHelper, conversationWindowManager, streamTracker, 0, wikiContextService,
|
||||
streamingHelper, conversationWindowManager, streamTracker,
|
||||
configuredMaxOutputTokens, wikiContextService,
|
||||
skillCatalogRenderer, toolDisclosureService, progressLedgerService);
|
||||
reasoningNode.setPrefixBudgetPlan(prefixBudgetPlan);
|
||||
reasoningNode.setAutoDemotedTools(autoDemotedTools);
|
||||
|
||||
@ -376,6 +376,27 @@ public class ReasoningNode implements NodeAction {
|
||||
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,
|
||||
NodeStreamingChatHelper streamingHelper,
|
||||
ConversationWindowManager conversationWindowManager,
|
||||
@ -1234,11 +1255,11 @@ public class ReasoningNode implements NodeAction {
|
||||
default -> 16384;
|
||||
};
|
||||
builder.thinking(org.springframework.ai.anthropic.api.AnthropicApi.ThinkingType.ENABLED, budgetTokens);
|
||||
builder.maxTokens(budgetTokens + maxOutputTokens);
|
||||
builder.maxTokens(budgetTokens + effectiveMaxOutputTokens());
|
||||
builder.temperature(1.0);
|
||||
log.info("[ReasoningNode] Anthropic extended thinking enabled: model={}, budget={}", currentModel, budgetTokens);
|
||||
} else {
|
||||
builder.maxTokens(maxOutputTokens);
|
||||
builder.maxTokens(effectiveMaxOutputTokens());
|
||||
if (thinkingOn && !isClaudeModel) {
|
||||
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
|
||||
// the failover layer misreads as "model not found"; clamp so a
|
||||
// DashScope-backed model never overflows the provider limit.
|
||||
int effectiveMaxTokens = maxOutputTokens;
|
||||
int effectiveMaxTokens = effectiveMaxOutputTokens();
|
||||
if (chatModel instanceof com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel
|
||||
&& effectiveMaxTokens > DASHSCOPE_MAX_OUTPUT_TOKENS) {
|
||||
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