优化: 供应商级采样温度覆盖配置

受限模型(经路由透传)仅接受特定 temperature 其它值 400;
provider config JSON 带 temperature 字段则覆盖默认,未配置走 0.7,设置页可编辑。
This commit is contained in:
lxy
2026-09-29 13:33:49 +08:00
parent 7cb32d1380
commit 378c363632
5 changed files with 155 additions and 17 deletions
+117 -16
View File
@@ -342,7 +342,7 @@ use context_lifecycle::maybe_auto_compress;
mod title_lifecycle;
#[allow(unused_imports)]
mod knowledge_lifecycle;
mod workflow_context;
pub(crate) mod workflow_context;
// ============================================================
// L2 统一状态机(ConvState enum + 转换守卫,单一真相源)。
@@ -360,7 +360,10 @@ pub mod command_lock;
pub(crate) mod helpers;
pub(crate) use helpers::try_continue_agent_loop;
pub(crate) use helpers::infer_goal_from_tool_calls;
pub(crate) use helpers::{tool_call_signature, is_repetitive_exploration, is_pure_greeting, is_tool_call_over_limit};
pub(crate) use helpers::{
tool_call_signature, is_repetitive_exploration, is_pure_greeting, is_tool_call_over_limit,
DestructiveLoopSample, destructive_loop_sample, detect_destructive_loop,
};
// ============================================================
// 单 Provider 流式结果 + fallback 辅助
@@ -490,11 +493,21 @@ async fn stream_with_retry(
resolved_model: &str,
last_reasoning_content: &Option<String>,
) -> StreamOutcome {
// 供应商级采样温度覆盖:provider config JSON 带 "temperature" 字段则用之,
// 治受限模型(如 Kimi 系经 u-switch 透传)仅接受 temperature=1、其它值 400 的问题;
// 未配置/非法 JSON 走默认 0.7。candidate 在重试循环内不变,解析提到循环外只做一次。
let temperature = candidate
.config
.as_deref()
.and_then(|cfg| serde_json::from_str::<serde_json::Value>(cfg).ok())
.and_then(|v| v.get("temperature").and_then(|t| t.as_f64()))
.map(|t| t as f32)
.or(Some(0.7));
for retry_attempt in 0..=max_retries {
let retry_request = CompletionRequest {
model: resolved_model.to_string(),
messages: messages.to_vec(),
temperature: Some(0.7),
temperature,
max_tokens: Some(8192),
stream: true,
tools: if tool_defs.is_empty() { None } else { Some(tool_defs.to_vec()) },
@@ -1367,6 +1380,11 @@ pub(crate) async fn run_agentic_loop(
let mut stall_count: u32 = 0;
let mut stall_warned: bool = false;
// G2 破坏性回环计数器(独立于 stall_count,互不干扰):写删同路径回环对检出累计,
// 治「写完就删」自毁循环(签名范式对写删交替漏网)。警示/阈值复用 STALL_BREAKER_*。
let mut loop_count: u32 = 0;
let mut loop_warned: bool = false;
// 目标验证续轮计数(GOAL_VERIFY_ENABLED 门控):末轮无 tool_calls 但 pinned_goals 未达成
// 且无 applied.matched_target 证据时强制续轮,每续 +1。达 GOAL_VERIFY_MAX_ROUNDS 强制收敛
// + AiCompleted 标注 goal_unverified。续轮内调了工具则重置(说明 LLM 在行动非空转)。
@@ -2079,12 +2097,14 @@ pub(crate) async fn run_agentic_loop(
// G2 探索熔断:连续空结果无进展检查。抽至 check_stall_breaker helper(扁平重构)。
// 返回 true=已熔断(loop 应 return),false=继续。stall_count/stall_warned 传入传出。
if STALL_BREAKER_ENABLED {
let (tripped, new_count, new_warned) = check_stall_breaker(
let (tripped, new_count, new_warned, new_loop_count, new_loop_warned) = check_stall_breaker(
&session_arc, &app_handle, &conv_id, iteration,
stall_count, stall_warned,
stall_count, stall_warned, loop_count, loop_warned,
).await;
stall_count = new_count;
stall_warned = new_warned;
loop_count = new_loop_count;
loop_warned = new_loop_warned;
if tripped {
guard.reset().await;
return;
@@ -2533,15 +2553,20 @@ fn emit_circuit_breaker_tripped(app_handle: &AppHandle, conv_id: &str, max_count
}
// ── check_stall_breaker: G2 探索熔断检查(扁平抽自原嵌套 6 层块) ──
// 返回 (tripped, new_stall_count, new_stall_warned):
// 返回 (tripped, new_stall_count, new_stall_warned, new_loop_count, new_loop_warned):
// - tripped=true → 已 emit AiHelpRequired 且 loop 应 return(调用方需先 guard.reset 再 return)
// - tripped=false → 正常继续,新 stall_count/warned 供调用方回写。
// - tripped=false → 正常继续,新 stall/loop 计数与警示位供调用方回写。
//
// 2026-08-01 根本性重构:判定从「末尾 Tool 结果是否空(is_empty_tool_result)」改为
// 「最近 N 个 assistant tool_calls 签名是否重复(tool_call_signature + is_repetitive_exploration)」。
// 治旧范式误熔断正常排除式搜索(grep 无匹配是有效排除信号,非漂移)。熔断骨架(计数/警示/
// 阈值/emit/return)不变,只换判定输入。
//
// 2026-09-29 补破坏性回环检测:签名范式对「写完就删」自毁循环漏网(write_file/delete_file
// 走签名兜底分支,写删交替每步签名都不同,唯一率≈0.83 不触发)。采样处并行维护
// (工具, 路径) 结构化缓冲喂 detect_destructive_loop,独立计数器与 stall 并存互不干扰,
// 两段式(警示/阈值)复用 STALL_BREAKER_WARN_FIRST/THRESHOLD。
//
// 嵌套 ≤ 3 层:取签名→计数/警示→达阈值 emit,各段早返回。
async fn check_stall_breaker(
session_arc: &Arc<Mutex<AiSession>>,
@@ -2550,11 +2575,14 @@ async fn check_stall_breaker(
iteration: usize,
stall_count: u32,
stall_warned: bool,
) -> (bool, u32, bool) {
loop_count: u32,
loop_warned: bool,
) -> (bool, u32, bool, u32, bool) {
// 1) 取最近 N 个 assistant tool_calls 签名:倒序扫 messages,遇 Assistant 且有 tool_calls
// 就抽取每工具签名(name + arguments JSON parse),累积到 STALL_BREAKER_SAMPLE_SIZE 个。
// arguments 非合法 JSON 时 to_string 兜底(签名仍可比,只是兜底全量)。
let signatures: Vec<String> = {
// 同时并行抽取回环样本(写/删工具记路径,其他占位),两缓冲同序位对齐。
let (signatures, loop_samples): (Vec<String>, Vec<DestructiveLoopSample>) = {
let session = session_arc.lock().await;
// BE2只取尾部 BREAKER_TAIL_N 条(签名抽取只需最近几轮工具调用,
// 全量 clone 纯浪费;STALL_BREAKER_SAMPLE_SIZE=12 ≪ 40,样本充足)。
@@ -2563,6 +2591,7 @@ async fn check_stall_breaker(
None => Vec::new(),
};
let mut sigs: Vec<String> = Vec::new();
let mut loops: Vec<DestructiveLoopSample> = Vec::new();
for m in messages.iter().rev() {
if sigs.len() >= STALL_BREAKER_SAMPLE_SIZE {
break;
@@ -2579,18 +2608,20 @@ async fn check_stall_breaker(
let args: serde_json::Value =
serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::Value::Null);
sigs.push(tool_call_signature(&tc.function.name, &args));
loops.push(destructive_loop_sample(&tc.function.name, &args));
}
}
}
// 倒序收集的签名需反转为时间正序,保持 is_repetitive_exploration 的语义稳定
// (虽纯函数对顺序不敏感,但反转后日志/调试更直观)。
// (虽纯函数对顺序不敏感,但反转后日志/调试更直观)。回环检测依赖序位间隔,必须正序。
sigs.reverse();
sigs
loops.reverse();
(sigs, loops)
};
// 本轮无工具调用(纯文本轮或首轮)→ 不计入也不重置,保持 stall_count。
// 本轮无工具调用(纯文本轮或首轮)→ 不计入也不重置,保持 stall_count/loop_count。
if signatures.is_empty() {
return (false, stall_count, stall_warned);
return (false, stall_count, stall_warned, loop_count, loop_warned);
}
// 2) 签名重复判定 → 更新 stall_count / stall_warned(沿用原 reset 语义)
@@ -2602,6 +2633,16 @@ async fn check_stall_breaker(
new_warned = false;
}
// 2.5) 破坏性回环判定(独立计数器):窗口内有同路径写删回环对 → 累计,无 → 清零重来。
let (mut new_loop_count, mut new_loop_warned) = (loop_count, loop_warned);
let loop_hit_path = detect_destructive_loop(&loop_samples);
if loop_hit_path.is_some() {
new_loop_count += 1;
} else {
new_loop_count = 0;
new_loop_warned = false;
}
// 3) 两段式警示:达 THRESHOLD-1 首次警示(给 LLM 自纠机会)
if STALL_BREAKER_WARN_FIRST
&& new_count == STALL_BREAKER_THRESHOLD.saturating_sub(1)
@@ -2611,9 +2652,40 @@ async fn check_stall_breaker(
insert_stall_warning(session_arc, conv_id, iteration, new_count).await;
}
// 4) 熔断:达 THRESHOLD → emit AiHelpRequired + 返回 tripped=true
// 3.5) 回环两段式警示(对齐 stall WARN_FIRST,独立警示位)
if STALL_BREAKER_WARN_FIRST
&& new_loop_count == STALL_BREAKER_THRESHOLD.saturating_sub(1)
&& !new_loop_warned
{
new_loop_warned = true;
insert_destructive_loop_warning(session_arc, conv_id, iteration, new_loop_count, loop_hit_path.as_deref()).await;
}
// 4) 回环熔断:独立达 THRESHOLD → emit(与 stall 熔断谁先达谁触发)
if new_loop_count >= STALL_BREAKER_THRESHOLD {
tracing::warn!(
conv_id = %conv_id,
loop_count = new_loop_count,
hit_path = %loop_hit_path.as_deref().unwrap_or("?"),
sample_size = loop_samples.len(),
"[ai] G2 破坏性回环熔断:连续 {} 轮检测到同路径写删回环,疑似自毁循环停止", new_loop_count
);
let path_desc = loop_hit_path.as_deref().unwrap_or("未知路径");
let _ = app_handle.emit(
"ai-chat-event",
AiChatEvent::AiHelpRequired {
reason: format!("连续 {} 轮检测到对同一文件({})反复写删,疑似「写完就删」自毁循环已停止", new_loop_count, path_desc),
context: "写文件后又删除同一文件的紧凑交替,可能在反复销毁自己的工作成果而无进展。".into(),
options: vec!["回顾目标".into(), "换思路".into(), "停止".into()],
conversation_id: Some(conv_id.to_string()),
},
);
return (true, new_count, new_warned, new_loop_count, new_loop_warned);
}
// 5) stall 熔断:达 THRESHOLD → emit AiHelpRequired + 返回 tripped=true
if new_count < STALL_BREAKER_THRESHOLD {
return (false, new_count, new_warned);
return (false, new_count, new_warned, new_loop_count, new_loop_warned);
}
tracing::warn!(
conv_id = %conv_id,
@@ -2630,7 +2702,36 @@ async fn check_stall_breaker(
conversation_id: Some(conv_id.to_string()),
},
);
(true, new_count, new_warned)
(true, new_count, new_warned, new_loop_count, new_loop_warned)
}
// ── insert_destructive_loop_warning: 破坏性回环软提示 insert system 消息(对齐 insert_stall_warning) ──
async fn insert_destructive_loop_warning(
session_arc: &Arc<Mutex<AiSession>>,
conv_id: &str,
iteration: usize,
loop_count: u32,
path: Option<&str>,
) {
let goal_text = if STALL_BREAKER_GOAL_REMIND {
fetch_goal_summary(session_arc, conv_id).await
} else {
String::new()
};
let path_desc = path.unwrap_or("同一文件");
let warn_text = format!(
"⚠ 检测到对 {} 反复写删(写完又删的回环),可能在销毁自己的工作成果{},请回顾目标停止删除或换思路。",
path_desc, goal_text
);
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(&warn_text));
tracing::info!(
conv_id = %conv_id, iteration, loop_count,
"[ai] G2 破坏性回环:写删回环警示已 insert(软提示,给 LLM 自纠机会)"
);
}
}
// ── insert_stall_warning: G2 软提示 insert system 消息(给 LLM 自纠机会) ──