优化: 弱模型治理收尾(AC-2探索预算:单轮上限+目录去重+禁绕行 + AC-5失败画像:合法目标回灌+相近锚点提示)

This commit is contained in:
lxy
2026-08-08 19:38:53 +08:00
parent 02c8d8e5ea
commit fe780c0084
8 changed files with 648 additions and 12 deletions
+123 -1
View File
@@ -506,8 +506,18 @@ pub fn register(
}));
}
// L2: old_text 精确匹配(CAS 语义)
// 失败时附相近片段提示(AC-5):把文件里与 old_text 相似的几行列给 LLM,
// 供其对照真实缩进/空格修正锚点,避免靠猜反复重试。完全无关(无相似行)则
// 退回原提示,不误导。
if !content.contains(old_text) {
anyhow::bail!("未找到目标文本,文件可能已被修改");
let similar = similar_line_fragments(&content, old_text, 3);
if similar.is_empty() {
anyhow::bail!("未找到目标文本,文件可能已被修改");
}
anyhow::bail!(
"未找到目标文本,文件可能已被修改。相近片段供核对 old_text 是否精确(注意缩进/空格):\n{}",
similar.join("\n")
);
}
let mc = content.matches(old_text).count();
match_count = mc;
@@ -1222,6 +1232,82 @@ pub fn register(
);
}
// ============================================================
// patch_file 失败提示辅助 — old_text 不匹配时给 LLM 相近锚点
// ============================================================
/// 在 `content` 中找出与 `needle` 最相近的若干行(行号+内容)。
///
/// 用途:patch_file 模式1 old_text 精确匹配失败时,把相近片段附进错误信息,让 LLM
/// 对照真实缩进/空格修正锚点,避免靠猜反复重试(AC-5 patch_file 27 次失败降频)。
///
/// 相似度算法:字符多重集 Dice 系数(2*公共字符数 / 两串字符数之和,0-100 整数百分比),
/// 仅对「首非空白字符相同」的行做评分(快速预筛,绝大多数行不相干直接跳过)。
/// `needle` 取首行 + 前 120 字符作探针(超长 old_text 罕见,同时约束扫描成本,
/// 1MB 文件全量逐行 O(n) 可接受)。阈值 40% 防误导性建议(完全无关文本不提示)。
fn similar_line_fragments(content: &str, needle: &str, max: usize) -> Vec<String> {
// 探针取 needle 首行,截前 120 字符(评分用,兼顾精度与性能)
let first_line = needle.lines().next().unwrap_or(needle);
let mut probe = String::new();
for c in first_line.chars().take(120) {
probe.push(c);
}
if probe.trim().is_empty() {
return Vec::new();
}
// 预计算探针字符计数(行级评分复用,避免每行重建)
use std::collections::HashMap;
let mut probe_counts: HashMap<char, u32> = HashMap::new();
for c in probe.chars() {
*probe_counts.entry(c).or_insert(0) += 1;
}
let probe_len = probe.chars().count() as u32;
let probe_first = probe.trim_start().chars().next();
let mut scored: Vec<(u32, String)> = Vec::new();
for (idx, line) in content.lines().enumerate() {
let trimmed = line.trim_end();
if trimmed.is_empty() {
continue;
}
// 快速预筛:首非空白字符不同则跳过(大文件绝大多数行不相干)
match (probe_first, trimmed.trim_start().chars().next()) {
(Some(a), Some(b)) if a != b => continue,
_ => {}
}
let line_len = trimmed.chars().count() as u32;
if line_len == 0 {
continue;
}
// 公共字符计数(探针计数约束下扫描行字符)
let mut counts = probe_counts.clone();
let mut common = 0u32;
for c in trimmed.chars() {
if let Some(cnt) = counts.get_mut(&c) {
if *cnt > 0 {
*cnt -= 1;
common += 1;
}
}
}
// Dice = 2*common / (probe_len + line_len),乘 100 转百分比整数
let dice = (common * 200) / (probe_len + line_len).max(1);
if dice >= 40 {
let shown: String = if line.chars().count() > 100 {
line.chars().take(100).collect::<String>() + ""
} else {
line.to_string()
};
scored.push((dice, format!("{}: {}", idx + 1, shown)));
}
}
scored.sort_by(|a, b| b.0.cmp(&a.0));
scored.truncate(max);
scored.into_iter().map(|(_, s)| s).collect()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1377,6 +1463,42 @@ mod tests {
assert!(!windowed.starts_with(""), "match 在行首,win_start=0 不前导 …");
assert!(windowed.contains("(±20字符窗口)"));
}
// ============================================================
// similar_line_fragments — patch_file old_text 相近锚点提示
// ============================================================
#[test]
fn similar_fragments_finds_whitespace_drift_line() {
// 典型失败:old_text 缩进漂移(4 空格 vs 2 空格),字符重叠度高 → 应命中同一行
let content = "fn main() {\n let x = 1;\n let y = 2;\n}\n";
let needle = " let x = 1;"; // 缩进从 2 变 4,内容同
let hits = similar_line_fragments(content, needle, 3);
assert!(
hits.iter().any(|h| h.contains("行 2")),
"缩进漂移应命中第 2 行,实际: {hits:?}"
);
}
#[test]
fn similar_fragments_empty_when_no_relation() {
// 完全无关文本 → 空列表(退回原提示,不误导)
let content = "fn main() {\n let x = 1;\n}\n";
let hits = similar_line_fragments(content, "pub struct TotallyDifferent", 3);
assert!(hits.is_empty(), "无关文本不应给相近提示,实际: {hits:?}");
}
#[test]
fn similar_fragments_multiline_needle_uses_first_line() {
// 多行 old_text:探针取首行,仍能定位到目标行
let content = "start\n let value = compute();\n println!(\"{}\", value);\nend\n";
let needle = " let value = compute();\n println!(\"{}\", value);\n"; // 首行同内容
let hits = similar_line_fragments(content, needle, 3);
assert!(
hits.iter().any(|h| h.contains("行 2")),
"多行 old_text 应命中首行所在行,实际: {hits:?}"
);
}
}