feat: make context compression policy configurable and stabilize compression UX

This commit is contained in:
JOJO 2026-04-06 16:51:43 +08:00
parent 7e03e3cd5f
commit bc939a0098
8 changed files with 450 additions and 14 deletions

View File

@ -23,6 +23,19 @@ MAX_CONSIDERATION_ITEMS = 10
TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"]
THINKING_INTERVAL_MIN = 1
THINKING_INTERVAL_MAX = 50
DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS = 80_000
DEFAULT_SHALLOW_COMPRESS_KEEP_RECENT_TOOLS = 15
DEFAULT_SHALLOW_COMPRESS_MAX_REPLACE_PER_ROUND = 10
DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOOL_CALLS_INTERVAL = 10
DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS = 150_000
MIN_COMPRESSION_TRIGGER_TOKENS = 1_000
MAX_COMPRESSION_TRIGGER_TOKENS = 2_000_000
MIN_SHALLOW_KEEP_RECENT_TOOLS = 0
MAX_SHALLOW_KEEP_RECENT_TOOLS = 500
MIN_SHALLOW_MAX_REPLACE_PER_ROUND = 1
MAX_SHALLOW_MAX_REPLACE_PER_ROUND = 200
MIN_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL = 1
MAX_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL = 200
DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"enabled": False,
@ -44,6 +57,11 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"image_compression": "original", # original / 1080p / 720p / 540p
"auto_shallow_compress_enabled": False,
"auto_deep_compress_enabled": False,
"shallow_compress_trigger_tokens": None,
"shallow_compress_keep_recent_tools": None,
"shallow_compress_max_replace_per_round": None,
"shallow_compress_trigger_tool_calls_interval": None,
"deep_compress_trigger_tokens": None,
"silent_tool_disable": False, # 禁用工具时不向模型插入提示
"enhanced_tool_display": True, # 增强工具显示
}
@ -58,6 +76,8 @@ __all__ = [
"ensure_personalization_config",
"build_personalization_prompt",
"sanitize_personalization_payload",
"resolve_context_compression_settings",
"validate_context_compression_settings",
]
@ -198,6 +218,71 @@ def sanitize_personalization_payload(
else:
base["auto_deep_compress_enabled"] = bool(base.get("auto_deep_compress_enabled", False))
if "shallow_compress_trigger_tokens" in data:
base["shallow_compress_trigger_tokens"] = _sanitize_optional_int(
data.get("shallow_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
)
else:
base["shallow_compress_trigger_tokens"] = _sanitize_optional_int(
base.get("shallow_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
)
if "shallow_compress_keep_recent_tools" in data:
base["shallow_compress_keep_recent_tools"] = _sanitize_optional_int(
data.get("shallow_compress_keep_recent_tools"),
min_value=MIN_SHALLOW_KEEP_RECENT_TOOLS,
max_value=MAX_SHALLOW_KEEP_RECENT_TOOLS,
)
else:
base["shallow_compress_keep_recent_tools"] = _sanitize_optional_int(
base.get("shallow_compress_keep_recent_tools"),
min_value=MIN_SHALLOW_KEEP_RECENT_TOOLS,
max_value=MAX_SHALLOW_KEEP_RECENT_TOOLS,
)
if "shallow_compress_max_replace_per_round" in data:
base["shallow_compress_max_replace_per_round"] = _sanitize_optional_int(
data.get("shallow_compress_max_replace_per_round"),
min_value=MIN_SHALLOW_MAX_REPLACE_PER_ROUND,
max_value=MAX_SHALLOW_MAX_REPLACE_PER_ROUND,
)
else:
base["shallow_compress_max_replace_per_round"] = _sanitize_optional_int(
base.get("shallow_compress_max_replace_per_round"),
min_value=MIN_SHALLOW_MAX_REPLACE_PER_ROUND,
max_value=MAX_SHALLOW_MAX_REPLACE_PER_ROUND,
)
if "deep_compress_trigger_tokens" in data:
base["deep_compress_trigger_tokens"] = _sanitize_optional_int(
data.get("deep_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
)
else:
base["deep_compress_trigger_tokens"] = _sanitize_optional_int(
base.get("deep_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
)
if "shallow_compress_trigger_tool_calls_interval" in data:
base["shallow_compress_trigger_tool_calls_interval"] = _sanitize_optional_int(
data.get("shallow_compress_trigger_tool_calls_interval"),
min_value=MIN_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL,
max_value=MAX_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL,
)
else:
base["shallow_compress_trigger_tool_calls_interval"] = _sanitize_optional_int(
base.get("shallow_compress_trigger_tool_calls_interval"),
min_value=MIN_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL,
max_value=MAX_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL,
)
# 静默禁用工具提示
if "silent_tool_disable" in data:
base["silent_tool_disable"] = bool(data.get("silent_tool_disable"))
@ -242,6 +327,7 @@ def save_personalization_config(base_dir: PathLike, payload: Dict[str, Any]) ->
"""Persist sanitized personalization config and return it."""
existing = load_personalization_config(base_dir)
config = sanitize_personalization_payload(payload, fallback=existing)
validate_context_compression_settings(config)
path = _to_path(base_dir)
_ensure_parent(path)
with open(path, "w", encoding="utf-8") as f:
@ -249,6 +335,78 @@ def save_personalization_config(base_dir: PathLike, payload: Dict[str, Any]) ->
return config
def _sanitize_optional_int(value: Any, *, min_value: int, max_value: int) -> Optional[int]:
if value is None or value == "":
return None
if isinstance(value, bool):
return None
try:
parsed = int(value)
except (TypeError, ValueError):
return None
if parsed < min_value:
return min_value
if parsed > max_value:
return max_value
return parsed
def resolve_context_compression_settings(config: Optional[Dict[str, Any]]) -> Dict[str, int]:
data = config or {}
shallow_trigger = _sanitize_optional_int(
data.get("shallow_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
) or DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS
shallow_keep_recent = _sanitize_optional_int(
data.get("shallow_compress_keep_recent_tools"),
min_value=MIN_SHALLOW_KEEP_RECENT_TOOLS,
max_value=MAX_SHALLOW_KEEP_RECENT_TOOLS,
)
if shallow_keep_recent is None:
shallow_keep_recent = DEFAULT_SHALLOW_COMPRESS_KEEP_RECENT_TOOLS
shallow_max_replace = _sanitize_optional_int(
data.get("shallow_compress_max_replace_per_round"),
min_value=MIN_SHALLOW_MAX_REPLACE_PER_ROUND,
max_value=MAX_SHALLOW_MAX_REPLACE_PER_ROUND,
) or DEFAULT_SHALLOW_COMPRESS_MAX_REPLACE_PER_ROUND
shallow_trigger_interval = _sanitize_optional_int(
data.get("shallow_compress_trigger_tool_calls_interval"),
min_value=MIN_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL,
max_value=MAX_SHALLOW_TRIGGER_TOOL_CALLS_INTERVAL,
) or DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOOL_CALLS_INTERVAL
deep_trigger = _sanitize_optional_int(
data.get("deep_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
) or DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS
if deep_trigger <= shallow_trigger:
deep_trigger = shallow_trigger + 1
return {
"shallow_trigger_tokens": shallow_trigger,
"shallow_keep_recent_tools": shallow_keep_recent,
"shallow_max_replace_per_round": shallow_max_replace,
"shallow_trigger_tool_calls_interval": shallow_trigger_interval,
"deep_trigger_tokens": deep_trigger,
}
def validate_context_compression_settings(config: Optional[Dict[str, Any]]) -> None:
data = config or {}
shallow_trigger = _sanitize_optional_int(
data.get("shallow_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
) or DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS
deep_trigger = _sanitize_optional_int(
data.get("deep_compress_trigger_tokens"),
min_value=MIN_COMPRESSION_TRIGGER_TOKENS,
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
) or DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS
if deep_trigger <= shallow_trigger:
raise ValueError("深压缩触发上下文必须大于浅压缩触发上下文")
def build_personalization_prompt(
config: Optional[Dict[str, Any]],
include_header: bool = True

View File

@ -38,6 +38,7 @@ from modules.personalization_manager import (
save_personalization_config,
THINKING_INTERVAL_MIN,
THINKING_INTERVAL_MAX,
resolve_context_compression_settings,
)
from modules.skill_hint_manager import SkillHintManager
from modules.upload_security import UploadSecurityError
@ -566,11 +567,12 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
personal_config = load_personalization_config(workspace.data_dir)
except Exception:
personal_config = {}
compression_settings = resolve_context_compression_settings(personal_config)
auto_deep_enabled = bool(personal_config.get("auto_deep_compress_enabled", False))
current_tokens_for_deep = web_terminal.context_manager.get_current_context_tokens(conversation_id)
if (
auto_deep_enabled
and current_tokens_for_deep > 150_000
and current_tokens_for_deep > compression_settings["deep_trigger_tokens"]
and not web_terminal.context_manager.is_compression_in_progress()
):
web_terminal.context_manager._set_meta_flag("is_ultra_long_conversation", True)

View File

@ -12,8 +12,9 @@ from .security import compact_web_search_result
from .chat_flow_helpers import detect_tool_failure
from .chat_flow_runner_helpers import resolve_monitor_path, resolve_monitor_memory, capture_monitor_snapshot
from utils.tool_result_formatter import format_tool_result_for_context
from utils.context_manager import AUTO_SHALLOW_PLACEHOLDER
from config import TOOL_CALL_COOLDOWN
from modules.personalization_manager import load_personalization_config
from modules.personalization_manager import load_personalization_config, resolve_context_compression_settings
from .deep_compression import run_deep_compression
@ -457,12 +458,21 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
personal_config = load_personalization_config(workspace_data_dir) if workspace_data_dir else {}
except Exception:
personal_config = {}
compression_settings = resolve_context_compression_settings(personal_config)
auto_shallow_enabled = bool(personal_config.get("auto_shallow_compress_enabled", False))
auto_deep_enabled = bool(personal_config.get("auto_deep_compress_enabled", False))
compressed_count = 0
try:
compressed_count = int(
web_terminal.context_manager.on_tool_call_finished(function_name, enable_shallow=auto_shallow_enabled) or 0
web_terminal.context_manager.on_tool_call_finished(
function_name,
enable_shallow=auto_shallow_enabled,
shallow_trigger_tokens=compression_settings["shallow_trigger_tokens"],
deep_trigger_tokens=compression_settings["deep_trigger_tokens"],
shallow_batch_size=compression_settings["shallow_max_replace_per_round"],
shallow_keep_recent_tools=compression_settings["shallow_keep_recent_tools"],
shallow_trigger_tool_calls_interval=compression_settings["shallow_trigger_tool_calls_interval"],
) or 0
)
except Exception as exc:
debug_log(f"[ContextCompression] 工具后浅压缩检测失败: {exc}")
@ -470,8 +480,34 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
sender('shallow_compression', {
"conversation_id": conversation_id,
"compressed_count": compressed_count,
"keep_recent_tools": 15,
"keep_recent_tools": compression_settings["shallow_keep_recent_tools"],
})
# 关键修复同一轮工具循环里API 继续调用使用的是本地 messages不是每次重建上下文
# 因此需要把已打标的旧 tool 结果同步替换为占位符,避免日志看起来“弹窗触发但请求未替换”。
if auto_shallow_enabled and messages:
try:
marked_keys = set()
for item in (web_terminal.context_manager.conversation_history or []):
if not isinstance(item, dict) or item.get("role") != "tool":
continue
item_meta = item.get("metadata") or {}
if not item_meta.get("auto_shallow_compacted"):
continue
marked_keys.add((str(item.get("tool_call_id") or ""), str(item.get("name") or "")))
if marked_keys:
replaced_in_loop = 0
for msg in messages:
if not isinstance(msg, dict) or msg.get("role") != "tool":
continue
key = (str(msg.get("tool_call_id") or ""), str(msg.get("name") or ""))
if key in marked_keys and msg.get("content") != AUTO_SHALLOW_PLACEHOLDER:
msg["content"] = AUTO_SHALLOW_PLACEHOLDER
replaced_in_loop += 1
if replaced_in_loop > 0:
debug_log(f"[ContextCompression] 同步替换本轮 messages 中已压缩 tool 结果: {replaced_in_loop}")
except Exception as exc:
debug_log(f"[ContextCompression] 同步替换本轮 messages 失败: {exc}")
debug_log(f"💾 增量保存:工具结果 {function_name}")
if function_name != "wait_sub_agent":
system_message = result_data.get("system_message") if isinstance(result_data, dict) else None
@ -483,7 +519,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
current_context_tokens = web_terminal.context_manager.get_current_context_tokens(conversation_id)
if (
auto_deep_enabled
and current_context_tokens > 150_000
and current_context_tokens > compression_settings["deep_trigger_tokens"]
and not web_terminal.context_manager.is_compression_in_progress()
):
web_terminal.context_manager._set_meta_flag("is_ultra_long_conversation", True)

View File

@ -250,7 +250,7 @@
:tool-settings="toolSettings"
:tool-settings-loading="toolSettingsLoading"
:settings-open="settingsOpen"
:compressing="compressing"
:compressing="compressing || compressionInProgress"
:current-conversation-id="currentConversationId"
:icon-style="iconStyle"
:tool-category-icon="toolCategoryIcon"

View File

@ -51,7 +51,12 @@ export const taskPollingMethods = {
// 检查事件的 conversation_id 是否匹配当前对话
// 如果不匹配,忽略该事件(避免切换对话后旧任务的事件显示到新对话中)
if (eventType !== 'shallow_compression' && eventData.conversation_id && this.currentConversationId) {
const crossConversationAllowed = new Set([
'shallow_compression',
'conversation_resolved',
'compression_finished'
]);
if (!crossConversationAllowed.has(eventType) && eventData.conversation_id && this.currentConversationId) {
if (eventData.conversation_id !== this.currentConversationId) {
console.log(`[DEBUG_AWAITING] 忽略不匹配的事件`, {
eventType,
@ -935,9 +940,19 @@ export const taskPollingMethods = {
if (!data || typeof data !== 'object') {
return;
}
const wasInProgress = !!this.compressionInProgress;
this.compressionInProgress = !!data.in_progress;
this.compressionMode = data.mode || '';
this.compressionStage = data.stage || '';
if (this.compressionInProgress && !wasInProgress) {
const modeLabel = this.compressionMode === 'manual' ? '手动' : '自动';
this.uiPushToast({
title: '压缩中',
message: `对话正在${modeLabel}压缩,请稍候…`,
type: 'info',
duration: 2600
});
}
if (!this.compressionInProgress) {
this.compressionError = data.error || '';
}
@ -952,6 +967,16 @@ export const taskPollingMethods = {
if (newId && newId !== this.currentConversationId) {
await this.loadConversation(newId, { force: true });
}
this.conversationsOffset = 0;
if (typeof this.loadConversationsList === 'function') {
await this.loadConversationsList();
}
this.uiPushToast({
title: '压缩完成',
message: '已自动切换到压缩后的新对话',
type: 'success',
duration: 2400
});
},
handleShallowCompression(data: any, eventIdx?: number) {

View File

@ -522,6 +522,76 @@
</span>
<span>自动深层压缩</span>
</label>
<div class="compression-custom-grid">
<label class="compression-input-row">
<span>浅压缩触发上下文</span>
<input
type="number"
min="1000"
step="1"
placeholder="默认 80000"
:value="form.shallow_compress_trigger_tokens ?? ''"
@input="handleCompressionNumberInput('shallow_compress_trigger_tokens', $event)"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="compression-input-row">
<span>浅压缩保留最近 Tool 条数</span>
<input
type="number"
min="0"
step="1"
placeholder="默认 15"
:value="form.shallow_compress_keep_recent_tools ?? ''"
@input="handleCompressionNumberInput('shallow_compress_keep_recent_tools', $event)"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="compression-input-row">
<span>浅压缩每轮最多替换条数</span>
<input
type="number"
min="1"
step="1"
placeholder="默认 10"
:value="form.shallow_compress_max_replace_per_round ?? ''"
@input="handleCompressionNumberInput('shallow_compress_max_replace_per_round', $event)"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="compression-input-row">
<span>浅压缩触发间隔Tool 调用次数</span>
<input
type="number"
min="1"
step="1"
placeholder="默认 10"
:value="form.shallow_compress_trigger_tool_calls_interval ?? ''"
@input="handleCompressionNumberInput('shallow_compress_trigger_tool_calls_interval', $event)"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="compression-input-row">
<span>深压缩触发上下文</span>
<input
type="number"
min="1000"
step="1"
placeholder="默认 150000"
:value="form.deep_compress_trigger_tokens ?? ''"
@input="handleCompressionNumberInput('deep_compress_trigger_tokens', $event)"
@focus="personalization.clearFeedback()"
/>
</label>
</div>
<div class="compression-actions-row">
<button type="button" class="compression-reset-btn" @click.prevent="restoreCompressionDefaults">
恢复默认
</button>
</div>
<p class="field-desc compression-custom-hint">
自定义项留空则使用默认值深压缩触发上下文必须大于浅压缩触发上下文
</p>
</div>
<div class="behavior-field">
<div class="behavior-field-header">
@ -939,6 +1009,12 @@ const activeTab = ref<PersonalTab>('preferences');
const swipeState = ref<{ startY: number; active: boolean }>({ startY: 0, active: false });
type RunModeValue = 'fast' | 'thinking' | 'deep' | null;
type CompressionField =
| 'shallow_compress_trigger_tokens'
| 'shallow_compress_keep_recent_tools'
| 'shallow_compress_max_replace_per_round'
| 'shallow_compress_trigger_tool_calls_interval'
| 'deep_compress_trigger_tokens';
const runModeOptions: Array<{ id: string; label: string; desc: string; value: RunModeValue; badge?: string }> = [
{ id: 'fast', label: '快速模式', desc: '追求响应速度,跳过思考模型', value: 'fast' },
@ -1289,6 +1365,28 @@ const restoreThinkingInterval = () => {
personalization.setThinkingInterval(null);
};
const handleCompressionNumberInput = (key: CompressionField, event: Event) => {
const target = event.target as HTMLInputElement | null;
if (!target || !target.value) {
personalization.updateField({ key, value: null });
return;
}
const parsed = Number(target.value);
if (!Number.isFinite(parsed)) {
personalization.updateField({ key, value: null });
return;
}
personalization.updateField({ key, value: Math.round(parsed) });
};
const restoreCompressionDefaults = () => {
personalization.updateField({ key: 'shallow_compress_trigger_tokens', value: null });
personalization.updateField({ key: 'shallow_compress_keep_recent_tools', value: null });
personalization.updateField({ key: 'shallow_compress_max_replace_per_round', value: null });
personalization.updateField({ key: 'shallow_compress_trigger_tool_calls_interval', value: null });
personalization.updateField({ key: 'deep_compress_trigger_tokens', value: null });
};
const isPresetActive = (value: number) => {
if (form.value.thinking_interval === null || typeof form.value.thinking_interval === 'undefined') {
return value === thinkingIntervalDefault.value;
@ -1362,6 +1460,55 @@ const applyThemeOption = (theme: ThemeKey) => {
</script>
<style scoped>
.compression-custom-grid {
margin-top: 10px;
display: grid;
gap: 10px;
}
.compression-input-row {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--claude-text-secondary);
font-size: 12px;
}
.compression-input-row input {
border: 1px solid var(--claude-border);
border-radius: 10px;
background: var(--claude-panel);
color: var(--claude-text);
padding: 8px 10px;
font-size: 13px;
}
.compression-custom-hint {
margin-top: 8px;
}
.compression-actions-row {
margin-top: 4px;
display: flex;
justify-content: flex-end;
}
.compression-reset-btn {
border: 1px solid var(--claude-border);
border-radius: 10px;
padding: 6px 12px;
background: var(--claude-panel);
color: var(--claude-text);
font-size: 12px;
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.compression-reset-btn:hover {
transform: translateY(-1px);
box-shadow: 0 8px 18px rgba(61, 57, 41, 0.14);
}
.usage-summary-page {
display: flex;
flex-direction: column;

View File

@ -24,6 +24,11 @@ interface PersonalForm {
image_compression: string;
auto_shallow_compress_enabled: boolean;
auto_deep_compress_enabled: boolean;
shallow_compress_trigger_tokens: number | null;
shallow_compress_keep_recent_tools: number | null;
shallow_compress_max_replace_per_round: number | null;
shallow_compress_trigger_tool_calls_interval: number | null;
deep_compress_trigger_tokens: number | null;
}
interface LiquidGlassPosition {
@ -60,6 +65,8 @@ interface PersonalizationState {
const DEFAULT_INTERVAL = 10;
const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 };
const DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS = 80000;
const DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS = 150000;
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep'];
const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
@ -83,7 +90,12 @@ const defaultForm = (): PersonalForm => ({
default_model: 'kimi-k2.5',
image_compression: 'original',
auto_shallow_compress_enabled: false,
auto_deep_compress_enabled: false
auto_deep_compress_enabled: false,
shallow_compress_trigger_tokens: null,
shallow_compress_keep_recent_tools: null,
shallow_compress_max_replace_per_round: null,
shallow_compress_trigger_tool_calls_interval: null,
deep_compress_trigger_tokens: null
});
const defaultExperimentState = (): ExperimentState => ({
@ -234,10 +246,29 @@ export const usePersonalizationStore = defineStore('personalization', {
default_model: typeof data.default_model === 'string' ? data.default_model : fallbackModel,
image_compression: typeof data.image_compression === 'string' ? data.image_compression : 'original',
auto_shallow_compress_enabled: !!data.auto_shallow_compress_enabled,
auto_deep_compress_enabled: !!data.auto_deep_compress_enabled
auto_deep_compress_enabled: !!data.auto_deep_compress_enabled,
shallow_compress_trigger_tokens: this.normalizeCompressionNumber(data.shallow_compress_trigger_tokens),
shallow_compress_keep_recent_tools: this.normalizeCompressionNumber(data.shallow_compress_keep_recent_tools),
shallow_compress_max_replace_per_round: this.normalizeCompressionNumber(data.shallow_compress_max_replace_per_round),
shallow_compress_trigger_tool_calls_interval: this.normalizeCompressionNumber(data.shallow_compress_trigger_tool_calls_interval),
deep_compress_trigger_tokens: this.normalizeCompressionNumber(data.deep_compress_trigger_tokens)
};
this.clearFeedback();
},
normalizeCompressionNumber(value: any): number | null {
if (value === null || typeof value === 'undefined' || value === '') {
return null;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return null;
}
const rounded = Math.round(parsed);
if (rounded <= 0) {
return null;
}
return rounded;
},
applyPersonalizationMeta(payload: any) {
if (payload && typeof payload.thinking_interval_default === 'number') {
this.thinkingIntervalDefault = payload.thinking_interval_default;
@ -325,6 +356,13 @@ export const usePersonalizationStore = defineStore('personalization', {
this.status = '';
this.error = '';
try {
const shallowTrigger =
this.normalizeCompressionNumber(this.form.shallow_compress_trigger_tokens) ?? DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS;
const deepTrigger =
this.normalizeCompressionNumber(this.form.deep_compress_trigger_tokens) ?? DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS;
if (deepTrigger <= shallowTrigger) {
throw new Error('深压缩触发上下文必须大于浅压缩触发上下文');
}
const resp = await fetch('/api/personalization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@ -544,7 +544,17 @@ class ContextManager:
except Exception as exc:
print(f"[ContextCompression] 更新压缩状态失败: {exc}")
def on_tool_call_finished(self, tool_name: Optional[str] = None, *, enable_shallow: bool = True) -> int:
def on_tool_call_finished(
self,
tool_name: Optional[str] = None,
*,
enable_shallow: bool = True,
shallow_trigger_tokens: int = 80_000,
deep_trigger_tokens: int = 150_000,
shallow_batch_size: int = 10,
shallow_keep_recent_tools: int = 15,
shallow_trigger_tool_calls_interval: int = 10,
) -> int:
"""每次工具调用完成后触发:更新计数并执行自动浅压缩。返回本轮浅压缩条数。"""
if not self.current_conversation_id:
return 0
@ -563,9 +573,22 @@ class ContextManager:
self._set_meta_flag("tool_call_count", tool_count, save=False)
current_tokens = self.get_current_context_tokens(self.current_conversation_id)
if current_tokens > 80_000 and not self._get_meta_flag("is_long_conversation", False):
shallow_trigger_tokens = max(1, int(80_000 if shallow_trigger_tokens is None else shallow_trigger_tokens))
deep_trigger_tokens = max(
shallow_trigger_tokens + 1,
int(150_000 if deep_trigger_tokens is None else deep_trigger_tokens),
)
shallow_batch_size = max(1, int(10 if shallow_batch_size is None else shallow_batch_size))
shallow_keep_recent_tools = max(0, int(15 if shallow_keep_recent_tools is None else shallow_keep_recent_tools))
shallow_trigger_tool_calls_interval = max(
1,
int(10 if shallow_trigger_tool_calls_interval is None else shallow_trigger_tool_calls_interval),
)
was_long_conversation = bool(self._get_meta_flag("is_long_conversation", False))
if current_tokens > shallow_trigger_tokens and not was_long_conversation:
self._set_meta_flag("is_long_conversation", True, save=False)
if current_tokens > 150_000 and not self._get_meta_flag("is_ultra_long_conversation", False):
just_marked_long = (not was_long_conversation) and bool(self._get_meta_flag("is_long_conversation", False))
if current_tokens > deep_trigger_tokens and not self._get_meta_flag("is_ultra_long_conversation", False):
self._set_meta_flag("is_ultra_long_conversation", True, save=False)
# 每 10 次工具调用触发一次浅压缩(当 long 已标记)
@ -573,12 +596,19 @@ class ContextManager:
should_try_shallow = (
bool(enable_shallow)
and bool(self._get_meta_flag("is_long_conversation", False))
and (tool_count - last_checkpoint >= 10)
and (
just_marked_long
or (last_checkpoint <= 0 and tool_count > 0)
or (tool_count - last_checkpoint >= shallow_trigger_tool_calls_interval)
)
)
changed = False
compressed_count = 0
if should_try_shallow:
compressed = self._run_auto_shallow_compression(batch_size=10, keep_recent_tools=15)
compressed = self._run_auto_shallow_compression(
batch_size=shallow_batch_size,
keep_recent_tools=shallow_keep_recent_tools,
)
self._set_meta_flag("last_shallow_compress_tool_count", tool_count, save=False)
compressed_count = max(0, int(compressed or 0))
changed = compressed > 0