新增: F-15阶段3 agentic loop自动压缩(loop顶部检测+压缩+降级原裁剪)
This commit is contained in:
@@ -25,12 +25,13 @@ use df_storage::models::AiProviderRecord;
|
||||
|
||||
use crate::state::{AppState, LlmConcurrency};
|
||||
|
||||
use super::audit::process_tool_calls;
|
||||
use super::compress::compress_via_llm;
|
||||
use super::conversation::{save_conversation, TokenAccumulator};
|
||||
use super::knowledge_inject::maybe_spawn_extraction;
|
||||
use super::prompt::{build_system_prompt, get_active_provider};
|
||||
use super::stream_recv::{stream_llm, StreamResult};
|
||||
use super::title::{ensure_conversation_title, spawn_ensure_title};
|
||||
use super::audit::process_tool_calls;
|
||||
|
||||
use super::{AiChatEvent, AiSession, ErrorType};
|
||||
|
||||
@@ -42,6 +43,11 @@ use super::{AiChatEvent, AiSession, ErrorType};
|
||||
/// 热改下次发消息生效(与 llm_concurrency 传 Arc 实时反映的区别)。
|
||||
pub const DEFAULT_MAX_AGENT_ITERATIONS: usize = 10;
|
||||
|
||||
/// 压缩保护区条数(对齐 clear/compress IPC 的 PROTECT_COUNT=6)。
|
||||
/// protect_start = len.saturating_sub(PROTECT_COUNT):保护区内的最近 6 条(含本轮三元组)
|
||||
/// 不参与压缩,避免压缩正在使用的活跃消息。
|
||||
const PROTECT_COUNT: usize = 6;
|
||||
|
||||
/// 流式对话失败自动重试默认次数(F-260616-07 / 决策 a1)
|
||||
///
|
||||
/// 默认 3 次(初次 + 2 次重试)。复用 retry::backoff_delay 退避(1s→2s→4s+jitter) +
|
||||
@@ -250,6 +256,124 @@ pub(crate) async fn run_agentic_loop(
|
||||
});
|
||||
}
|
||||
|
||||
// F-15 阶段3: 自动压缩(智能裁剪)——在 build_for_request 之前预处理。
|
||||
//
|
||||
// 触发条件:history_tokens > budget*0.6 且 保护区外有可压缩消息 且 未在压缩中。
|
||||
//
|
||||
// 流程(对齐阶段2 ai_chat_compress_context IPC 的 read-but-don't-mutate 模式):
|
||||
// ① 读 active 克隆(不改 status / 不扣 token)→ 喂 LLM 出摘要;
|
||||
// ② LLM 成功 → compress_old_messages(标 compressed + 扣 token)+ insert_at(摘要 system);
|
||||
// ③ LLM 失败 → 消息状态完全不变(未改 status / 未扣 token),降级走原 build_for_request 裁剪。
|
||||
//
|
||||
// 口径决策(任务让"你判断"):选**延迟 mutate(成功才改)**而非"失败回滚 status"。
|
||||
// 理由:ContextManager.history_tokens 字段私有、无 set_history_tokens 公开接口;
|
||||
// 若先 compress_old_messages(扣 token)再 LLM,失败回滚需精确恢复 history_tokens,
|
||||
// 但 ChatMessage 克隆不含 token_count,无法等量加回——回滚 token 不精确。
|
||||
// 延迟 mutate 则失败时零副作用(消息状态/token 完全不变),语义最干净。
|
||||
// 注:延迟 mutate 的窗口(active_msgs 读出→LLM 出摘要期间)不持锁,但 loop 串行无并发
|
||||
// (本函数独占 session_arc,工具执行/审批分支在 stream 之后),故此窗口内 messages 不变。
|
||||
//
|
||||
// 安全(FR-S1):复用 loop 顶部已 build+验证 的 provider(不再 build_provider_for 重复 resolve
|
||||
// keyring),api_key 经 df_storage::secret 闭环;summary/error payload/日志均不含 api_key。
|
||||
// is_compressing 防重入:set_compressing(true/false) 成对(LLM 调用前后均复位)。
|
||||
// 单轮问答(history_tokens 未超 0.6*budget)不触发,零行为变化。
|
||||
let prev_compressing = session_arc.lock().await.messages.is_compressing();
|
||||
if !prev_compressing {
|
||||
// 读触发条件(history_tokens / budget / has_compressible_messages),持锁快照判断。
|
||||
let (should_compress, protect_start, pre_compress_tokens) = {
|
||||
let session = session_arc.lock().await;
|
||||
let mgr = &session.messages;
|
||||
let protect_start = mgr.len().saturating_sub(PROTECT_COUNT);
|
||||
let history_tokens = mgr.history_tokens();
|
||||
let budget = mgr.budget_limit();
|
||||
// 触发阈值 0.6*budget(整数比避免浮点):budget*6/10 < history_tokens
|
||||
let should = protect_start > 0
|
||||
&& (budget as u64) * 6 / 10 < history_tokens as u64
|
||||
&& mgr.has_compressible_messages(protect_start);
|
||||
(should, protect_start, history_tokens)
|
||||
};
|
||||
|
||||
if should_compress {
|
||||
// emit 压缩开始 + 置位防重入 + 读 active 克隆(不改 status)。
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompressing {
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
let (active_msgs, lang) = {
|
||||
let mut session = session_arc.lock().await;
|
||||
session.messages.set_compressing(true);
|
||||
// 读 active 克隆(不改 status):filter is_active,LLM 失败则消息状态完全不变。
|
||||
let active_msgs: Vec<ChatMessage> = session.messages.messages_mut()
|
||||
[..protect_start]
|
||||
.iter()
|
||||
.filter(|t| t.message.is_active())
|
||||
.map(|t| t.message.clone())
|
||||
.collect();
|
||||
let lang = session.agent_language.clone()
|
||||
.unwrap_or_else(|| "zh-CN".to_string());
|
||||
(active_msgs, lang)
|
||||
};
|
||||
|
||||
// 压缩调用(复用 loop 顶部已 build 的 provider,api_key 经 secret 闭环)。
|
||||
// 成功 → Some(summary);失败 → Err;无 active 可压缩(active_msgs 空)→ 视为 noop。
|
||||
let compress_outcome: Result<Option<String>, String> = if active_msgs.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
compress_via_llm(
|
||||
provider.as_ref(),
|
||||
&provider_config,
|
||||
active_msgs,
|
||||
&lang,
|
||||
&llm_concurrency,
|
||||
).await.map(Some)
|
||||
};
|
||||
|
||||
match compress_outcome {
|
||||
Ok(Some(summary)) => {
|
||||
// LLM 成功 → 标 compressed(扣 token)+ 摘要 system 插首位 + set_compressing(false)。
|
||||
// compress_old_messages 幂等:此时 status 仍是 active(本流程未先标),它会把
|
||||
// [..protect_start] 内 active 标 compressed 并扣 token。返回的 cloned 与之前读的
|
||||
// active_msgs 等价(LLM 调用期间 messages 不变,见上方口径决策注)。
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
let _compressed = session.messages.compress_old_messages(protect_start);
|
||||
session.messages.insert_at(0, ChatMessage::system(&summary));
|
||||
session.messages.set_compressing(false);
|
||||
}
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
iteration,
|
||||
pre_tokens = pre_compress_tokens,
|
||||
"[ai] 自动压缩成功,摘要已插首位"
|
||||
);
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompressed {
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
summary,
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
// 保护区外无 active 可压缩(已全 compressed/archived)→ noop,仅复位 is_compressing。
|
||||
session_arc.lock().await.messages.set_compressing(false);
|
||||
}
|
||||
Err(e) => {
|
||||
// LLM 失败 → 消息状态完全不变(未改 status / 未扣 token)。
|
||||
// set_compressing(false) 复位 + emit AiError(message 不含 api_key)。
|
||||
// 不阻塞 loop:继续走下方 build_for_request 原裁剪路径(保最近 6 条)。
|
||||
session_arc.lock().await.messages.set_compressing(false);
|
||||
tracing::warn!(
|
||||
conv_id = %conv_id,
|
||||
error = %e,
|
||||
"[ai] 自动压缩失败,降级走原裁剪(build_for_request)"
|
||||
);
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error: format!("自动上下文压缩失败,已降级为普通裁剪: {}", e),
|
||||
error_type: Some(ErrorType::Unknown),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建请求消息(超预算时自动裁剪旧消息,保护工具调用三元组 + 最近 6 条)
|
||||
// F-260616-13: sys_tokens 已在 loop 外缓存;本块构建的 messages 在本轮重试循环中复用
|
||||
// (本轮 stream_llm 不持 session_arc、不改 messages,重试无 push 发生,重建等价于复用)。
|
||||
|
||||
Reference in New Issue
Block a user