优化: 缓存命中字段解析与提示词前缀稳定化

- openai 兼容口径补解析嵌套 cached_tokens
- cache 显示区分无数据与真零,估算轮不渲染
- 轨迹展开折叠空过程记录行降噪
- token 统计行恒完整可见不截断
- openai_compat 纳入 system 稳定易变拆分
- 系统警示与主题标记改尾部注入不位移前缀
- 工具结果视图压缩按入库时刻冻结
- 会话内工具集冻结仅增不减
- anthropic 缓存补消息级断点,设置页加开关
- 补 intent 测试对 Agent domain 覆盖
This commit is contained in:
lxy
2026-10-01 19:35:21 +08:00
parent 2187ffe173
commit 423acfdd00
14 changed files with 499 additions and 99 deletions
+83 -24
View File
@@ -680,22 +680,66 @@ pub(crate) async fn run_agentic_loop(
// 分离:即使将来 PLANNING_ENABLED 翻 true,plan_hint 内部 PLAN_HINT_ENABLED 关闭时
// filter_tool_defs_planned 仍退扁平(双层开关,任一关闭均退旧行为)。
let tool_defs: Vec<df_ai::provider::ToolDefinition> = if pure_greeting {
Vec::new() // 纯问候 → 空工具集(tools=None,强制纯文本回复)
} else if conf >= INTENT_CONF_THRESHOLD {
let filtered = if df_ai::planner::PLANNING_ENABLED {
// Phase 1:plan_hint 编排排序。intent_label 供 plan_hint 备用(当前规则纯关键词驱动)。
filter_tool_defs_planned(&all_defs, &intent, intent.as_str(), &user_text)
} else {
// 旧行为:intent 收敛扁平子集(零行为变更,flag 默认关走此路)。
filter_tool_defs(&all_defs, &intent)
};
if filtered.len() < 3 {
all_defs // 兜底:过滤<3(漂移/误收敛)回全量
} else {
filtered
}
Vec::new() // 纯问候 → 空工具集(tools=None,强制纯文本回复;仅作用当轮,不写冻结集)
} else {
all_defs // 低置信 fallback 全量
let computed: Vec<df_ai::provider::ToolDefinition> = if conf >= INTENT_CONF_THRESHOLD {
let filtered = if df_ai::planner::PLANNING_ENABLED {
// Phase 1:plan_hint 编排排序。intent_label 供 plan_hint 备用(当前规则纯关键词驱动)。
filter_tool_defs_planned(&all_defs, &intent, intent.as_str(), &user_text)
} else {
// 旧行为:intent 收敛扁平子集(零行为变更,flag 默认关走此路)。
filter_tool_defs(&all_defs, &intent)
};
if filtered.len() < 3 {
all_defs.clone() // 兜底:过滤<3(漂移/误收敛)回全量
} else {
filtered
}
} else {
all_defs.clone() // 低置信 fallback 全量
};
// 跨轮工具集冻结(前缀稳定):tools 数组在请求中位于 system 之后、消息之前,
// 每轮按末条 user 消息重新裁剪会让数组跨轮变化 → 前缀字节断,自动前缀缓存
// (DeepSeek/GLM/Qwen)与 Anthropic 显式缓存全部失效。改按会话冻结:首次裁剪结果
// 入冻结集,后续轮只增不减(新意图带来的工具并入,已并入的不移除)。
// 顺序稳定:冻结集是 all_defs 的子集过滤,all_defs 顺序恒定 → 数组字节只增不改位。
let mut frozen_names = {
let session = session_arc.lock().await;
session
.conv_read(&conv_id)
.and_then(|c| c.frozen_tool_names.clone())
};
let mut changed = false;
match frozen_names.as_mut() {
Some(prev) => {
for d in &computed {
if !prev.contains(&d.function.name) {
prev.push(d.function.name.clone());
changed = true;
}
}
}
None => {
if !computed.is_empty() {
frozen_names = Some(computed.iter().map(|d| d.function.name.clone()).collect());
changed = true;
}
}
}
if changed {
let mut session = session_arc.lock().await;
// contains_key 守卫防 conv() 惰性建(循环入口已桥接,正常恒存在,防御性保留)。
if session.per_conv.contains_key(&conv_id) {
session.conv(&conv_id).frozen_tool_names = frozen_names.clone();
}
}
match &frozen_names {
Some(names) => all_defs
.into_iter()
.filter(|d| names.contains(&d.function.name))
.collect(),
None => computed,
}
};
tracing::info!(
conv_id = %conv_id,
@@ -1294,10 +1338,12 @@ async fn prepare_round_inputs(
"── 用户已切换话题(从「{}」到「{}」),请以新话题为准 ──",
old_topic, new_topic
);
// 尾部追加而非 insert_at(0):头部插入位移整条前缀破坏 prompt 缓存,
// 语义「提示当前轮」放末尾同样生效(紧邻本轮内容,LLM 必见)。
let mut session = session_arc.lock().await;
if session.per_conv.contains_key(conv_id) {
let conv = session.conv(conv_id);
conv.messages.insert_at(0, ChatMessage::system(&marker_text));
conv.messages.push(ChatMessage::system(&marker_text));
tracing::info!(
conv_id = %conv_id,
iteration,
@@ -1429,15 +1475,26 @@ async fn prepare_round_inputs(
// 故即使摘要有误/过度压缩,下次 build_for_request 仍从 DB 全量重建,可自愈。
//
// TOOL_RESULT_COMPRESS_ENABLED=false(常量开关)→ 跳过(原样送 LLM,排障/对比用)。
// 压缩决策经 per_conv.tool_view_decisions 冻结:首次判定后沿用,不再随阈值翻转
// (防同一条历史跨轮在全文/摘要间翻转致前缀断)。写回仅在 conv 存活时做(防惰性建)。
let messages: Vec<ChatMessage> = if TOOL_RESULT_COMPRESS_ENABLED {
let history_tokens_snapshot: u32 = {
let (history_tokens_snapshot, mut view_decisions) = {
let session = session_arc.lock().await;
session
.conv_read(conv_id)
.map(|c| c.messages.history_tokens())
.unwrap_or(0)
let conv = session.conv_read(conv_id);
(
conv.map(|c| c.messages.history_tokens()).unwrap_or(0),
conv.map(|c| c.tool_view_decisions.clone()).unwrap_or_default(),
)
};
summarize_tool_results(messages, history_tokens_snapshot, conv_id, iteration)
let messages =
summarize_tool_results(messages, history_tokens_snapshot, &mut view_decisions, conv_id, iteration);
if !view_decisions.is_empty() {
let mut session = session_arc.lock().await;
if session.per_conv.contains_key(conv_id) {
session.conv(conv_id).tool_view_decisions = view_decisions;
}
}
messages
} else {
messages
};
@@ -1767,7 +1824,7 @@ async fn heartbeat_loop(
/// 注入目标验证续轮 nudge(强制续轮,非软提示)。
///
/// 文案随 goal_verify_rounds 递进:第 1 次请验证,第 2 次(将达上限)明确最后机会。
/// 复用 insert_stall_warning 的 insert_at(0, system) 范式 + fetch_goal_summary 拼目标。
/// 复用 insert_stall_warning 的尾部追加 system 范式 + fetch_goal_summary 拼目标。
async fn insert_goal_verify_nudge(
session_arc: &Arc<Mutex<AiSession>>,
conv_id: &str,
@@ -1789,7 +1846,9 @@ async fn insert_goal_verify_nudge(
let mut session = session_arc.lock().await;
if session.per_conv.contains_key(conv_id) {
let conv = session.conv(conv_id);
conv.messages.insert_at(0, ChatMessage::system(&nudge_text));
// 尾部追加而非 insert_at(0):nudge 紧邻本轮内容语义不变,且不位移既有前缀字节
// (insert_at(0) 会把整条历史后移,自动前缀缓存/Anthropic 显式缓存全失效)。
conv.messages.push(ChatMessage::system(&nudge_text));
tracing::info!(
conv_id = %conv_id, iteration, verify_rounds,
"[GOAL-VERIFY] 目标未验证,注入续轮 nudge(强制续轮)"