mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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.extern.slf4j.Slf4j;
|
||||
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.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.ToolResponseMessage;
|
||||
@ -118,6 +119,18 @@ public class ConversationWindowManager {
|
||||
private final MemoryManager memoryManager;
|
||||
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
|
||||
* existing 3-arg constructor callers in tests stay source-compatible.
|
||||
@ -251,7 +264,12 @@ public class ConversationWindowManager {
|
||||
|
||||
int effectiveMax = (maxInputTokens != null && maxInputTokens > 0)
|
||||
? 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 currentMsgTokens = TokenEstimator.estimateTokens(currentUserMessage) + TokenEstimator.PER_MESSAGE_OVERHEAD;
|
||||
|
||||
@ -30,6 +30,9 @@ import vip.mate.config.PrefixBudgetProperties;
|
||||
@EnableConfigurationProperties(PrefixBudgetProperties.class)
|
||||
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 ConversationWindowProperties windowProperties;
|
||||
|
||||
@ -66,10 +69,21 @@ public class PrefixBudgetPlanner {
|
||||
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(
|
||||
true, effectiveMax, profile, injectionBudget,
|
||||
(int) (injectionBudget * shares.getMemory() / sum),
|
||||
(int) (injectionBudget * shares.getWiki() / sum),
|
||||
wikiTokens,
|
||||
(int) (injectionBudget * shares.getSkill() / sum),
|
||||
(int) (injectionBudget * shares.getExtensionCatalog() / sum),
|
||||
(int) (injectionBudget * shares.getLedger() / sum),
|
||||
|
||||
@ -749,6 +749,27 @@ public class ReasoningNode implements NodeAction {
|
||||
|
||||
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 调用区域 =======
|
||||
// nextLlmCallCount 在首次 streamCall 之前计算。
|
||||
// 所有退出路径(正常、stopped、fatal error、CancellationException)都必须写回此值。
|
||||
@ -778,7 +799,19 @@ public class ReasoningNode implements NodeAction {
|
||||
|
||||
NodeStreamingChatHelper.StreamResult result;
|
||||
try {
|
||||
result = streamingHelper.streamCall(chatModel, prompt, conversationId, "reasoning");
|
||||
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");
|
||||
}
|
||||
|
||||
// PTL 处理:结构化压缩后重试。复用 nonHistoryPrefix 保证重试
|
||||
// Prompt 仍带 wiki / runtime context;早期的 tail-only 路径会把
|
||||
|
||||
@ -44,11 +44,35 @@ class PrefixBudgetPlannerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4k window → MINIMAL profile")
|
||||
@DisplayName("4k window → MINIMAL profile, wiki injection disabled outright")
|
||||
void minimalProfile() {
|
||||
PrefixBudgetPlan plan = planner.plan(4096, 500, 500);
|
||||
assertEquals(PrefixBudgetPlan.Profile.MINIMAL, plan.profile());
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user