vLLM 交错思考(Interleaved Thinking)实战:在工具调用之间让模型"边想边调"
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
本篇基于 vLLM 仓库中的 Interleaved Thinking 特性文档,系统讲解交错思考(在工具调用之间穿插推理)在 vLLM 中的工作原理、启用方式与完整调用链:读者将掌握如何用--reasoning-parser与--tool-call-parser组合启用该特性,如何在多轮对话中把推理过程(reasoning)原样回传给模型,以及背后 Kimi-K2 / MiniMax-M2 两类推理解析器(ReasoningParser)的源码实现与流式行为。
什么是交错思考(Interleaved Thinking)
交错思考允许模型在多次工具调用之间进行推理:模型发起一次工具调用、拿到工具结果后,不是直接生成最终回答,而是先对结果进行一段推理(thinking),再决定下一步动作。由此可以实现:
- 在决定下一步之前,先对工具调用的结果进行推理;
- 在多个工具调用之间穿插推理步骤,形成"调用 → 推理 → 再调用"的链条;
- 基于中间结果做出更细致的判断;
- 对外暴露其选择工具的透明推理过程。
文档同时给出重要提示:交错思考会增加 token 消耗和响应延迟(每多一轮推理就多一段思考 token)。启用前应权衡预算与性能要求——如果你的 Agent 场景以少量、确定性强的工具调用为主,未必需要开启。
与普通"思考模型"的区别
普通思考模型(如 DeepSeek-R1 类)只在最终回答前输出一段<think>推理;交错思考则把推理步骤插入到工具调用循环内部:
用户消息 → 推理 1 → 工具调用 1 → 工具结果 1 → 推理 2 → 工具调用 2 → 工具结果 2 → 推理 3 → 最终回答因此对服务端的两个核心要求是:
- 能正确切分模型输出中的"推理部分"与"正文部分"(reasoning parser);
- 能正确识别与解析工具调用(tool call parser),并支持客户端在下一轮把上一次的 reasoning 作为 assistant 消息的一部分回传,保证多轮上下文完整。
vLLM 中支持交错思考的模型
根据 docs/features/interleaved_thinking.md,vLLM 当前支持的交错思考模型及其对应的 Reasoning Parser 名称如下:
| 模型系列 | Reasoning Parser 名称 |
|---|---|
| moonshotai/Kimi-K2-Thinking | kimi_k2 |
| MiniMaxAI/MiniMax-M2 | minimax_m2 |
这两个名称是--reasoning-parser命令行参数的取值。它们并非独立的 Python 手写解析器,而是注册到 vLLM 统一解析引擎(Parser Engine)中的适配器:
kimi_k2:在 vllm/reasoning/kimi_k2_reasoning_parser.py 中直接别名到vllm.parser.engine.registered_adapters里的KimiK2ParserReasoningAdapter,解析逻辑统一由vllm/parser/下的解析器(如 vllm/parser/kimi_k2.py)驱动;minimax_m2:在 vllm/reasoning/minimax_m2_reasoning_parser.py 中定义了MiniMaxM2ReasoningParser与MiniMaxM2AppendThinkReasoningParser两个实现。
所有可用的 parser 名称在 vllm/reasoning/init.py 的_REASONING_PARSERS_TO_REGISTER注册表中统一定义(kimi_k2指向kimi_k2_reasoning_parser.KimiK2ReasoningParser,minimax_m2指向MiniMaxM2ReasoningParser,另有minimax_m2_append_think变体),并通过ReasoningParserManager懒加载注册——--reasoning-parser传的名称必须能在此注册表中找到。
MiniMax-M2 解析器的实现细节
MiniMax-M2 的推理格式有一个特殊之处(见源码注释):模型不生成<think>起始 token,只生成</think>结束 token;</think>之前的所有内容都是推理,之后是正式回答。vllm/reasoning/minimax_m2_reasoning_parser.py 中的核心方法体现这一语义:
is_reasoning_end(input_ids):从后向前扫描 token,找到最后一个</think>(end token)或<think>(start token);只有当它是 end token 时返回 True。这使得服务端能判断"推理阶段是否已结束",从而在流式输出时决定 delta 进入reasoning还是content字段;extract_content_ids(input_ids):直接返回全部 token id(内容边界由 token 位置确定,而非标记切分)。
仓库内对应的单元测试 tests/reasoning/test_minimax_m2_reasoning_parser.py 覆盖了多种边界场景,均可作为行为依据:
| 测试用例 | 模型输出 | 期望 reasoning | 期望 content |
|---|---|---|---|
| simple_reasoning | This is a reasoning section</think>This is the rest | This is a reasoning section | This is the rest |
| no_end_token(流式中) | This is reasoning in progress | 同左 | None(推理未结束,全部算 reasoning) |
| multiple_lines | 多行推理 + 多行回答 | 多行推理部分 | 多行回答部分 |
| code_in_reasoning | 推理中含代码块 | 含代码块的推理 | Here is the code. |
| empty_streaming | 空输出 | None | None |
这些用例验证了:只要</think>尚未出现,流式输出期间所有 delta 都归入 reasoning;出现</think>后 delta 才切换到 content。这正是交错思考在"工具结果 → 模型再次思考"阶段的关键行为——工具结果回传后,模型先吐出的新 token 会先进入 reasoning 流。
启用方式:服务端启动与请求参数
服务端启动
以 MiniMax-M2 为例,需要同时启用 tool call parser 与 reasoning parser,并打开自动工具选择:
vllm serve MiniMaxAI/MiniMax-M2 \ --tensor-parallel-size 4 \ --tool-call-parser minimax_m2 \ --reasoning-parser minimax_m2 \ --enable-auto-tool-choice各参数含义(结合 vllm/engine/arg_utils.py 中的参数定义):
--tool-call-parser minimax_m2:注册工具调用解析器,负责把模型输出中的工具调用标记解析为 OpenAI 格式的tool_calls结构;--reasoning-parser minimax_m2:注册推理解析器。源码中该参数属于StructuredOutputsConfig.reasoning_parser(arg_utils.py 中reasoning_parser字段默认取StructuredOutputsConfig.reasoning_parser,随后写入self.reasoning_config.reasoning_parser),即推理切分能力挂在结构化输出/推理配置体系下;--enable-auto-tool-choice:允许模型自行决定是否调用工具(对应请求中的tool_choice="auto")。
对 Kimi-K2-Thinking 服务,同样思路将两处 parser 名换成kimi_k2:
vllm serve moonshotai/Kimi-K2-Thinking \ --tool-call-parser kimi_k2 \ --reasoning-parser kimi_k2 \ --enable-auto-tool-choice客户端:带推理回传的两轮工具调用
下面是原文档给出的完整可运行示例(天气查询工具),其要点是:第二轮回传 assistant 消息时必须带上reasoning字段,否则模型丢失上一轮的思考上下文,交错推理链会断裂。
""" vllm serve MiniMaxAI/MiniMax-M2 \ --tensor-parallel-size 4 \ --tool-call-parser minimax_m2 \ --reasoning-parser minimax_m2 \ --enable-auto-tool-choice """ import json from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy") def get_current_weather(location: str, unit: "str"): """Get the current weather in a given location""" if unit == "celsius": return f"The current temperature in {location} is 22°C." else: return f"The current temperature in {location} is 72°F." tools = [ { "type": "function", "function": { "name": "get_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City and state, e.g., 'San Francisco, CA'", }, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, "required": ["location", "unit"], }, } } ] messages = [{"role": "user", "content": "What's the weather in Fahrenheit like in San Francisco?"}] response = client.chat.completions.create( model=client.models.list().data[0].id, messages=messages, tools=tools, tool_choice="auto", ) tool_call = response.choices[0].message.tool_calls[0].function messages.append( { "role": "assistant", "tool_calls": response.choices[0].message.tool_calls, "reasoning": response.choices[0].message.reasoning, # append reasoning } ) # Simulate tool execution available_tools = {"get_weather": get_current_weather} completion_tool_calls = response.choices[0].message.tool_calls for call in completion_tool_calls: tool_to_call = available_tools[call.function.name] args = json.loads(call.function.arguments) result = tool_to_call(**args) messages.append( { "role": "tool", "content": result, "tool_call_id": call.id, "name": call.function.name, } ) response_2 = client.chat.completions.create( model=client.models.list().data[0].id, messages=messages, tools=tools, tool_choice="auto", ) print(response_2.choices[0].message.content)这段示例完整演示了交错思考的三步闭环:
- 第一轮:用户提问 → 模型输出推理 + 工具调用(
tool_calls[0]),此时message.reasoning携带本次工具调用前的思考内容; - 上下文拼接:把 assistant 消息(含
tool_calls与reasoning)和role: "tool"的工具结果追加进messages; - 第二轮:模型收到工具结果后,会先推理再回答——
response_2的message.reasoning即为对工具结果的思考,message.content为最终回答。
推理字段的协议层支持
"回传 reasoning 才能维持推理链"之所以成立,是因为 vLLM 的 Chat Completions 协议把推理内容做成了一等字段:
- 请求侧,assistant 历史消息支持
reasoning键(vllm/entrypoints/openai/chat_completion/protocol.py 中存在对msg.get("reasoning")的处理逻辑,将客户端回传的推理内容并入渲染后的提示词); - 响应侧,消息体定义了
reasoning: str | None字段(同文件第 72 行附近),流式与非流式都会填充; - 另有
include_reasoning: bool = True的默认值(该文件两处出现),控制是否默认包含推理输出;以及reasoning_effort参数用于支持按推理强度档位请求(对支持该参数的模型,会转成enable_thinking等用户侧开关)。
也就是说,交错思考在协议层面的闭环是:reasoning parser 把输出切分成 reasoning/content → API 返回message.reasoning→ 客户端回传 → chat template 将其重新注入提示词 → 模型基于完整推理历史继续思考。缺任何一环,模型"看到"的历史都会丢失思考内容。
实现链路总览
结合源码结构,一次交错思考请求在 vLLM 内部的处理链路如下(从源码结构看,各组件职责分离清晰):
- 解析引擎层(vllm/parser/engine/):
parser_engine.py、streaming_parser_engine.py、incremental_lexer.py、token_id_scanner.py构成增量式流式解析框架。reasoning 与 tool call 两类事件由events.py定义,registered_adapters.py将具体模型的解析规则适配到该引擎; - ReasoningParser 抽象层(vllm/reasoning/abs_reasoning_parsers.py):定义
ReasoningParser基类与ReasoningParserManager,各模型解析器通过 vllm/reasoning/init.py 中的注册表懒加载; - 工具解析层(vllm/tool_parsers/):
kimi_k2_tool_parser.py、minimax_m2_tool_parser.py等分别解析两种模型的 `
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考