优化: aichat效率剩余(压缩后台化防阻塞/审计批量事务/只读缓存轮内去重/流式增量渲染/AiCommandOutput合批/双渲染合并) + 跨端加固(df-project路径保留大小写/tunnel文档更正supervisor重连/relay固定时间比较与帧上限/启动校验) + 销账
This commit is contained in:
@@ -41,7 +41,7 @@ use approval::{detect_retry_count, handle_approval_tool};
|
||||
// (list_tool_executions 是 #[tauri::command],ToolExecutionDto 供前端 DTO 序列化)。
|
||||
pub mod record;
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use record::{audit_tool_call, query_audit_history, record_audit};
|
||||
pub(crate) use record::{audit_tool_call, build_audit_record, query_audit_history, record_audit};
|
||||
#[allow(unused_imports)]
|
||||
pub use record::{
|
||||
list_tool_executions, tool_failure_stats, ToolExecutionDto, ToolExecutionPage, ToolExecQuery,
|
||||
@@ -70,7 +70,7 @@ pub(crate) use finalize::audit_finalize;
|
||||
// cache(audit/cache.rs):高危工具去重缓存 + 只读工具缓存。
|
||||
// 第三批从本文件抽离,行为零变更。
|
||||
mod cache;
|
||||
pub(super) use cache::{cache_hit_warning, detect_listing_bypass, find_cached_readonly_result, insert_listing_bypass_warning, pending_placeholder_for};
|
||||
pub(super) use cache::{cache_hit_warning, detect_listing_bypass, find_cached_readonly_result, insert_listing_bypass_warning, pending_placeholder_for, readonly_cache_args_key};
|
||||
|
||||
// data_change(audit/data_change.rs):AR-11 数据变更联动刷新。
|
||||
// 第四批从本文件抽离,行为零变更。pub(crate) use 保持 emit_data_changed 对 crate 内可见
|
||||
@@ -594,16 +594,37 @@ pub(crate) async fn process_tool_calls(
|
||||
// 同参只读工具成功执行过,命中则直接回填缓存结果跳过真执行,断 LLM 失忆死循环。
|
||||
// 安全边界见 find_cached_readonly_result 文档(仅白名单只读工具 + 仅 completed 成功结果)。
|
||||
//
|
||||
// AC-EFF-T1-2(2026-08-09)轮内去重:同轮重复 (tool, args_key) 只真执行一次。
|
||||
// LLM 单轮可能发多条同参只读调用(失忆重调),原实现首查未命中(本轮首个尚未落 messages)
|
||||
// 致重复全部真执行(重复 I/O + 重复 tool_result 回灌)。现进程内暂存首个结果
|
||||
// (in_round_cache),后续同 key 复用——语义同跨轮缓存,仅因首个在本轮未落库故内存暂存。
|
||||
// 命中计数递增威慑(cache_hit_warning 同口径);key 与跨轮缓存同源(readonly_cache_args_key),
|
||||
// 防两处 key 口径漂移。跨轮重复仍走 find_cached_readonly_result(历史扫描,未变)。
|
||||
//
|
||||
// find_cached_readonly_result 内部短 lock + 锁外 DB 查,本段不持锁。
|
||||
// in_round_cache:同轮 (tool, args_key) -> (首个结果内容, 已见同参次数)。首个出现:
|
||||
// 跨轮缓存命中 → 存 (content, hit_count);未命中 → 真执行,backfill 后存 (content, 1)。
|
||||
// in_round_dupes:同轮重复的 draft(延后到 backfill 后复用 in_round_cache,防双执行)。
|
||||
let mut in_round_cache: HashMap<(String, String), (String, u32)> = HashMap::new();
|
||||
let mut seen_round_keys: HashSet<(String, String)> = HashSet::new();
|
||||
let mut in_round_dupes: Vec<(ToolCallDraft, RiskLevel, String)> = Vec::new();
|
||||
let mut low_risk_uncached: Vec<(ToolCallDraft, serde_json::Value, RiskLevel)> = Vec::with_capacity(low_risk.len());
|
||||
for (draft, args, risk_level) in low_risk {
|
||||
let args_key = readonly_cache_args_key(&draft.name, &args);
|
||||
let key = (draft.name.clone(), args_key.clone());
|
||||
if !seen_round_keys.insert(key) {
|
||||
// 轮内重复:延后复用首个结果(首个已在 seen/uncached/backfill),不重复历史扫描+DB 查。
|
||||
in_round_dupes.push((draft, risk_level, args_key));
|
||||
continue;
|
||||
}
|
||||
let cached = find_cached_readonly_result(session_arc, conv_id, &audit_repo, &draft.name, &args).await;
|
||||
if let Some((cached_content, hit_count)) = cached {
|
||||
// 缓存命中:直接 push tool_result + 审计(decided_by=cache_hit 标记缓存来源),
|
||||
// 不走真执行 + 不重emit Started/Completed(避免误导前端工具又执行了一次)。
|
||||
// 缓存命中:存轮内缓存(供后续同 key 复用)+ 直接 push tool_result + 审计
|
||||
// (decided_by=cache_hit 标记缓存来源),不走真执行 + 不重emit Started/Completed。
|
||||
// AC-1 根治:弱模型不知道结果来自缓存,仍死循环重调同参工具。此处给回填的
|
||||
// tool_result 前置「重复调用拦截」警告头(机制化提示,LLM 能看到 tool_result),
|
||||
// 命中次数递增威慑,告知勿再重复调用相同参数的工具。
|
||||
in_round_cache.insert((draft.name.clone(), args_key), (cached_content.clone(), hit_count));
|
||||
let warned_content = format!("{}{}", cache_hit_warning(&draft.name, hit_count), cached_content);
|
||||
// emit Completed 携带缓存结果供前端折叠卡片展示(与 find_cached_high_risk_result 一致)。
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
@@ -680,7 +701,12 @@ pub(crate) async fn process_tool_calls(
|
||||
}
|
||||
})).await;
|
||||
|
||||
// 串行回填 tool_result + 审计(短 lock push + 锁外 audit)
|
||||
// 串行回填 tool_result + 审计收集(短 lock push + 锁外收集),审计改单事务批量插入。
|
||||
// AC-EFF-T1-1(2026-08-09):原每结果 audit_tool_call(每次 spawn_blocking + 单行 INSERT,
|
||||
// 单连接 Mutex 串行)合并为 build_audit_record 收集 + insert_batch 一次事务批量,
|
||||
// 砍 N 次串行 INSERT 尾巴(治 aichat 效率走查 T1-1)。审计失败不阻断主流程(记日志)。
|
||||
let mut audit_records: Vec<df_storage::models::AiToolExecutionRecord> =
|
||||
Vec::with_capacity(results.len());
|
||||
for (draft, raw_result, risk_level, outcome) in results {
|
||||
let (status, content) = match outcome {
|
||||
Ok(c) => ("completed", c),
|
||||
@@ -717,7 +743,89 @@ pub(crate) async fn process_tool_calls(
|
||||
RiskLevel::Medium => "auto_takeover_medium",
|
||||
RiskLevel::High => "auto_takeover_all",
|
||||
};
|
||||
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, status, risk_level, Some(content), Some(decided_by), current_message_id).await;
|
||||
// AC-EFF-T1-2 轮内去重回填:真执行结果存入 in_round_cache,供同轮后续同 key 复用
|
||||
// (首个执行 → count=1,轮内重复命中时递增;key 与跨轮缓存同源)。
|
||||
{
|
||||
let args_val = serde_json::from_str(&draft.args).unwrap_or(serde_json::Value::Null);
|
||||
let exec_key = (draft.name.clone(), readonly_cache_args_key(&draft.name, &args_val));
|
||||
in_round_cache.entry(exec_key).or_insert_with(|| (content.clone(), 1));
|
||||
}
|
||||
audit_records.push(build_audit_record(
|
||||
conv_id, &draft.id, &draft.name, &draft.args, status, risk_level,
|
||||
Some(content), Some(decided_by), current_message_id,
|
||||
));
|
||||
}
|
||||
// 单事务批量 INSERT(一次 spawn_blocking,砍 N 次串行尾巴;失败仅记日志不阻断)。
|
||||
if !audit_records.is_empty() {
|
||||
let batch_len = audit_records.len();
|
||||
if let Err(e) = audit_repo.insert_batch(audit_records).await {
|
||||
tracing::error!(
|
||||
conv_id = %conv_id,
|
||||
batch = batch_len,
|
||||
"[ai] insert_batch 批量写审计记录失败(已回滚,共 {} 条): {}",
|
||||
batch_len,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// AC-EFF-T1-2 轮内重复处理:复用首个结果(已回填 in_round_cache),跳过重复真执行。
|
||||
// 语义同跨轮缓存命中:emit Completed + push tool_result + 审计(decided_by=cache_hit),
|
||||
// 命中计数递增威慑(cache_hit_warning 同口径)。首个结果来自跨轮命中(前段已存)或
|
||||
// 本轮真执行 backfill(上段已存),故必命中;防御性缺失则丢弃(不重复执行,LLM 下轮可重调)。
|
||||
let mut dupe_audit_records: Vec<df_storage::models::AiToolExecutionRecord> =
|
||||
Vec::with_capacity(in_round_dupes.len());
|
||||
for (draft, risk_level, args_key) in in_round_dupes {
|
||||
let key = (draft.name.clone(), args_key);
|
||||
let Some((content, count)) = in_round_cache.get(&key).cloned() else {
|
||||
tracing::warn!(
|
||||
conv_id = %conv_id,
|
||||
tool = %draft.name,
|
||||
tc_id = %draft.id,
|
||||
"[ai] 轮内去重:首个结果缺失(理论不可达),丢弃重复调用(防御降级)"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let new_count = count + 1;
|
||||
in_round_cache.insert(key, (content.clone(), new_count));
|
||||
let warned_content = format!("{}{}", cache_hit_warning(&draft.name, new_count), content);
|
||||
// emit Completed 携带缓存结果供前端折叠卡片展示。
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
result: serde_json::Value::String(warned_content.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
// 短 lock 段:push tool_result(纯写,无 await)
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &warned_content));
|
||||
}
|
||||
dupe_audit_records.push(build_audit_record(
|
||||
conv_id, &draft.id, &draft.name, &draft.args, "completed", risk_level,
|
||||
Some(warned_content), Some("cache_hit"), current_message_id,
|
||||
));
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
tool = %draft.name,
|
||||
tc_id = %draft.id,
|
||||
hit_count = new_count,
|
||||
"[ai] 只读工具轮内去重命中(第 {} 次同参,本轮首个已执行),跳过重复真执行",
|
||||
new_count
|
||||
);
|
||||
}
|
||||
if !dupe_audit_records.is_empty() {
|
||||
let dup_len = dupe_audit_records.len();
|
||||
if let Err(e) = audit_repo.insert_batch(dupe_audit_records).await {
|
||||
tracing::error!(
|
||||
conv_id = %conv_id,
|
||||
batch = dup_len,
|
||||
"[ai] insert_batch 轮内去重审计批量写失败(已回滚,共 {} 条): {}",
|
||||
dup_len,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user