修复: 上下文流式与工具签名域清债
- tool_signature 归一化补参,合法换参不再误触探索熔断 - SSE 超限截头改对齐事件边界保尾,流不再永久破损 - content_filter 纳入终态判定,无 DONE 流也收敛 - sanitize 占位补头前缀短路只吞 orphan 的盲区消除 - intent 关键词加词边界,history 不再误判 - 恢复重放后重置话题锚点,URL 图 token 地板不被稀释
This commit is contained in:
@@ -196,8 +196,10 @@ pub fn sanitize_messages(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
/// 占位与非占位统一改"补头自愈":任何工具结果都不丢(除非无 tool_call_id 的异常数据,无法
|
||||
/// 补头配对,只能保留——由协议层兜底)。
|
||||
///
|
||||
/// **未闭合 vs TOOL_MISSING_PREFIX 头**:已带 TOOL_MISSING_PREFIX 的 id 视为已配对(出口断言
|
||||
/// 补的占位头),不重复补头(否则同 id 双头致 400)。
|
||||
/// **未闭合 vs TOOL_MISSING_PREFIX 头**:判定只看 head_ids.contains——真配对(占位头在)由
|
||||
/// contains 覆盖,不会重复补头;头不在的 prefix id result(前轮占位头被裁剪丢)即 orphan,
|
||||
/// 同样补头自愈。补出的头 id 为前缀叠加(tool_missing_tool_missing_X,双层前缀),头与
|
||||
/// result 改写后 id 一致,唯一配对闭合(双层前缀为已知无害副作用)。
|
||||
pub fn drop_reverse_orphans(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
use std::collections::HashSet;
|
||||
// 收集所有保留 assistant 头的 tool_call.id(正向 orphan 处理后残留的头里的 id)。
|
||||
@@ -227,16 +229,14 @@ pub fn drop_reverse_orphans(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
out.push(m);
|
||||
continue;
|
||||
};
|
||||
// 已配对(头在)→ 原样保留。
|
||||
// 已配对(头在)→ 原样保留。TOOL_MISSING_PREFIX 前缀 id 无需特判:真配对(占位头在)
|
||||
// 已被 contains 覆盖;头不在(前轮占位头被裁剪丢掉)即是 orphan,同样走补头自愈。
|
||||
// 补出的头 id 为前缀叠加(tool_missing_tool_missing_X,双层前缀),头+result 改写后
|
||||
// id 一致,唯一配对闭合,功能正确(双层前缀为已知无害副作用)。
|
||||
if head_ids.contains(id) {
|
||||
out.push(m);
|
||||
continue;
|
||||
}
|
||||
// TOOL_MISSING_PREFIX 占位头 id:已被出口断言/上一轮补过,视为配对,不重复补头。
|
||||
if id.starts_with(TOOL_MISSING_PREFIX) {
|
||||
out.push(m);
|
||||
continue;
|
||||
}
|
||||
// orphan tool_result(无头)→ **补占位头自愈**(不丢 result)。
|
||||
// 头 id 复用 result 的 tool_call_id 精确配对(同 assert_placeholder_pairing 自愈模式)。
|
||||
let head_id = format!("{}{}", TOOL_MISSING_PREFIX, id);
|
||||
@@ -306,13 +306,16 @@ pub fn assert_placeholder_pairing(
|
||||
.collect();
|
||||
|
||||
// 找 orphan tool_result(id 不在 head_ids)。占位/非占位都需补头(否则 400)。
|
||||
// 注:TOOL_MISSING_PREFIX 前缀 id 不豁免——真配对(占位头在)已被 head_ids.contains
|
||||
// 覆盖;头不在的 prefix id result(前轮占位头被裁剪丢)正是 orphan,必须补头闭合。
|
||||
// 补出的头 id 为前缀叠加(tool_missing_tool_missing_X,双层前缀),唯一配对,功能正确。
|
||||
// 单次遍历产出新 Vec:orphan result 前插一个占位头(同 id),其余原样保留保序。
|
||||
let mut healed = 0u32;
|
||||
let mut out: Vec<ChatMessage> = Vec::with_capacity(messages.len());
|
||||
for m in messages.into_iter() {
|
||||
if matches!(m.role, MessageRole::Tool) {
|
||||
if let Some(id) = m.tool_call_id.as_deref() {
|
||||
if !head_ids.contains(id) && !id.starts_with(TOOL_MISSING_PREFIX) {
|
||||
if !head_ids.contains(id) {
|
||||
// orphan result → 前补占位头(TOOL_MISSING_PREFIX + 原 id,确保唯一且与 result 配对)
|
||||
let head_id = format!("{}{}", TOOL_MISSING_PREFIX, id);
|
||||
let head = ChatMessage::assistant_with_tools(
|
||||
@@ -419,6 +422,11 @@ pub fn ensure_sequence_legal(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
last.tool_calls.get_or_insert_with(Vec::new).extend(calls);
|
||||
}
|
||||
}
|
||||
// 多模态 parts 随合并迁移:空 content 纯图消息的 content 拼接为空操作,
|
||||
// parts 不迁移则图片片随整条消息丢弃,图对 LLM 隐身
|
||||
if let Some(parts) = m.parts {
|
||||
last.parts.get_or_insert_with(Vec::new).extend(parts);
|
||||
}
|
||||
merged += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -921,9 +929,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_reverse_orphans_no_redundant_head_for_tool_missing_prefix_id() {
|
||||
// 边界:orphan result 的 id 已是 TOOL_MISSING_PREFIX(出口断言/上轮补过)→ 视为已配对,
|
||||
// 不重复补头(否则同 id 双头致 400)。
|
||||
fn drop_reverse_orphans_heals_prefix_id_orphan_with_nested_prefix_head() {
|
||||
// 边界(盲区修复回归):orphan result 的 id 已是 TOOL_MISSING_PREFIX 但占位头不在
|
||||
// (前轮占位头被裁剪丢)→ 即 orphan,同样补头自愈(旧"前缀即已配对"豁免会吞掉此场景,
|
||||
// 三元组不闭合直送 provider 400)。补出的头 id 为双层前缀叠加
|
||||
// (tool_missing_tool_missing_X),头与 result 改写后 id 一致,唯一配对闭合。
|
||||
let msgs = vec![
|
||||
ChatMessage::user("问题"),
|
||||
ChatMessage::tool_result("tool_missing_already", "已补过头的结果"),
|
||||
@@ -936,17 +946,39 @@ mod tests {
|
||||
.filter(|m| matches!(m.role, MessageRole::Tool))
|
||||
.count(),
|
||||
1,
|
||||
"TOOL_MISSING_PREFIX id 的 result 应保留"
|
||||
"TOOL_MISSING_PREFIX id 的 orphan result 应保留(补头自愈)"
|
||||
);
|
||||
// 不补新头(原样保留,无新 TOOL_MISSING_PREFIX 占位头插入)
|
||||
let new_heads = sanitized
|
||||
// 补双层前缀占位头(头 id 含原 id 且以前缀开头)
|
||||
let healed_heads: Vec<&str> = sanitized
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::Assistant))
|
||||
.filter_map(|m| m.tool_calls.as_ref())
|
||||
.flatten()
|
||||
.filter(|c| c.id.starts_with(TOOL_MISSING_PREFIX))
|
||||
.count();
|
||||
assert_eq!(new_heads, 0, "TOOL_MISSING_PREFIX id result 不应重复补头");
|
||||
.map(|c| c.id.as_str())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
healed_heads.len(),
|
||||
1,
|
||||
"prefix id orphan 应补 1 个占位头, 实际 {:?}",
|
||||
healed_heads
|
||||
);
|
||||
assert!(
|
||||
healed_heads[0].starts_with(TOOL_MISSING_PREFIX)
|
||||
&& healed_heads[0][TOOL_MISSING_PREFIX.len()..].starts_with(TOOL_MISSING_PREFIX),
|
||||
"补的头 id 应为双层前缀叠加(tool_missing_tool_missing_X), 实际 {}",
|
||||
healed_heads[0]
|
||||
);
|
||||
// result 的 tool_call_id 改写为占位头 id(闭合配对)
|
||||
let result_id = sanitized
|
||||
.iter()
|
||||
.find(|m| matches!(m.role, MessageRole::Tool))
|
||||
.and_then(|m| m.tool_call_id.clone())
|
||||
.expect("应有 tool_result");
|
||||
assert_eq!(
|
||||
result_id,
|
||||
healed_heads[0],
|
||||
"result id 应与补的头 id 一致(闭合配对)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1205,4 +1237,21 @@ mod tests {
|
||||
sanitized.first().map(|m| format!("{:?}", m.role))
|
||||
);
|
||||
}
|
||||
|
||||
/// 连续同 role 合并须迁移多模态 parts:空 content 纯图消息与相邻同 role 消息
|
||||
/// 合并时,图片片并入前一条(此前只拼 content,纯图消息整条丢弃,图对 LLM 隐身)
|
||||
#[test]
|
||||
fn sequence_merge_moves_multimodal_parts() {
|
||||
let pure_image = ChatMessage::user_parts(
|
||||
"",
|
||||
vec![crate::provider::ContentPart::image_url("https://x/a.png")],
|
||||
);
|
||||
let fixed = ensure_sequence_legal(vec![pure_image, ChatMessage::user("继续")]);
|
||||
|
||||
assert_eq!(fixed.len(), 1, "连续 user 应合并为一条");
|
||||
let parts = fixed[0].parts.as_ref().expect("图片片应迁移到合并结果");
|
||||
assert_eq!(parts.len(), 1);
|
||||
assert!(parts[0].is_image(), "迁移的应是图片片");
|
||||
assert_eq!(fixed[0].content, "继续");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user