目录
OpenAI Chat API 的执行过程解析
/      

OpenAI Chat API 的执行过程解析

OpenAI Chat API 的执行过程解析

已生成图像1.png

本文以 nano-vllm 开源项目为例,探讨大模型(如 Qwen3-0.6B)是如何在底层处理标准的 OpenAI 格式聊天请求的。该模型尺寸较小,非常适合在本地电脑部署和调试。


1. 客户端发起请求 (Request)

用户通过 HTTP 发起一个符合 OpenAI 规范的 API 调用,携带了系统提示词和用户输入:

curl --request POST \
  --url http://localhost:8000/v1/chat/completions \
  --data '{
    "model": "Qwen3-0.6B",
    "stream": false,
    "messages": [
        {
            "role": "system",
            "content": "你是一个Java开发工程师"
        },
        {
            "role": "user",
            "content": "你是谁"
        }
    ],
    "temperature": 0.7
}'

2. API 层:模板渲染 (Chat Template)

接收到 JSON 请求后,模型引擎并不会直接吃下这串 JSON,而是会通过 tokenizer_config.json 中定义的一段 Jinja2 模板 (chat_template),将 messages 数组渲染成一段连续的纯文本。

在这个渲染过程中,模板会自动处理 <|im_start|><|im_end|> 等特殊控制符,甚至还会自动注入关于工具调用 (Tools) 的隐藏系统指令。

渲染后的纯文本请求体 (普通对话) 大概长这样:

<|im_start|>system
你是一个Java开发工程师<|im_end|>
<|im_start|>user
你是谁<|im_end|>
<|im_start|>assistant

如果带有工具 (Function Calling) 呢?
当 API 请求里带上了 tools 参数(比如在 Cherry Studio 里开启了 MCP 插件),模板会自动在开头注入极其详尽的 # Tools 系统指令和 XML 标签定义。
此时渲染出的纯文本请求体 (带工具) 长这样:

<|im_start|>system
# 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": "mcp__CherryFetch__fetchHtml", "description": "Fetch a website and return the content as HTML", "parameters": {"type": "object", "properties": {"url": {"type": "string", "description": "URL of the website to fetch"}}, "required": ["url"]}}}
... (这里会列出所有你开启的工具的详细 JSON Schema,篇幅较长)
</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
你是谁<|im_end|>
<|im_start|>assistant

提示:末尾的 <|im_start|>assistant 同样是模板自动追加的(add_generation_prompt=True),相当于给大模型递上话筒,暗示它“接下来该你发言了,决定是输出工具调用指令,还是普通聊天”。

💡 点击展开查看模型底层的原版 Jinja2 模板代码
{%- if tools %}
    {{- '<|im_start|>system\n' }}
... (此处省略复杂的判断逻辑,它主要负责处理 system/user/assistant 的拼接,以及 function calling 标签的注入)
{%- 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 %}
{%- endif %}

3. 分词层:文本转 Token (Tokenization)

大模型是不认识汉字和字母的,它只认识数字。渲染好的长文本会被喂给模型的 Tokenizer,被切分成词元并转换成对应的 token_ids

# 转换结果示例:
[151644, 8948, 198, 56568, 101909, 15041, 100013, 105503, 151645, 198, 151644, 872, 198, 105043, 100165, 151645, 198, 151644, 77091, 198]

(比如 151644 就代表了特殊的 <|im_start|> 标签)


4. 引擎层:调度与生成 (Prefill & Decode)

拿到 token_ids 后,任务会被放入 LLMEngine 的调度队列 (Scheduler) 中。大模型的生成实际上是一个不断循环的过程。

# 文件:nanovllm/engine/llm_engine.py 
# 这是引擎的控制循环
while not self.is_finished():
    self.step()

每次调用 step(),引擎都会做一件事:从调度器里拿任务 -> 送给 GPU 算 -> 算出一个字 -> 存起来。
大模型的推理严格分为两个截然不同的阶段:Prefill(预填充)Decode(解码)

阶段一:Prefill(预填充,俗称“阅读理解”)

这是处理用户新请求的第一步,也是大模型性能爆发的阶段。假设用户发来了长度为 $N=100$ 个 Token 的 Prompt(包含了系统提示词、历史记录和新问题):

  1. 矩阵并行计算:大模型不会一个词一个词地读。相反,它会将这 100 个 Token 组合成一个形状为 [1, 100, 隐藏层维度] 的矩阵,一次性、并行地扔给 GPU 的所有计算核心。
  2. KV Cache 的诞生:在经过模型内部的每一层注意力机制 (Attention) 时,这 100 个词都会各自计算出属于自己的特征向量 Key (K)Value (V)。为了避免以后重复算,引擎会把这 100 个词的 K 和 V 统统存进 GPU 的显存里(在咱们这个仓库里是由 block_manager 按页表分页存储的)。这就形成了所谓的 KV Cache(上下文记忆)
  3. 第一个字的诞生:当矩阵流过所有网络层后,序列中的最后一个词(第 100 个词)已经“看”到了前面的所有内容,它包含了整个句子的上下文特征。模型就拿着这个词的输出去做概率映射,推测出接下来该接哪个词。概率最高的那一个,就是模型回复给你的第一个字
  4. 特点:这是一次纯正的、满载的超大规模矩阵乘法运算,极度考验 GPU 的浮点算力(Compute-bound),但并行效率极高。

阶段二:Decode(解码,俗称“一个字一个字写”)

当第一个字生成后(假设叫 $T_1$),就进入了漫长的 Decode 阶段。这个时候,推理过程发生了本质的变化:

  1. 输入参数锐减:为了效率,引擎绝对不会把之前的 100 个词加上新词再算一遍。它只把新生成的这 1 个字($T_1$,长度 $N=1$)作为输入,重新送进 GPU。
  2. 提取历史记忆:这 1 个新字独自走到 Attention 层时,它需要知道前面的上下文。这时,模型直接从显存里读取刚才在 Prefill 阶段存好的那 100 个词的 KV Cache。
  3. 追加新记忆并预测:新字 $T_1$ 也会计算出自己的 $K$ 和 $V$,并把它们追加(Append)到之前的 KV Cache 列表尾部,让显存里的记忆从 100 个变成了 101 个。接着,$T_1$ 拿着自己的 Query 向量,去跟这 101 个历史记忆“套近乎”(做点积注意力计算),从而推断出第二个字($T_2$)是什么。
  4. 循环往复:生成 $T_2$ 后,$T_2$ 再被单独送进 GPU($N=1$),去提取 101 个词的记忆,追加成 102 个,然后预测第三个字……以此类推。每次输入长度永远只有 1,直到预测出 <|im_end|> 等结束符,循环才会终止。
  5. 特点:这个阶段没法并行,必须等前一个字出来才能算后一个字(自回归特性)。而且因为 GPU 每次只做一丁点儿计算(算 1 个字),却要像无底洞一样从显存里把成百上千个词的 KV Cache 数据全部读一遍,它极度受限于 GPU 的显存读取速度/带宽(Memory-bound)。这也是为什么算力再猛的卡,大模型“吐字”的速度也有天花板。

让我们看看仓库里 scheduler.pyllm_engine.py 的具体源码是怎么配合实现这一点的:

💻 核心源码 1: scheduler.py 中的调度与预填充(Prefill)逻辑
# 文件:nanovllm/engine/scheduler.py
def schedule(self) -> tuple[list[Sequence], bool]:
    scheduled_seqs = []
    num_batched_tokens = 0
  
    # 【1. Prefill 预填充阶段】
    # 优先看 waiting 队列里有没有刚进来的新请求
    while self.waiting and len(scheduled_seqs) < self.max_num_seqs:
        seq = self.waiting[0]
      
        # 计算当前这批还能塞下多少个 Token
        remaining = self.max_num_batched_tokens - num_batched_tokens
        if remaining == 0:
            break
          
        # 如果是全新的请求(还没分配页表 block_table)
        if not seq.block_table:
            # 检查显存里有没有能够共享的前缀缓存 (Prefix Caching)
            num_cached_blocks = self.block_manager.can_allocate(seq)
            if num_cached_blocks == -1: # 显存不足
                break
            # 扣除掉已经缓存的,剩下的就是要实际计算的 Token 数量
            num_tokens = seq.num_tokens - num_cached_blocks * self.block_size
        else:
            num_tokens = seq.num_tokens - seq.num_cached_tokens
          
        if remaining < num_tokens and scheduled_seqs:  
            # 只有第一个请求允许被“截断(Chunked Prefill)”,后面的放不下就不塞了
            break
          
        # 正式向显存申请物理块 (Block)
        if not seq.block_table:
            self.block_manager.allocate(seq, num_cached_blocks)
          
        # 记录本次将要计算多少个 Token(如果是长文本会被截断分批算)
        seq.num_scheduled_tokens = min(num_tokens, remaining)
        num_batched_tokens += seq.num_scheduled_tokens
      
        # 如果这个请求的所有 Token 都预填充完了,转移到 running 状态
        if seq.num_cached_tokens + seq.num_scheduled_tokens == seq.num_tokens:
            seq.status = SequenceStatus.RUNNING
            self.waiting.popleft()
            self.running.append(seq)
          
        scheduled_seqs.append(seq)
      
    if scheduled_seqs:
        # 如果有新请求,直接返回,并且标记 is_prefill = True
        return scheduled_seqs, True

    # 【2. Decode 阶段】
    # 如果没新请求了,那就接着处理 running 队列里正在生成的请求
    while self.running and len(scheduled_seqs) < self.max_num_seqs:
        seq = self.running.popleft()
      
        # 检查是否还有显存能放得下新生成的这 1 个字
        while not self.block_manager.can_append(seq):
            # ... (此处省略显存不足时的抢占 preempt 逻辑)
            pass
        else:
            # Decode 每次只计划生成 1 个 Token
            seq.num_scheduled_tokens = 1
            seq.is_prefill = False
            self.block_manager.may_append(seq)
            scheduled_seqs.append(seq)
      
    self.running.extendleft(reversed(scheduled_seqs))
    # 返回 False,表示当前是在 Decode 逐字解码
    return scheduled_seqs, False
💻 核心源码 2: llm_engine.py 的单步执行逻辑
# 文件:nanovllm/engine/llm_engine.py
def step(self):
    # 1. 向 Scheduler 要任务。如果拿到了新请求 is_prefill 就是 True;否则是 False(Decode)
    seqs, is_prefill = self.scheduler.schedule()
  
    # 2. 把任务扔给底层模型 (ModelRunner) 跑一次 PyTorch 前向传播
    # 如果是 Prefill,这里会一次性算完成千上万个词的特征;
    # 如果是 Decode,这里只算这 1 个新词的特征。
    token_ids = self.model_runner.call("run", seqs, is_prefill)
  
    # 3. 后处理:把刚生成的这个字 (token_ids) 追加到请求的尾部
    # 如果碰到了 <|im_end|>,就把这个请求标记为 FINISHED
    self.scheduler.postprocess(seqs, token_ids, is_prefill)
  
    # 4. 把已经完成(FINISHED)的请求捞出来,准备返回给客户端
    outputs = [(seq.seq_id, seq.completion_token_ids) for seq in seqs if seq.is_finished]
  
    return outputs, num_tokens

5. 解码与返回结果 (Decode & Response)

循环结束后,模型生成了一大串毫无意义的数字数组(token_ids)。此时还需要进行最后一步:将 Token ID 解码回人类能读懂的中文/英文字符

这也非常简单,只需调用 Tokenizerdecode 方法。你可以把它理解为查字典的逆向操作:

# 模型在 Decode 阶段辛辛苦苦算出来的 Token IDs 数组:
[104198, 104210, 45, 12567, 99361, 100013, 9370, 15469, 110498]

# 调用 tokenizer.decode() 后还原的文本:
"我是基于NLP技术开发的AI助手"
💻 核心源码 3: llm_engine.py 中的解码代码
# 文件:nanovllm/engine/llm_engine.py
def generate(...):
    # ... (前面的 while not self.is_finished() 大循环结束)
  
    # 拿到按顺序排好的 token_ids 数组
    outputs = [outputs[seq_id] for seq_id in sorted(outputs.keys())]
  
    # 调用 tokenizer.decode 进行查表解码,转换回字符串文本
    outputs = [{"text": self.tokenizer.decode(token_ids), "token_ids": token_ids} for token_ids in outputs]
  
    return outputs

得到解码后的纯文本后,API 层(api_server.py)会将这段文本包装成 OpenAI 标准的 JSON 格式返回给客户端:

{
    "id": "chatcmpl-008d9871d53b4238bea4549a45af9686",
    "object": "chat.completion",
    "created": 1783751250,
    "model": "Qwen3-0.6B",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "<think>\n好的,用户问我是谁。我需要先确认...避免冗长。\n</think>\n\n我是基于NLP技术开发的AI助手,主要为用户提供帮助和支持。如果您有任何问题或需要帮助,请随时告诉我!"
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 10,
        "completion_tokens": 211,
        "total_tokens": 221
    }
}

(如果是流式请求 stream: true,则会在第4步的循环中,每预测出一个字就立刻进行 decode 并通过 SSE 分块返回)

DeepSeek-V4 Chat Template

{%- macro render_tools_block(eff_tools) -%}
{%- set tl_ns = namespace(lines=[]) -%}
{%- for t in eff_tools -%}
    {%- if t.function is defined -%}
        {%- set tl_ns.lines = tl_ns.lines + [t.function | tojson(ensure_ascii=false)] -%}
    {%- else -%}
        {%- set tl_ns.lines = tl_ns.lines + [t | tojson(ensure_ascii=false)] -%}
    {%- endif -%}
{%- endfor -%}
{{- "\n\n## Tools\n\nYou have access to a set of tools to help answer the user's question. You can invoke tools by writing a \"<|DSML|tool_calls>\" block like the following:\n\n<|DSML|tool_calls>\n<|DSML|invoke name=\"$TOOL_NAME\">\n<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</|DSML|parameter>\n...\n</|DSML|invoke>\n<|DSML|invoke name=\"$TOOL_NAME2\">\n...\n</|DSML|invoke>\n</|DSML|tool_calls>\n\nString parameters should be specified as is and set `string=\"true\"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string=\"false\"`.\n\nIf thinking_mode is enabled (triggered by <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.\n\nOtherwise, output directly after </think> with tool calls or final response.\n\n### Available Tool Schemas\n\n" -}}
{{- tl_ns.lines | join("\n") -}}
{{- "\n\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.\n" -}}
{%- endmacro -%}
{%- macro render_response_format(rf) -%}
{{- "\n\n## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n" -}}
{{- rf | tojson(ensure_ascii=false) -}}
{%- endmacro -%}
{%- if thinking_mode is not defined -%}{%- set thinking_mode = "thinking" -%}{%- endif -%}
{%- if drop_thinking is not defined -%}{%- set drop_thinking = true -%}{%- endif -%}
{%- if reasoning_effort is not defined -%}{%- set reasoning_effort = none -%}{%- endif -%}
{%- if tools is not defined -%}{%- set tools = none -%}{%- endif -%}
{%- if add_generation_prompt is not defined -%}{%- set add_generation_prompt = false -%}{%- endif -%}
{%- set tools_ns = namespace(has_any=false) -%}
{%- if tools -%}{%- set tools_ns.has_any = true -%}{%- endif -%}
{%- for m in messages -%}
    {%- if m.tools -%}{%- set tools_ns.has_any = true -%}{%- endif -%}
{%- endfor -%}
{%- set effective_drop = drop_thinking and (not tools_ns.has_any) -%}
{%- set mns = namespace(list=[]) -%}
{%- for msg in messages -%}
    {%- if msg.role == "tool" -%}
        {%- set tblock = {"type": "tool_result", "tool_use_id": msg.get("tool_call_id", ""), "content": msg.content} -%}
        {%- if mns.list|length > 0 and mns.list[-1].role == "user" and "content_blocks" in mns.list[-1] -%}
            {%- set last = mns.list[-1] -%}
            {%- set mns.list = mns.list[:-1] + [dict(last, content_blocks=last.content_blocks + [tblock])] -%}
        {%- else -%}
            {%- set mns.list = mns.list + [{"role": "user", "content_blocks": [tblock]}] -%}
        {%- endif -%}
    {%- elif msg.role == "user" -%}
        {%- set text_block = {"type": "text", "text": msg.get("content", "")} -%}
        {%- if mns.list|length > 0 and mns.list[-1].role == "user" and "content_blocks" in mns.list[-1] and mns.list[-1].get("task") is none -%}
            {%- set last = mns.list[-1] -%}
            {%- set mns.list = mns.list[:-1] + [dict(last, content_blocks=last.content_blocks + [text_block])] -%}
        {%- else -%}
            {%- set new_msg = {"role": "user", "content": msg.get("content", ""), "content_blocks": [text_block]} -%}
            {%- if msg.get("task") is not none -%}
                {%- set new_msg = dict(new_msg, task=msg.task) -%}
            {%- endif -%}
            {%- if msg.get("wo_eos") is not none -%}
                {%- set new_msg = dict(new_msg, wo_eos=msg.wo_eos) -%}
            {%- endif -%}
            {%- set mns.list = mns.list + [new_msg] -%}
        {%- endif -%}
    {%- else -%}
        {%- set mns.list = mns.list + [msg] -%}
    {%- endif -%}
{%- endfor -%}
{%- if tools -%}
    {%- set anchor_ns = namespace(found=false) -%}
    {%- for m in mns.list -%}
        {%- if m.role == "system" or m.role == "developer" -%}{%- set anchor_ns.found = true -%}{%- endif -%}
    {%- endfor -%}
    {%- if not anchor_ns.found -%}
        {%- set mns.list = [{"role": "system", "content": ""}] + mns.list -%}
    {%- endif -%}
{%- endif -%}
{%- set lu = namespace(idx=-1) -%}
{%- for m in mns.list -%}
    {%- if m.role == "user" or m.role == "developer" -%}
        {%- set lu.idx = loop.index0 -%}
    {%- endif -%}
{%- endfor -%}
{%- set fns = namespace(list=[], lu_idx=-1) -%}
{%- if thinking_mode == "thinking" and effective_drop -%}
    {%- for m in mns.list -%}
        {%- if not (m.role == "developer" and loop.index0 < lu.idx) -%}
            {%- if loop.index0 == lu.idx -%}{%- set fns.lu_idx = fns.list|length -%}{%- endif -%}
            {%- set fns.list = fns.list + [m] -%}
        {%- endif -%}
    {%- endfor -%}
{%- else -%}
    {%- set fns.list = mns.list -%}
    {%- set fns.lu_idx = lu.idx -%}
{%- endif -%}
{%- set att = namespace(idx=-1, sys=-1) -%}
{%- if tools -%}
    {%- for m in fns.list -%}
        {%- if m.role == "developer" and att.idx == -1 -%}{%- set att.idx = loop.index0 -%}{%- endif -%}
        {%- if m.role == "system" and att.sys == -1 -%}{%- set att.sys = loop.index0 -%}{%- endif -%}
    {%- endfor -%}
    {%- if att.idx == -1 -%}{%- set att.idx = att.sys -%}{%- endif -%}
{%- endif -%}
{{- "<|begin▁of▁sentence|>" -}}
{%- if thinking_mode == "thinking" and reasoning_effort == "max" -%}
{{- "Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n" -}}
{%- endif -%}
{%- for msg in fns.list -%}
    {%- set idx = loop.index0 -%}
    {%- set is_last = (idx == fns.list|length - 1) -%}
    {%- set next_role = (fns.list[idx + 1].role) if (not is_last) else none -%}
    {%- set prev_has_task = (idx > 0) and (fns.list[idx - 1].get("task") is not none) -%}
    {%- set eff_tools = none -%}
    {%- if msg.tools -%}{%- set eff_tools = msg.tools -%}
    {%- elif idx == att.idx -%}{%- set eff_tools = tools -%}{%- endif -%}
    {%- if msg.role == "system" or msg.role == "developer" -%}
        {%- if msg.role == "developer" -%}{{- "<|User|>" -}}{%- endif -%}
        {{- (msg.get("content", "") or "") -}}
        {%- if eff_tools -%}{{- render_tools_block(eff_tools) -}}{%- endif -%}
        {%- if msg.response_format is defined and msg.response_format -%}{{- render_response_format(msg.response_format) -}}{%- endif -%}
    {%- elif msg.role == "user" -%}
        {{- "<|User|>" -}}
        {%- set parts_ns = namespace(parts=[]) -%}
        {%- for b in msg.content_blocks -%}
            {%- if b.type == "text" -%}
                {%- set parts_ns.parts = parts_ns.parts + [b.get("text", "")] -%}
            {%- elif b.type == "tool_result" -%}
                {%- set tc_content = b.get("content", "") -%}
                {%- if tc_content is iterable and tc_content is not string and tc_content is not mapping -%}
                    {%- set txt_ns = namespace(texts=[]) -%}
                    {%- for sub in tc_content -%}
                        {%- if sub.type == "text" -%}
                            {%- set txt_ns.texts = txt_ns.texts + [sub.get("text", "")] -%}
                        {%- else -%}
                            {%- set txt_ns.texts = txt_ns.texts + ["[Unsupported " ~ sub.type ~ "]"] -%}
                        {%- endif -%}
                    {%- endfor -%}
                    {%- set tc_content = txt_ns.texts | join("\n\n") -%}
                {%- endif -%}
                {%- set parts_ns.parts = parts_ns.parts + ["<tool_result>" ~ tc_content ~ "</tool_result>"] -%}
            {%- else -%}
                {%- set parts_ns.parts = parts_ns.parts + ["[Unsupported " ~ b.type ~ "]"] -%}
            {%- endif -%}
        {%- endfor -%}
        {{- parts_ns.parts | join("\n\n") -}}
    {%- elif msg.role == "latest_reminder" -%}
        {{- "<|latest_reminder|>" -}}{{- msg.content -}}
    {%- elif msg.role == "assistant" -%}
        {%- set rc = msg.get("reasoning_content", "") or "" -%}
        {%- if (thinking_mode == "thinking") and (not prev_has_task) and ((not effective_drop) or idx > fns.lu_idx) -%}
            {{- rc -}}{{- "</think>" -}}
        {%- endif -%}
        {{- msg.get("content", "") or "" -}}
        {%- if msg.tool_calls -%}
            {{- "\n\n<|DSML|tool_calls>\n" -}}
            {%- set tc_ns = namespace(lines=[]) -%}
            {%- for tc in msg.tool_calls -%}
                {%- if tc.function is defined -%}
                    {%- set tc_name = tc.function.name -%}{%- set tc_args = tc.function.arguments -%}
                {%- else -%}
                    {%- set tc_name = tc.name -%}{%- set tc_args = tc.arguments -%}
                {%- endif -%}
                {%- set p_ns = namespace(lines=[]) -%}
                {%- if tc_args is mapping -%}
                    {%- for key, value in tc_args.items() -%}
                        {%- if value is string -%}
                            {%- set p_ns.lines = p_ns.lines + ['<|DSML|parameter name="' ~ key ~ '" string="true">' ~ value ~ '</|DSML|parameter>'] -%}
                        {%- else -%}
                            {%- set p_ns.lines = p_ns.lines + ['<|DSML|parameter name="' ~ key ~ '" string="false">' ~ (value | tojson(ensure_ascii=false)) ~ '</|DSML|parameter>'] -%}
                        {%- endif -%}
                    {%- endfor -%}
                {%- else -%}
                    {%- set p_ns.lines = p_ns.lines + ['<|DSML|parameter name="arguments" string="true">' ~ (tc_args | string) ~ '</|DSML|parameter>'] -%}
                {%- endif -%}
                {%- set tc_ns.lines = tc_ns.lines + ['<|DSML|invoke name="' ~ tc_name ~ '">\n' ~ (p_ns.lines | join("\n")) ~ '\n</|DSML|invoke>'] -%}
            {%- endfor -%}
            {{- tc_ns.lines | join("\n") -}}{{- "\n</|DSML|tool_calls>" -}}
        {%- endif -%}
        {%- if not msg.get("wo_eos") -%}{{- "<|end▁of▁sentence|>" -}}{%- endif -%}
    {%- else -%}
        {{- raise_exception("Unknown role: " ~ msg.role) -}}
    {%- endif -%}
    {%- set need_transition = is_last or (next_role == "assistant") or (next_role == "latest_reminder") -%}
    {%- set this_task = msg.get("task", none) -%}
    {%- if need_transition and this_task is not none -%}
        {%- set task_tokens = {"action": "<|action|>", "query": "<|query|>", "authority": "<|authority|>", "domain": "<|domain|>", "title": "<|title|>", "read_url": "<|read_url|>"} -%}
        {%- if this_task not in task_tokens -%}{{- raise_exception("Invalid task: " ~ this_task) -}}{%- endif -%}
        {%- if this_task == "action" -%}
            {{- "<|Assistant|>" -}}{{- "<think>" if thinking_mode == "thinking" else "</think>" -}}
        {%- endif -%}
        {{- task_tokens[this_task] -}}
    {%- elif need_transition and (msg.role == "user" or msg.role == "developer") and not (is_last and not add_generation_prompt) -%}
        {{- "<|Assistant|>" -}}
        {%- if thinking_mode == "thinking" -%}
            {{- "<think>" if (not effective_drop) or idx >= fns.lu_idx else "</think>" -}}
        {%- else -%}
            {{- "</think>" -}}
        {%- endif -%}
    {%- endif -%}
{%- endfor -%}

1. messages(强制必传)

  • 说明:就是包含历史对话的列表。
  • 支持的高级字段:除了常规的 rolecontent 外,这个模板还支持在单个 message 里解析 taskwo_eos(是否去掉结尾符)、reasoning_content(用来存 <think> 里的思考过程)等非常硬核的内部参数。

2. tools(可选,默认 none

  • 说明:就是我们刚才测试的那个包含函数信息的 JSON 列表。
  • 特性:如果你传了它,模板会自动生成一大段 ## Tools 以及 <|DSML|tool_calls> 的底层 XML 标签体系,也就是 DeepSeek 自己的工具调用格式。

3. add_generation_prompt(可选,默认 false

  • 说明:是否在渲染出来的文本末尾,强行追加大模型接话的占位符(比如 <|Assistant|><think>)。在 API Server 生成请求时一般都会传 True

4. thinking_mode(可选,默认 "thinking"

  • 说明DeepSeek-V4 专属控制变量。如果保持默认的 "thinking",模板会在最后引导模型回答时,自动帮你加上 <think> 标签,强制模型先进行深度思考再输出结果。

5. drop_thinking(可选,默认 true

  • 说明高级显存优化参数。当你进行多轮对话时,之前的轮次可能已经积攒了极其庞大的思考过程(可能高达上万字)。如果这个变量为 true(且当前没有工具调用),模板会在拼装历史记录时,自动丢弃掉历史轮次的思考内容,从而大幅节省你的 Context 长度和 Token 消耗。

6. reasoning_effort(可选,默认 none

  • 说明隐藏的满血模式开关
  • 彩蛋:如果你在代码里传入 reasoning_effort="max",模板会在整段提示词的最开头强行注入一段“鸡血”级别的 System Prompt(“Reasoning Effort: Absolute maximum... You MUST be very thorough...”),强制要求模型做绝对极端的深度思考,绝不允许抄近道!

渲染示例

{
  "model": "DeepSeek-V4-Flash",
  "stream": false,
  "messages": [
    {
      "role": "system",
      "content": "你是一个无所不知的AI助手,能够使用工具获取外部信息来解答用户问题。"
    },
    {
      "role": "user",
      "content": "请帮我查一下今天巴黎的天气情况。"
    },
    {
      "role": "assistant",
      "reasoning_content": "我调用工具查一下",
      "content": null,
      "tool_calls": [
        {
          "id": "call_8a9f2b1c",
          "type": "function",
          "function": {
            "name": "get_weather",
            "arguments": "{\"location\": \"Paris\", \"unit\": \"celsius\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_8a9f2b1c",
      "name": "get_weather",
      "content": "{\"temperature\": 22, \"description\": \"晴朗,微风\", \"humidity\": \"45%\"}"
    },
    {
      "role": "user",
      "content": "听起来不错!那伦敦呢?也是晴天吗?"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "获取指定城市的实时天气数据",
        "parameters": {
          "type": "object",
          "properties": {
            "location": {
              "type": "string",
              "description": "城市名称,例如:Beijing, Paris, London"
            },
            "unit": {
              "type": "string",
              "enum": [
                "celsius",
                "fahrenheit"
              ],
              "description": "温度单位"
            }
          },
          "required": [
            "location"
          ]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "search_news",
        "description": "在互联网上搜索指定话题的最新新闻",
        "parameters": {
          "type": "object",
          "properties": {
            "query": {
              "type": "string",
              "description": "搜索关键词"
            }
          },
          "required": [
            "query"
          ]
        }
      }
    }
  ]
}

渲染后的模板

<|begin▁of▁sentence|>Reasoning Effort: Absolute maximum with no shortcuts permitted.
You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.
Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.

你是一个无所不知的AI助手,能够使用工具获取外部信息来解答用户问题。

## Tools

You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<|DSML|tool_calls>" block like the following:

<|DSML|tool_calls>
<|DSML|invoke name="$TOOL_NAME">
<|DSML|parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</|DSML|parameter>
...
</|DSML|invoke>
<|DSML|invoke name="$TOOL_NAME2">
...
</|DSML|invoke>
</|DSML|tool_calls>

String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.

If thinking_mode is enabled (triggered by <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.

Otherwise, output directly after </think> with tool calls or final response.

### Available Tool Schemas

{"name": "get_weather", "description": "获取指定城市的实时天气数据", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "城市名称,例如:Beijing, Paris, London"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位"}}, "required": ["location"]}}
{"name": "search_news", "description": "在互联网上搜索指定话题的最新新闻", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "搜索关键词"}}, "required": ["query"]}}

You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
<|User|>请帮我查一下今天巴黎的天气情况。<|Assistant|><think>我调用工具查一下</think>

<|DSML|tool_calls>
<|DSML|invoke name="get_weather">
<|DSML|parameter name="arguments" string="true">{"location": "Paris", "unit": "celsius"}</|DSML|parameter>
</|DSML|invoke>
</|DSML|tool_calls><|end▁of▁sentence|><|User|><tool_result>{"temperature": 22, "description": "晴朗,微风", "humidity": "45%"}</tool_result>

听起来不错!那伦敦呢?也是晴天吗?<|Assistant|><think>

标题:OpenAI Chat API 的执行过程解析
作者:gitsilence
地址:https://blog.lacknb.cn/articles/2026/07/11/1783760699988.html