修复: 后端若干bug

This commit is contained in:
2026-06-24 00:27:09 +08:00
parent 0a8db6504a
commit 96e554ce22
5 changed files with 320 additions and 19 deletions

View File

@@ -286,6 +286,73 @@ impl ToolDomain {
// ---- 识别器 -----------------------------------------------------------------
/// 意图识别 L0 去噪:剥离 mention 结构化前缀(`[项目: DevFlow]` / `[任务: xxx]` 等)。
///
/// mention 前缀是系统注入的结构标签,语义已由 augmentation @ 通道单独 resolve
/// 注入 system promptcontent 里残留的前缀只污染关键词识别——实测会话
/// `[项目: DevFlow] 这个错误是项目里的 miniapp 报错` 被误判 `Intent::Project`
/// 收敛砍掉 File domain 致 `read_file` 对 LLM 不可见agent 被迫反复 `list_tasks`。
///
/// 剥离规则:扫描 `[`,若紧跟已知 mention kind项目/任务/想法/技能,
/// project/task/idea/skill大小写不敏感+ 冒号(全/半角 `:`/``),则连同到
/// 首个 `]` 整段移除;未闭合或非已知 kind 的 `[...]` 原样保留(代码片段/错误码)。
///
/// kind 表与 `MentionRef`project/task/idea/skill对齐——单一真相源新增 mention
/// kind 时此处同步。不折叠空白:关键词 `contains` 匹配对空白不敏感,剥离即可。
fn strip_mention_tags(message: &str) -> String {
const KINDS: &[&str] = &["项目", "任务", "想法", "技能", "project", "task", "idea", "skill"];
let chars: Vec<char> = message.chars().collect();
let n = chars.len();
let mut out = String::with_capacity(message.len());
let mut i = 0;
while i < n {
if chars[i] == '[' {
if let Some(close) = try_match_mention(&chars, i, KINDS) {
i = close + 1; // 跳过整段 mention含 `]`
continue;
}
}
out.push(chars[i]);
i += 1;
}
out
}
/// 探测 `chars[open] == '['` 起的 mention 段:`[kind: ...]`。
///
/// 返回匹配 `]` 的索引(`open` 指向 `[`。kind 大小写不敏感ASCII+ 中文直比;
/// 冒号接受全角 ``/半角 `:`kind 后允许空白;未闭合 `]` 返 `None`(不当 mention保留原样
fn try_match_mention(chars: &[char], open: usize, kinds: &[&str]) -> Option<usize> {
let after = open + 1;
for k in kinds {
let kc: Vec<char> = k.chars().collect();
if chars.len() < after + kc.len() {
continue;
}
let slice = &chars[after..after + kc.len()];
if !kc.iter().zip(slice.iter()).all(|(a, b)| a.eq_ignore_ascii_case(b)) {
continue;
}
let mut j = after + kc.len();
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
if j >= chars.len() || (chars[j] != ':' && chars[j] != '') {
continue;
}
j += 1;
while j < chars.len() {
if chars[j] == ']' {
return Some(j);
}
j += 1;
}
// 未闭合 `]` → 不当 mention保留原样
return None;
}
None
}
/// 意图识别器(无状态,所有方法均为纯函数,可 `Default` 构造)。
#[derive(Debug, Clone, Default)]
pub struct IntentRecognizer;
@@ -299,14 +366,16 @@ impl IntentRecognizer {
///
/// 优先级SPECIFIC > ENTITY > GENERIC。高分组的胜者直接返回不累积低分组。
pub fn recognize(message: &str) -> (Intent, f32) {
// L0 去噪:先剥离 mention 结构标签(防 `[项目: x]` 把代码任务误判 Project
let cleaned = strip_mention_tags(message);
// 依次按优先级组求胜者。高分组有命中即提前返回。
if let Some(hit) = best_in_group(message, SPECIFIC_GROUP) {
if let Some(hit) = best_in_group(&cleaned, SPECIFIC_GROUP) {
return hit;
}
if let Some(hit) = best_in_group(message, ENTITY_GROUP) {
if let Some(hit) = best_in_group(&cleaned, ENTITY_GROUP) {
return hit;
}
if let Some(hit) = best_in_group(message, GENERIC_GROUP) {
if let Some(hit) = best_in_group(&cleaned, GENERIC_GROUP) {
return hit;
}
(Intent::Unknown, 0.0)
@@ -347,7 +416,7 @@ fn best_in_group(message: &str, group: &[IntentGroup]) -> Option<(Intent, f32)>
/// 返回空 `Vec` 表示该意图**无工具收敛**Chat或**未识别**Unknown
/// 上游应走**全量 fallback**(即不过滤工具,交全量给 LLM
///
/// 设计Code → [file, http]File → [file]Project/Task/Idea → [data]
/// 设计Code → [file, http]File → [file]Project/Task/Idea → [data, file](加 file 防「提项目/任务 → 误判 → 砍只读探索」,见 L1
/// Http → [http]Search → [file](含 search_filesConversation → [];
/// Chat → []Debug → [file, exec, http, data](调试常需跑命令+读文件+查 API+查任务/工作流状态,CR-25 审查🟡-1 加 data 防"调试任务"丢 Data 工具);
/// **仅 Debug 含 Exec**(用户明确"运行/测试/构建/调试"才暴露 run_command,
@@ -357,9 +426,12 @@ pub fn tool_subset_for(intent: &Intent) -> Vec<&'static str> {
Intent::Code => &[ToolDomain::File, ToolDomain::Http],
Intent::Debug => &[ToolDomain::File, ToolDomain::Exec, ToolDomain::Http, ToolDomain::Data],
Intent::File => &[ToolDomain::File],
Intent::Project => &[ToolDomain::Data],
Intent::Task => &[ToolDomain::Data],
Intent::Idea => &[ToolDomain::Data],
// Project/Task/Idea 加 File:用户提"项目/任务"时常是在其内编码/排查
// (实测会话 [项目:x] 报错 → Project → 砍 File 致 read_file 对 LLM 不可见)。
// 保留只读探索;写工具有 RiskLevel/审批兜底,"分心"远好于"断手"。
Intent::Project => &[ToolDomain::Data, ToolDomain::File],
Intent::Task => &[ToolDomain::Data, ToolDomain::File],
Intent::Idea => &[ToolDomain::Data, ToolDomain::File],
Intent::Conversation => &[],
Intent::Search => &[ToolDomain::File],
Intent::Http => &[ToolDomain::Http],
@@ -608,6 +680,47 @@ mod tests {
assert_eq!(i, Intent::Search);
}
// --- strip_mention_tagsL0 去噪)---
#[test]
fn strip_removes_all_mention_kinds() {
assert_eq!(strip_mention_tags("[项目: DevFlow] 重构代码"), " 重构代码");
assert_eq!(strip_mention_tags("[任务: 修复bug] 看看"), " 看看");
assert_eq!(strip_mention_tags("[想法: x][技能: y] 闲聊"), " 闲聊");
// 英文 kind + 大小写不敏感
assert_eq!(strip_mention_tags("[Project: x] fix this"), " fix this");
assert_eq!(strip_mention_tags("[TASK: y] do it"), " do it");
// 全角冒号
assert_eq!(strip_mention_tags("[项目DevFlow] 阅读"), " 阅读");
}
#[test]
fn strip_preserves_non_mention_brackets() {
// 非 mention 的 [...] 原样保留(代码片段/错误码)
assert_eq!(strip_mention_tags("数组 [1,2,3] 求和"), "数组 [1,2,3] 求和");
assert_eq!(strip_mention_tags("[error] 致命"), "[error] 致命"); // 'error' 非 kind
assert_eq!(strip_mention_tags("[项目报错] 看"), "[项目报错] 看"); // kind 后无冒号
// 未闭合的 mention-like 不剥离(防误吞)
assert_eq!(strip_mention_tags("[项目: 未闭合"), "[项目: 未闭合");
}
#[test]
fn recognize_mention_prefix_not_misread_as_project() {
// 核心修复:[项目: DevFlow] 帮我重构代码 → Code(非 Project)
// 原 bug:mention 前缀的"项目"命中 PROJECT(1.0)→ 收敛砍 File → read_file 不可见。
let (i, c) = IntentRecognizer::recognize("[项目: DevFlow] 帮我重构这段代码");
assert_eq!(i, Intent::Code);
assert!(c >= 0.7, "重构+代码 应达 Code 阈值, 实际 conf={}", c);
}
#[test]
fn recognize_real_project_word_still_hits_project() {
// 用户口语真"项目"(非 mention 前缀)仍命中 Project——预处理只去系统噪声。
// 正是 L1(tool_subset_for 加 File)不可省的佐证:口语误判靠映射兜底。
let (i, _) = IntentRecognizer::recognize("创建项目并绑定目录");
assert_eq!(i, Intent::Project);
}
#[test]
fn recognize_http_zh() {
let (i, _) = IntentRecognizer::recognize("调用接口请求这个 api");
@@ -713,11 +826,13 @@ mod tests {
}
#[test]
fn subset_project_is_data() {
fn subset_project_is_data_and_file() {
// L1 修复:Project 加 File domain(防"提项目 → 误判 Project → 砍只读探索")。
// Data(create_project/bind_directory)+ File 只读(read_file)共存。
let s = tool_subset_for(&Intent::Project);
assert!(s.contains(&"create_project"));
assert!(s.contains(&"bind_directory"));
assert!(!s.contains(&"read_file"));
assert!(s.contains(&"read_file"), "Project 应保留 read_file(L1)");
}
#[test]