当前位置: 首页 > news >正文

vLLM自定义对话模板

为什么 vLLM 要支持自定义对话模板?

把大语言模型接入线上服务时,我们习惯以systemuserassistant这样的结构发送消息:

[{"role":"system","content":"你是一个专业的助手。"},{"role":"user","content":"请解释什么是 KV Cache。"}]

但对模型而言,这些结构化字段并不能直接被理解。模型最终接收到的,始终是一段经过分词后的文本和 token 序列。问题在于,不同模型在训练时使用的“对话文本格式”并不相同:有的使用 ChatML,有的使用[INST]...[/INST],有的使用 Llama 3 风格的角色标记,也有模型采用自己定义的特殊 token。

这正是对话模板(Chat Template)存在的意义。它负责将接口层传入的标准消息,转换为目标模型在训练阶段最熟悉的 prompt 格式。例如,同样一句用户问题,可能需要被拼接为:

<|system|> 你是一个专业的助手。 <|user|> 请解释什么是 KV Cache。 <|assistant|>

也可能需要采用完全不同的格式:

<s>[INST] <<SYS>> 你是一个专业的助手。 <</SYS>> 请解释什么是 KV Cache。 [/INST]

比如说如果我们使用LLaMA Factory进行微调,LLaMA Factory微调使用的模板是自己写好的,在下面的代码里面


但是使用vLLM这些推理框架的时候,他们使用的是模型配置文件里面的模板

如果模板与模型训练时的格式不匹配,轻则回答质量下降,重则出现角色混乱、系统提示词失效、重复输出标签、多轮对话错位,甚至工具调用无法正常工作。

因此,vLLM 提供自定义对话模板的能力,并不只是为了“灵活配置提示词”。更重要的是,它让服务端能够准确适配不同模型、不同微调数据格式,以及工具调用、多模态等更复杂的推理场景。理解这一点,是正确部署和使用 vLLM 对话服务的第一步。

导出LLaMA Factory的模板

在LLaMA Factory的template.py文件中有很多内部方法可以生成模板,我们可以使用这些方法来生成我们的模板

创建导出文件

LLaMA-Factory/src/llamafactory目录下创建一个export_template.py的文件


内容如下

importsysimportos# 将项目根目录添加到Python路径root_dir=os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))sys.path.append(root_dir)fromllamafactory.data.templateimportTEMPLATESfromtransformersimportAutoTokenizer# 初始化分词器(任意支持的分词器均可)tokenizer=AutoTokenizer.from_pretrained("/home/gillbert/code/hugging_face_test/modelscope_test/llm/models/Qwen--Qwen3.5-2B/snapshots/master")# 获取模板对象template_name="qwen3"# 该名称是LLaMA Factory template.py中有的模型名称template=TEMPLATES[template_name]# 修复分词器的Jinja模板template.fix_jinja_template(tokenizer)# 输出模板的Jinja格式print("="*60)print(tokenizer.chat_template)

执行该脚本,会得到如下输出

(llamafactory)➜ data git:(main)✗ python export_template.py============================================================{%-setimage_count=namespace(value=0)%}{%-setvideo_count=namespace(value=0)%}{%- macro render_content(content, do_vision_count,is_system_content=false)%}{%-ifcontent is string %}{{- content}}{%-elifcontent is iterable and content is not mapping %}{%-foritemincontent %}{%-if'image'initem or'image_url'initem or item.type=='image'%}{%-ifis_system_content %}{{- raise_exception('System message cannot contain images.')}}{%- endif %}{%-ifdo_vision_count %}{%-setimage_count.value=image_count.value +1%}{%- endif %}{%-ifadd_vision_id %}{{-'Picture '~ image_count.value ~': '}}{%- endif %}{{-'<|vision_start|><|image_pad|><|vision_end|>'}}{%-elif'video'initem or item.type=='video'%}{%-ifis_system_content %}{{- raise_exception('System message cannot contain videos.')}}{%- endif %}{%-ifdo_vision_count %}{%-setvideo_count.value=video_count.value +1%}{%- endif %}{%-ifadd_vision_id %}{{-'Video '~ video_count.value ~': '}}{%- endif %}{{-'<|vision_start|><|video_pad|><|vision_end|>'}}{%-elif'text'initem %}{{- item.text}}{%-else%}{{- raise_exception('Unexpected item type in content.')}}{%- endif %}{%- endfor %}{%-elifcontent is none or content is undefined %}{{-''}}{%-else%}{{- raise_exception('Unexpected content type.')}}{%- endif %}{%- endmacro %}{%-ifnot messages %}{{- raise_exception('No messages provided.')}}{%- endif %}{%-iftools and tools is iterable and tools is not mapping %}{{-'<|im_start|>system\n'}}{{-"# Tools\n\nYou have access to the following functions:\n\n<tools>"}}{%-fortoolintools %}{{-"\n"}}{{- tool|tojson}}{%- endfor %}{{-"\n</tools>"}}{{-'\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>'}}{%-ifmessages[0].role=='system'%}{%-setcontent=render_content(messages[0].content, false,true)|trim %}{%-ifcontent %}{{-'\n\n'+ content}}{%- endif %}{%- endif %}{{-'<|im_end|>\n'}}{%-else%}{%-ifmessages[0].role=='system'%}{%-setcontent=render_content(messages[0].content, false,true)|trim %}{{-'<|im_start|>system\n'+ content +'<|im_end|>\n'}}{%- endif %}{%- endif %}{%-setns=namespace(multi_step_tool=true,last_query_index=messages|length -1)%}{%-formessageinmessages[::-1]%}{%-setindex=(messages|length -1)- loop.index0 %}{%-ifns.multi_step_tool and message.role=="user"%}{%-setcontent=render_content(message.content,false)|trim %}{%-ifnot(content.startswith('<tool_response>')and content.endswith('</tool_response>'))%}{%-setns.multi_step_tool=false%}{%-setns.last_query_index=index %}{%- endif %}{%- endif %}{%- endfor %}{%-ifns.multi_step_tool %}{{- raise_exception('No user query found in messages.')}}{%- endif %}{%-formessageinmessages %}{%-setcontent=render_content(message.content,true)|trim %}{%-ifmessage.role=="system"%}{%-ifnot loop.first %}{{- raise_exception('System message must be at the beginning.')}}{%- endif %}{%-elifmessage.role=="user"%}{{-'<|im_start|>'+ message.role +'\n'+ content +'<|im_end|>'+'\n'}}{%-elifmessage.role=="assistant"%}{%-setreasoning_content=''%}{%-ifmessage.reasoning_content is string %}{%-setreasoning_content=message.reasoning_content %}{%-else%}{%-if'</think>'incontent %}{%-setreasoning_content=content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n')%}{%-setcontent=content.split('</think>')[-1].lstrip('\n')%}{%- endif %}{%- endif %}{%-setreasoning_content=reasoning_content|trim %}{%-ifloop.index0>ns.last_query_index %}{{-'<|im_start|>'+ message.role +'\n<think>\n'+ reasoning_content +'\n</think>\n\n'+ content}}{%-else%}{{-'<|im_start|>'+ message.role +'\n'+ content}}{%- endif %}{%-ifmessage.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}{%-fortool_callinmessage.tool_calls %}{%-iftool_call.function is defined %}{%-settool_call=tool_call.function %}{%- endif %}{%-ifloop.first %}{%-ifcontent|trim %}{{-'\n\n<tool_call>\n<function='+ tool_call.name +'>\n'}}{%-else%}{{-'<tool_call>\n<function='+ tool_call.name +'>\n'}}{%- endif %}{%-else%}{{-'\n<tool_call>\n<function='+ tool_call.name +'>\n'}}{%- endif %}{%-iftool_call.arguments is defined %}{%-forargs_name, args_valueintool_call.arguments|items %}{{-'<parameter='+ args_name +'>\n'}}{%-setargs_value=args_value|tojson|safeifargs_value is mapping or(args_value is sequence and args_value is not string)elseargs_value|string %}{{- args_value}}{{-'\n</parameter>\n'}}{%- endfor %}{%- endif %}{{-'</function>\n</tool_call>'}}{%- endfor %}{%- endif %}{{-'<|im_end|>\n'}}{%-elifmessage.role=="tool"%}{%-ifloop.previtem and loop.previtem.role!="tool"%}{{-'<|im_start|>user'}}{%- endif %}{{-'\n<tool_response>\n'}}{{- content}}{{-'\n</tool_response>'}}{%-ifnot loop.last and loop.nextitem.role!="tool"%}{{-'<|im_end|>\n'}}{%-elifloop.last %}{{-'<|im_end|>\n'}}{%- endif %}{%-else%}{{- raise_exception('Unexpected message role.')}}{%- endif %}{%- endfor %}{%-ifadd_generation_prompt %}{{-'<|im_start|>assistant\n'}}{%-ifenable_thinking is defined and enable_thinking istrue%}{{-'<think>\n'}}{%-else%}{{-'<think>\n\n</think>\n\n'}}{%- endif %}{%- endif %}

将刚才生成的jinja模板保存到文件qwen.jinja中,我们可以在vLLM启动模型的时候使用该对话模板

vllm serve /home/gillbert/code/vllm_test/llm/models/Qwen--Qwen3.5-0.8B/snapshots/master\--tensor-parallel-size1\--gpu-memory-utilization0.9\--max-model-len8192\--host0.0.0.0\--port8000\--api-key123456\--enable-auto-tool-choice\--tool-call-parser hermes\--chat-template ./qwen.jinja

我们可以使用Open WebUI测试一下


http://www.jsqmd.com/news/1408474/

相关文章:

  • Android应用集成腾讯TBS X5内核:解决WebView兼容性问题与性能优化实战
  • FlashAttention 3.7技术解析:AI推理加速与本地部署实战指南
  • 诚信的拼装式村镇污水处理器直销厂家怎么选?看准这几点不踩坑 - 装修教育财税推荐2026
  • 2026年智慧园区公司怎么选?对比这5点不踩坑
  • Node系列 · Node基础:文件 I/O
  • OLED屏幕技术原理与STM32驱动实战:从7T1C电路到SSD1306应用
  • 2026年重庆云石胶服务商怎么选?深耕渝东南近20年的实力派值得一看 - 装修教育财税推荐2026
  • Python工厂函数:从基础概念到实战应用的设计模式解析
  • 程序员高含金量证书盘点:从AWS到Kubernetes的实战认证指南
  • 楼宇微网虚拟储能系统建模与优化调度实践
  • 基于强化学习的自适应RAG检索深度优化:从Actor-Critic到工程实践
  • 三极管与MOS管电路符号快速识别指南:从原理到实战
  • Spring Boot + Kafka + Redis + RAG:互联网大厂 Java 面试故事集
  • 河北廊架雕塑厂家怎么选?这家源头工厂的性价比值得细看 - 装修教育财税推荐2026
  • 无惧伪装与盲区!镜像视界步态动力学+人脸服饰识别,实现全域跨镜精准溯源
  • Scratch 3.0 图形化编程入门:从零制作“疯狂海鸥冲浪记”游戏
  • 异构视觉智能体去中心化涌现通信:从原理到工程实践
  • 基于QLabel的工业级指示灯系统实现与优化
  • 丙午年七月初四过日子
  • 深入解析Python字节码缓存:从pyc文件机制到开发部署实战
  • Unity中文显示问题全解析:从字体渲染原理到TextMeshPro实战解决方案
  • 2026年优选北京酒回收实体店门店电话,天津老酒变现指南 - 装修教育财税推荐2026
  • LLM智能体安全评估:ForesightSafety-SAGE框架的自动化压力测试实践
  • Incorporate与Integrate深度解析:从概念差异到技术写作实战
  • 临夏州热门的埋弧焊直缝管/滚筒烘干机钢管制造厂 - 行业推荐官-2
  • Xteink 便携电子阅读器获新功能:可访问受 DRM 保护电子书,使用更便捷!
  • 0816晨间日记
  • 2026年宽带选择指南:三大运营商对比与300M/500M/1000M套餐实测分析
  • 多Agent系统实战:从架构设计到行业分析报告生成的AI团队协作
  • Windows 11任务栏图标失效与快捷方式箭头问题深度修复指南