修复: AI 对话/工具可靠性(sanitize 三元组 + G2 签名重复 + handshake 不杀 loop + 空 tool_call id 兜底)

治 5 个对话停止/工具失败根因:sanitize 三元组按 id 配对治 400;G2 探索熔断从结果空
改签名重复判定(治误停正常探索);handshake 删越权强杀活 loop(generating 归 guard 单源);
空 tool_call id 兜底 gen_<index>(治 SenseNova 工具结果路由错位)。
This commit is contained in:
lxy
2026-08-02 02:21:30 +08:00
parent c76e77bd3c
commit 57d6a2d066
8 changed files with 991 additions and 185 deletions
+56
View File
@@ -118,6 +118,29 @@ impl ToolCall {
}
}
/// 解析点统一兜底:tool_call.id 空 → 生成唯一 fallback,非空原样。
///
/// 根因(实证会话 01f05167 SenseNova flash-lite):某些 providerSenseNova 兼容缺陷)
/// 返回空 `tool_call.id`"")。OpenAI 协议要求 id 唯一。DevFlow 多 tool_call 按 id
/// 路由结果,id 空时所有结果落到同一 key`audit/mod.rs:203` 的 `seen_ids` 去重把空 id
/// 视为相同,只留首个 tool_call)→ AI 看到「所有调用同一结果」,工具全失败。
///
/// 兜底在**解析点**生成 fallback idraw 非空用 raw,空用 `format!("{prefix}_{index}")`
/// index 取 tool_call 在数组中的位置,保证同 assistant 内多 tool_call id 唯一)。
/// 下游(工具执行 / tool 结果回填 tool_call_id)从解析后的 `ToolCall.id` 取,不重复生成,
/// 确保 assistant tool_call.id 与 tool 结果 tool_call_id 匹配(防 sanitize 三元组断裂)。
///
/// 三处解析点共用本 helperDRY):OpenAI 同步 `parse_tool_calls`prefix=`gen_tool`)、
/// OpenAI 流式 chunkprefix=`gen_stream`)、Anthropic 同步 + 流式(prefix=`gen_anthropic` /
/// `gen_anthropic_stream`)。正常 providerOpenAI/Claude/GLM id 非空)原样透传零介入。
pub fn tool_call_id_or_fallback(raw: &str, index: usize, prefix: &str) -> String {
if !raw.is_empty() {
raw.to_string()
} else {
format!("{prefix}_{index}")
}
}
/// LLM Provider trait
#[async_trait]
pub trait LlmProvider: Send + Sync {
@@ -435,4 +458,37 @@ mod tests {
let deserialized: CompletionResponse = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.reasoning_content, Some("r1 thought".to_string()));
}
/// CR-空 idtool_call_id_or_fallback 共享 helper —— 空 raw → fallback,非空原样。
#[test]
fn tool_call_id_or_fallback_non_empty_passthrough() {
assert_eq!(tool_call_id_or_fallback("call_abc", 0, "gen_tool"), "call_abc");
assert_eq!(tool_call_id_or_fallback("x", 5, "p"), "x");
}
#[test]
fn tool_call_id_or_fallback_empty_generates_with_index() {
assert_eq!(tool_call_id_or_fallback("", 0, "gen_tool"), "gen_tool_0");
assert_eq!(tool_call_id_or_fallback("", 1, "gen_tool"), "gen_tool_1");
assert_eq!(tool_call_id_or_fallback("", 7, "gen_stream"), "gen_stream_7");
}
#[test]
fn tool_call_id_or_fallback_empty_unique_per_index() {
// 同 prefix 不同 index → 不同 fallback(保证同 assistant 多 tool_call id 唯一)
let ids: Vec<String> = (0..3).map(|i| tool_call_id_or_fallback("", i, "gen_tool")).collect();
let mut sorted = ids.clone();
sorted.sort();
sorted.dedup();
assert_eq!(ids.len(), sorted.len(), "各 index fallback 应唯一: {:?}", ids);
}
#[test]
fn tool_call_id_or_fallback_prefix_distinguishes_sources() {
// 不同 prefix 区分来源(同步 gen_tool / 流式 gen_stream / anthropic gen_anthropic
assert_ne!(
tool_call_id_or_fallback("", 0, "gen_tool"),
tool_call_id_or_fallback("", 0, "gen_stream")
);
}
}