修复: 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:
@@ -118,6 +118,29 @@ impl ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析点统一兜底:tool_call.id 空 → 生成唯一 fallback,非空原样。
|
||||
///
|
||||
/// 根因(实证会话 01f05167 SenseNova flash-lite):某些 provider(SenseNova 兼容缺陷)
|
||||
/// 返回空 `tool_call.id`("")。OpenAI 协议要求 id 唯一。DevFlow 多 tool_call 按 id
|
||||
/// 路由结果,id 空时所有结果落到同一 key(`audit/mod.rs:203` 的 `seen_ids` 去重把空 id
|
||||
/// 视为相同,只留首个 tool_call)→ AI 看到「所有调用同一结果」,工具全失败。
|
||||
///
|
||||
/// 兜底在**解析点**生成 fallback id:raw 非空用 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 三元组断裂)。
|
||||
///
|
||||
/// 三处解析点共用本 helper(DRY):OpenAI 同步 `parse_tool_calls`(prefix=`gen_tool`)、
|
||||
/// OpenAI 流式 chunk(prefix=`gen_stream`)、Anthropic 同步 + 流式(prefix=`gen_anthropic` /
|
||||
/// `gen_anthropic_stream`)。正常 provider(OpenAI/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-空 id:tool_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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user