修复: 消息三元组完整性(sanitize 补头自愈 orphan tool_result,治 AI 看不到工具结果致静默停)

This commit is contained in:
lxy
2026-08-01 16:34:33 +08:00
parent 2b08e32c26
commit 1a5d543fa1
2 changed files with 400 additions and 84 deletions
+57
View File
@@ -162,6 +162,63 @@ fn replace_tool_result_updates_tokens() {
assert!(after > before);
}
// ============================================================
// 三元组完整性 P1(根治):裁剪/压缩必须保证三元组原子性,
// 不出现 tool_result 残留但 tool_call 头被裁的 orphan(根因:AI Coding 静默停)。
// ============================================================
#[test]
fn trim_never_produces_orphan_tool_result_without_head() {
// 三元组完整性 P1 回归:超预算裁剪后,发送视图中**任何 tool_result 的 tool_call_id 都必须
// 有对应 assistant tool_call 头**(不残留 orphan result)。裁剪按 build_eviction_units 的
// 三元组原子单元(Head+Tail+Standalone Assistant 同进同出),trim_end 始终落在单元边界,
// 永不切断三元组。
//
// 布局:5 旧 user/assistant 文本(淘汰区) + 完整三元组(tc_mid) + 6 新(保护区) = 14 条
// 强制小预算使裁剪切到三元组边界(整体保留或整体丢弃,不在中间切)。
let mut mgr = ContextManager::new(cfg(120));
for i in 0..5 {
mgr.push(ChatMessage::user(&format!("旧消息 {} 用于撑爆预算的较长文本", i)));
}
// 完整三元组(裁剪边界附近)
mgr.push(ChatMessage::assistant_with_tools(
"调中间工具",
vec![ToolCall::new("tc_mid", "read_file", "{}")],
));
mgr.push(ChatMessage::tool_result("tc_mid", "中间工具结果"));
mgr.push(ChatMessage::assistant("中间完成"));
// 保护区(最近 PROTECT_COUNT=6 条)
for i in 0..6 {
mgr.push(ChatMessage::user(&format!("新消息 {} 保护区", i)));
}
let (msgs, trimmed) = mgr.build_for_request(0);
assert!(trimmed, "应触发裁剪");
// 收集发送视图中所有 assistant 头的 tool_call.id
use std::collections::HashSet;
let head_ids: HashSet<String> = msgs
.iter()
.filter(|m| matches!(m.role, MessageRole::Assistant))
.filter_map(|m| m.tool_calls.as_ref())
.flatten()
.map(|c| c.id.clone())
.collect();
// 每个 tool_result 的 tool_call_id 都必须在 head_ids 内(不残留 orphan result)
let orphans: Vec<&str> = msgs
.iter()
.filter(|m| matches!(m.role, MessageRole::Tool))
.filter_map(|m| m.tool_call_id.as_deref())
.filter(|id| !head_ids.contains(*id))
.collect();
assert!(
orphans.is_empty(),
"裁剪后不应有 orphan tool_result(无对应头), 实际 orphans={:?}, heads={:?}",
orphans, head_ids
);
}
#[test]
fn restore_rebuilds_token_cache() {
let mut mgr = ContextManager::new(cfg(100_000));