修复: 压缩边界对齐与断路器学习语义

- 压缩边界按对话单元回退,不再劈开工具对
- 停止与接管退出补跳过结果,畸形不再新增
- 失败指纹纳入参数,改参重试不再累计熔断
- 成功断连失败段,红绿循环不再误熔断
- 脚本非零退出豁免,同命令死磕由签名熔断兜底
- 熔断前加自纠警示轮,临时路径写删豁免
This commit is contained in:
lxy
2026-10-01 19:35:22 +08:00
parent b8416dea63
commit 4417b3ddd3
4 changed files with 416 additions and 47 deletions
+84 -2
View File
@@ -618,11 +618,24 @@ impl ContextManager {
/// 也只扣首次标记的 token。返回的 Vec 仅含**本次新标记**的消息(已 compressed 的不返)。
///
/// **单向不可逆**:压缩后 DB 原始消息保留,但 LLM 上下文里被 is_active 白名单隔离
/// (sanitize step0 过滤)。`compress_end` 越界自动 clamp 到 `messages.len()`。
/// (sanitize step0 过滤)。`compress_end` 越界自动 clamp 到 `messages.len()`,且实际
/// 压缩边界会回退到最近的对话单元边界(三元组不劈开,见方法内注释)。
///
/// 返回空 Vec 表示本批次无可压缩消息(全部已 compressed 或范围空),调用方据此跳过 LLM 调用。
pub fn compress_old_messages(&mut self, compress_end: usize) -> Vec<ChatMessage> {
let end = compress_end.min(self.messages.len());
// 压缩边界按对话单元(三元组)对齐:调用方传入的 end(如 len - PROTECT_COUNT)周期性
// 落在单元中间(assistant 头与其 tool 结果之间),只压半个三元组会让保护区以孤立的
// 头或尾开头,成为 sanitize 每请求告警的确定性供给。按全长分组取「完整单元边界中
// 不超过 raw_end 的最大者」回退(只少压不多压,不侵占保护区);三元组要么整段压缩
// 要么整段保留。空范围/空历史无单元,取 0。
let raw_end = compress_end.min(self.messages.len());
let end = self
.build_eviction_units(self.messages.len())
.iter()
.map(|u| u.end)
.take_while(|&e| e <= raw_end)
.last()
.unwrap_or(0);
let mut newly_compressed = Vec::new();
for t in self.messages[..end].iter_mut() {
if t.message.is_active() {
@@ -656,3 +669,72 @@ impl ContextManager {
#[cfg(test)]
mod manager_tests;
#[cfg(test)]
mod compress_boundary_tests {
use super::*;
use crate::provider::ToolCall;
fn mgr() -> ContextManager {
ContextManager::new(ContextConfig {
max_tokens: 100_000,
..Default::default()
})
}
/// 构造 4 个周期轮次,每轮 [user, assistant(tool_calls), tool_result, assistant]:
/// user 独占一个单元,后三条(头+尾+紧随独立 assistant)合并为一个单元,
/// 单元边界落在 4/8/12/16。
fn periodic_rounds() -> ContextManager {
let mut m = mgr();
for r in 0..4 {
m.push(ChatMessage::user(&format!("问题 {}", r)));
m.push(ChatMessage::assistant_with_tools(
"",
vec![ToolCall::new(format!("c{}", r), "run_command", "{}")],
));
m.push(ChatMessage::tool_result(format!("c{}", r), "ok"));
m.push(ChatMessage::assistant("小结"));
}
m
}
#[test]
fn compress_end_mid_unit_retreats_to_unit_boundary() {
let mut m = periodic_rounds();
// len=16,调用方口径 len - PROTECT_COUNT(6) = 10,落在第 3 轮三元组中间
// (9 头 / 10 尾 / 11 独立)。旧行为会把头压掉却留下孤立的 tool 结果。
// 回退到完整单元边界 9:只多压前面的 user,不劈开三元组(只少压不多压)。
let compressed = m.compress_old_messages(10);
assert_eq!(compressed.len(), 9, "边界回退到完整单元边界 9");
let msgs = m.messages_mut();
for i in 9..16 {
assert!(msgs[i].message.is_active(), "第 {} 条应保持 active", i);
}
// 关键回归点:第 3 轮三元组(9 头 / 10 尾 / 11 独立)整体保持 active,头未被劈开
assert!(msgs[9].message.is_active(), "三元组头不应被单独压缩");
assert!(msgs[10].message.is_active(), "三元组尾不应残留 active 而头被压");
assert!(msgs[11].message.is_active());
assert_eq!(msgs[9].group, MessageGroup::ToolCallHead);
}
#[test]
fn compress_end_on_boundary_unchanged() {
let mut m = periodic_rounds();
// end=8 恰为单元边界,行为与对齐前一致(整 8 条压缩)。
let compressed = m.compress_old_messages(8);
assert_eq!(compressed.len(), 8);
let msgs = m.messages_mut();
assert!(msgs[..8].iter().all(|t| !t.message.is_active()));
assert!(msgs[8..].iter().all(|t| t.message.is_active()));
}
#[test]
fn compress_end_oversized_still_compresses_all() {
let mut m = periodic_rounds();
// 越界 clamp 到 len:全量单元分组后末单元 end=len,语义与改动前一致。
let compressed = m.compress_old_messages(999);
assert_eq!(compressed.len(), 16);
assert_eq!(m.history_tokens(), 0);
}
}