执行摘要
本 PR 为 Rust 前端正确实现 continue_final_message 参数,采用 Transformers v5 的 sentinel 机制,在消息后追加标记并在渲染后截断,避免了原透传方式实际无效的问题。同时调整 CI 配置解锁相关测试,整体影响可控。
功能与动机
PR 指出此前 continue_final_message 仅透传给 chat template,但绝大多数 Hugging Face 模板并不响应此参数,导致参数形同虚设。为对齐 Transformers v5 行为,Rust 前端改为 sentinel 方式:无需模板感知即可正确截断 prompt。
实现拆解
- 核心逻辑修改:在
rust/src/chat/src/renderer/hf/mod.rs 的 apply_chat_template_inner 方法中,将 messages 改为可变,并在渲染前判断 continue_final_message 标志,若为 true 则调用新函数 append_continue_final_message_tag 在最后一条消息的文本末尾追加 sentinel tag CONTINUE_FINAL_MESSAGE_TAG。
- 渲染后截断:渲染完成后,调用
truncate_prompt_at_continue_final_message_tag 在 sentinel 最右侧出现位置截断 prompt,丢弃模板添加的后缀(如 <|eot_id|>)。
- 新常量与辅助函数:定义常量
CONTINUE_FINAL_MESSAGE_TAG(与 Transformers v5 一致),新增 append_continue_final_message_tag 和 truncate_prompt_at_continue_final_message_tag 两个函数,分别处理不同消息内容格式(纯文本或 OpenAI 多段内容)和截断逻辑。
- CI 配置解锁测试:修改
.buildkite/test-amd.yaml 和 .buildkite/test_areas/rust_frontend.yaml,在 Rust 前端 Serve/Admin Coverage 命令中去掉 not test_tokenize_chat 过滤条件,允许该测试运行。
rust/src/chat/src/renderer/hf/mod.rs
核心实现文件,新增 sentinel 追加和截断逻辑,修改 apply_chat_template_inner 以支持 continue_final_message
// Sentinel 标记常量,与 Transformers v5 相同
const CONTINUE_FINAL_MESSAGE_TAG: &str = "CONTINUE_FINAL_MESSAGE_TAG ";
/// 在最终消息的尾部文本后追加 [`CONTINUE_FINAL_MESSAGE_TAG`],
/// 返回原始文本用于渲染后校验。
fn append_continue_final_message_tag(
message: &mut TemplateMessage,
) -> Result<String> {
// 获取最后一个可变的文本切片
let text = match &mut message.content {
TemplateContent::String(text) => Some(text),
// 对于多段内容,反向查找最后一个文本部分
TemplateContent::OpenAi(parts) => parts
.iter_mut()
.rev()
.find_map(|part| match part {
TemplateContentPart::Text { text } => Some(text),
TemplateContentPart::Image => None,
}),
};
let text = text.ok_or_else(|| {
Error::ChatTemplate(
"continue_final_message 已设置但最终消息中没有可延续的文本"
.to_string(),
)
})?;
let original = text.clone();
text.push_str(CONTINUE_FINAL_MESSAGE_TAG);
Ok(original)
}
/// 在渲染后的 prompt 中定位最后出现的 [`CONTINUE_FINAL_MESSAGE_TAG`],
/// 截断丢弃其后所有字符,从而去除模板添加的后缀。
fn truncate_prompt_at_continue_final_message_tag(
prompt: String,
final_message_text: &str,
) -> Result<String> {
// 使用 rfind 确保取最右侧的 tag(因为用户 prompt 中也可能有)
let tag_start = prompt
.rfind(CONTINUE_FINAL_MESSAGE_TAG)
.ok_or_else(|| {
Error::ChatTemplate(
"渲染后的 prompt 中未找到 sentinel 标记。 \
可能是模板丢弃了最终消息内容",
)
})?;
let mut truncated = prompt;
truncated.truncate(tag_start);
// 去除模板在消息后可能插入的空白字符,与 Transformers v5 保持一致
Ok(truncated.trim_end().to_string())
}
评论区精华
本 PR 没有实质性的 review 讨论。claude[bot] 自动提示配置代码审查,njhill 直接批准,无质疑或建议。
风险与影响
风险较低。主要风险:1)sentinel 字符串 'CONTINUE_FINAL_MESSAGE_TAG ' 可能出现在用户 prompt 或模板输出中,虽然截断使用 rfind 取最右侧匹配以降低误截断风险,但仍存在边界情况(与 Transformers v5 行为一致)。2)若最终消息无文本内容(仅图片),append_continue_final_message_tag 会返回错误,需确保调用前已校验。3)仅影响 Rust 前端且 continue_final_message 为可选参数,回归影响面窄。
关联脉络
本 PR 是 Rust 前端逐步完善的功能之一,近期 frontend 模块已有多个改进(如 bad_words 支持 #46793)。此 PR 移除了一个测试排除,表明 Rust 前端更多测试通过,向稳定迈进。
参与讨论