修复: 上下文流式与工具签名域清债

- tool_signature 归一化补参,合法换参不再误触探索熔断
- SSE 超限截头改对齐事件边界保尾,流不再永久破损
- content_filter 纳入终态判定,无 DONE 流也收敛
- sanitize 占位补头前缀短路只吞 orphan 的盲区消除
- intent 关键词加词边界,history 不再误判
- 恢复重放后重置话题锚点,URL 图 token 地板不被稀释
This commit is contained in:
lxy
2026-10-03 15:35:05 +08:00
parent 499aa49e49
commit c1e5b96d75
10 changed files with 638 additions and 73 deletions
+53 -1
View File
@@ -420,7 +420,7 @@ fn best_in_group(message: &str, group: &[IntentGroup]) -> Option<(Intent, f32)>
for rule in cand.rules.iter() {
// 英文关键词用小写匹配;中文不受 to_lowercase 影响(no-op)。
let needle = rule.kw.to_lowercase();
if lower.contains(&needle) {
if kw_matches(&lower, &needle) {
score += rule.weight;
matched = true;
}
@@ -437,6 +437,26 @@ fn best_in_group(message: &str, group: &[IntentGroup]) -> Option<(Intent, f32)>
best
}
/// 关键词命中判定:ASCII 关键词要求两侧词边界(词字符 = ASCII 字母数字 + 下划线,与正则
/// `\b` 语义一致),防子串误判——"history" 不命中 "hi"(误判 Chat)、"rapid" 不命中
/// "api"(误判 Http)。非 ASCII(中文)关键词保持子串语义:中文无词边界概念,直接 contains。
fn kw_matches(haystack: &str, needle: &str) -> bool {
if !needle.is_ascii() {
return haystack.contains(needle);
}
let is_word_char = |c: char| c.is_ascii_alphanumeric() || c == '_';
haystack.match_indices(needle).any(|(pos, m)| {
haystack[..pos]
.chars()
.next_back()
.map_or(true, |c| !is_word_char(c))
&& haystack[pos + m.len()..]
.chars()
.next()
.map_or(true, |c| !is_word_char(c))
})
}
// ---- Intent → 工具子集映射 --------------------------------------------------
/// 按 Intent 返回工具名子集。
@@ -854,6 +874,38 @@ mod tests {
assert_eq!(i, Intent::Http);
}
// --- ASCII 关键词词边界(防子串误判) ---
#[test]
fn word_boundary_history_not_chat() {
// "history" 内嵌 "hi" 不再判 Chat(无其他关键词 → Unknown)
let (i, c) = IntentRecognizer::recognize("show me the history");
assert_eq!(i, Intent::Unknown, "history 不应命中 hi, 实际 conf={}", c);
}
#[test]
fn word_boundary_rapid_not_http() {
// "rapid" 内嵌 "api" 不再判 Http
let (i, c) = IntentRecognizer::recognize("rapid growth");
assert_eq!(i, Intent::Unknown, "rapid 不应命中 api, 实际 conf={}", c);
}
#[test]
fn standalone_english_keywords_still_hit() {
// 词边界不误伤独立词命中
let (i, _) = IntentRecognizer::recognize("call the api");
assert_eq!(i, Intent::Http);
let (i, _) = IntentRecognizer::recognize("hi");
assert_eq!(i, Intent::Chat);
}
#[test]
fn cjk_adjacent_ascii_keyword_still_hits() {
// 中文紧邻 ASCII 关键词:非 ASCII 字符算边界,不破坏中文场景命中
let (i, _) = IntentRecognizer::recognize("调用这个api");
assert_eq!(i, Intent::Http);
}
// --- tool_subset_for ---
#[test]