优化: aichat效率批(save无变化捷径/断路器尾窗/system缓存指纹/会话列表刷新收敛) + 新增 LLM prompt caching(cache_control开关默认关/system稳定段易变段分离治缓存命中率) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:13 +08:00
parent 4df91ed155
commit 7eb2e25ed5
13 changed files with 698 additions and 180 deletions
+62 -37
View File
@@ -251,7 +251,9 @@ async fn save_conversation_inner(
// 读基线与读 messages 在同一 lock 段(快,无 await),保证两者一致性快照。
// 借用顺序:先 clone session 顶层字段(immutable borrow),再 conv(conv_id) mutable borrow,
// 避免同时持有 session 的 mut 和 immut 借用(E0502)。
let (mut msgs, provider_id, created_at, pinned_goals, persisted_count, needs_full_rewrite) = {
// BE1(AC-EFF-L0-1):无变化捷径提前到克隆之前——同一 lock 段内先判「消息数未增且非 dirty」,
// 无变化则不 clone(每轮无变化 save 省一次全量 clone + 后续 truncate/records 构建)。
let (provider_id, created_at, pinned_goals, msg_len, persisted_count, needs_full_rewrite, mut persist_msgs) = {
let mut session = session_arc.lock().await;
// 已删除对话直接跳过写入(loop 在「push 后、save 前」被删除的幽灵复活根治):
// 此处不惰性重建 per_conv、不触发 INSERT 空壳 + insert_batch 复活。
@@ -264,50 +266,66 @@ async fn save_conversation_inner(
return;
}
let __wait = __lock_start.elapsed();
// 先取 session 顶层 owned 字段(immutable borrow 即刻结束),再取 conv mutable borrow,
// 避免同时持有 session 的 mut 和 immut 借用(E0502)。
let provider_id = session.active_provider_id.clone();
let created_at = session.active_conv_created_at.clone();
let conv = session.conv(conv_id);
let cloned = (
conv.messages.all_messages_clone(),
let msg_len = conv.messages.len();
let persisted_count = conv.messages.persisted_msg_count();
let needs_full_rewrite = conv.messages.needs_full_rewrite();
// 无变化捷径提前判定:消息数未增且非 dirty → DB ai_messages 与内存完全一致,
// 免 clone(下方直接走元数据 update_full,消息写入整段跳过)。
let cloned = if !needs_full_rewrite && msg_len == persisted_count {
Vec::new()
} else {
conv.messages.all_messages_clone()
};
let pinned_goals = conv.pinned_goals.clone();
let _ = __wait; // 诊断:lock 等待时长(下行 eprintln 输出 lock 段总时长)
(
provider_id,
created_at,
conv.pinned_goals.clone(),
conv.messages.persisted_msg_count(),
conv.messages.needs_full_rewrite(),
);
let _ = __wait; // 诊断:lock 等待时长(下行 eprintln 输出 lock 段总时长)
cloned
pinned_goals,
msg_len,
persisted_count,
needs_full_rewrite,
cloned,
)
};
let __lock_total = __lock_start.elapsed();
if __lock_total > std::time::Duration::from_millis(50) {
eprintln!("[LOCK-DIAG] save clone 段持锁 {:?} (含等待,>50ms 报告,定位 session lock 长持有者)", __lock_total);
eprintln!("[LOCK-DIAG] save 快照段持锁 {:?} (含等待,>50ms 报告,定位 session lock 长持有者)", __lock_total);
}
// truncate 在锁外(clone 副本上操作,不影响 session 真相源)
// T2: 展开 namespace 引用为原文再落库
{
let session = session_arc.lock().await;
for m in &mut msgs {
if df_ai::namespace_store::is_namespace_ref(&m.content) {
if let Some(original) = session.namespace_store.read_only(&m.content) {
m.content = original.to_string();
// BE1:仅当消息确有可能变化(非无变化捷径)才对 clone 副本做 namespace 展开 + truncate。
let no_message_change = !needs_full_rewrite && msg_len == persisted_count;
if !no_message_change {
// truncate 在锁外(clone 副本上操作,不影响 session 真相源)
// T2: 展开 namespace 引用为原文再落库
{
let session = session_arc.lock().await;
for m in &mut persist_msgs {
if df_ai::namespace_store::is_namespace_ref(&m.content) {
if let Some(original) = session.namespace_store.read_only(&m.content) {
m.content = original.to_string();
}
}
}
}
}
for m in &mut msgs {
// P0-2:tool result 是结构化 JSON(provider 读工具返回原样),
// 中段截断会插裸换行+中文省略标记破坏 JSON 结构致 reload/重发 parse FAIL。
// tool result 体量已由 read_file limit 控源,此处跳过 content 截断,仅截 assistant/user 文本。
if !matches!(m.role, df_ai::provider::MessageRole::Tool) {
m.content = truncate_for_persist(&m.content);
}
// F-260614-05 Phase 2a: parts(Image base64) 同样截断(替换占位 Text 片),
// 防大体量图把对话 JSON 撑爆。仅作用于持久化副本,不污染内存真相源。
if let Some(parts) = m.parts.as_ref() {
m.parts = truncate_parts_for_persist(parts);
for m in &mut persist_msgs {
// P0-2:tool result 是结构化 JSON(provider 读工具返回原样),
// 中段截断会插裸换行+中文省略标记破坏 JSON 结构致 reload/重发 parse FAIL。
// tool result 体量已由 read_file limit 控源,此处跳过 content 截断,仅截 assistant/user 文本。
if !matches!(m.role, df_ai::provider::MessageRole::Tool) {
m.content = truncate_for_persist(&m.content);
}
// F-260614-05 Phase 2a: parts(Image base64) 同样截断(替换占位 Text 片),
// 防大体量图把对话 JSON 撑爆。仅作用于持久化副本,不污染内存真相源。
if let Some(parts) = m.parts.as_ref() {
m.parts = truncate_parts_for_persist(parts);
}
}
}
let persist_msgs = msgs;
// 序列化当前 conv 的挂起审批快照:从 session.pending_approvals 筛选本 conv 条目,
@@ -335,12 +353,19 @@ async fn save_conversation_inner(
// 首次落库(Ok(None))走 insert_batch(空表,直接插全量,无需 DELETE)。
let now = now_millis();
let msg_created_at = created_at.clone().unwrap_or_else(|| now.clone());
let total_len = persist_msgs.len();
let records: Vec<df_storage::models::AiMessageRecord> = persist_msgs
.iter()
.enumerate()
.map(|(seq, m)| message_to_record(m, conv_id, seq as i64, &msg_created_at))
.collect();
// BE1:no_message_change 时 persist_msgs 为空,records 也空(下方无变化捷径直接跳过消息写入,
// 仅元数据 update_full);非无变化时 records 由全量 persist_msgs 构建(append 切片仍按 persisted_count)。
let records: Vec<df_storage::models::AiMessageRecord> = if no_message_change {
Vec::new()
} else {
persist_msgs
.iter()
.enumerate()
.map(|(seq, m)| message_to_record(m, conv_id, seq as i64, &msg_created_at))
.collect()
};
// BE1:total_len 用 clone 后权威条数(no_message_change 时用 lock 读的 msg_len,=persisted_count)。
let total_len = if no_message_change { msg_len } else { persist_msgs.len() };
let conv_repo = AiConversationRepo::new(db);
let msg_repo = df_storage::crud::AiMessageRepo::new(db);