- 人工智能
- 大模型
- 预训练
- 微调
- LoRA
- RLHF
- 强化学习
- 分布式训练
【免费下载链接】PaddleNLP
Easy-to-use and powerful LLM and SLM library with awesome model zoo.
多轮对话精调(Multi-round SFT)是当前开源 Chat 模型落地最核心的场景之一,而不同模型(Llama、Qwen、ChatGLM 等)的对话模板拼接规则各不相同。本文以 docs/zh/llm/docs/chat_template.md 为主体,结合 llm/utils/data.py、paddlenlp/transformers/tokenizer_utils.py 等源码实现,系统讲解 PaddleNLP 如何通过一个chat_template.json配置将多轮对话前处理标准化,并完整覆盖从模板构造、Jinja2 渲染原理、训练数据截断策略到run_finetune.py启动精调的实操链路。读完本文,你将能独立为自己的模型编写chat_template配置、构造多轮训练数据,并实现训练/推理阶段动态自定义 system prompt。
一、为什么需要chat_template:多轮对话前处理的标准化
随着开源社区中 Chat 类型模型越来越多,PaddleNLP 已集成 Llama、Qwen、ChatGLM 等系列模型。在推理侧,PaddleNLP 提供了apply_chat_template接口,只需调用该函数即可将对话历史与用户最新 query 按模型指定规则拼接,实现不同模型定制化 Prompt 规则的统一推理。
然而多轮对话训练精调的场景同样快速增长,且不同模型的多轮对话模板构造规则互不一致。若每个模型各自实现一套训练前处理逻辑,代码会迅速膨胀且难以维护。为此 PaddleNLP 设计了chat_template机制,将"模板规则"与"训练代码"解耦:只需一份 JSON 配置,即可为该模型添加多轮对话精调训练支持,并同时复用于推理侧的apply_chat_template,真正实现训练与推理两端的前处理标准化。
二、chat_template.json的配置结构与字段语义
2.1 配置文件命名与存放位置
chat_template的配置文件默认名为chat_template.json。在 paddlenlp/utils/env.py 中可以看到常量定义:
CHAT_TEMPLATE_CONFIG_NAME = "chat_template.json"该文件通常与模型权重、tokenizer_config.json存放在同一模型目录下。在 paddlenlp/transformers/tokenizer_utils.py 的from_pretrained逻辑中,加载 tokenizer 时会自动探测同目录下的chat_template.json并完成初始化:
# load chat-template chat_template_file = os.path.join(tokenizer_config_file_dir, CHAT_TEMPLATE_CONFIG_NAME) if not os.path.exists(chat_template_file): return tokenizer tokenizer.init_chat_template(chat_template_file)因此,判断某个模型是否支持 chat-template 的最直接方式,就是看其模型目录下是否存在chat_template.json文件。目前 PaddleNLP 正在持续为各系列模型补齐该文件。
2.2 配置示例:以 Qwen Chat 模型为例
以qwen-14b-chat为例,其chat_template.json配置内容如下(该配置参考自 Qwen 官方生成工具中定义的对话拼接规则):
{ "system": "You are a helpful assistant.", "conversation": ["\n<|im_start|>user\n{{user}}<|im_end|>\n<|im_start|>assistant\n", "{{bot}}<|im_end|>"], "query": "\n<|im_start|>user\n{{query}}<|im_end|>\n<|im_start|>assistant\n" }2.3 三个字段的语义与使用边界
从源码中的ChatTemplate数据类(paddlenlp/transformers/tokenizer_utils.py)可以确认这三个字段的数据结构:
@dataclass class ChatTemplate: conversation: list[str] | None = None system: str | None = None query: str = None各字段含义如下:
| 字段 | 类型 | 必选 | 用途 |
|---|---|---|---|
system | str | 否 | 系统提示词,拼接在整段对话最前面;配置中可包含 Jinja2 变量(用于动态 system prompt) |
conversation | list[str] | 是 | 恰好两个元素,分别对应 User 侧与 Bot 侧的对话模板片段;User 片段在训练时不参与 loss 计算,Bot 片段参与 loss 计算 |
query | str | 是 | 仅用于推理场景的模板,负责将最新的单条用户 query 按模型规则拼接 |
query与conversation的内容非常相似,但面向不同场景设计:query 只用于推理,query 与 conversation 一起用于训练。以 Qwen 为例,conversation的第一段覆盖了完整的user → assistant提示语(含用户内容与生成前缀),而query就是该段去掉{{bot}}生成区之后的"待生成前缀",两者天然配套。
2.4 关键注意点(文档核心约束)
query和conversation为必选项,二者内容非常类似,主要是为应对推理和训练两种场景分别设计。- 分词时不得添加特殊 token:由于训练和推理过程中会在文本中额外添加独特 token 标记,包括
bos_token、eos_token以及<|im_start|>这类自定义标记,基于chat_template的分词会将add_special_tokens参数始终设置为False。这一点在源码中同样被强制约束:apply_chat_template在调用 tokenizer 前显式写入tokenizer_kwargs["add_special_tokens"] = False(见 tokenizer_utils.py)。 conversation必须是两个元素的数组,分别对应 User 与 Bot 的对话内容。源码render_conversation中对此有断言检查:assert len(conversation_data) == 2, "Each round/turn of conversation must be two participants, eg: [user-query, bot-query]"。- 训练 loss 计算规则:User 内容在训练过程中不参与 loss 计算,Bot 内容参与 loss 计算。实现上对应 llm/utils/data.py 中 labels 的构造:User 部分填充
-100,Bot 部分保留真实 token id。 system长度约束:训练过程中 system 文本的长度不可大于max_length。源码中对应断言len(system_ids) < data_args.max_length。system不可被截断:在长度裁剪时 system 永远保留,只对对话部分做截断。
三、底层渲染机制:基于 Jinja2 的模板引擎
chat_template.json之所以能同时服务于训练与推理,关键在于底层采用Jinja2 模板引擎渲染。在 tokenizer_utils.py 中,ChatTemplate._compile_jinja_template使用ImmutableSandboxedEnvironment(沙箱环境,trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True)编译模板,并注册了raise_exception、regex_findall、tojson等辅助函数与过滤器,保证模板能力足够且安全。
3.1 核心渲染方法
ChatTemplate提供四个核心方法(均位于 tokenizer_utils.py 的ChatTemplate类中):
render_system(context_data):渲染system字段模板。若未配置system则返回空字符串。渲染时会把context_data(如动态 system 内容)注入模板上下文。render_conversation(conversation_data, index, context_data):渲染conversation的两个模板片段。conversation_data会被归一化为{"user": ..., "bot": ..., "index": ...}字典后再渲染,因此模板中可使用{{user}}、{{bot}}、{{index}}等变量。render_query(query, index, context_data):渲染query字段,以query=query, index=index为上下文。__call__:将整段多轮对话渲染成最终文本。流程为:先渲染 system,再对前 n-1 轮逐轮调用render_conversation拼接,最后一轮用render_query收尾(此时上下文还会注入length、is_first、is_last等辅助变量,供模板做更精细的控制)。
3.2 训练侧编码:encode_chat_inputs
训练数据侧对应的入口是encode_chat_inputs(tokenizer_utils.py),它会返回按轮次切分的 token id 对:
- 每轮对话先调用
render_conversation得到渲染后的user_input与bot_output; - 再分别以
add_special_tokens=False编码成user_ids与bot_ids; - 最终产出
{"system": system_ids, "conversations": [[user_ids, bot_ids], ...]}结构,供训练侧做长度裁剪与 labels 构造。
其 docstring 描述的拼接范式为:Turn 0: bos + system + sep + user / bot + eos,Turn t: sep + bot + query / bot + eos。
此外,PaddleNLP 同时兼容 HuggingFace 风格的 Jinja2 字符串模板(Template类型)与 PaddleNLP 风格的ChatTemplateJSON 配置,encode_chat_inputs与apply_chat_template都会按类型自动分流处理,迁移成本低。
四、训练侧数据流:从 JSON 数据到input_ids/labels
4.1 多轮训练数据格式
使用chat_template训练时,训练数据需要保证为如下格式——src与tgt分别为等长的字符串数组,一一对应每一轮的 User 输入与 Bot 输出:
{"src": ["user-1", "user-2", ..., "user-n"], "tgt": ["bot-1", "bot-2", ..., "bot-n"]} ...源码 llm/utils/data.py 的tokenize_rounds_example中对此有明确校验:
assert len(example["src"]) == len(example["tgt"]), "the length of `src` and `tgt` field must be same." conversations = [[src, tgt] for src, tgt in zip(example["src"], example["tgt"])]4.2 训练数据的切分与 loss 掩码
在 llm/utils/data.py 的tokenize_rounds_example(L131-L201)中,完整流程为:
- 取出
context字段(默认为{}),并注入context_data["is_training"] = True; - 调用
tokenizer.encode_chat_inputs将每轮对话编码为[user_ids, bot_ids]对; - 弹出并单独保存
system_ids,对 system 长度做断言校验; - 构造
input_ids与labels:User 部分的 labels 置为-100(不参与 loss),Bot 部分保留真实 token id,并将 system 部分的 labels 也置为-100; - 若 tokenizer 需要
position_ids,则按序列长度生成。
随后convert_rounds_example_common(L315-L352)完成最终的 shift 操作(input_ids, labels = input_ids[:-1], labels[1:])并组装features。在convert_example_common(L204-L246)中,一旦检测到tokenizer.chat_template is not None,训练数据就会自动走上述多轮处理分支,否则退化为传统的 src/tgt 单轮拼接方式。
4.3 截断策略:单轮截断 vs 多轮截断
chat_template的截断策略在tokenize_rounds_example中有非常清晰的实现,与文档描述完全一致:
- 多轮对话(超过一轮):计算训练 token 总长度时,从最后一轮开始从后往前累加每一轮的对话长度(包含 User 与 Bot 的总 token 数);若累加到当前轮时总长度超过
max_length(扣除 system 长度后的剩余容量),则截断当前轮次,且不再计算更早的历史对话,直接构造训练数据:
for index in range(len(conversations_ids) - 1, -1, -1): user_input_ids, bot_input_ids = conversations_ids[index][0], conversations_ids[index][1] # break when the length of current conversations is greater than max_length if len(input_ids) + len(user_input_ids) + len(bot_input_ids) > max_length: if index < len(conversations_ids) - 1: break ...这种"保尾去头"的策略保证了最新的对话轮次优先完整保留,符合对话精调中"最近对话最相关"的直觉。
- 仅有一轮对话:此时基于 token 长度直接截断,伪代码为
(system_tokens + conversation_tokens)[:max_length]。源码中的实现是当只有一轮时,对user_input_ids按src_length - len(system_ids)截断、对bot_input_ids按剩余容量截断,保证至少保留一轮完整数据:
if index < len(conversations_ids) - 1: break user_input_ids = user_input_ids[: data_args.src_length - len(system_ids)] bot_input_ids = bot_input_ids[: max_length - len(user_input_ids)] should_break = True无论哪种策略,system都始终保留在前部且不被截断(input_ids = system_ids + input_ids)。
4.4 特例:ChatGLM 的单轮精调
需要特别说明的是,不同模型族的数据流存在差异。从 llm/utils/data.py 的get_convert_example可见,chatglm走独立的convert_example_chatglm分支,且在启用chat_template时只会通过convert_multi_rounds_to_single_round将多轮对话压平为单轮(历史轮次全部拼入 src、最新 Bot 回复作为 tgt)后再做常规编码:
if tokenizer.chat_template is not None: # chatglm only support single-round finetune example = convert_multi_rounds_to_single_round(example, tokenizer)convert_multi_rounds_to_single_round(L22-L37)会依次渲染 system、前 n-1 轮完整对话,最后一轮仅保留 user 前缀拼入src,而tgt只保留最新一轮的 bot 回复。
五、启动精调:run_finetune.py与--chat_template参数
5.1 参数初始化逻辑
将构造好的chat_template.json传入llm/run_finetune.py即可启用多轮精调。训练入口在 llm/run_finetune.py 中调用init_chat_template:
init_chat_template(tokenizer, model_args.model_name_or_path, data_args.chat_template)其中data_args.chat_template的定义见 llm/predict/predictor.py 中同名参数(run_finetune.py复用该参数定义),其 help 文本完整描述了四种取值行为。
5.2 四种取值行为
init_chat_template的核心实现位于 paddlenlp/trl/llm_utils.py(L629-L673),--chat_template参数的完整行为矩阵如下:
| 参数取值 | 行为 |
|---|---|
未设置(默认None) | 不显式干预,tokenizer 若自带chat_template.json则仍会按默认方式加载 |
字符串"none" | 显式关闭 chat-template:tokenizer.chat_template = None,即使模型自带模板也不使用 |
与model_name_or_path一致 | 使用模型自带的chat_template.json文件(默认加载行为) |
| 目录路径 | 自动查找该目录下的chat_template.json并加载 |
| 文件路径 | 直接加载该 JSON 文件中的 chat-template 配置 |
其中前三种情形对应的源码分支:
if str(chat_template_file).lower() == "none": tokenizer.chat_template = None # delete the chat_template from tokenizer return if chat_template_file == model_name_or_path: if tokenizer.chat_template is None: logger.warning(f"there is not `chat_template.json` file in the `{model_name_or_path}`") return # it will load the `chat_template.json` file by default if os.path.isdir(chat_template_file): local_chat_template_file_path = os.path.join(chat_template_file, "chat_template.json") ...5.3 命令行实操示例
- 使用模型自带 chat-template(当
chat_template与model_name_or_path一致时默认使用模型自带模板):
python run_finetune.py ... --model_name_or_path qwen/qwen-7b-chat --chat_template qwen/qwen-7b-chat注意:并非所有模型都支持 chat-template,PaddleNLP 正在全力支持中,可通过模型目录是否带有
chat_template.json文件来判断。
- 使用自定义 chat-template:
python run_finetune.py ... --chat_template ./qwen_14b_chat_template.json- 显式不使用 chat-template(
--chat_template none):此时按普通单轮精调流程处理。
5.4 训练约束:eval_with_do_generation
启用chat_template后,llm/run_finetune.py 会强制将eval_with_do_generation置为False,因为多轮模板拼装后的评测不应再走生成式(generation)评估路径:
# if using chat_template, data_args.eval_with_do_generation must be false if tokenizer.chat_template is not None: data_args.eval_with_do_generation = False同样的初始化逻辑也贯穿在 DPO/RL/RM 等对齐训练(如 llm/alignment/rl/run_rl.py、llm/alignment/dpo/run_dpo.py)、量化(llm/run_quantization.py)、Embedding(llm/run_embedding.py)以及推理(llm/predict/predictor.py)等全链路入口中,保证同一套模板配置在整个模型生命周期内行为一致。
六、自定义 system prompt:Jinja2 变量与context字段
6.1 在模板中声明 system 变量占位符
默认情况下system字段是固定字符串。若希望在训练或推理过程中动态调整 system prompt,需要保证chat_template.json中的system配置包含 Jinja2 变量占位符,并尽量保留默认参数(通过 Jinja2 的默认值语法{{ var | 'default' }},即在变量后用|提供默认值)。以 Qwen 模板为例,可做如下修改:
{ - "system": "You are a helpful assistant.", + "system": "{{system | 'You are a helpful assistant.'}}", "conversation": ["\n<|im_start|>user\n{{user}}<|im_end|>\n<|im_start|>assistant\n", "{{bot}}<|im_end|>"], "query": "\n<|im_start|>user\n{{query}}<|im_end|>\n<|im_start|>assistant\n", }这里需要开发者手动调整
chat_template.json来实现动态 system prompt,{{user}}、{{bot}}、{{query}}等同样是 Jinja2 变量占位符的用法示例。
6.2 训练数据通过context字段注入 system
调整模板后,训练数据中需要增加context字段将动态system内容传入:
{"src": ["user-1", "user-2", ..., "user-n"], "tgt": ["bot-1", "bot-2", ..., "bot-n"], "context": {"system": "你是一个擅长做任务的人工智能助手"}} ...在渲染chat_template时,以上数据中的context会被作为 Jinja2 的上下文数据使用,从而实现在训练数据集中为每条训练数据定制各自的 system prompt。对应的源码链路为:tokenize_rounds_example中context_data = example.get("context", {}),随后传入encode_chat_inputs的context_data,最终由render_system(context_data)完成渲染;而_init_context_data还会额外注入is_training标记,便于模板区分训练/推理场景。
6.3 推理侧的动态 system
推理侧同样支持:调用apply_chat_template(conversation, tokenize=False, context_data={...})时传入context_data(tokenizer_utils.py 的ChatTemplateMixin),即可在推理时动态传入 system 内容;llm/predict/predictor.py 中多处推理路径即通过apply_chat_template(sentence, tokenize=False)完成对话模板拼装。这样训练与推理两端使用同一份chat_template.json,模板规则天然对齐,避免了"训练一套、推理一套"带来的 prompt 漂移问题。
七、参考示例与验证途径
仓库中的tests/fixtures/chat_template.json提供了一个结构完整的参考模板(Human/Bot 风格),可用于对照理解三个字段的写法:
{ "system": "你是一个人工智能助手\n", "conversation": ["Human: {{user}}<sep> Bot:", "{{bot}}\n"], "query": "Human: {{query}}<sep> Bot:" }调试时可以按以下顺序自检:
- 检查模型目录下是否存在
chat_template.json,或确认--chat_template指向的文件路径有效; - 确认
conversation恰好两个元素、query与conversation均已配置; - 确认模板中的变量名与数据字段一致(
{{user}}、{{bot}}、{{query}},以及动态 system 时的{{system}}和context字段); - 确认
max_length大于 system 长度,避免训练中断言失败; - 启动训练时检查
eval_with_do_generation是否已被自动置为False。
总结
PaddleNLP 的chat_template机制用一份chat_template.json统一了多轮对话在训练与推理两侧的模板拼接规则:system承载全局系统提示(支持 Jinja2 动态渲染)、conversation定义 User/Bot 轮次模板并天然划分 loss 计算边界、query覆盖推理侧的最新 query 拼装。配合init_chat_template的灵活取值策略与"保尾去头"的多轮截断算法,开发者只需编写模板与按src/tgt格式组织数据,即可借助llm/run_finetune.py快速完成任意 Chat 模型的多轮对话精调,并可无缝扩展 DPO、RL、量化等下游场景。
- 人工智能
- 大模型
- 预训练
- 微调
- LoRA
- RLHF
- 强化学习
- 分布式训练
【免费下载链接】PaddleNLP
Easy-to-use and powerful LLM and SLM library with awesome model zoo.
相关推荐
PaddleNLP 多轮对话精调指南:从 chat_template 配置到源码级原理解析
PaddleNLP 多轮对话精调指南:从 chat_template 配置到源码级原理解析 导读 本教程以 PaddleNLP 的多轮对话精调能力为主线,讲解如
人工智能大模型预训练微调LoRARLHF强化学习分布式训练模型推理服务推理引擎模型量化模型压缩本地部署NLPPaddleNLP 对话生成模板(chat_template)完全指南:多轮对话构造与自定义模板实战
PaddleNLP 对话生成模板(chat_template)完全指南:多轮对话构造与自定义模板实战 导读 PaddleNLP 内置了对话模板(chat_tem
人工智能大模型预训练微调LoRARLHF强化学习分布式训练模型推理服务推理引擎模型量化模型压缩本地部署NLPXTuner 单轮对话指令微调:从数据格式到 SFT 训练的全流程指南
XTuner 单轮对话指令微调:从数据格式到 SFT 训练的全流程指南 单轮对话指令微调(Single turn SFT)的目标是让预训练模型学会"根据一条指令
大模型模型微调
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考