LiteRT-LM 中 Qwen 2.5 的 Chat Template 与模型元数据配置深度解析
【免费下载链接】LiteRT-LMLiteRT-LM is Google's production-ready, high-performance, open-source inference framework for deploying Large Language Models on edge devices.项目地址: https://gitcode.com/GitHub_Trending/li/LiteRT-LM
LiteRT-LM 在 models/qwen2_5/ 目录下为 Qwen 2.5 模型家族提供了官方(canonical)的提示词模板(Chat Template)与模型元数据(LlmMetadata)配置。本文以该目录的 README.md 为骨架,结合 chat_template.jinja、LlmMetadataProto.pbtext 及配套测试用例,完整讲解这套模板的渲染规则、工具调用(Tool Calling)标准化格式、元数据字段含义,以及如何通过 Bazel 测试验证模板与 golden 输出的一致性,帮助你在端侧推理场景中正确使用并扩展 Qwen 2.5 的对话能力。
一、目录结构与职责定位
按照 LiteRT-LM 的模型配置规范,每个受支持模型家族在 models/ 下拥有独立目录,Qwen 2.5 目录内包含三类核心资产:
| 文件 | 职责 |
|---|---|
| chat_template.jinja | 官方提示词模板,基于 Jinja2 语法(运行时由 Minijinja 密闭渲染),负责将结构化对话轮次与工具声明转换成模型特定的提示词字符串 |
| LlmMetadataProto.pbtext | 模型元数据(text-format protobuf),描述起始/停止 token、采样参数、模型类型、是否支持函数调用,并内嵌一份与chat_template.jinja内容一致的 Jinja 模板字符串 |
| BUILD | Bazel 构建配置,通过chat_template_test宏把模板与元数据接入自动化测试 |
这三者被统一约束为"一致性"关系:LlmMetadataProto.pbtext中的jinja_prompt_template字段必须与chat_template.jinja逐字节一致,否则测试会失败(详见下文"测试与验证"章节)。
二、Chat Template 全流程逐段解析
chat_template.jinja 共 68 行,处理五类职责:内容格式化宏、System/工具声明块、普通对话轮次、助手工具调用、工具响应合并。下面逐段说明。
2.1format_content宏:多模态内容归一化
模板开头定义一个内容格式化宏,用于把消息的content列表转成纯文本:
{%- macro format_content(content) -%} {%- for item in content -%} {%- if item['type'] == 'text' -%} {{- item['text'] -}} {%- elif item['type'] == 'tool_response' -%} {%- if item['response'] is mapping or item['response'] is sequence -%} {{- item['response'] | tojson -}} {%- else -%} {{- item['response'] | string -}} {%- endif -%} {%- endif -%} {%- endfor -%} {%- endmacro -%}关键点:
- 仅处理
text与tool_response两种 content part;tool_response中的response若是 JSON 对象/数组则用内置tojson过滤器序列化,若是标量则转字符串。这与 models/README.md 中"content 是严格的多模态 part 列表"的约定一致——纯文本也要包装为{"type": "text", "text": "..."}对象。 - 模板对
image、audio、video等 part 不输出文本(Qwen 2.5 纯文本家族场景下通常不会出现),因此这一实现是安全的。
2.2 System 与工具声明块
{%- set loop_messages = messages[1:] if messages[0]['role'] == 'system' else messages -%} {#- Handle System/Tool Definitions Block -#} {%- if tools or messages[0]['role'] == 'system' -%} {{- '<|im_start|>system\n' -}} {%- if messages[0]['role'] == 'system' -%} {{- format_content(messages[0]['content']) -}} {%- endif -%} {%- if tools -%} {%- if messages[0]['role'] == 'system' -%} {{- '\n\n' -}} {%- endif -%} {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" -}} {%- for tool in tools -%} {{- '\n' + tool | tojson -}} {%- endfor -%} {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>" -}} {%- endif -%} {{- '<|im_end|>\n' -}} {%- endif -%}逻辑要点:
- 系统消息提前剥离:若首条消息是
system,先取出其内容单独渲染成system角色开头的块,并从后续循环中剔除(messages[1:]),保证系统提示只出现一次。 - 工具签名注入:当存在
tools列表时,每个工具声明通过tojson输出为单行 JSON,整体包裹在<tools>...</tools>XML 标签内,并附上一段固定的指令文本,要求模型以<tool_call>内 JSON 对象的形式返回函数名与参数。这是 README 中"Features & Standardization"第 1 点的落地实现。
2.3 普通消息循环:user / system / assistant
{%- for message in loop_messages -%} {%- set content = format_content(message['content']) -%} {%- if message['role'] == 'user' or message['role'] == 'system' -%} {{- '<|im_start|>' + message['role'] + '\n' + content + '<|im_end|>\n' -}} {%- elif message['role'] == 'assistant' -%} ... {%- elif message['role'] == 'tool' -%} ... {%- endif -%} {%- endfor -%}Qwen 2.5 使用 ChatML 风格的分隔符:每条消息以<|im_start|>{role}\n开头、<|im_end|>\n结尾。这一约定同时在元数据的prompt_templates中登记(见第三节)。
2.4 助手工具调用(tool_calls)渲染
{{- '<|im_start|>' + message['role'] + '\n' + content -}} {%- if message['tool_calls'] -%} {%- for tool_call in message['tool_calls'] -%} {%- if content or not loop.first -%} {{- '\n' -}} {%- endif -%} {%- set function = tool_call['function'] if 'function' in tool_call else tool_call -%} {%- set args = function['arguments'] if function['arguments'] is string else function['arguments'] | tojson -%} {{- '<tool_call>\n{"name": "' + function['name'] + '", "arguments": ' + args + '}\n</tool_call>' -}} {%- endfor -%} {%- endif -%} {{- '<|im_end|>\n' -}}要点:
- 兼容两种
tool_calls结构:标准 OpenAI 风格(含function子对象)或扁平结构(直接含name/arguments)。 arguments若已是 JSON 字符串则原样输出,否则tojson序列化。- 每条工具调用渲染为一个
<tool_call>...</tool_call>块;多条调用之间用换行分隔,支持并行工具调用场景。
2.5 工具响应合并到 user 角色
{%- elif message['role'] == 'tool' -%} {%- if loop.first or loop_messages[loop.index0 - 1]['role'] != 'tool' -%} {{- '<|im_start|>user' -}} {%- endif -%} {{- '\n<tool_response>\n' + content + '\n</tool_response>' -}} {%- if loop.last or loop_messages[loop.index0 + 1]['role'] != 'tool' -%} {{- '<|im_end|>\n' -}} {%- endif -%} {%- endif -%}这是 README 中"Multi-step tool responses are wrapped within<tool_response>tags under thetoolrole"的具体实现,有两个巧妙设计:
- 连续
tool消息合并:相邻的多条tool响应会被折叠进同一个user块(只有第一条前面输出<|im_start|>user,最后一条后面才输出<|im_end|>),避免碎片化的消息结构。 - 角色映射:最终渲染时
tool响应统一以user角色承载,内容外层包<tool_response>...</tool_response>,符合 Qwen 2.5 训练数据中"工具结果由 user 转述"的格式习惯。
2.6 生成提示(Generation Prompt)
{%- if add_generation_prompt -%} {{- '<|im_start|>assistant\n' -}} {%- endif -%}当add_generation_prompt为真时,在末尾追加空的assistant起始块,引导模型开始续写。该变量在 models/README.md 中登记为可选参数,默认值为true。
三、LlmMetadataProto.pbtext:模型元数据逐字段详解
LlmMetadataProto.pbtext 是对应 runtime/proto/llm_metadata.proto 中LlmMetadataProto消息的 text-format 实例,头部注释明确标注了 proto 文件与消息类型。各字段含义如下:
| 字段 | 值 | 说明 |
|---|---|---|
start_token | <\|endoftext\|> | 序列起始 token |
stop_tokens | <\|im_end\|> | 停止生成 token,即 ChatML 消息结束符 |
prompt_templates.user | prefix<\|im_start\|>user\n,suffix<\|im_end\|>\n | 非 Jinja 路径下的 user 消息包装格式 |
prompt_templates.model | prefix<\|im_start\|>assistant\n,suffix<\|im_end\|>\n | 非 Jinja 路径下的 assistant 消息包装格式 |
sampler_params | TOP_P类型,k=40,p=0.95,temperature=1.0 | 默认采样策略:top-p 采样,p 值 0.95 |
max_num_tokens | 4096 | 单次推理最大生成长度(默认值) |
llm_model_type | qwen2p5 {} | 模型家族标记,驱动引擎选择对应的计算/图执行路径 |
supports_function_calling | true | 声明该模型支持函数调用,是启用工具链路的前提 |
jinja_prompt_template | (整段模板字符串) | 内嵌的 Jinja 模板,与chat_template.jinja内容必须一致 |
min_runtime_version | "0.18.0" | 运行该模型配置所需的最低 LiteRT-LM 运行时版本 |
两个值得注意的工程细节:
- 双轨模板机制:元数据中既有结构化的
prompt_templates(简单 user/model 包装),又有完整的jinja_prompt_template。运行时优先使用 Jinja 渲染复杂对话(多轮、工具调用),结构化模板则作为轻量兜底。为保证二者不会分叉,测试会强制校验"pbtext 内嵌模板 == 磁盘上的.jinja文件"。 supports_function_calling与模型类型:qwen2p5类型 +true声明共同构成工具调用能力的声明式契约;引擎侧只有在元数据明确声明时才允许注入工具相关处理逻辑。
四、Tool Calling 标准化格式速览
结合 README 与 chat_template.jinja 的实现,Qwen 2.5 在 LiteRT-LM 中的工具调用统一遵循以下三组 XML 标签约定:
工具签名声明:放入 system 提示中的
<tools>...</tools>标签内,每个工具以单行 JSON(OpenAPI 风格)表示,如:{"type": "function", "function": {"name": "get_weather", "description": "Get current weather for a location.", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "City name"}}, "required": ["location"]}}}助手函数调用:以
<tool_call>...</tool_call>包裹 JSON 对象,格式固定为{"name": <function-name>, "arguments": <args-json-object>},如:<tool_call> {"name": "get_weather", "arguments": {"location": "London"}} </tool_call>工具响应:多步工具结果包裹在
<tool_response>标签下,最终承载于user角色消息中:<tool_response> {"location": "London", "temperature": "18C"} </tool_response>
这套格式是模板硬编码的标准化指令,模型在 system 提示中读到<tools>后,会被引导输出严格匹配<tool_call>格式的 JSON,从而被下游解析器稳定提取。
五、输入消息结构(渲染入口契约)
模板的输入由 LiteRT-LM 运行时构造,顶层字段在 models/README.md 中给出正式定义:
| 字段 | 类型 | 说明 |
|---|---|---|
messages | array | (必填)对话历史,元素为消息对象 |
tools | array | (可选)可用的工具(函数声明)列表 |
enable_thinking | boolean | (可选)是否让模型先输出推理过程 |
add_generation_prompt | boolean | (可选)是否追加模型轮次前缀,默认true |
bos_token | string | (可选)序列起始 token,如<bos>、<s> |
消息对象的核心角色有四种:system(系统指令、人格与工具 schema)、user(用户提问)、assistant(模型回复,可含tool_calls)、tool(工具执行结果,content 中为tool_responsepart)。arguments既可以是 JSON 对象也可以是序列化 JSON 字符串;当存在tool_calls时content允许为空。
六、测试与验证:如何证明模板正确
6.1 BUILD 中的测试接线
models/qwen2_5/BUILD 通过chat_template_test宏生成测试目标:
load("//models:chat_template_test.bzl", "chat_template_test") exports_files([ "chat_template.jinja", "LlmMetadataProto.pbtext", ]) chat_template_test( name = "chat_template_test", chat_template = "chat_template.jinja", pbtext_files = ["LlmMetadataProto.pbtext"], )pbtext_files的传入意味着测试还会校验元数据内嵌模板与磁盘模板的一致性(见 chat_template_test.bzl 的参数说明)。
6.2 测试用例与 golden 输出
Qwen 2.5 提供了三组测试输入(位于 testdata/input/):
- system_instruction.json:带系统指令的普通多轮对话,验证
<|im_start|>system块的渲染; - tools.json:完整工具链路(声明工具 → 两次工具调用 → 两次工具响应 → 最终文本回复),验证并行/多步工具调用;
- tool_response_ending.json:以工具响应结尾的对话,验证"相邻 tool 消息合并进单个 user 块"的边界行为。
每组输入在 testdata/golden/ 下有两个期望输出:*.txt(普通模式)与*-thinking.txt(thinking 模式)。以 tools.txt 为例,其渲染结果完整展示了# Tools声明块、<tools>签名、<tool_call>调用、<tool_response>结果合并以及末尾的<|im_start|>assistant生成提示。
6.3 测试运行器的工作原理
models/chat_template_test_runner.cc 是测试的可执行主体,其关键流程:
- 读取
--chat_template、--input_dir、--golden_dir等命令行参数(另支持--update_golden更新 golden、--pbtext_files校验元数据、--mirror_chat_templates校验镜像模板); - 用
runtime/components/prompt_template.h提供的PromptTemplate构造模板对象并调用Apply()渲染每个输入 JSON; - 每个输入同时以
enable_thinking=false/true两种模式渲染,若存在对应 thinking golden 文件则一并比对; - 逐字符比对渲染结果与 golden 文件(
EXPECT_EQ),不匹配即报告输入/输出/golden 三路信息; - 最后解析各
pbtext文件中的jinja_prompt_template字段,与磁盘模板逐字节比对。
运行全部模型家族的模板测试只需:
bazel test //models/...新增测试用例时,只需在testdata/input/放入输入 JSON、在testdata/golden/放入期望文本(或先以--update_golden生成),无需改动 C++ 代码。
七、在推理流程中的位置与扩展建议
从源码结构看,这套模板与元数据在 LiteRT-LM 推理链路中的定位可以概括为:运行时将多轮会话(含工具声明与执行结果)整理为messages/tools结构,经PromptTemplate渲染为模型输入文本;引擎依据LlmMetadataProto中的停止 token、采样参数与模型类型执行生成,并通过supports_function_calling决定是否启用工具解析与执行回路。
若你要为其他 Qwen 2.5 变体(如更长上下文、带思考模式的版本)复用此配置,建议保持chat_template.jinja与jinja_prompt_template的镜像关系不变,仅调整max_num_tokens、sampler_params、stop_tokens等生成相关字段,并为本目录补齐对应的 testdata 用例,确保任何改动都经过 golden 回归验证。
【免费下载链接】LiteRT-LMLiteRT-LM is Google's production-ready, high-performance, open-source inference framework for deploying Large Language Models on edge devices.项目地址: https://gitcode.com/GitHub_Trending/li/LiteRT-LM
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考