修复: 压缩边界对齐与断路器学习语义
- 压缩边界按对话单元回退,不再劈开工具对 - 停止与接管退出补跳过结果,畸形不再新增 - 失败指纹纳入参数,改参重试不再累计熔断 - 成功断连失败段,红绿循环不再误熔断 - 脚本非零退出豁免,同命令死磕由签名熔断兜底 - 熔断前加自纠警示轮,临时路径写删豁免
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user