mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(agent): 小上下文降级档——紧凑/底线档收紧注入、压缩触发比自适应、prefix 分块统计与预超限快速失败
This commit is contained in:
parent
bf0d64e46a
commit
36d1f1027d
@ -3,6 +3,7 @@ package vip.mate.agent.context;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.ai.chat.messages.Message;
|
import org.springframework.ai.chat.messages.Message;
|
||||||
import org.springframework.ai.chat.messages.SystemMessage;
|
import org.springframework.ai.chat.messages.SystemMessage;
|
||||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||||
@ -118,6 +119,18 @@ public class ConversationWindowManager {
|
|||||||
private final MemoryManager memoryManager;
|
private final MemoryManager memoryManager;
|
||||||
private final ConversationService conversationService;
|
private final ConversationService conversationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional — adaptive compaction trigger for small context windows.
|
||||||
|
* Setter-injected so the many direct test constructions keep the plain
|
||||||
|
* configured ratio (null → previous behavior).
|
||||||
|
*/
|
||||||
|
private PrefixBudgetPlanner prefixBudgetPlanner;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
public void setPrefixBudgetPlanner(PrefixBudgetPlanner prefixBudgetPlanner) {
|
||||||
|
this.prefixBudgetPlanner = prefixBudgetPlanner;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional spill store, injected via setter so unit tests and the two
|
* Optional spill store, injected via setter so unit tests and the two
|
||||||
* existing 3-arg constructor callers in tests stay source-compatible.
|
* existing 3-arg constructor callers in tests stay source-compatible.
|
||||||
@ -251,7 +264,12 @@ public class ConversationWindowManager {
|
|||||||
|
|
||||||
int effectiveMax = (maxInputTokens != null && maxInputTokens > 0)
|
int effectiveMax = (maxInputTokens != null && maxInputTokens > 0)
|
||||||
? maxInputTokens : properties.getDefaultMaxInputTokens();
|
? maxInputTokens : properties.getDefaultMaxInputTokens();
|
||||||
int triggerThreshold = (int) (effectiveMax * properties.getCompactTriggerRatio());
|
// Small windows compact later (higher trigger ratio): summarizing at
|
||||||
|
// 75% of an 8k window throws away room it cannot afford to lose.
|
||||||
|
double triggerRatio = prefixBudgetPlanner != null
|
||||||
|
? prefixBudgetPlanner.compactTriggerRatioFor(effectiveMax, properties.getCompactTriggerRatio())
|
||||||
|
: properties.getCompactTriggerRatio();
|
||||||
|
int triggerThreshold = (int) (effectiveMax * triggerRatio);
|
||||||
|
|
||||||
int systemTokens = TokenEstimator.estimateTokens(systemPrompt);
|
int systemTokens = TokenEstimator.estimateTokens(systemPrompt);
|
||||||
int currentMsgTokens = TokenEstimator.estimateTokens(currentUserMessage) + TokenEstimator.PER_MESSAGE_OVERHEAD;
|
int currentMsgTokens = TokenEstimator.estimateTokens(currentUserMessage) + TokenEstimator.PER_MESSAGE_OVERHEAD;
|
||||||
|
|||||||
@ -30,6 +30,9 @@ import vip.mate.config.PrefixBudgetProperties;
|
|||||||
@EnableConfigurationProperties(PrefixBudgetProperties.class)
|
@EnableConfigurationProperties(PrefixBudgetProperties.class)
|
||||||
public class PrefixBudgetPlanner {
|
public class PrefixBudgetPlanner {
|
||||||
|
|
||||||
|
/** COMPACT-profile ceiling for the wiki relevance injection (~one page). */
|
||||||
|
static final int COMPACT_WIKI_TOKEN_CAP = 2000;
|
||||||
|
|
||||||
private final PrefixBudgetProperties properties;
|
private final PrefixBudgetProperties properties;
|
||||||
private final ConversationWindowProperties windowProperties;
|
private final ConversationWindowProperties windowProperties;
|
||||||
|
|
||||||
@ -66,10 +69,21 @@ public class PrefixBudgetPlanner {
|
|||||||
sum = 1.0;
|
sum = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Profile-specific wiki clamps: knowledge-base reference pages are the
|
||||||
|
// most dispensable block on a small window — the wiki tools stay
|
||||||
|
// callable, only the automatic pre-injection shrinks. COMPACT caps it
|
||||||
|
// at roughly one page; MINIMAL disables it outright.
|
||||||
|
int wikiTokens = (int) (injectionBudget * shares.getWiki() / sum);
|
||||||
|
wikiTokens = switch (profile) {
|
||||||
|
case NORMAL -> wikiTokens;
|
||||||
|
case COMPACT -> Math.min(wikiTokens, COMPACT_WIKI_TOKEN_CAP);
|
||||||
|
case MINIMAL -> 0;
|
||||||
|
};
|
||||||
|
|
||||||
PrefixBudgetPlan plan = new PrefixBudgetPlan(
|
PrefixBudgetPlan plan = new PrefixBudgetPlan(
|
||||||
true, effectiveMax, profile, injectionBudget,
|
true, effectiveMax, profile, injectionBudget,
|
||||||
(int) (injectionBudget * shares.getMemory() / sum),
|
(int) (injectionBudget * shares.getMemory() / sum),
|
||||||
(int) (injectionBudget * shares.getWiki() / sum),
|
wikiTokens,
|
||||||
(int) (injectionBudget * shares.getSkill() / sum),
|
(int) (injectionBudget * shares.getSkill() / sum),
|
||||||
(int) (injectionBudget * shares.getExtensionCatalog() / sum),
|
(int) (injectionBudget * shares.getExtensionCatalog() / sum),
|
||||||
(int) (injectionBudget * shares.getLedger() / sum),
|
(int) (injectionBudget * shares.getLedger() / sum),
|
||||||
|
|||||||
@ -749,6 +749,27 @@ public class ReasoningNode implements NodeAction {
|
|||||||
|
|
||||||
Prompt prompt = new Prompt(promptMessages, options);
|
Prompt prompt = new Prompt(promptMessages, options);
|
||||||
|
|
||||||
|
// Prefix accounting: how much of the window the never-trimmed prefix
|
||||||
|
// (system prompt + runtime context + wiki + skill catalog + ledger)
|
||||||
|
// and the advertised tool schemas consume. Logged on the turn's first
|
||||||
|
// call so a small-window overflow is diagnosable per block instead of
|
||||||
|
// surfacing as an opaque provider 400.
|
||||||
|
int prefixEstimateTokens = TokenEstimator.estimateTokens(nonHistoryPrefix);
|
||||||
|
int toolSchemaEstimateTokens = TokenEstimator.estimateToolsTokens(activeCallbacks);
|
||||||
|
if (accessor.llmCallCount() == 0) {
|
||||||
|
log.info("[ReasoningNode] Prefix accounting conv={}: window={} tokens, prefix={} "
|
||||||
|
+ "(system+context+wiki+skills+ledger), toolSchemas={}, history={}",
|
||||||
|
conversationId, loopContextWindowTokens(), prefixEstimateTokens,
|
||||||
|
toolSchemaEstimateTokens, TokenEstimator.estimateTokens(messages));
|
||||||
|
}
|
||||||
|
// The prefix cannot be compacted (history compaction is the only lever),
|
||||||
|
// so a prefix that alone exceeds the window makes the request doomed —
|
||||||
|
// fail fast with the same PROMPT_TOO_LONG shape a provider rejection
|
||||||
|
// would produce instead of sending it. Gated on budgeting being active
|
||||||
|
// (an estimation false-positive must not block requests otherwise).
|
||||||
|
boolean prefixOverflow = prefixBudgetPlan != null && prefixBudgetPlan.enabled()
|
||||||
|
&& prefixEstimateTokens + toolSchemaEstimateTokens > prefixBudgetPlan.effectiveMaxTokens();
|
||||||
|
|
||||||
// ======= LLM 调用区域 =======
|
// ======= LLM 调用区域 =======
|
||||||
// nextLlmCallCount 在首次 streamCall 之前计算。
|
// nextLlmCallCount 在首次 streamCall 之前计算。
|
||||||
// 所有退出路径(正常、stopped、fatal error、CancellationException)都必须写回此值。
|
// 所有退出路径(正常、stopped、fatal error、CancellationException)都必须写回此值。
|
||||||
@ -778,7 +799,19 @@ public class ReasoningNode implements NodeAction {
|
|||||||
|
|
||||||
NodeStreamingChatHelper.StreamResult result;
|
NodeStreamingChatHelper.StreamResult result;
|
||||||
try {
|
try {
|
||||||
|
if (prefixOverflow) {
|
||||||
|
String overflowMessage = "Prompt 前缀估算 " + (prefixEstimateTokens + toolSchemaEstimateTokens)
|
||||||
|
+ " tokens(注入块 " + prefixEstimateTokens + " + 工具 schema " + toolSchemaEstimateTokens
|
||||||
|
+ ")已超过模型上下文窗口 " + prefixBudgetPlan.effectiveMaxTokens()
|
||||||
|
+ " tokens,历史压缩无法解决——请精简 Agent 身份 prompt、减少绑定工具/技能,"
|
||||||
|
+ "或换用更大窗口的模型";
|
||||||
|
log.error("[ReasoningNode] {}", overflowMessage);
|
||||||
|
result = new NodeStreamingChatHelper.StreamResult(null, null, null, List.of(), false,
|
||||||
|
0, 0, false, overflowMessage,
|
||||||
|
NodeStreamingChatHelper.ErrorType.PROMPT_TOO_LONG, false, 0, 0, 0);
|
||||||
|
} else {
|
||||||
result = streamingHelper.streamCall(chatModel, prompt, conversationId, "reasoning");
|
result = streamingHelper.streamCall(chatModel, prompt, conversationId, "reasoning");
|
||||||
|
}
|
||||||
|
|
||||||
// PTL 处理:结构化压缩后重试。复用 nonHistoryPrefix 保证重试
|
// PTL 处理:结构化压缩后重试。复用 nonHistoryPrefix 保证重试
|
||||||
// Prompt 仍带 wiki / runtime context;早期的 tail-only 路径会把
|
// Prompt 仍带 wiki / runtime context;早期的 tail-only 路径会把
|
||||||
|
|||||||
@ -44,11 +44,35 @@ class PrefixBudgetPlannerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("4k window → MINIMAL profile")
|
@DisplayName("4k window → MINIMAL profile, wiki injection disabled outright")
|
||||||
void minimalProfile() {
|
void minimalProfile() {
|
||||||
PrefixBudgetPlan plan = planner.plan(4096, 500, 500);
|
PrefixBudgetPlan plan = planner.plan(4096, 500, 500);
|
||||||
assertEquals(PrefixBudgetPlan.Profile.MINIMAL, plan.profile());
|
assertEquals(PrefixBudgetPlan.Profile.MINIMAL, plan.profile());
|
||||||
assertEquals(Math.max(0, (int) (4096 * 0.15) - 1000), plan.injectionBudgetTokens());
|
assertEquals(Math.max(0, (int) (4096 * 0.15) - 1000), plan.injectionBudgetTokens());
|
||||||
|
assertEquals(0, plan.wikiTokens());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("COMPACT profile caps the wiki injection at roughly one page")
|
||||||
|
void compactCapsWiki() {
|
||||||
|
properties.getShares().setWiki(1.0);
|
||||||
|
properties.getShares().setMemory(0.0);
|
||||||
|
properties.getShares().setSkill(0.0);
|
||||||
|
properties.getShares().setExtensionCatalog(0.0);
|
||||||
|
properties.getShares().setLedger(0.0);
|
||||||
|
PrefixBudgetPlan plan = planner.plan(20000, 0, 0);
|
||||||
|
assertEquals(PrefixBudgetPlan.Profile.COMPACT, plan.profile());
|
||||||
|
assertTrue(plan.injectionBudgetTokens() > PrefixBudgetPlanner.COMPACT_WIKI_TOKEN_CAP);
|
||||||
|
assertEquals(PrefixBudgetPlanner.COMPACT_WIKI_TOKEN_CAP, plan.wikiTokens());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("plan carries an independent tool-schema budget from toolSchemaRatio")
|
||||||
|
void toolSchemaBudget() {
|
||||||
|
PrefixBudgetPlan plan = planner.plan(16384, 0, 0);
|
||||||
|
assertEquals((int) (16384 * 0.25), plan.toolSchemaBudgetTokens());
|
||||||
|
properties.setEnabled(false);
|
||||||
|
assertEquals(Integer.MAX_VALUE, planner.plan(16384, 0, 0).toolSchemaBudgetTokens());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user