环境搭建

创建vllm,最好单独搭建一个vllm的环境

1
2
3
4
conda create -n vllm python=3.12 -y
conda activate vllm
pip install vllm
pip show vllm # 0.8.5.post1

命令行启动vllm

qwen3开源包括两款MoE模型:Qwen3-235B-A22B(2350多亿总参数、 220多亿激活参数),以及Qwen3-30B-A3B(300亿总参数、30亿激活参数);
以及六个Dense模型:Qwen3-32B、Qwen3-14B、Qwen3-8B、Qwen3-4B、Qwen3-1.7B和Qwen3-0.6B。

首先尝试在T4显卡(15G显存,80G内存)启动dense稠密向量的qwen3-4b模型,由于T4不支持bf16,所以需要修改为float16格式(--dtype=half

1
vllm serve Qwen/Qwen3-4B --dtype=half

发现跑不起来,显存太少,修改了--gpu-memory-utilization=0.95 --max-model-len=16384依然无法跑起来,于是用1.7B模型跑起来了

1
vllm serve Qwen/Qwen3-1.7B --dtype=half

在8卡V100上面部署 MOE模型,需要指定TP为8,type不能指定为half!

1
vllm serve Qwen/Qwen3-30B-A3B --dtype=float32  --tensor-parallel-size 8

代码调用vllm

可以直接import vllm库来进行离线推理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from vllm import LLM, SamplingParams

# 输入几个问题
prompts = [
"你好,你是谁?",
"法国的首都在哪里?",
]

# 设置初始化采样参数
sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=100)

# 加载模型,确保路径正确
llm = LLM(model="Qwen/Qwen3-1.7B", trust_remote_code=True, max_model_len=4096, dtype="half")

# 展示输出结果
outputs = llm.generate(prompts, sampling_params)

# 打印输出结果
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
#print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
print(f"Prompt: {prompt!r}, Generated text: {output!r}")

我们也可以暴露openai格式的http接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from pydantic import BaseModel,Field
from typing import List, Optional, Union
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.async_llm_engine import AsyncLLMEngine
from vllm.sampling_params import SamplingParams

app = FastAPI(title="OpenAI-compatible API Server")

# 允许跨域
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)

# 初始化异步引擎
async_engine_args = AsyncEngineArgs(
model="Qwen/Qwen3-1.7B",
trust_remote_code=True,
max_model_len=4096,
dtype="half",
)
engine = AsyncLLMEngine.from_engine_args(async_engine_args)

# 修正后的请求模型
class CompletionRequest(BaseModel):
model: str = Field("default-model", description="模型名称占位符") # OpenAI 格式必填字段
prompt: Union[str, List[str]] # 支持字符串或数组
max_tokens: int = 100
temperature: float = 0.8
top_p: float = 0.95
stream: bool = False
# 添加其他OpenAI兼容字段
stop: Optional[Union[str, List[str]]] = None
presence_penalty: float = 0.0

class Config:
# 允许接收未定义的字段(某些客户端会发送额外参数)
extra = "allow"

# # 需要流式的花,调用该函数即可
# # 流式处理分支
# if request.stream:
# return StreamingResponse(
# generate_stream(request.prompt, sampling_params),
# media_type="text/event-stream",
# headers={
# 'Cache-Control': 'no-cache',
# 'Connection': 'keep-alive'
# }
# )
async def generate_stream(prompt: str, sampling_params: SamplingParams):
request_id = "stream-request-123"
results_generator = engine.generate(prompt, sampling_params, request_id)

# 首先生成第一个结果
first_iteration = True
async for request_output in results_generator:
if first_iteration:
first_iteration = False
continue # 跳过第一个空结果

text = request_output.outputs[0].text
yield f"data: {json.dumps({
'id': request_id,
'object': 'text_completion',
'created': int(time.time()),
'choices': [{
'text': text,
'index': 0,
'logprobs': None,
'finish_reason': None
}]
})}\n\n"

# 发送结束标记
yield "data: [DONE]\n\n"

@app.post("/v1/completions")
async def create_completion(request: CompletionRequest):
try:
sampling_params = SamplingParams(
temperature=request.temperature,
top_p=request.top_p,
max_tokens=request.max_tokens
)
request_id = "0" # 简单示例,实际需生成唯一ID
results_generator = engine.generate(
request.prompt, sampling_params, request_id)

# 非流式处理
final_output = None
async for request_output in results_generator:
final_output = request_output

return {
"choices": [{
"text": final_output.outputs[0].text,
"index": 0,
"finish_reason": "length"
}]
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)

用curl调用如下:

1
2
3
4
5
6
7
curl -X POST http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "your-model-name",
"prompt": "法国的首都在哪里?",
"max_tokens": 50
}'

curl测试vllm

  1. 获取支持的模型:
1
curl -X GET http://localhost:8000/v1/models

返回如下:

1
{"object":"list","data":[{"id":"Qwen/Qwen3-1.7B","object":"model","created":1746610595,"owned_by":"vllm","root":"Qwen/Qwen3-1.7B","parent":null,"max_model_len":40960,"permission":[{"id":"modelperm-a2b3973921cc44a99947865b1baee7cc","object":"model_permission","created":1746610595,"allow_create_engine":false,"allow_sampling":true,"allow_logprobs":true,"allow_search_indices":false,"allow_view":true,"allow_fine_tuning":false,"organization":"*","group":null,"is_blocking":false}]}]}
  1. 调用模型进行问答,关闭推理
    Qwen3 模型会在回复前进行思考。这种行为可以通过硬开关(完全禁用思考)或软开关(模型遵循用户关于是否应该思考的指令)来控制。

硬开关在 vLLM 中可以通过以下 API 调用配置使用。要禁用思考,请使用"chat_template_kwargs": {"enable_thinking": false}参数

1
2
3
4
5
6
7
8
9
10
11
curl http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "Qwen/Qwen3-1.7B",
"messages": [
{"role": "user", "content": "介绍一下这个qwen3模型吧,不要思考"}
],
"temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
"max_tokens": 32768,
"chat_template_kwargs": {"enable_thinking": false}
}'

返回如下:

1
{"id":"chatcmpl-db7f824768084631b9eb0e8a0094d004","object":"chat.completion","created":1746610895,"model":"Qwen/Qwen3-1.7B","choices":[{"index":0,"message":{"role":"assistant","reasoning_content":null,"content":"<think>\n</think>\n\nQwen3 是通义实验室研发的大型语言模型,属于通义千问系列。它在多个方面进行了优化和升级,包括但不限于:\n\n1. **参数量和计算能力**:Qwen3 拥有更大的参数量,能够处理更复杂的语言任务,同时在计算效率上也有提升。\n\n2. **多语言支持**:Qwen3 支持多种语言,能够进行跨语言的理解和生成,这使得它在多语言应用场景中更具优势。\n\n3. **指令遵循能力**:Qwen3 在遵循指令和执行任务方面表现优异,能够处理各种复杂的指令,包括多轮对话和任务执行。\n\n4. **上下文长度**:Qwen3 的上下文长度更长,能够处理更长的对话历史,从而提供更连贯和准确的回答。\n\n5. **多模态能力**:虽然 Qwen3 主要专注于文本处理,但它的多模态能力也在不断提升,能够处理图像、音频等多媒体信息。\n\n6. **安全性与可靠性**:Qwen3 在安全性方面进行了加强,确保模型在使用过程中不会产生有害或不适当的内容。\n\nQwen3 的这些改进使其在多个领域,如客服、内容创作、教育、科研等,都具有广泛的应用前景。如果你有具体的应用场景或问题,可以告诉我,我可以提供更详细的介绍。","tool_calls":[]},"logprobs":null,"finish_reason":"stop","stop_reason":null}],"usage":{"prompt_tokens":18,"total_tokens":309,"completion_tokens":291,"prompt_tokens_details":null},"prompt_logprobs":null}

硬开关在 vLLM 中可以通过以下 API 调用配置使用。要禁用思考,请使用"chat_template_kwargs": {"enable_thinking": false}参数

1
2
3
4
5
6
7
8
9
10
11
curl http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "Qwen/Qwen3-1.7B",
"messages": [
{"role": "user", "content": "介绍一下这个qwen3模型吧"}
],
"temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
"max_tokens": 32768,
"chat_template_kwargs": {"enable_thinking": false}
}'

返回如下:

1
{"id":"chatcmpl-b45919d5eb6745c686a1017f92a64e12","object":"chat.completion","created":1746612269,"model":"Qwen/Qwen3-1.7B","choices":[{"index":0,"message":{"role":"assistant","reasoning_content":null,"content":"Qwen3 是通义实验室研发的一系列大语言模型之一,属于通义千问系列。它是在 Qwen2 的基础上进行优化和升级的版本,具有更强的性能和更丰富的功能。\n\nQwen3 模型在以下几个方面进行了改进:\n\n1. **参数量更大**:Qwen3 的参数量比 Qwen2 更大,从而能够更好地理解和生成自然语言。\n\n2. **训练数据更全面**:Qwen3 采用了更丰富的训练数据,覆盖了更多领域和语言风格,使得模型在多样化的任务上表现更好。\n\n3. **性能提升**:在多个基准测试中,Qwen3 的性能显著优于 Qwen2,特别是在生成文本、理解任务和多语言支持等方面。\n\n4. **功能增强**:Qwen3 支持更多的功能,比如更精细的文本生成、更强的推理能力、更好的对话交互体验等。\n\n5. **多语言支持**:Qwen3 支持多种语言的处理,包括中文、英文、日文、韩文等,使得模型在多语言场景下更具适应性。\n\nQwen3 适用于多种应用场景,如客服、内容创作、教育、科研等,能够提供更精准、更自然的交互体验。\n\n如果你有具体的问题或需要进一步了解某个方面,欢迎继续提问!","tool_calls":[]},"logprobs":null,"finish_reason":"stop","stop_reason":null}],"usage":{"prompt_tokens":22,"total_tokens":303,"completion_tokens":281,"prompt_tokens_details":null},"prompt_logprobs":null}
  1. 调用模型问答,格式化推理
    默认情况下模型推理会输出在content里面,如下所示:
1
{"id":"chatcmpl-93778bd8c9ef48b48d189f1530edd9a3","object":"chat.completion","created":1746610826,"model":"Qwen/Qwen3-1.7B","choices":[{"index":0,"message":{"role":"assistant","reasoning_content":null,"content":"<think>\n嗯,用户让我介绍一下Qwen3模型。首先,我需要确认用户对Qwen系列模型的了解程度。Qwen系列是通义实验室开发的大型语言模型,Qwen3是其中最新版本,可能在性能、功能或架构上有改进。\n\n用户可能想知道Qwen3的主要特点,比如参数量、训练数据、应用场景,或者它与其他模型的区别。我需要确保信息准确,同时避免过于技术化的术语,保持易懂。\n\n另外,用户可能对Qwen3的更新有哪些具体改进感兴趣,比如是否在多语言支持、推理速度、生成质量等方面有提升。还要考虑用户可能的深层需求,比如他们是否在使用Qwen3进行特定任务,需要了解其优势。\n\n需要注意的是,Qwen3可能在2023年或之后发布,但具体细节可能需要查阅官方资料。如果有不确定的地方,应该指出需要参考官方文档或最新公告。同时,要确保回答结构清晰,分点说明,让用户容易理解。\n\n最后,保持回答的友好和专业,避免使用过于复杂的术语,确保信息全面且易于消化。可能还需要提醒用户如果需要更详细的信息,可以提供更具体的方面,比如应用场景或技术细节。\n</think>\n\nQwen3是通义实验室推出的最新一代大型语言模型,基于通义千问系列的最新研究成果,旨在提升模型的多语言理解、生成、推理能力以及与人类的交互体验。以下是关于Qwen3的一些关键信息:\n\n---\n\n### **1. 模型基础**\n- **参数量**:Qwen3的参数量显著增加,相比前代模型(如Qwen2)提升了约2倍,使得模型在处理复杂任务时更具能力。\n- **训练数据**:基于海量多语言文本数据(包括但不限于书籍、论文、网页、对话等),覆盖全球范围的多样化内容。\n- **训练方式**:采用自监督学习和监督学习结合的方式,通过大规模数据预训练和微调优化模型性能。\n\n---\n\n### **2. 核心能力**\n- **多语言支持**:支持16种主流语言(如中文、英文、日语、韩语、西班牙语等),能够处理跨语言的翻译、问答、代码生成等任务。\n- **高质量生成**:在文本生成、对话交互、代码编写、逻辑推理等方面表现优异,能够生成流畅、自然、符合语境的文本。\n- **推理能力**:具备强大的逻辑推理能力,能够处理复杂问题,如数学计算、逻辑推导、因果关系分析等。\n- **上下文理解**:支持长文本处理,能够理解并生成连贯的多段对话或长篇内容。\n\n---\n\n### **3. 与前代模型的对比**\n- **Qwen3**:\n  - **参数量**:比Qwen2大约2倍,模型容量更大。\n  - **性能提升**:在多个基准测试中(如GLUE、SQuAD、CodeXGLUE等)表现优于前代模型。\n  - **效率优化**:在保持高性能的同时,优化了推理速度,适合实时应用场景。\n- **Qwen2**:\n  - 早期版本,参数量约为70亿,性能在多语言和复杂任务上有所提升,但相比Qwen3仍有差距。\n\n---\n\n### **4. 应用场景**\n- **内容创作**:如文章撰写、故事生成、诗歌创作等。\n- **代码生成**:支持多种编程语言(如Python、Java、C++等)的代码编写。\n- **对话交互**:用于客服、智能助手、教育辅导等场景。\n- **多语言支持**:适合需要跨语言服务的全球化业务(如翻译、客服、国际教育等)。\n- **推理任务**:如数学问题解答、逻辑推理、因果分析等。\n\n---\n\n### **5. 技术特点**\n- **模型架构**:基于Transformer架构,采用多层Transformer编码器和解码器结构。\n- **训练策略**:结合自监督学习(如掩码语言模型)和监督学习(如任务特定的微调)。\n- **优化技术**:引入了高效的训练和推理优化技术,减少计算资源消耗。\n\n---\n\n### **6. 未来方向**\n- **持续迭代**:Qwen3是通义实验室持续优化和迭代的成果,后续可能推出更多版本(如Qwen4)。\n- **开放生态**:可能提供API接口,支持开发者集成到各类应用中。\n\n---\n\n### **注意事项**\n- **具体细节**:Qwen3的具体技术细节(如参数量、训练数据规模、优化方法等)可能需要参考官方文档或最新公告。\n- **适用场景**:建议根据实际需求选择合适的版本(如Qwen3的多语言能力是否满足需求)。\n\n---\n\n如果你有特定的应用场景或技术问题,可以进一步说明,我可以提供更针对性的分析!","tool_calls":[]},"logprobs":null,"finish_reason":"stop","stop_reason":null}],"usage":{"prompt_tokens":15,"total_tokens":1051,"completion_tokens":1036,"prompt_tokens_details":null},"prompt_logprobs":null}

我们希望模型能够将推理过程结构化解析到reasoning_content里面,为此,vllm启动模型时需要修改参数:

1
vllm serve Qwen/Qwen3-1.7B --dtype=half --enable-reasoning --reasoning-parser deepseek_r1

发送问题

1
2
3
4
5
6
7
8
9
10
curl http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "Qwen/Qwen3-1.7B",
"messages": [
{"role": "user", "content": "介绍一下这个qwen3模型吧"}
],
"temperature": 0.6,
"top_p": 0.95,
"top_k": 20,
"max_tokens": 32768
}'

这样便能将推理过程分开

1
{"id":"chatcmpl-c85e18f541634e9d9a98c4bf07bc06c0","object":"chat.completion","created":1746616790,"model":"Qwen/Qwen3-1.7B","choices":[{"index":0,"message":{"role":"assistant","reasoning_content":"\n嗯,用户让我介绍一下Qwen3模型。首先,我需要确认Qwen3是否真的存在,因为之前我可能没有接触过这个模型。可能用户是想问Qwen系列中的某个版本,比如Qwen2或者Qwen3。不过根据我的知识库,Qwen系列是通义实验室研发的,目前最新的是Qwen2,而Qwen3可能是一个后续的版本,但不确定是否有官方发布。\n\n接下来,我需要考虑用户可能的背景。他们可能对大模型技术感兴趣,或者在寻找特定的应用场景,比如对话、代码生成、多语言支持等。用户可能没有明确说明,但需要我提供全面的信息,包括模型的架构、性能、应用场景、优势等。\n\n然后,我要确保信息的准确性。如果Qwen3确实存在,我需要查找相关资料,比如官方文档或技术博客,确认其特点,比如参数量、训练数据、技术架构(如Transformer结构)、性能指标(如推理速度、参数量、训练数据量)等。同时,要提到其在不同任务上的表现,比如文本生成、代码生成、多语言支持、推理效率等。\n\n另外,用户可能关心Qwen3是否支持多语言,是否有特别的优化,比如支持中文、英文、日文等,或者是否有特定的行业应用,比如金融、医疗、教育等。此外,可能还需要提到模型的训练方式,比如是否使用了大规模数据,是否进行了微调,或者是否有特定的训练策略。\n\n还要注意潜在的问题,比如Qwen3是否已经发布,是否有公开的论文或技术文档,是否需要特定的环境或工具来使用。如果Qwen3是尚未发布的版本,可能需要说明当前的进展,或者建议用户关注官方渠道获取最新信息。\n\n最后,确保回答结构清晰,分点说明,涵盖关键点,如模型架构、性能、应用场景、优势、未来展望等,同时保持语言简洁易懂,避免使用过于专业的术语,除非必要。同时,提醒用户如果有具体需求,可以进一步询问,以便提供更针对性的信息。\n","content":"\n\nQwen3 是通义实验室研发的大型语言模型系列中的最新版本,属于通义千问(Qwen)系列的后续迭代。根据目前公开信息,Qwen3 作为通义千问的最新版本,具备以下特点和优势:\n\n---\n\n### **1. 模型架构与参数**\n- **参数量**:Qwen3 的参数量显著增加,相比前代模型(如 Qwen2)提升了数十倍,具体数值需参考官方发布。\n- **训练数据**:基于大规模多语言文本数据训练,支持中文、英文、日文、韩语等多语言任务。\n- **技术架构**:采用 **Transformer** 基础架构,结合 **多模态**(如图像、视频)和 **多语言** 的优化策略,提升跨语言理解能力。\n\n---\n\n### **2. 核心能力**\n- **文本生成**:支持高质量的自然语言生成,包括对话、写作、代码编写等,能够生成连贯、逻辑性强的文本。\n- **多语言支持**:在中文、英文、日文、韩语等多语言任务中表现优异,支持跨语言对话和翻译。\n- **代码生成**:具备强大的代码生成能力,可编写多种编程语言(如 Python、Java、C++ 等)的代码。\n- **推理效率**:通过优化模型结构和推理流程,显著提升推理速度,降低计算资源消耗。\n\n---\n\n### **3. 应用场景**\n- **通用对话**:适用于客服、虚拟助手等场景,支持多轮对话和复杂问题解答。\n- **代码开发**:辅助开发者编写代码、调试和优化,提升开发效率。\n- **多语言翻译**:支持实时翻译和跨语言文本理解,适用于国际业务场景。\n- **内容创作**:生成新闻、文章、故事等文本内容,满足不同需求。\n\n---\n\n### **4. 优势与改进**\n- **更强大的多语言能力**:相比 Qwen2,Qwen3 在多语言任务中表现更优,支持更多语言和更复杂的语境理解。\n- **更高的推理效率**:通过模型压缩和优化技术,降低推理延迟,提升实际应用中的响应速度。\n- **更丰富的应用场景**:结合行业需求,优化特定领域的任务(如金融、医疗、教育等)。\n\n---\n\n### **5. 未来展望**\n- **持续优化**:通义实验室将持续迭代 Qwen3,结合最新研究进展,进一步提升模型性能。\n- **开放生态**:可能提供 API 接口或开发工具,支持开发者集成到各类应用中。\n\n---\n\n### **注意事项**\n- **版本信息**:目前 Qwen3 是否已正式发布需以官方公告为准。若尚未发布,可能需关注通义千问系列的更新动态。\n- **技术细节**:具体参数、训练数据量、技术细节等需参考官方文档或技术白皮书。\n\n---\n\n如需进一步了解 Qwen3 的具体技术细节或应用场景,建议关注通义实验室的官方渠道(如官网、技术博客、GitHub 仓库等)获取最新信息。如果你有特定需求(如代码生成、多语言支持等),也可以告诉我,我可以提供更针对性的建议!","tool_calls":[]},"logprobs":null,"finish_reason":"stop","stop_reason":null}],"usage":{"prompt_tokens":15,"total_tokens":1135,"completion_tokens":1120,"prompt_tokens_details":null},"prompt_logprobs":null}

vllm函数调用

vllm的函数调用是基于transformers的分词器来实现函数的格式化输入,同时vllm实现了辅助函数实现函数参数的解析。

我们先来看一下transformers的tokenizer是如何进行函数的格式化输入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from transformers import AutoModelForCausalLM, AutoTokenizer
from tools import *
model_name_or_path = "Qwen/Qwen3-1.7B"
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
model = AutoModelForCausalLM.from_pretrained(
model_name_or_path,
torch_dtype="auto",
device_map="auto",
)
tools = TOOLS
messages = MESSAGES[:]

text = tokenizer.apply_chat_template(messages, tools=tools, add_generation_prompt=True, tokenize=False)
print(text)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=512)
output_text = tokenizer.batch_decode(outputs)[0][len(text):]

tokenizer会读取模型tokenizer_config.json中的chat_prompt聊天模板来将函数参数进行转换,它利用Jinja模板引擎隐藏了构建模型输入的复杂性。

  1. apply_chat_template内部调用render_jinja_template函数来解析jinja模板
  2. 编译tokenizer_config.json中的chat_prompt
  3. 将tools转换为json格式
  4. 使用编译好的聊天模板,将用户messages和json语法的工具替换到聊天模板中

qwen的聊天模板中包含了对 Hermes 风格工具调用的支持,chat_prompt如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{%- if tools %}
{{- '<|im_start|>system\\n' }}
{%- if messages[0].role == 'system' %}
{{- messages[0].content + '\\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><|im_end|>\\n\" }}\n{%- else %}
{%- if messages[0].role == 'system' %}
{{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}
{%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}
{%- set index = (messages|length - 1) - loop.index0 %}
{%- if ns.multi_step_tool and message.role == \"user\" and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
{%- set ns.multi_step_tool = false %}
{%- set ns.last_query_index = index %}
{%- endif %}\n{%- endfor %}\n{%- for message in messages %}
{%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}
{{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}
{%- elif message.role == \"assistant\" %}
{%- set content = message.content %}
{%- set reasoning_content = '' %}
{%- if message.reasoning_content is defined and message.reasoning_content is not none %}
{%- set reasoning_content = message.reasoning_content %}
{%- else %}
{%- if '</think>' in message.content %}
{%- set content = message.content.split('</think>')[-1].lstrip('\\n') %}
{%- set reasoning_content = message.content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}
{%- endif %}
{%- endif %}
{%- if loop.index0 > ns.last_query_index %}
{%- if loop.last or (not loop.last and reasoning_content) %}
{{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}
{%- else %}
{{- '<|im_start|>' + message.role + '\\n' + content }}
{%- endif %}
{%- else %}
{{- '<|im_start|>' + message.role + '\\n' + content }}
{%- endif %}
{%- 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 %}
{{- '<|im_end|>\\n' }}
{%- elif message.role == \"tool\" %}
{%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}
{{- '<|im_start|>user' }}
{%- endif %}
{{- '\\n<tool_response>\\n' }}
{{- message.content }}
{{- '\\n</tool_response>' }}
{%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}
{{- '<|im_end|>\\n' }}
{%- endif %}
{%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}
{{- '<|im_start|>assistant\\n' }}
{%- if enable_thinking is defined and enable_thinking is false %}
{{- '<think>\\n\\n</think>\\n\\n' }}
{%- endif %}\n{%- endif %}

经过jinja模板的编译,我们可以看到输出的文本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<|im_start|>system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.

Current Date: 2024-09-30

# Tools

You may call one or more functions to assist with the user query.

You are provided with function signatures within <tools></tools> XML tags:
<tools>
{"type": "function", "function": {"name": "get_current_temperature", "description": "Get current temperature at a location.", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The location to get the temperature for, in the format \"City, State, Country\"."}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit to return the temperature in. Defaults to \"celsius\"."}}, "required": ["location"]}}}
{"type": "function", "function": {"name": "get_temperature_date", "description": "Get temperature at a location and date.", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The location to get the temperature for, in the format \"City, State, Country\"."}, "date": {"type": "string", "description": "The date to get the temperature for, in the format \"Year-Month-Day\"."}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit to return the temperature in. Defaults to \"celsius\"."}}, "required": ["location", "date"]}}}
</tools>

For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call><|im_end|>
<|im_start|>user
What's the temperature in San Francisco now? How about tomorrow?<|im_end|>
<|im_start|>assistant

模型最终推理结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<think>
Okay, the user is asking for the current temperature in San Francisco and the temperature tomorrow. Let me check the available functions.

There's get_current_temperature which requires a location and unit. The user didn't specify the unit, so I'll default to celsius. Then, for tomorrow's temperature, I need to use get_temperature_date. The date parameter should be in Year-Month-Day format. Since today is September 30, 2024, tomorrow would be October 1, 2024.

Wait, the user didn't specify the unit for tomorrow, but the current one is default. I should probably mention both units if possible, but the functions might not support that. Let me check the function parameters again. The unit is optional for get_current_temperature, and the default is celsius. For get_temperature_date, the unit is optional with default celsius. So I can call both functions with the default unit.

So first, call get_current_temperature with location "San Francisco" and unit celsius. Then, call get_temperature_date with location "San Francisco", date "2024-10-01", and unit celsius. Then provide both results to the user.
</think>

<tool_call>
{"name": "get_current_temperature", "arguments": {"location": "San Francisco"}}
</tool_call>
<tool_call>
{"name": "get_temperature_date", "arguments": {"location": "San Francisco", "date": "2024-10-01"}}
</tool_call><|im_end|>

由于这种格式的模型输出没有进行切分,为此,使用vllm进行部署时,可以让vllm按hermes规则将模型的输出进行进行到tool_call里面

1
vllm serve Qwen/Qwen3-1.7B --dtype half --enable-auto-tool-choice --tool-call-parser hermes

回答如下:

1
ChatCompletion(id='chatcmpl-a8b1586cc7cf4bbc849787e85f40805b', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content='<think>\nOkay, let me see. The user is asking for the current temperature in San Francisco and the temperature tomorrow. So I need to use the available functions.\n\nFirst, the current temperature. There\'s the get_current_temperature function. It requires the location and unit. The user didn\'t specify the unit, so I\'ll default to celsius. So I\'ll call that function with location "San Francisco" and unit "celsius".\n\nThen, for tomorrow\'s temperature, I need the get_temperature_date function. The parameters are location, date, and unit. The date is tomorrow, which would be 2024-10-01 if today is September 30. Wait, but the current date is 2024-09-30. So tomorrow would be 2024-10-01. I\'ll set the date to that and keep the unit as celsius again. \n\nWait, the user might not care about the unit, but since the first function uses celsius by default, maybe I should stick with that. But the second one can also use celsius. So I\'ll make two separate calls: one for current and one for tomorrow\'s date.\n\nI need to check if the date format is correct. The date parameter should be in Year-Month-Day format. So 2024-10-01 is correct. \n\nSo the tool calls will be two separate entries. First for current, then for tomorrow\'s date.\n</think>\n\n', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='chatcmpl-tool-1e80b27fc1534ac4bb3637de5e2d2cae', function=Function(arguments='{"location": "San Francisco", "unit": "celsius"}', name='get_current_temperature'), type='function'), ChatCompletionMessageToolCall(id='chatcmpl-tool-600487a6d6164fcc9e9b21336e01fa90', function=Function(arguments='{"location": "San Francisco", "date": "2024-10-01", "unit": "celsius"}', name='get_temperature_date'), type='function')], reasoning_content=None), stop_reason=None)], created=1746761516, model='Qwen/Qwen3-1.7B', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=383, prompt_tokens=412, total_tokens=795, completion_tokens_details=None, prompt_tokens_details=None), prompt_logprobs=None)

思考过程也可以让vllm解析出来

1
vllm serve Qwen/Qwen3-1.7B --dtype half --enable-auto-tool-choice --tool-call-parser hermes  --enable-reasoning --reasoning-parser deepseek_r1

输出如下:

1
ChatCompletion(id='chatcmpl-173d92e969ad42378cd54bba366993f9', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content='\n\n', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='chatcmpl-tool-3c64e62980844b2993f17edd151a6862', function=Function(arguments='{"location": "San Francisco"}', name='get_current_temperature'), type='function'), ChatCompletionMessageToolCall(id='chatcmpl-tool-de7be2853fd741cc8a8980fa7f62310b', function=Function(arguments='{"location": "San Francisco", "date": "2024-10-01"}', name='get_temperature_date'), type='function')], reasoning_content='\nOkay, the user is asking for the current temperature in San Francisco and the temperature tomorrow. Let me check the available functions.\n\nThere\'s get_current_temperature which requires a location and unit. The default unit is celsius. Then there\'s get_temperature_date which needs location, date, and unit. The date should be in Year-Month-Day format.\n\nSo for the current temperature, I\'ll use get_current_temperature with location "San Francisco" and default unit. For tomorrow, I need to calculate the date. Today is September 30, 2024. Tomorrow would be October 1, 2024. So the date parameter would be "2024-10-01". \n\nI need to make two separate function calls. First for today, then for tomorrow. Make sure to specify the unit as celsius unless the user prefers Fahrenheit, but they didn\'t mention it, so default is okay.\n'), stop_reason=None)], created=1746761680, model='Qwen/Qwen3-1.7B', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=254, prompt_tokens=412, total_tokens=666, completion_tokens_details=None, prompt_tokens_details=None), prompt_logprobs=None)

可见,think过程放到了reasoning_content字段,函数调用放到了tool_calls字段

参考文献

  1. vllm中文文档 https://vllm.hyper.ai/docs/inference-and-serving/openai_compatible_server
  2. qwen3快速开始 https://qwen.readthedocs.io/zh-cn/latest/deployment/vllm.html
  3. llm工具使用原理 https://huggingface.co/blog/zh/unified-tool-use
  4. qwen函数调用原理 https://qwen.readthedocs.io/zh-cn/latest/framework/function_call.html