新增: 消息级溯源 P2 切 ai_messages + AI Chat 跑题改进 P0-P2 + 标题诊断 + 苛刻测
消息级溯源 P2(一次性切读 b + 备份保留):
- 批次A 读路径切 ai_messages(state.rs AppState.ai_messages + 映射 ChatMessage↔AiMessageRecord +
switch/export load 切 + fallback 兜底)
- 批次B 写路径切 ai_messages(replace_conversation 单事务全量重写 + save/clear 切 + 旧 messages 备份)
- 摘要/压缩不改 updated_at(save_conversation touch_updated_at,compress false 防时间分组跳变)
AI Chat 跑题改进 P0-P2(5 改进,治跑题 5 根因,三原则优雅/可靠/易迭代):
- P0 系统提示聚焦(## 聚焦准则独立段)+ 意图接入 loop(filter_tool_defs 收敛工具 29→5-10,三重 fallback)
- P1 压缩增强(compress prompt 主题锚点 + 失败关键词兜底 extract_keyword_summary)+
工具结果压缩(should_summarize + extract_key_info view-only 不改持久化)
- P2 主题检测(TrackedMessage.topic + 双高置信保守标记 + tokenize 2-gram 修复中文锚点)
- title LLM complete 失败诊断日志(助定位返空根因)
- 跑题 P0 审查🟡修复(Debug 加 Data 域 + threshold 注释)
苛刻测 38 条(边界/对抗:全漂移/全停用词/全错误行/2KB 边界/连续主题切换/中文 2-gram/emoji)
df-ai 227 passed + workspace EXIT 0
This commit is contained in:
@@ -9,6 +9,7 @@ use df_storage::db::Database;
|
||||
|
||||
use crate::commands::now_millis;
|
||||
|
||||
use super::commands::message_to_record;
|
||||
use super::AiSession;
|
||||
|
||||
/// Token 用量累加器(agent loop 生命周期内各轮叠加)
|
||||
@@ -130,15 +131,22 @@ pub(crate) fn truncate_parts_for_persist(parts: &[df_ai::provider::ContentPart])
|
||||
|
||||
/// 保存对话到数据库(按 conv_id 写库,不受 active_conversation_id 切换影响)
|
||||
///
|
||||
/// 写 messages + updated_at + 累加 token 用量 + 首次落库的 model;标题由 ensure_conversation_title 单独生成。
|
||||
/// 写 ai_messages(每条消息一行,F-260619-03 拆分存储)+ updated_at + 累加 token 用量 +
|
||||
/// 首次落库的 model;标题由 ensure_conversation_title 单独生成。
|
||||
/// token 走累加模式:upsert 读旧值叠加,保证审批暂停→恢复跨 loop 实例不覆盖丢失。
|
||||
/// model 仅首次落库写入 + 旧记录缺值时补填(不覆盖历史已存值,兼容本次改造前的老对话)。
|
||||
///
|
||||
/// F-260619-03 批次 B:写路径切 ai_messages(全量重写,replace_conversation)。
|
||||
/// 内存 ContextManager 是运行时真相源,save 是同步点——每轮全量回写 ai_messages,
|
||||
/// compress/replace/edit/clear_context 内存改 status/content 后下轮 save 自动覆盖。
|
||||
/// 旧 messages JSON 列**保留不写**(作备份,防 reload fallback 读旧脏数据),不赋新值也不置空。
|
||||
pub(crate) async fn save_conversation(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
db: &Arc<Database>,
|
||||
conv_id: &str,
|
||||
usage: Option<&df_ai::provider::TokenUsage>,
|
||||
model: Option<&str>,
|
||||
touch_updated_at: bool,
|
||||
) {
|
||||
// 取 messages + 懒创建首次落库所需的 provider_id/created_at
|
||||
// 工具结果(content)超 50KB 时截断头尾各 ~20KB + 中段标注,防大体量结果(read_file 1MB 洞 /
|
||||
@@ -149,7 +157,7 @@ pub(crate) async fn save_conversation(
|
||||
// loop 内 save 由 run_agentic_loop 入参 conv_id 透传;IPC 路径(commands.rs)save 也传 conv_id。
|
||||
// conv() 惰性建:save 路径 conv 必然已建(send/regenerate/edit/switch 均先 conv());若极端
|
||||
// 未建(如启动恢复无 live conv),conv() 建空 PerConvState,save 空 messages(幂等不污染)。
|
||||
let (messages_json, provider_id, created_at) = {
|
||||
let (persist_msgs, provider_id, created_at) = {
|
||||
let mut session = session_arc.lock().await;
|
||||
let mut msgs = session.conv(conv_id).messages.all_messages_clone();
|
||||
for m in &mut msgs {
|
||||
@@ -161,18 +169,34 @@ pub(crate) async fn save_conversation(
|
||||
}
|
||||
}
|
||||
(
|
||||
serde_json::to_string(&msgs).unwrap_or_else(|_| "[]".to_string()),
|
||||
msgs,
|
||||
session.active_provider_id.clone(),
|
||||
session.active_conv_created_at.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// F-260619-03 批次 B:映射 Vec<ChatMessage> → Vec<AiMessageRecord>(带 seq 索引 + conv_id)
|
||||
// 全量重写 ai_messages(单事务 DELETE + INSERT OR IGNORE,原子无中间空窗)。
|
||||
// created_at 用对话级 created_at(老对话 None 时 now 兜底),保证消息创建时间与对话一致。
|
||||
let now = now_millis();
|
||||
let msg_created_at = created_at.clone().unwrap_or_else(|| now.clone());
|
||||
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();
|
||||
|
||||
let conv_repo = AiConversationRepo::new(db);
|
||||
let msg_repo = df_storage::crud::AiMessageRepo::new(db);
|
||||
match conv_repo.get_by_id(conv_id).await {
|
||||
Ok(Some(mut rec)) => {
|
||||
// 已落库:更新 messages + updated_at;token 累加(读旧值+新值,跨 loop 实例防覆盖)
|
||||
rec.messages = messages_json;
|
||||
rec.updated_at = now_millis();
|
||||
// 已落库:更新对话元数据(token/model/updated_at);messages JSON 列**不赋新值**(保留旧值作备份)。
|
||||
// updated_at 按 touch_updated_at 条件改(用户活跃=true 反映最后活跃,
|
||||
// 系统摘要/压缩=false 防会话时间分组"昨天→今天"跳变);
|
||||
// token 累加(读旧值+新值,跨 loop 实例防覆盖)。
|
||||
if touch_updated_at {
|
||||
rec.updated_at = now_millis();
|
||||
}
|
||||
if let Some(u) = usage {
|
||||
rec.prompt_tokens = accumulate_tokens(rec.prompt_tokens, u.prompt_tokens);
|
||||
rec.completion_tokens = accumulate_tokens(rec.completion_tokens, u.completion_tokens);
|
||||
@@ -187,13 +211,21 @@ pub(crate) async fn save_conversation(
|
||||
if !list.iter().any(|x| x == m) { list.push(m.to_string()); }
|
||||
rec.models = Some(serde_json::to_string(&list).unwrap_or_else(|_| "[]".to_string()));
|
||||
}
|
||||
// update_full 仍写 messages 列(保留旧值,本批不改 messages 字段),写元数据 + updated_at
|
||||
if let Err(e) = conv_repo.update_full(&rec).await {
|
||||
tracing::warn!("更新对话失败 {conv_id}: {e}");
|
||||
tracing::warn!("更新对话元数据失败 {conv_id}: {e}");
|
||||
}
|
||||
// 消息拆分存储:全量重写 ai_messages
|
||||
if let Err(e) = msg_repo.replace_conversation(conv_id, records).await {
|
||||
tracing::warn!("全量重写 ai_messages 失败 {conv_id}: {e}");
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// 懒创建首次落库(此为空对话不落库的落库点:走到这里 messages 必非空)
|
||||
let now = now_millis();
|
||||
// messages JSON 列首次落库也写(兼容未跑迁移的老库 fallback 读路径),
|
||||
// 同时写 ai_messages(新读路径真相源)。
|
||||
let messages_json = serde_json::to_string(&persist_msgs).unwrap_or_else(|_| "[]".to_string());
|
||||
let conv_created = created_at.unwrap_or_else(|| now.clone());
|
||||
let rec = df_storage::models::AiConversationRecord {
|
||||
id: conv_id.to_string(),
|
||||
title: None,
|
||||
@@ -205,12 +237,17 @@ pub(crate) async fn save_conversation(
|
||||
pinned: false,
|
||||
prompt_tokens: usage.map(|u| u.prompt_tokens as i64),
|
||||
completion_tokens: usage.map(|u| u.completion_tokens as i64),
|
||||
created_at: created_at.unwrap_or_else(|| now.clone()),
|
||||
created_at: conv_created.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
if let Err(e) = conv_repo.insert(rec).await {
|
||||
tracing::warn!("落库对话失败 {conv_id}: {e}");
|
||||
}
|
||||
// 消息拆分存储:首次落库同样全量写 ai_messages
|
||||
// (records 已用 conv_created 作 created_at,与对话记录一致)
|
||||
if let Err(e) = msg_repo.replace_conversation(conv_id, records).await {
|
||||
tracing::warn!("首次写 ai_messages 失败 {conv_id}: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!("读取对话 {conv_id} 失败: {e}"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user