vllm0.8.5:自定义聊天模板qwen_nonthinking.jinja,从根本上避免模型输出<think>标签
一、环境
vllm:0.8.5
大语言模型:DeepSeek-R1-Distill-Qwen-1.5B
内存:128G
GPU:无
二、qwen_nonthinking.jinja内容如下:
2.1、内容解读
第一部分:如果存在可用工具(tools)
{%- if tools %}
判断是否存在外部提供的工具(函数),如果有,则进入工具模式。
显示系统头 + 工具说明
{{- '<|System|>\n' }} {%- if messages[0].role == 'system' %}
{{- messages[0].content + '\n\n' }}
{%- endif %}
- 输出 <|System|> 标记。
- 如果第一条消息是 system 角色,输出其内容,并加两个换行。
输出工具列表说明
{{- "# Tools\n\nYou may call one or more functions..." }} <tools>
{%- for tool in tools %}
{{- "\n" }}
{{- tool | tojson }}
{%- endfor %}
</tools>
- 向模型说明可以使用的工具。
- 使用 <tools>...</tools> 包裹每个工具的 JSON 定义(如函数名、参数等)。
- tool | tojson 将 Python/JSON 对象转为标准 JSON 字符串。
指导模型如何调用函数
{{- "\n</tools>\n\nFor each function call... within <tool_call><tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": ...}\n<tool_call>\n" }}
告诉模型:当你想调用函数时,请使用如下格式:
<tool_call>{"name": "get_weather", "arguments": {"city": "Beijing"}}</tool_call>
注意:这里的 <tool_call> 是一个自定义 XML-like 标签,用来清晰地标记函数调用的开始和结束,避免与普通文本混淆。
第二部分:如果没有工具
{%- else %} {%- if messages[0].role == 'system' %}
{{- '<|System|>\n' + messages[0].content + '\n<|End|>\n' }}
{%- endif %}
{%- endif %}
- 不显示工具相关说明。
- 仅输出第一条 system 消息(如果有),并用 <|End|> 结束。
第三部分:设置命名空间变量(用于判断上下文)
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
创建一个命名空间 ns,用于在循环中跨迭代共享变量:
- multi_step_tool: 判断是否处于多步工具调用流程。
- last_query_index: 记录最后一次“用户主动提问”的位置。
向前遍历消息,判断是否为多步工具调用
{%- for message in messages[::-1] %} {%- set index = (messages|length - 1) - loop.index0 %}
{%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('<tool_call>') and message.content.endswith('<tool_call>')) %}
{%- set ns.multi_step_tool = false %}
{%- set ns.last_query_index = index %}
{%- endif %}
{%- endfor %}
- 倒序遍历 messages(从最新到最旧)。
- 找到第一个 role == "user" 且内容不是以 <tool_call> 开头/结尾的消息(即不是工具调用指令)。
- 把这个消息的索引记为 last_query_index,表示这是用户最后一次“真正提问”的位置。
- 用于后续判断哪些回复需要加上 <|End|>。
作用:防止在工具调用链中错误地终止对话流。
第四部分:主循环 —— 遍历每条消息并格式化输出
{%- for message in messages %}
对每条消息进行处理。
设置内容变量
{%- if message.content is string %} {%- set content = message.content %}
{%- else %}
{%- set content = '' %}
{%- endif %}
安全获取 content,避免非字符串类型出错。
情况一:用户消息(User)
{%- if message.role == "user" %} {{- '<|User|>\n' + content + '\n<|End|>\n' }}
格式:
<|User|>[用户内容]
<|End|>
情况二:助手消息(Assistant)
{%- elif message.role == "assistant" %}
处理 <think>...</think> 推理块(可选)
{%- set reasoning_content = '' %} {%- if '</think>' in content %}
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
{%- endif %}
- 如果内容包含 <think>推理内容</think>,则提取推理部分(虽然这里没使用),并把剩余部分作为正式回复。
- 这是为了支持“先思考,再回答”的格式。
根据是否是“最后一问之后的回复”,决定是否加 <|End|>
{%- if loop.index0 > ns.last_query_index %} {{- '<|Assistant|>\n' + content.lstrip('\n') + '\n<|End|>\n' }}
{%- else %}
{{- '<|Assistant|>\n' + content + '\n<|End|>\n' }}
{%- endif %}
- 如果该回复是在用户最后一次提问之前,不加 <|End|>(可能是中间步骤)。
- 如果是在之后,加上 <|End|> 表示回应完成。
处理工具调用(Tool Calls)
{%- if message.tool_calls %} {%- for tool_call in message.tool_calls %}
{%- if (loop.first and content) or (not loop.first) %}
{{- '\n' }}
{%- endif %}
{%- if tool_call.function %}
{%- set tool_call = tool_call.function %}
{%- endif %}
{{- '<tool_call>\n{"name": "' }}
{{- tool_call.name }}
{{- '", "arguments": ' }}
{%- if tool_call.arguments is string %}
{{- tool_call.arguments }}
{%- else %}
{{- tool_call.arguments | tojson }}
{%- endif %}
{{- '}\n</tool_call>' }}
{%- endfor %}
{%- endif %}
- 如果助手要调用函数,生成如下格式:
<tool_call>{"name": "search_web", "arguments": {"query": "天气预报"}}</tool_call><tool_call>{"name": "get_time", "arguments": {}}</tool_call>
- 支持多个函数调用。
- arguments 可以是字符串或对象,自动处理。
情况三:工具执行结果(Tool)
{%- elif message.role == "tool" %} {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
{{- '<|User|>' }}
{%- endif %}
{{- '\n<tool_call>\n' }}
{{- content }}
{{- '\n</tool_call>' }}
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
{{- '\n<|End|>\n' }}
{%- endif %}
- 工具返回的结果被视为“用户侧输入”,所以用 <|User|> 标记。
- 多个连续的 tool 消息会被合并到同一个 <|User|> 块中。
- 每个工具结果用 <tool_call>\ntool_result\n</tool_call> 包裹,与函数调用请求对称。
- 最后一个工具结果后加 <|End|>。
示例输出:
<|User|><tool_call>
{"result": "北京今天晴,气温25°C"}
</tool_call>
<tool_call>
{"result": "当前时间:2025-08-03 18:30"}
</tool_call>
<|End|>
第五部分:生成提示(用于触发模型回复)
{%- if add_generation_prompt %} {{- '<|Assistant|>\n<think>\n\n</think>\n\n' }}
{%- endif %}
- 如果设置了 add_generation_prompt=True,在最后加上:
<|Assistant|><think>
</think>
- 表示“现在轮到模型输出了”,并且鼓励模型先进行内部推理(<think>)。
- 这个标记不会被模型重复输出,而是作为生成起点。
总结:这个模板的作用
功能 | 实现方式 |
✅ 支持函数调用 | 用 <tool_call>...<tool_call> 包裹 JSON 格式的调用请求 |
✅ 接收工具结果 | 工具返回也用 <tool_call>...</tool_call> 标记,归为用户输入 |
✅ 支持多步工具调用 | 通过 ns.last_query_index 判断上下文 |
✅ 支持内部推理 | 使用 <think>...</think> 分隔思考与回答 |