修复: AI上下文邻接性收口 + 设置页i18n缺失键
- df-ai 三元组邻接性收口: sanitize/openai_compat 闭合判定由「id 全局存在」收紧为「紧后 tool 连续段覆盖」,夹层序列(后台压缩/用户插话楔入)不再直送 provider 致 400 insufficient tool messages - 设置页补 settings.loading 缺失键(zh/en),修版本信息返回前渲染字面串 - 看板: 907 接手核查补注(实现口径=剥除非归位,有损)+ 遗留登记 909 + 908 销账
This commit is contained in:
@@ -42,12 +42,28 @@ pub fn sanitize_messages(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
.filter(|m| m.is_active())
|
||||
.collect();
|
||||
|
||||
// step 1:已闭合的 tool_call_id 集合(role=Tool 消息全部提供过的 id)
|
||||
let resolved_ids: HashSet<&str> = messages
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::Tool))
|
||||
.filter_map(|m| m.tool_call_id.as_deref())
|
||||
.collect();
|
||||
// step 1:相邻性判定的依据——每个 assistant 头「紧后 tool 连续段」覆盖的 id 集合。
|
||||
// 口径与 openai_compat::sanitize_openai_triplets 一致(2026-10-09 收紧):
|
||||
// 协议要求 tool 响应必须紧随其 assistant(tool_calls);原实现按「id 全局存在」判定
|
||||
// (位置无关),会把被 system/user 隔开的游离响应误判为已闭合,放行畸形序列致 400
|
||||
// 「insufficient tool messages」。故判定改为「紧后连续段覆盖」——未覆盖的 call 视同
|
||||
// 未闭合处理(剥除),其游离响应随后由 step3 按 orphaned_ids 丢弃,自动闭环。
|
||||
let mut adjacency: std::collections::HashMap<usize, HashSet<String>> =
|
||||
std::collections::HashMap::new();
|
||||
for i in 0..messages.len() {
|
||||
if !matches!(messages[i].role, MessageRole::Assistant) {
|
||||
continue;
|
||||
}
|
||||
let mut seg: HashSet<String> = HashSet::new();
|
||||
let mut j = i + 1;
|
||||
while j < messages.len() && matches!(messages[j].role, MessageRole::Tool) {
|
||||
if let Some(id) = messages[j].tool_call_id.as_deref() {
|
||||
seg.insert(id.to_string());
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
adjacency.insert(i, seg);
|
||||
}
|
||||
|
||||
// step 2:判定每个 assistant 头的闭合状态,产出需丢弃的 id 集合
|
||||
// orphaned_ids:未闭合的 tool_call.id(含占位 id),其 tool_result 要丢
|
||||
@@ -72,7 +88,7 @@ pub fn sanitize_messages(messages: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||
.iter()
|
||||
.partition(|c| {
|
||||
!c.id.starts_with(TOOL_MISSING_PREFIX)
|
||||
&& resolved_ids.contains(c.id.as_str())
|
||||
&& adjacency.get(&i).is_some_and(|s| s.contains(&c.id))
|
||||
});
|
||||
|
||||
match (resolved.is_empty(), unresolved.is_empty()) {
|
||||
@@ -554,6 +570,42 @@ mod tests {
|
||||
|
||||
// ── 占位配对完整性(解 400 orphan):反向 orphan 检测 + 出口自愈 ──
|
||||
|
||||
/// 邻接性回归(2026-10-09 400 根因):tool 响应与 head 之间被夹层(system/user)隔开时,
|
||||
/// 闭合判定按「紧后 tool 连续段覆盖」而非「id 全局存在」——被隔开的 call 视同未闭合
|
||||
/// (剥除/整头丢),其游离响应随 orphaned_ids 一并丢弃,保证发送序列满足「head 后紧邻其
|
||||
/// 响应」。防 OpenAI 400「insufficient tool messages following tool_calls message」。
|
||||
#[test]
|
||||
fn sanitize_strips_calls_when_response_separated_by_interloper() {
|
||||
// [user] [assistant(c1,c2)] [system 夹层] [tool c1] [tool c2]
|
||||
let msgs = vec![
|
||||
ChatMessage::user("go"),
|
||||
ChatMessage::assistant_with_tools(
|
||||
"",
|
||||
vec![
|
||||
ToolCall::new("c1".to_string(), "t", "{}"),
|
||||
ToolCall::new("c2".to_string(), "t", "{}"),
|
||||
],
|
||||
),
|
||||
ChatMessage::system("夹层提示"),
|
||||
ChatMessage::tool_result("c1", "r1"),
|
||||
ChatMessage::tool_result("c2", "r2"),
|
||||
];
|
||||
let out = sanitize_messages(msgs);
|
||||
// 夹层隔开 → 两 call 均不在紧后连续段 → 无任何带 tool_calls 的 assistant 存活
|
||||
assert!(
|
||||
out.iter()
|
||||
.all(|m| m.tool_calls.as_ref().map_or(true, |c| c.is_empty())),
|
||||
"被夹层隔开的 call 应被剥除(不得残留 head+tool_calls), 实际 {:?}",
|
||||
out
|
||||
);
|
||||
// 游离响应无头配对 → 丢弃;序列内不得残留 role=Tool
|
||||
assert!(
|
||||
out.iter().all(|m| !matches!(m.role, MessageRole::Tool)),
|
||||
"游离 tool 响应应被丢弃, 实际 {:?}",
|
||||
out
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_heals_reverse_orphan_tool_result_with_placeholder_head() {
|
||||
// 三元组完整性 P0:反向 orphan(tool_result 无对应 tool_call 头)→ 补占位头自愈(不丢)。
|
||||
|
||||
Reference in New Issue
Block a user