修复+重构: 全库走查真bug+架构+P1/P2 后端 crate
- df-nodes: schema required 对齐 + docker POSIX 注入防御 + HumanNode timeout 1800 + parse_review_json(verdict规范/score clamp/正则兜底) - df-mcp: update 实体校验(防跨实体 B-260801-01) - df-storage: keyring 迁移失败达阈值清除明文 - df-ai: router estimated_context+tier tiebreak+DataReadOnly 兜底 + sanitize step4 显式不制造 orphan - df-ideas: adversarial tier:None 对齐
This commit is contained in:
@@ -358,16 +358,50 @@ pub fn assert_placeholder_pairing(
|
||||
/// 注:连续同 role(user/user、assistant/assistant)现实极少——裁剪按三元组原子保护不产生连续 user,
|
||||
/// archived/compressed 过滤后由摘要 system 占位——故本轮不合并(合并会破坏裁剪保护区语义 + 改变条数,
|
||||
/// 致 over_budget_trims_old 等测试失败)。若运行时日志显示连续 role 也是 1214 来源,再补合并。
|
||||
///
|
||||
/// **不制造 orphan(自洽,根本修)**:开头 skip assistant 头时,**显式连带 skip 其后命中的 tool_result**
|
||||
/// (id 在被 skip 头的 tool_calls 内)。旧实现依赖隐式巧合——367 行 `Assistant | Tool` 联合判断 +
|
||||
/// `fixed.is_empty()` 在整组 skip 期间保持 true,使开头整组(head + result)被一起丢。这个契约脆弱:
|
||||
/// (1) 若有人改开头 skip 只针对 Assistant(为修连续 role),立刻制造 orphan;
|
||||
/// (2) step3.5(`drop_reverse_orphans`)在 step4 前跑,无法预见 step4 自身 skip 制造的 orphan;
|
||||
/// (3) memory `ai-router-sanitize-refactor-debt` 走查标 P1 隐式契约债。
|
||||
/// 根本修:step4 主动跟踪自己 skip 掉的 head 的 tool_call.id,后续 tool_result 命中即连带丢,
|
||||
/// 把"不制造 orphan"从隐式巧合变为显式机制——不依赖下游 `assert_placeholder_pairing` 出口断言兜底。
|
||||
/// 保留 367 行"开头 tool_result 也 skip"(防御性兜底:防 step3.5 未跑/开关关闭时孤儿 result 漏网)。
|
||||
pub fn ensure_sequence_legal(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
use std::collections::HashSet;
|
||||
let mut skipped = 0u32;
|
||||
let mut merged = 0u32;
|
||||
// 被 step4 自身 skip 掉的 assistant 头的 tool_call.id 集合。
|
||||
// 后续 tool_result 若命中(id 在此集合),说明其头被 step4 skip 掉了 → 连带丢(不制造 orphan)。
|
||||
let mut skipped_head_ids: HashSet<String> = HashSet::new();
|
||||
let mut fixed: Vec<ChatMessage> = Vec::with_capacity(messages.len());
|
||||
for m in messages {
|
||||
// 首条必须 user:skip 开头 assistant/tool(无前置 user 的孤儿)
|
||||
// 首条必须 user:skip 开头 assistant/tool(无前置 user 的孤儿)。
|
||||
// assistant 头被 skip 时,记录其 tool_call.id,后续 result 命中即连带丢(根本修:不制造 orphan)。
|
||||
if fixed.is_empty() && matches!(m.role, MessageRole::Assistant | MessageRole::Tool) {
|
||||
if matches!(m.role, MessageRole::Assistant) {
|
||||
if let Some(calls) = m.tool_calls.as_ref() {
|
||||
for c in calls {
|
||||
skipped_head_ids.insert(c.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
// 自洽兜底(根本修):tool_result 的 id 命中被 skip 头集合 → 其头已被 step4 自身丢,
|
||||
// 留下即 orphan(直送 provider 400)。显式连带丢,不依赖下游出口断言兜底。
|
||||
// 注:仅丢"step4 自己制造的 orphan",step3.5 已补头的 result 不受影响(其头在 fixed 中,id 不在集合)。
|
||||
if matches!(m.role, MessageRole::Tool) {
|
||||
if m.tool_call_id
|
||||
.as_deref()
|
||||
.is_some_and(|id| skipped_head_ids.contains(id))
|
||||
{
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 连续同 role 合并(user content;assistant content+tool_calls;Tool 不合并——
|
||||
// 连续 tool_result 由 anthropic_compat flush_tool_results 合并成 user blocks,此处合会丢 id)
|
||||
if let Some(last) = fixed.last_mut() {
|
||||
@@ -394,7 +428,7 @@ pub fn ensure_sequence_legal(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
tracing::warn!(
|
||||
skipped,
|
||||
merged,
|
||||
"序列修复:skip 开头非 user + 合并连续同 role(view-only,避免 Anthropic/GLM 1214)"
|
||||
"序列修复:skip 开头非 user(含连带丢 orphan result)+ 合并连续同 role(view-only,避免 Anthropic/GLM 1214)"
|
||||
);
|
||||
}
|
||||
fixed
|
||||
@@ -1017,4 +1051,157 @@ mod tests {
|
||||
"result id 应与补头 id 一致(闭合配对)"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// step4 不制造 orphan(根本修:显式整组 skip,不依赖下游出口断言)
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn ensure_sequence_legal_skips_leading_assistant_head_with_its_tool_result() {
|
||||
// 根本修核心场景( memory ai-router-sanitize-refactor-debt P1 隐式契约债):
|
||||
// 开头 user 被 step0/裁剪裁掉后,序列以合法 assistant 工具三元组头开头:
|
||||
// [asst(head, tc_a)] [tool_result(tc_a)] [user "实问"]
|
||||
// step4 skip 开头 assistant 头(首条非 user)→ 必须连带 skip 其后的 tool_result(tc_a),
|
||||
// 否则 tool_result 无头成 orphan(直送 provider 400)。
|
||||
//
|
||||
// 旧实现依赖隐式巧合:367 行 `Assistant | Tool` 联合判断 + fixed.is_empty() 整组保持 true,
|
||||
// 使开头 tool_result 也被 skip。本测验证根本修后**显式机制**(跟踪被 skip head 的 id,
|
||||
// result 命中即连带丢)——即使将来有人改 367 行只 skip Assistant,本测仍通过(连带丢兜底)。
|
||||
let msgs = vec![
|
||||
ChatMessage::assistant_with_tools(
|
||||
"调用工具",
|
||||
vec![ToolCall::new("tc_a", "read_file", "{}")],
|
||||
),
|
||||
ChatMessage::tool_result("tc_a", "工具结果"),
|
||||
ChatMessage::user("实问"),
|
||||
];
|
||||
let fixed = ensure_sequence_legal(msgs);
|
||||
|
||||
// 输出仅剩首条 user(开头三元组整组丢,不留 orphan tool_result)
|
||||
assert_eq!(
|
||||
fixed.len(),
|
||||
1,
|
||||
"开头 asst(head)+tool_result 应整组 skip, 实际 {} 条: {:?}",
|
||||
fixed.len(),
|
||||
fixed.iter().map(|m| format!("{:?}", m.role)).collect::<Vec<_>>()
|
||||
);
|
||||
assert!(matches!(fixed[0].role, MessageRole::User));
|
||||
assert_eq!(fixed[0].content, "实问");
|
||||
// 核心断言:输出中不得有任何 tool_result(无 orphan)
|
||||
let tool_count = fixed
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::Tool))
|
||||
.count();
|
||||
assert_eq!(tool_count, 0, "step4 不应留下 orphan tool_result");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_sequence_legal_no_orphan_when_leading_user_dropped_mid_sequence() {
|
||||
// 根本修进阶场景:开头 user 被裁 + 中间还有合法三元组(其 head 也在开头被 skip)。
|
||||
// 序列:[asst(head1, tc_a)] [tool(tc_a)] [asst(head2, tc_b)] [tool(tc_b)] [user "问"]
|
||||
// 旧隐式契约:整组都 skip(fixed 全程空,所有 Assistant|Tool 命中 367)。
|
||||
// 根本修后:head1 skip(记 tc_a),tc_a 连带丢(命中集合);head2 skip(记 tc_b),tc_b 连带丢。
|
||||
// 结果:只剩 [user "问"]。验证多组三元组在开头被整组 skip 不留 orphan。
|
||||
let msgs = vec![
|
||||
ChatMessage::assistant_with_tools(
|
||||
"调1",
|
||||
vec![ToolCall::new("tc_a", "fn_a", "{}")],
|
||||
),
|
||||
ChatMessage::tool_result("tc_a", "结果A"),
|
||||
ChatMessage::assistant_with_tools(
|
||||
"调2",
|
||||
vec![ToolCall::new("tc_b", "fn_b", "{}")],
|
||||
),
|
||||
ChatMessage::tool_result("tc_b", "结果B"),
|
||||
ChatMessage::user("问"),
|
||||
];
|
||||
let fixed = ensure_sequence_legal(msgs);
|
||||
assert_eq!(
|
||||
fixed.len(),
|
||||
1,
|
||||
"开头两组三元组应整组 skip, 实际 {:?}",
|
||||
fixed.iter().map(|m| format!("{:?}", m.role)).collect::<Vec<_>>()
|
||||
);
|
||||
assert!(matches!(fixed[0].role, MessageRole::User));
|
||||
// 不留任何 orphan tool_result
|
||||
let tool_count = fixed
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::Tool))
|
||||
.count();
|
||||
assert_eq!(tool_count, 0, "不应留下 orphan tool_result");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_sequence_legal_preserves_well_formed_triplet_after_user() {
|
||||
// 根本修回归保护:user 在前 + 完整合法三元组在后 → 三元组原样保留(不被误丢)。
|
||||
// 验证根本修的"连带丢 orphan"只针对"被 step4 自己 skip 的头",
|
||||
// 不影响 user 之后正常三元组(其 head 进 fixed 不被 skip,id 不入 skipped_head_ids)。
|
||||
let msgs = vec![
|
||||
ChatMessage::user("开场"),
|
||||
ChatMessage::assistant_with_tools(
|
||||
"调",
|
||||
vec![ToolCall::new("tc_ok", "fn", "{}")],
|
||||
),
|
||||
ChatMessage::tool_result("tc_ok", "结果"),
|
||||
ChatMessage::user("收尾"),
|
||||
];
|
||||
let fixed = ensure_sequence_legal(msgs);
|
||||
// 全保留:首条 user 已合法,后续三元组闭合,无 skip 无合并
|
||||
assert_eq!(
|
||||
fixed.len(),
|
||||
4,
|
||||
"合法序列应原样保留, 实际 {} 条",
|
||||
fixed.len()
|
||||
);
|
||||
assert!(matches!(fixed[0].role, MessageRole::User));
|
||||
assert!(matches!(fixed[1].role, MessageRole::Assistant));
|
||||
assert!(matches!(fixed[2].role, MessageRole::Tool));
|
||||
assert!(matches!(fixed[3].role, MessageRole::User));
|
||||
// tool_result 保留(配对闭合,非 orphan)
|
||||
let tool = fixed
|
||||
.iter()
|
||||
.find(|m| matches!(m.role, MessageRole::Tool))
|
||||
.expect("合法三元组的 tool_result 应保留");
|
||||
assert_eq!(tool.tool_call_id.as_deref(), Some("tc_ok"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_messages_step4_self_contained_no_orphan_without_exit_assert() {
|
||||
// 端到端根本修验证(不经出口断言):模拟"开头 user 被裁 + 直接调 sanitize_messages"
|
||||
// (即用户担心的"若直接调 sanitize_messages 不经出口,orphan 直送 provider 400"路径)。
|
||||
// 根本修后:step4 自洽,sanitize_messages 输出本身不含 step4 制造的 orphan,
|
||||
// 不依赖调用方再跑 assert_placeholder_pairing 兜底。
|
||||
//
|
||||
// 场景:step3.5(PLACEHOLDER_INTEGRITY_ENABLED=true)跑过后,开头三元组闭合,
|
||||
// 但 step4 skip 开头 assistant 头 → 根本修连带丢其 result。
|
||||
// 输入模拟"开头 user 被 step0 过滤掉":序列以 assistant head 开头。
|
||||
let orphan_inducing = vec![
|
||||
ChatMessage::assistant_with_tools(
|
||||
"调工具",
|
||||
vec![ToolCall::new("tc_step4", "fn", "{}")],
|
||||
),
|
||||
ChatMessage::tool_result("tc_step4", "工具结果"),
|
||||
ChatMessage::user("后续问题"),
|
||||
];
|
||||
let sanitized = sanitize_messages(orphan_inducing);
|
||||
|
||||
// 核心断言:sanitize_messages 输出无 orphan tool_result(不依赖出口断言)
|
||||
let orphan_tool_results: Vec<_> = sanitized
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::Tool))
|
||||
.collect();
|
||||
assert!(
|
||||
orphan_tool_results.is_empty(),
|
||||
"step4 根本修:sanitize_messages 输出不应含 orphan tool_result(自洽,不依赖出口断言), 实际 {:?}",
|
||||
sanitized.iter().map(|m| format!("{:?}", m.role)).collect::<Vec<_>>()
|
||||
);
|
||||
// 首条必须是 user(step4 序列修复后)
|
||||
assert!(
|
||||
sanitized
|
||||
.first()
|
||||
.is_some_and(|m| matches!(m.role, MessageRole::User)),
|
||||
"首条必须是 user, 实际 {:?}",
|
||||
sanitized.first().map(|m| format!("{:?}", m.role))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user