PR 分析报告: [Bugfix] [Rust Frontend] Fix stop string truncation with repeated matches
1. 执行摘要
该 PR 修复了 Rust 前端中 matches_stop_string 函数使用 rposition() 导致同一 stop 字符串在单次解码增量中多次出现时截断位置错误的 bug。将方法替换为 position() 以匹配 Python V1 detokenizer 的从左至右搜索行为。变更仅涉及一个文件,一行核心逻辑,并包含新增测试,风险极低。
2. 功能与动机
PR body 指出,当同一 stop 字符串在新解码文本中出现多次时(例如 `output="Answer
",stop="\n"),Rust 实现使用rposition()选择搜索窗口内的最右侧匹配,而 Python V1 detokenization 使用find()选择最左侧匹配。这导致在include_stop_str_in_output=false时,Rust 可能截断到"Answer\n"`,错误地将 stop 字符串保留在输出中。
3. 实现拆解
- 核心逻辑变更:在
rust/src/text/src/output/decoded.rs 的 matches_stop_string 函数中,将 output[start_off..].windows(len).rposition(|w| w == ss) 替换为 output[start_off..].windows(len).position(|w| w == ss)。rposition 返回从右向左搜索的第一个匹配的位置,而 position 返回从左向右搜索的第一个匹配的位置。
- 新增测试用例:在同一个文件的测试模块中新增
stop_string_matches_leftmost_with_multiple_new_bytes 测试,使用 stops = vec!["\n"] 和 `output = "Answer
"、new_bytes = 2,验证函数返回Some((0, 6))`,即第一个换行符的位置(索引 6),而非第二个换行符的位置(索引 7)。
rust/src/text/src/output/decoded.rs
唯一修改的文件,包含核心逻辑变更和新增测试。修改了 matches_stop_string 函数中的搜索方向,并添加了测试用例 stop_string_matches_leftmost_with_multiple_new_bytes。
关键源码片段
rust/src/text/src/output/decoded.rs
唯一修改的文件,包含核心逻辑变更和新增测试。修改了 matches_stop_string 函数中的搜索方向,并添加了测试用例 stop_string_matches_leftmost_with_multiple_new_bytes。
// rust/src/text/src/output/decoded.rs
/// 如果匹配到 stop 字符串,返回 (stop 字符串索引, stop 字符串在 output 中的起始字节位置)
/// 变更:使用 position() 而非 rposition() 来查找最左侧匹配,与 Python V1 detokenizer 的 find() 行为一致
fn matches_stop_string(stops: &[String], output: &str, new_bytes: usize) -> Option<(usize, usize)> {
// 按字节操作以避免 UTF-8 边界问题
let output = output.as_bytes();
// 计算搜索窗口的起始偏移:output 总字节数 + 1 - new_bytes
let next_off = (output.len() + 1) - new_bytes;
stops.iter()
.map(|ss| (ss.as_bytes(), ss.len(), next_off.saturating_sub(ss.len())))
.enumerate()
.find_map(|(ss_idx, (ss, len, start_off))| {
// 在 [start_off..] 的子切片上搜索 stop 字符串
output[start_off..]
.windows(len)
.position(|w| w == ss) // 原来为 .rposition(...),从右向左搜索
.map(|pos| (ss_idx, start_off + pos))
})
}
#[cfg(test)]
mod tests {
/* ... */
#[test]
fn stop_string_matches_leftmost_with_multiple_new_bytes() {
// 测试:同一 stop 字符串在新解码文本中出现多次时,应匹配最左侧
let stops = vec!["\n".to_string()];
let result = matches_stop_string(&stops, "Answer
", 2);
// 期望匹配到索引 6(第一个换行符位置),而非索引 7(第二个换行符)
assert_eq!(result, Some((0, 6)));
}
/* ... */
}
5. 评论区精华
- BugenZhao 请求 Codex 审查,Codex 回复未发现重大问题。
- njhill 和 BugenZhao 均批准了该 PR,表述简洁,无争议。
6. 风险与影响
风险:变更仅将 .rposition() 替换为 .position(),语义明确,且通过单元测试覆盖,回归概率极低。需注意该变更使 Rust 行为与 Python V1 一致,但若其他部分依赖了右侧匹配的行为则可能受影响,理论上不存在。
影响:影响 Rust 前端中所有使用 matches_stop_string 的场景,修复了边缘 case 下 stop 字符串截断不正确的问题。用户可见,但仅影响特定场景。
7. 关联脉络
该 PR 是独立的 bugfix,与同仓库近期历史 PR 无直接功能关联。但属于持续改进 Rust 前端质量的一部分,与 #46163、#45935 等同属 bugfix 类别。
参与讨论