如何在 Transformers 中为对话模型编写自定义 chat template?
2026/9/10 8:22:51 网站建设 项目流程

如何在 Transformers 中为对话模型编写自定义 chat template?

【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers

当你给一个对话模型写新的对话格式——比如模型官方没有附带 chat template、或者你想调整控制 token 的布局——就需要编写一个自定义 chat template。在 Transformers 中,chat template 是存放在 tokenizer 的chat_template属性里的一段 Jinja 模板(Jinja 的语法与 Python 很接近)。写好模板并赋值给 tokenizer 后,每次调用apply_chat_template都会使用它,并且save_pretrained时会随 tokenizer 一起保存到磁盘。本文的完整路径是:查看现有模板作为参考 → 编写 Jinja 模板 → 赋给 tokenizer → 用apply_chat_template验证渲染结果 → 保存到磁盘。

一个关键前提:模板必须与模型训练时使用的格式完全一致。同样是 Mistral-7B 基座微调出来的模型,Mistral-7B-Instruct 使用[INST]/[/INST]标记用户消息,而 Zephyr-7B 使用<|user|>/<|assistant|>标记角色;控制 token 用错会导致模型表现大幅下降。所以模板中的 token、空白、布局都要逐字对齐模型训练格式,不能凭感觉写。

第一步:查看现有模板作为起点

最简单的起点是参考已有模板。对任何 chat 模型执行print(tokenizer.chat_template)就能打印出它正在使用的模板,建议从不调用工具、不支持 RAG 的简单模型开始看,工具类模型的模板可能非常复杂:

from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1") print(tokenizer.chat_template)

文档给出的一个基础模板示例长这样,它遍历消息列表,逐条输出角色、内容和结束 token;当add_generation_prompt=True时再追加 assistant 消息的开头标记:

{%- for message in messages %} {{- '<|' + message['role'] + '|>\n' }} {{- message['content'] + eos_token }} {%- endfor %} {%- if add_generation_prompt %} {{- '<|assistant|>\n' }} {%- endif %}

编写模板:变量、函数与空白控制

模板里固定的常量只有messages变量和add_generation_prompt布尔值,但你还可以访问传给apply_chat_template任意其他关键字参数——最常用的附加变量是tools(JSON schema 格式的工具列表),文档建议工具一律用这个命名,保持与标准 API 的兼容。

另外可以直接按名字访问tokenizer.special_tokens_map里的特殊 token,例如{{- bos_token }}{{- eos_token }}。模板里还有两个可调用的函数,写法是{{- function_name(argument) }}

  • raise_exception(msg):抛出TemplateException,适合调试或对错误用法发出警告;
  • strftime_now(format_str):按指定格式获取当前日期时间,等价于 Python 的datetime.now().strftime(format_str),常用于 system 消息里。

一个容易踩的坑是空白。Jinja 会原样输出文本块前后的空白,而多出来的空白如果不在模型训练数据里,会伤害模型性能。解决办法是在 Jinja 行语法里加-(如{%- for ... %}{{- ... }}),这样既能用缩进排版,又不会把缩进渲染进输出。对比下面两个版本:

{% for message in messages %} {{ message['role'] + message['content'] }} {% endfor %}

上面这段没有用-,输出会带入多余空白。推荐写法:

{%- for message in messages %} {{- message['role'] + message['content'] }} {%- endfor %}

跨语言实现兼容性

如果你的模板会被非 Python 的 Jinja 实现使用(例如用 JavaScript 或 Rust 部署),需要做三处替换:

  • 把 Python 方法换成 Jinja 过滤器:string.lower()string|lowerdict.items()dict|dictitemsstring.strip()string|trim
  • 把 Python 特有的TrueFalseNone换成truefalsenone
  • 直接渲染 dict 或 list 时不同实现可能给出不同结果(比如字符串引号从单引号变成双引号),加上tojson过滤器保持一致。

超长模板:用独立文件维护

工具调用、RAG 等新特性的模板可能超过 100 行。把模板写进独立文件更方便调试——文件里的行号与模板解析或执行错误的行号一一对应:

# 把当前模板导出到文件 open("template.jinja", "w").write(tokenizer.chat_template)

编辑完成后再读回 tokenizer:

tokenizer.chat_template = open("template.jinja").read()

多模态模板的注意点

对多模态模型,chat_template属性设在processor上而不是 tokenizer 上,而且消息的content经常是内容 dict 列表而不是单一字符串,模板里需要检查每个内容项的类型并分别处理。一般原则是模板不要直接访问图片或视频数据(那是 processor 在模板渲染之后处理的事),遇到图片/视频内容时输出一个特殊 token(如<|image|><|video|>),由 processor 稍后展开成对应的 token 序列。具体输出哪些 token 取决于模型,文档强烈建议先加载一个现成的多模态 processor 观察它的处理方式。示例(处理图文混排内容):

{%- for message in messages %} {%- if loop.index0 == 0 %} {{- bos_token }} {%- endif %} {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }} {%- if message['content'] is string %} {{- message['content'] }} {%- else %} {%- for content in message['content'] %} {%- if content['type'] == 'image' %} {{- '<|image|>' }} {%- elif content['type'] == 'text' %} {{- content['text'] }} {%- endif %} {%- endfor %} {%- endif %} {{- '<|eot_id|>' }} {%- endfor %} {%- if add_generation_prompt %} {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} {%- endif %}

注意并不是所有模型都这样处理——有的模型会把所有图片移到用户消息末尾。模板永远要与模型训练格式一致。

第二步:把模板赋给 tokenizer 并验证渲染结果

模板就绪后,赋给chat_template属性,再用apply_chat_template测试。输入是role/content键构成的消息 dict 列表,先设tokenize=False检查渲染出的字符串是否符合预期:

from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta") tokenizer.chat_template = "你的模板字符串" messages = [ {"role": "system", "content": "You are a friendly chatbot who always responds in the style of a pirate"}, {"role": "user", "content": "How many helicopters can a human eat in one sitting?"}, ] print(tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True))

文档中 Zephyr-7B 的对应示例输出如下(文档示例,你的模板会渲染出你自己定义的格式):

<|system|> You are a friendly chatbot who always responds in the style of a pirate</s> <|user|> How many helicopters can a human eat in one sitting?</s> <|assistant|>

确认渲染结果后,走完整生成链路验证。add_generation_prompt=True会在末尾追加 assistant 消息开头的 token——模型本质上是继续 token 序列,如果不含这个提示,它可能"继续"用户消息而不是回复。tokenize=True时可以直接返回张量(这是通常更安全的选项):

import torch from transformers import AutoModelForCausalLM, AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta") model = AutoModelForCausalLM.from_pretrained("HuggingFaceH4/zephyr-7b-beta", device_map="auto", dtype=torch.bfloat16) tokenized_chat = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors="pt").to(model.device) print(tokenizer.decode(tokenized_chat["input_ids"][0])) outputs = model.generate(**tokenized_chat, max_new_tokens=128) print(tokenizer.decode(outputs[0]))

文档示例中模型以海盗口吻回答了"人一次能吃几架直升机"(文档示例,实际生成内容因随机性和版本而异)。

验证时注意两点:

  • 如果你先apply_chat_template(tokenize=False)得到字符串、之后再手动 tokenize,要在 tokenize 时设add_special_tokens=False,否则<bos>/<eos>这类特殊 token 会被重复添加,伤害性能。tokenize=True则不存在这个问题;
  • add_generation_prompt只对模板里显式支持它的模型有效——有的模型(如 Llama)在 assistant 回复前没有特殊 token,此时该参数不产生效果。

第三步:保存到磁盘

save_pretrained/push_to_hub默认把模板写成独立的chat_template.jinja文件(仓库根目录),字符串模板就是一个文件;命名模板 dict 则default写入chat_template.jinja,其余每个名字写一个additional_chat_templates/<name>.jinja文件。保存时tokenizer_config.json里的chat_template字段会被移除以避免重复。加载时.jinja文件优先于 config 中内嵌的模板;存在多个命名模板时,apply_chat_template在传入tools时选用tool_use条目,否则选default

两种遗留格式(tokenizer_config.json内嵌的chat_template字段、多模态 processor 的chat_template.json)只保留用于向后兼容加载,文档明确警告不要再往里写模板,也没有支持的途径保存成遗留格式。如果仓库里旧格式与新的.jinja文件混用,processor 仓库加载时会直接报错。旧仓库可以"加载后再存一次"完成迁移:

from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("your-org/your-model") tokenizer.push_to_hub("your-org/your-model")

也可以不经过 hub,直接编辑 tokenizer 目录下的chat_template.jinja文件——文档指出这通常比操纵模板字符串更容易。

限制与后续

  • 模板没有"万能格式",token、空白、布局全部是模型特定的,写完后必须逐字核对模型训练格式;
  • 如果你要为支持工具调用的模型写模板,需要同时渲染工具定义(tools变量)、工具调用(assistant 消息的tool_calls键)和工具响应(tool角色消息),模板示例见 Writing a chat template 的 "Templates for tools" 章节;
  • 训练场景下,用模板预处理数据集时设add_generation_prompt=False,因为提示 assistant 回复的额外 token 在训练中没有帮助(示例见 Chat templates 的 "Model training" 章节);
  • 模板验证通过后,用push_to_hub上传到 Hub;即使你不是模型 owner,为 chat template 缺失或错误的模型补一个模板也是有价值的贡献。

【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询