feat(config): 压缩算法关键参数暴露到设置页面
将 4 个压缩参数(threshold_ratio、llm_compaction_threshold_ratio、 truncate_max_tokens、preserve_count)从硬编码提取到 config.json 顶层 compaction 节,前端新增 CompactionTab 供用户调整。 后端: - config: 新增 CompactionConfig 结构体,Config 新增 compaction 字段 - context_compressor: 4 个参数内聚到 ContextCompressor 实例字段; 新增 with_compaction_config() 构造函数和 truncate_tool_results() 方法; 对用户配置做防御性 clamp(ratio ∈ [0.1,1.0],整数 ≥ 1) - agent_loop: 调用 compressor.truncate_tool_results() 实现零参数耦合 - agent_factory: 新增 build_compressor() 方法,in-loop 与 sync 兜底 两条压缩路径共用同一套用户配置 - session: with_factories 改用 agent_factory.build_compressor(),修复 sync 压缩路径忽略用户配置的 P0 问题 前端: - types: 新增 CompactionConfig 接口,TabId 新增 'compaction' - 新建 CompactionTab.tsx,4 个 number input 分两组 SectionCard - constants + ConfigPage: 注册"上下文压缩" Tab(Archive 图标) 清理:删除无调用方的 from_provider_config/from_runtime_config/with_config 方法及 DEFAULT_THRESHOLD_RATIO/LLM_COMPACTION_THRESHOLD_RATIO 常量。 测试:context_compressor 18 + session 31 + agent_loop 42 全通过。
This commit is contained in:
parent
fe7037e4ad
commit
7b459b8ca1
@ -1443,17 +1443,15 @@ impl AgentLoop {
|
||||
|
||||
if let Some(prompt_tokens) = last_prompt_tokens {
|
||||
if compressor.should_compress_by_usage(prompt_tokens) {
|
||||
// 阶段 1a:工程化压缩(截断非子代理 tool 结果到 100 token,仅改内存)
|
||||
crate::agent::context_compressor::truncate_tool_results_in_place(
|
||||
&mut messages,
|
||||
100,
|
||||
);
|
||||
// 阶段 1a:工程化压缩(截断非子代理 tool 结果,仅改内存)
|
||||
// 参数内聚到 ContextCompressor,AgentLoop 不持有截断 token 数
|
||||
compressor.truncate_tool_results(&mut messages);
|
||||
engineering_compaction_applied = true;
|
||||
tracing::info!(
|
||||
iteration,
|
||||
prompt_tokens,
|
||||
threshold = compressor.threshold(),
|
||||
"Engineering compaction applied (tool results truncated to 100 tokens)"
|
||||
"Engineering compaction applied (tool results truncated)"
|
||||
);
|
||||
|
||||
// 阶段 1b:重新估算,判断是否需要 LLM 压缩(50% 阈值)
|
||||
|
||||
@ -3,7 +3,7 @@ use crate::bus::{
|
||||
ChatMessage, SYSTEM_CONTEXT_AGENT_PROMPT, SYSTEM_CONTEXT_HISTORY_COMPACTION,
|
||||
SYSTEM_CONTEXT_SCHEDULED_PROMPT,
|
||||
};
|
||||
use crate::config::LLMProviderConfig;
|
||||
use crate::config::{CompactionConfig, LLMProviderConfig};
|
||||
use crate::providers::{ChatCompletionRequest, LLMProvider, Message, create_provider};
|
||||
use crate::text::{char_count, take_prefix_chars};
|
||||
|
||||
@ -17,12 +17,6 @@ pub const SYSTEM_CONTEXT_HISTORY_COMPACTION_OLDER: &str = "history_compaction_ol
|
||||
/// System context marker for the light compression (newer segment) summary.
|
||||
pub const SYSTEM_CONTEXT_HISTORY_COMPACTION_NEWER: &str = "history_compaction_newer";
|
||||
|
||||
/// Default threshold ratio: compress when estimated tokens exceed 70% of context window.
|
||||
const DEFAULT_THRESHOLD_RATIO: f64 = 0.7;
|
||||
|
||||
/// LLM 压缩阈值比例:工程化压缩后仍超过此比例才调 LLM
|
||||
const LLM_COMPACTION_THRESHOLD_RATIO: f64 = 0.5;
|
||||
|
||||
// ============================================================================
|
||||
// HistoryUnit — atomic message units for compression
|
||||
// ============================================================================
|
||||
@ -275,6 +269,12 @@ pub struct ContextCompressor {
|
||||
context_window: usize,
|
||||
/// Threshold ratio to trigger compression (70% of context window).
|
||||
threshold_ratio: f64,
|
||||
/// LLM 压缩阈值比例(工程化压缩后仍超此比例才调 LLM)
|
||||
llm_compaction_threshold_ratio: f64,
|
||||
/// 三段压缩保留 unit 数(最旧 N + 最新 N)
|
||||
preserve_count: usize,
|
||||
/// 工程化压缩时 tool 结果截断 token 数
|
||||
truncate_max_tokens: usize,
|
||||
}
|
||||
|
||||
impl ContextCompressor {
|
||||
@ -655,13 +655,13 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
self.compress_two_segment_inner(history, provider).await
|
||||
}
|
||||
|
||||
/// 三段压缩核心逻辑:保留最旧5 + 最新5 unit,中间段用 LLM 压缩。
|
||||
/// 三段压缩核心逻辑:保留最旧 N + 最新 N unit,中间段用 LLM 压缩。
|
||||
///
|
||||
/// 策略:
|
||||
/// - SystemGuard 永远保留在头部(不计入 5 条配额)
|
||||
/// - SystemGuard 永远保留在头部(不计入 N 条配额)
|
||||
/// - 可压缩单元(UserMessage / AssistantText / ToolRound)按时间顺序:
|
||||
/// - 最旧 PRESERVE_COUNT 个 unit 原样保留
|
||||
/// - 最新 PRESERVE_COUNT 个 unit 原样保留
|
||||
/// - 最旧 preserve_count 个 unit 原样保留
|
||||
/// - 最新 preserve_count 个 unit 原样保留
|
||||
/// - 中间段用 LLM 生成摘要(system 消息,无 tool_calls)
|
||||
/// - ToolRound 原子性由 parse_to_units 保证,切分在 unit 边界
|
||||
/// - 中间段摘要为纯文本 system 消息,符合 API 提交要求
|
||||
@ -670,14 +670,14 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
history: &[ChatMessage],
|
||||
provider: &dyn LLMProvider,
|
||||
) -> Result<Vec<ChatMessage>, AgentError> {
|
||||
const PRESERVE_COUNT: usize = 5;
|
||||
let preserve_count = self.preserve_count;
|
||||
let tokens = estimate_tokens(history);
|
||||
tracing::info!(
|
||||
tokens = tokens,
|
||||
threshold = self.threshold(),
|
||||
msg_count = history.len(),
|
||||
preserve_count = PRESERVE_COUNT,
|
||||
"Starting three-segment compression (preserve oldest 5 + newest 5)"
|
||||
preserve_count = preserve_count,
|
||||
"Starting three-segment compression"
|
||||
);
|
||||
|
||||
let units = parse_to_units(history);
|
||||
@ -693,10 +693,10 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
}
|
||||
|
||||
// Step 2: If compressible units are too few, skip LLM compression
|
||||
if compressible.len() <= PRESERVE_COUNT * 2 {
|
||||
if compressible.len() <= preserve_count * 2 {
|
||||
tracing::info!(
|
||||
compressible_count = compressible.len(),
|
||||
preserve_threshold = PRESERVE_COUNT * 2,
|
||||
preserve_threshold = preserve_count * 2,
|
||||
"Too few compressible units, skipping LLM compaction"
|
||||
);
|
||||
let mut result = system_guards;
|
||||
@ -707,10 +707,10 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
}
|
||||
|
||||
// Step 3: Three-segment split
|
||||
let split = compressible.len() - PRESERVE_COUNT;
|
||||
let oldest_units = &compressible[..PRESERVE_COUNT];
|
||||
let split = compressible.len() - preserve_count;
|
||||
let oldest_units = &compressible[..preserve_count];
|
||||
let newest_units = &compressible[split..];
|
||||
let middle_units = &compressible[PRESERVE_COUNT..split];
|
||||
let middle_units = &compressible[preserve_count..split];
|
||||
|
||||
// Step 4: Build middle segment messages and transcript
|
||||
let middle_messages: Vec<ChatMessage> = middle_units
|
||||
@ -743,7 +743,7 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
// System guards first
|
||||
compressed.extend(system_guards);
|
||||
|
||||
// Oldest PRESERVE_COUNT units (raw)
|
||||
// Oldest preserve_count units (raw)
|
||||
for unit in oldest_units {
|
||||
compressed.extend(unit_to_messages(unit));
|
||||
}
|
||||
@ -756,7 +756,7 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
));
|
||||
}
|
||||
|
||||
// Newest PRESERVE_COUNT units (raw)
|
||||
// Newest preserve_count units (raw)
|
||||
for unit in newest_units {
|
||||
compressed.extend(unit_to_messages(unit));
|
||||
}
|
||||
@ -766,8 +766,8 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
original_msg_count = history.len(),
|
||||
final_tokens = estimate_tokens(&compressed),
|
||||
final_msg_count = compressed.len(),
|
||||
oldest_units = PRESERVE_COUNT,
|
||||
newest_units = PRESERVE_COUNT,
|
||||
oldest_units = preserve_count,
|
||||
newest_units = preserve_count,
|
||||
middle_units = middle_units.len(),
|
||||
"Three-segment compression completed"
|
||||
);
|
||||
@ -780,34 +780,45 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
// =========================================================================
|
||||
|
||||
/// Create a new compressor with the given context window size.
|
||||
/// 测试 fallback 路径:所有压缩参数用 CompactionConfig::default()。
|
||||
pub fn new(context_window: usize) -> Self {
|
||||
let default = CompactionConfig::default();
|
||||
Self {
|
||||
config: ContextCompressionConfig::default(),
|
||||
context_window,
|
||||
threshold_ratio: DEFAULT_THRESHOLD_RATIO,
|
||||
threshold_ratio: default.threshold_ratio,
|
||||
llm_compaction_threshold_ratio: default.llm_compaction_threshold_ratio,
|
||||
preserve_count: default.preserve_count,
|
||||
truncate_max_tokens: default.truncate_max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_provider_config(provider_config: &LLMProviderConfig) -> Self {
|
||||
Self::from_runtime_config(&AgentRuntimeConfig::from(provider_config.clone()))
|
||||
}
|
||||
|
||||
pub fn from_runtime_config(config: &AgentRuntimeConfig) -> Self {
|
||||
Self::with_config(
|
||||
config.context_window_tokens,
|
||||
ContextCompressionConfig {
|
||||
summary_max_chars: config.context_summary_char_budget,
|
||||
/// 从 runtime config + compaction config 构造(AgentFactory 生产路径调用)。
|
||||
/// 所有压缩参数内聚到 ContextCompressor,AgentLoop 零参数耦合。
|
||||
/// 对用户配置做防御性 clamp,避免极端值破坏压缩逻辑:
|
||||
/// - ratio 限制在 [0.1, 1.0]:过低导致每轮压缩,过高导致永不压缩
|
||||
/// - truncate_max_tokens 限制 >= 1:0 会把所有 tool 结果截断成空串
|
||||
/// - preserve_count 限制 >= 1:0 会丢失全部原始上下文
|
||||
pub fn with_compaction_config(
|
||||
context_window: usize,
|
||||
summary_max_chars: usize,
|
||||
compaction: &CompactionConfig,
|
||||
) -> Self {
|
||||
let clamp_ratio = |r: f64| r.clamp(0.1, 1.0);
|
||||
let threshold_ratio = clamp_ratio(compaction.threshold_ratio);
|
||||
let llm_compaction_threshold_ratio = clamp_ratio(compaction.llm_compaction_threshold_ratio);
|
||||
let preserve_count = compaction.preserve_count.max(1);
|
||||
let truncate_max_tokens = compaction.truncate_max_tokens.max(1);
|
||||
Self {
|
||||
config: ContextCompressionConfig {
|
||||
summary_max_chars,
|
||||
..ContextCompressionConfig::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Create with custom configuration.
|
||||
pub fn with_config(context_window: usize, config: ContextCompressionConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
context_window,
|
||||
threshold_ratio: DEFAULT_THRESHOLD_RATIO,
|
||||
threshold_ratio,
|
||||
llm_compaction_threshold_ratio,
|
||||
preserve_count,
|
||||
truncate_max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@ -827,7 +838,13 @@ OLDER SEGMENT (events from earlier in the session):
|
||||
|
||||
/// LLM 压缩阈值(50%):工程化压缩后用 estimate_tokens 判断是否需要 LLM 压缩。
|
||||
pub fn llm_compaction_threshold(&self) -> usize {
|
||||
(self.context_window as f64 * LLM_COMPACTION_THRESHOLD_RATIO) as usize
|
||||
(self.context_window as f64 * self.llm_compaction_threshold_ratio) as usize
|
||||
}
|
||||
|
||||
/// 工程化压缩:截断非子代理 tool 结果到 self.truncate_max_tokens。
|
||||
/// AgentLoop 调用此方法,不需要知道截断参数细节(参数内聚)。
|
||||
pub fn truncate_tool_results(&self, messages: &mut [ChatMessage]) {
|
||||
truncate_tool_results_in_place(messages, self.truncate_max_tokens);
|
||||
}
|
||||
|
||||
fn user_turn_ranges(&self, history: &[ChatMessage]) -> Vec<UserTurnRange> {
|
||||
|
||||
@ -81,6 +81,7 @@ impl InitWizard {
|
||||
image_context: crate::config::ImageContextConfig::default(),
|
||||
subagents: crate::config::SubagentsConfig::default(),
|
||||
experts: crate::config::ExpertsConfig::default(),
|
||||
compaction: crate::config::CompactionConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -845,6 +846,7 @@ impl InitWizard {
|
||||
image_context: existing.image_context.clone(),
|
||||
subagents: existing.subagents.clone(),
|
||||
experts: existing.experts.clone(),
|
||||
compaction: existing.compaction.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,8 @@ pub struct Config {
|
||||
pub subagents: SubagentsConfig,
|
||||
#[serde(default)]
|
||||
pub experts: ExpertsConfig,
|
||||
#[serde(default)]
|
||||
pub compaction: CompactionConfig,
|
||||
}
|
||||
|
||||
/// 图片上下文限制配置
|
||||
@ -72,6 +74,54 @@ impl Default for ImageContextConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// 上下文压缩算法配置(全局,所有 agent 共享)
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CompactionConfig {
|
||||
/// 工程化压缩触发阈值(占 context_window 的比例,0.0-1.0)
|
||||
/// 当 LLM 返回的 prompt_tokens 超过 context_window × 此比例时,触发压缩流程
|
||||
#[serde(default = "default_threshold_ratio")]
|
||||
pub threshold_ratio: f64,
|
||||
|
||||
/// LLM 压缩触发阈值(占 context_window 的比例,0.0-1.0)
|
||||
/// 工程化压缩(截断 tool 结果)后,若估算 token 仍超过此比例才调 LLM 压缩
|
||||
#[serde(default = "default_llm_compaction_threshold_ratio")]
|
||||
pub llm_compaction_threshold_ratio: f64,
|
||||
|
||||
/// 工程化压缩时 tool 结果截断到的 token 数
|
||||
/// 子代理返回(tool_name="task")不受此限制
|
||||
#[serde(default = "default_truncate_max_tokens")]
|
||||
pub truncate_max_tokens: usize,
|
||||
|
||||
/// LLM 三段压缩保留的最旧/最新 unit 数
|
||||
/// 压缩后保留最旧 N 个 unit + 中间段摘要 + 最新 N 个 unit
|
||||
#[serde(default = "default_preserve_count")]
|
||||
pub preserve_count: usize,
|
||||
}
|
||||
|
||||
fn default_threshold_ratio() -> f64 {
|
||||
0.7
|
||||
}
|
||||
fn default_llm_compaction_threshold_ratio() -> f64 {
|
||||
0.5
|
||||
}
|
||||
fn default_truncate_max_tokens() -> usize {
|
||||
100
|
||||
}
|
||||
fn default_preserve_count() -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
impl Default for CompactionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
threshold_ratio: default_threshold_ratio(),
|
||||
llm_compaction_threshold_ratio: default_llm_compaction_threshold_ratio(),
|
||||
truncate_max_tokens: default_truncate_max_tokens(),
|
||||
preserve_count: default_preserve_count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TimeConfig {
|
||||
#[serde(default = "default_timezone")]
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent::context_compressor::ContextCompressor;
|
||||
use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider, SystemPromptProvider};
|
||||
use crate::config::{LLMProviderConfig, ModelResolver};
|
||||
use crate::agent::{AgentError, AgentLoop, AgentRuntimeConfig, CompositeSystemPromptProvider, SystemPromptProvider};
|
||||
use crate::config::{CompactionConfig, LLMProviderConfig, ModelResolver};
|
||||
use crate::domain::CapabilityPolicy;
|
||||
use crate::experts::ExpertPromptProvider;
|
||||
use crate::experts::ExpertRuntime;
|
||||
@ -54,6 +54,8 @@ pub(crate) struct AgentFactory {
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
/// per-session 的用户模型选择(最高优先级,覆盖专家配置)
|
||||
model_selections: Arc<ModelSelectionStore>,
|
||||
/// 上下文压缩算法配置(所有 agent 共享)
|
||||
compaction_config: CompactionConfig,
|
||||
/// 实例创建时间戳(用于区分新旧 AgentFactory 实例)
|
||||
instance_id: u64,
|
||||
}
|
||||
@ -81,6 +83,7 @@ impl AgentFactory {
|
||||
prompt_repository: Arc<dyn PromptInjectionRepository>,
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
model_selections: Arc<ModelSelectionStore>,
|
||||
compaction_config: CompactionConfig,
|
||||
) -> Self {
|
||||
// 使用 Arc 指针地址作为实例标识符,用于区分新旧 AgentFactory 实例
|
||||
let instance_id = Arc::as_ptr(&tools) as u64;
|
||||
@ -98,10 +101,22 @@ impl AgentFactory {
|
||||
prompt_repository,
|
||||
model_resolver,
|
||||
model_selections,
|
||||
compaction_config,
|
||||
instance_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造 ContextCompressor(参数内聚到 ContextCompressor,CompactionConfig 注入)。
|
||||
/// AgentLoop(in-loop 压缩)和 Session(sync 兜底压缩)共用此方法,
|
||||
/// 确保两条压缩路径使用同一套用户配置的压缩参数。
|
||||
pub(crate) fn build_compressor(&self, runtime_config: &AgentRuntimeConfig) -> ContextCompressor {
|
||||
ContextCompressor::with_compaction_config(
|
||||
runtime_config.context_window_tokens,
|
||||
runtime_config.context_summary_char_budget,
|
||||
&self.compaction_config,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result<AgentLoop, AgentError> {
|
||||
let session_id = persistent_session_id(request.channel_name, request.session_chat_id);
|
||||
|
||||
@ -211,10 +226,9 @@ impl AgentFactory {
|
||||
let tool_chat_id = request
|
||||
.notification_chat_id
|
||||
.unwrap_or(request.session_chat_id);
|
||||
// 构建上下文压缩器(基于 effective_provider_config 的 context_window_tokens)
|
||||
let compressor = Arc::new(ContextCompressor::from_provider_config(
|
||||
&effective_provider_config,
|
||||
));
|
||||
// 构建上下文压缩器(参数内聚到 ContextCompressor,CompactionConfig 注入)
|
||||
let runtime_config = AgentRuntimeConfig::from(effective_provider_config.clone());
|
||||
let compressor = Arc::new(self.build_compressor(&runtime_config));
|
||||
let mut agent = agent
|
||||
.with_tool_context(ToolContext {
|
||||
channel_name: Some(request.channel_name.to_string()),
|
||||
|
||||
@ -120,6 +120,7 @@ impl GatewayState {
|
||||
mcp_config,
|
||||
Some(bus.clone()),
|
||||
Arc::new(crate::config::ModelResolver::from_config(&config)),
|
||||
config.compaction.clone(),
|
||||
)?;
|
||||
|
||||
// 诊断日志:记录新 GatewayState 的创建(用于排查重启后是否使用了新状态)
|
||||
|
||||
@ -9,7 +9,8 @@ use tokio::sync::RwLock;
|
||||
use crate::agent::AgentError;
|
||||
use crate::bus::MessageBus;
|
||||
use crate::config::{
|
||||
LLMProviderConfig, MemoryMaintenanceConfig, ModelResolver, SubagentsConfig, TaskConfig,
|
||||
CompactionConfig, LLMProviderConfig, MemoryMaintenanceConfig, ModelResolver, SubagentsConfig,
|
||||
TaskConfig,
|
||||
};
|
||||
use crate::gateway::model_selection::ModelSelectionStore;
|
||||
use crate::gateway::tool_registry_factory::ToolRegistryFactory;
|
||||
@ -57,6 +58,7 @@ pub(crate) fn build_session_manager(
|
||||
mcp_config: crate::mcp::McpConfig,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
compaction_config: CompactionConfig,
|
||||
) -> Result<
|
||||
(
|
||||
SessionManager,
|
||||
@ -84,6 +86,7 @@ pub(crate) fn build_session_manager(
|
||||
mcp_config,
|
||||
bus,
|
||||
model_resolver,
|
||||
compaction_config,
|
||||
)
|
||||
}
|
||||
|
||||
@ -105,6 +108,7 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
mcp_config: crate::mcp::McpConfig,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
compaction_config: CompactionConfig,
|
||||
) -> Result<
|
||||
(
|
||||
SessionManager,
|
||||
@ -313,6 +317,7 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
prompt_repository.clone(),
|
||||
model_resolver.clone(),
|
||||
model_selections.clone(),
|
||||
compaction_config,
|
||||
);
|
||||
let session_factory = SessionFactory::new(
|
||||
provider_config.clone(),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::agent::{AgentError, AgentLoop, ContextCompressor, EmittedMessageHandler};
|
||||
use crate::agent::{AgentError, AgentLoop, AgentRuntimeConfig, ContextCompressor, EmittedMessageHandler};
|
||||
#[cfg(test)]
|
||||
use crate::bus::SYSTEM_CONTEXT_SCHEDULED_PROMPT;
|
||||
use crate::bus::{ChatMessage, MessageBus, OutboundMessage};
|
||||
@ -301,6 +301,7 @@ impl Session {
|
||||
prompt_repository.clone(),
|
||||
model_resolver,
|
||||
Arc::new(super::model_selection::ModelSelectionStore::new()),
|
||||
crate::config::CompactionConfig::default(),
|
||||
);
|
||||
Self::with_factories(
|
||||
channel_name,
|
||||
@ -325,6 +326,10 @@ impl Session {
|
||||
skill_events: Arc<dyn SkillEventRepository>,
|
||||
store: Arc<SessionStore>,
|
||||
) -> Result<Self, AgentError> {
|
||||
// Session 的 compressor 用于 sync 兜底压缩路径(compaction.rs),
|
||||
// 必须与 AgentLoop 的 compressor 共用同一套用户配置的压缩参数。
|
||||
let runtime_config = AgentRuntimeConfig::from(provider_config.clone());
|
||||
let compressor = agent_factory.build_compressor(&runtime_config);
|
||||
Ok(Self {
|
||||
id: Uuid::new_v4(),
|
||||
channel_name: channel_name.clone(),
|
||||
@ -332,7 +337,7 @@ impl Session {
|
||||
provider_config: provider_config.clone(),
|
||||
skills,
|
||||
agent_factory,
|
||||
compressor: ContextCompressor::from_provider_config(&provider_config),
|
||||
compressor,
|
||||
history: SessionHistory::new(channel_name, conversations, skill_events),
|
||||
store,
|
||||
pending_cancel_tokens: HashMap::new(),
|
||||
@ -744,6 +749,7 @@ impl SessionManager {
|
||||
mcp_config,
|
||||
None,
|
||||
model_resolver,
|
||||
crate::config::CompactionConfig::default(),
|
||||
)
|
||||
.map(|(session_manager, _, _, _, _)| session_manager)
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ const ToolsTab = lazy(() => import('./tabs/ToolsTab').then((m) => ({ default: m.
|
||||
const MemoryTab = lazy(() => import('./tabs/MemoryTab').then((m) => ({ default: m.MemoryTab })));
|
||||
const SchedulerTab = lazy(() => import('./tabs/SchedulerTab').then((m) => ({ default: m.SchedulerTab })));
|
||||
const ImageTab = lazy(() => import('./tabs/ImageTab').then((m) => ({ default: m.ImageTab })));
|
||||
const CompactionTab = lazy(() => import('./tabs/CompactionTab').then((m) => ({ default: m.CompactionTab })));
|
||||
const TimeTab = lazy(() => import('./tabs/TimeTab').then((m) => ({ default: m.TimeTab })));
|
||||
const GatewayTab = lazy(() => import('./tabs/GatewayTab').then((m) => ({ default: m.GatewayTab })));
|
||||
const ConnectionTab = lazy(() => import('./tabs/ConnectionTab').then((m) => ({ default: m.ConnectionTab })));
|
||||
@ -212,6 +213,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
return <MemoryTab {...commonProps} />;
|
||||
case 'image':
|
||||
return <ImageTab {...commonProps} />;
|
||||
case 'compaction':
|
||||
return <CompactionTab {...commonProps} />;
|
||||
case 'subagents':
|
||||
return <SubagentsTab {...commonProps} setToast={showToast} />;
|
||||
case 'experts':
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
Server,
|
||||
Users,
|
||||
UserCheck,
|
||||
Archive,
|
||||
} from 'lucide-react';
|
||||
import type { TabId } from './types';
|
||||
|
||||
@ -54,6 +55,7 @@ export const TAB_GROUPS: TabGroup[] = [
|
||||
tabs: [
|
||||
{ id: 'gateway', label: '网关', icon: Server },
|
||||
{ id: 'scheduler', label: '调度器', icon: Calendar },
|
||||
{ id: 'compaction', label: '上下文压缩', icon: Archive },
|
||||
{ id: 'memory', label: '记忆维护', icon: Users },
|
||||
{ id: 'image', label: '图片上下文', icon: Image },
|
||||
{ id: 'time', label: '时间', icon: Clock },
|
||||
|
||||
87
web/src/components/Settings/tabs/CompactionTab.tsx
Normal file
87
web/src/components/Settings/tabs/CompactionTab.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
// CompactionTab - 上下文压缩算法配置
|
||||
import { Field, SectionCard } from '../ui';
|
||||
import { inputCls } from '../constants';
|
||||
import type { TabProps } from '../shared';
|
||||
|
||||
export function CompactionTab({ config, update }: TabProps) {
|
||||
return (
|
||||
<>
|
||||
<SectionCard title="压缩触发阈值">
|
||||
<Field
|
||||
label="工程化压缩阈值"
|
||||
hint="当 LLM 返回的 prompt_tokens 超过 context_window × 此比例时触发压缩。默认 0.7(70%)"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
max="1.0"
|
||||
value={config.compaction.threshold_ratio}
|
||||
onChange={(e) =>
|
||||
update('compaction', {
|
||||
...config.compaction,
|
||||
threshold_ratio: +e.target.value,
|
||||
})
|
||||
}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="LLM 压缩阈值"
|
||||
hint="工程化压缩(截断 tool 结果)后,若估算 token 仍超过此比例才调 LLM 压缩。默认 0.5(50%)"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
max="1.0"
|
||||
value={config.compaction.llm_compaction_threshold_ratio}
|
||||
onChange={(e) =>
|
||||
update('compaction', {
|
||||
...config.compaction,
|
||||
llm_compaction_threshold_ratio: +e.target.value,
|
||||
})
|
||||
}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
</SectionCard>
|
||||
<SectionCard title="压缩参数">
|
||||
<Field
|
||||
label="Tool 结果截断 Token 数"
|
||||
hint="工程化压缩时,非子代理的 tool 结果截断到此 token 数。子代理返回不受此限制。默认 100"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="10"
|
||||
value={config.compaction.truncate_max_tokens}
|
||||
onChange={(e) =>
|
||||
update('compaction', {
|
||||
...config.compaction,
|
||||
truncate_max_tokens: +e.target.value,
|
||||
})
|
||||
}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="三段保留 Unit 数"
|
||||
hint="LLM 压缩时保留最旧 N 个和最新 N 个 unit 原样,中间段用 LLM 生成摘要。默认 5"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.compaction.preserve_count}
|
||||
onChange={(e) =>
|
||||
update('compaction', {
|
||||
...config.compaction,
|
||||
preserve_count: +e.target.value,
|
||||
})
|
||||
}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
</SectionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -233,6 +233,13 @@ export interface SchedulerJobConfig {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CompactionConfig {
|
||||
threshold_ratio: number;
|
||||
llm_compaction_threshold_ratio: number;
|
||||
truncate_max_tokens: number;
|
||||
preserve_count: number;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
providers: Record<string, ProviderConfig>;
|
||||
models: Record<string, ModelConfig>;
|
||||
@ -246,6 +253,7 @@ export interface AppConfig {
|
||||
image_context: ImageContextConfig;
|
||||
subagents: SubagentsConfig;
|
||||
experts: ExpertsConfig;
|
||||
compaction: CompactionConfig;
|
||||
client: ClientConfig;
|
||||
channels: Record<string, ChannelConfig>;
|
||||
mcpServers: Record<string, McpServerConfig>;
|
||||
@ -266,7 +274,8 @@ export type TabId =
|
||||
| 'subagents'
|
||||
| 'experts'
|
||||
| 'mcp'
|
||||
| 'channels';
|
||||
| 'channels'
|
||||
| 'compaction';
|
||||
|
||||
export interface ConfigPageProps {
|
||||
onClose: () => void;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user