LocalAI 交错思考(Interleaved Thinking):让推理与工具调用在多轮对话中不断链
【免费下载链接】LocalAILocalAI is the open-source AI engine. Run any model - LLMs, vision, voice, image, video - on any hardware. No GPU required.项目地址: https://gitcode.com/GitHub_Trending/lo/LocalAI
LocalAI 将推理模型(reasoning model)的思考链与工具调用封装为同一个 assistant 轮次中的两个字段:reasoning与tool_calls。本篇围绕 LocalAI 文档中的Interleaved Thinking特性展开:先讲清"思考-工具-思考"的往返契约与字段命名规则,再给出 llama.cpp、vLLM、SGLang 三种后端的推理开关配置,最后附一个可直接复制的 curl 实操示例与已知限制。读完本文,你能在 LocalAI 上完整跑通"模型先推理、再调用工具、再基于工具结果继续推理"的多轮循环,并理解 LocalAI 源码中reasoning字段的入站别名解析与 vLLM 解析器自动配置的实现细节。
为什么工具调用循环需要"交错思考"
推理模型在回答前会"思考"。当这样的模型同时调用工具时,理想行为是:思考与工具调用同轮返回,且思考内容在工具结果回传后不丢失。LocalAI 把这个能力称为interleaved thinking:一个 assistant 轮次同时携带reasoning和tool_calls,客户端在下一轮把reasoning原样传回,模型就不会在追加工具结果后丢失已有的思维链。
这一点之所以关键,是因为工具调用循环天然是多轮的:模型推理 → 请求工具 → 客户端执行工具 → 携带工具结果再次调用模型。若没有交错思考,第一轮产生的推理会被丢弃,模型必须从零重建计划;有了交错思考,推理被回显到上下文中,模型从"断点处"继续。
往返契约(Round-trip contract)
一个既推理又调用工具的 assistant 轮次,两个字段并列返回:
reasoning:模型思考内容;tool_calls:结构化工具调用;finish_reason为tool_calls。
客户端执行完工具后,把会话再传回来,需要带上两部分:
- 原始 assistant 消息(包含它的
reasoning和tool_calls,不能省略); - 一条
toolrole 消息,携带工具执行结果。
LocalAI 会把传回的reasoning读回模型上下文,使思维链保持连续。
源码印证:reasoning 字段如何解析与透传
从源码结构看,这一契约的落点在消息 schema 层。Message 结构体 中reasoning是一个*string指针字段,注释标明它来自<thinking>...</thinking>标签的提取:
// Reasoning content extracted from <thinking>...</thinking> tags Reasoning *string `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`而 Message.UnmarshalJSON 专门处理了入站别名:vLLM、DeepSeek 风格的客户端在 assistant 轮次上发的是reasoning_content,LocalAI 接受该别名并映射到同一个内部字段;当两个字段同时出现时,规范的reasoning胜出:
if m.Reasoning == nil && aux.ReasoningContent != nil { m.Reasoning = aux.ReasoningContent }在跨进程透传上,Messages.ToProto 会把reasoning写进 gRPC 消息的ReasoningContent字段,使核心服务与后端 worker 之间完整携带思考内容。
字段命名规则
OpenAI chat completions(/v1/chat/completions)
响应中reasoning与tool_calls并列出现。入站 assistant 消息同时接受reasoning_content作为reasoning的别名——该别名存在的原因正是一些客户端(vLLM、DeepSeek、cogito)用reasoning_content这个名字输出该字段;两种命名都会被接受并映射到同一内部字段(即上文 UnmarshalJSON 的实现)。
Anthropic Messages(/v1/messages)
在 Anthropic 协议下,推理以thinkingcontent block 的形式携带。本地路径中,LocalAI 在tool_useblock 之前先发出一个thinkingblock,并把入站的thinkingblock 读回 reasoning。由于本地模型不产生加密签名,LocalAI 会为发出的 block 附加一个合成的不透明signature,且不会验证入站 block 的签名。thinkingblock 只在请求显式通过thinking参数开启时才发出:
{ "model": "your-reasoning-model", "max_tokens": 1024, "thinking": { "type": "enabled" }, "messages": [ { "role": "user", "content": "What is the weather in Rome?" } ] }从源码结构看,请求侧的开关字段是 AnthropicThinkingParam(thinking字段),而响应侧的thinkingblock 带有Thinking与Signature两个字段(见 Anthropic 内容块定义),与文档描述的"合成 signature、不校验"行为对应。
按后端启用推理
交错思考要求后端能把模型的"思考"与"最终回答"分离开。如何开启取决于后端。
llama.cpp
设置模型选项reasoning_format,可选值:none、auto、deepseek、deepseek-legacy。其中auto让后端根据模型的 chat template 自行选择;deepseek与deepseek-legacy强制使用 DeepSeek 风格的...提取。在 llama.cpp C++ 后端中,该选项在 grpc-server.cpp 中被解析并映射到 llama.cpp 的COMMON_REASONING_FORMAT_*枚举(NONE / AUTO / DEEPSEEK / DEEPSEEK_LEGACY)。
name: my-reasoning-model backend: llama-cpp parameters: model: my-reasoning-model.gguf options: - reasoning_format:autovLLM
将模型选项reasoning_parser设为该模型族对应的 vLLM 原生推理解析器。此外,LocalAI 自带一个自动配置钩子 hooks_vllm.go:它加载嵌入式表 parser_defaults.json,在模型加载时按模型 ID 匹配模型族,为已知模型族同时设置推理解析器与工具调用解析器——因此对从 gallery 导入的 vLLM 模型,这两项通常已经自动配置好了。
自动配置的逻辑见 applyParserDefaults:只有当用户在options中没有显式设置tool_parser:/reasoning_parser:时才注入默认值,用户显式配置永远优先。模型族到解析器的映射关系收录在 parser_defaults.json 中,例如:
| 模型族(匹配模式) | tool_parser | reasoning_parser |
|---|---|---|
| qwen3 | hermes | qwen3 |
| qwen2.5 | hermes | — |
| deepseek-r1 | deepseek_v3 | deepseek_r1 |
| deepseek-v3 | deepseek_v3 | deepseek_v3 |
| mistral-nemo / small / large、magistral | mistral | mistral |
| llama-3.1 / 3.2 / 3.3 | llama3_json | — |
| llama-4 | llama4_pythonic | — |
| gemma-4 | gemma4 | gemma4 |
| gpt-oss | openai | openai_gptoss |
| kimi-k2 | kimi_k2 | kimi_k2 |
| nemotron | — | nemotron_v3 |
| olmo | olmo3 | olmo3 |
name: my-vllm-model backend: vllm options: - reasoning_parser:deepseek_r1 - tool_parser:hermesSGLang
同时设置reasoning_parser与tool_call_parser两个模型选项:
name: my-sglang-model backend: sglang options: - reasoning_parser:deepseek-r1 - tool_call_parser:qwen25实操示例:一轮"推理 + 工具调用"
定义一个工具,并用一个需要先推理才能行动的提示词请求 chat completion:
curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{ "model": "your-reasoning-model", "messages": [ { "role": "user", "content": "What is the weather in Rome?" } ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Get the current weather for a city", "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } } ] }'响应消息同时携带推理与工具调用,且finish_reason为tool_calls:
{ "choices": [ { "finish_reason": "tool_calls", "message": { "role": "assistant", "content": "", "reasoning": "Okay, the user is asking about the weather in Rome... I need to call get_weather with city Rome.", "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Rome\"}" } } ] } } ] }继续对话的方式:执行get_weather,然后把会话发回去——先放 assistant 消息(保留其reasoning与tool_calls),再放一条携带工具结果的tool消息。LocalAI 会把传回的 reasoning 喂回上下文,模型即可从中断的思维链处继续。
已知限制
- 流式模式下的 Anthropic
thinkingblock。流式模式下,thinkingblock 目前只在经过 llama.cpp C++ autoparser 的工具调用路径上发出。普通的纯文本流式轮次,或走 inline token 路径解析的工具轮次,都不会流式输出thinkingblock;等价的非流式请求则会返回。即非流式在所有分支上都输出 thinking,只有流式路径存在这一缺口。 - 上游 llama.cpp 在最新混合推理模型上的泄漏。在最新的混合推理模型(Qwen3.5 / Qwen3.6)上,存在一个上游 llama.cpp bug:工具调用可能泄漏进
reasoning_content,而不是被解析进tool_calls。该问题已在 ggml-org/llama.cpp 上游讨论 #23351 中跟踪(外部链接从略)。 - 推理预算与工具调用的竞争。如果推理模型在输出完工具调用之前就耗尽了输出预算(
max_tokens),则不会产生任何工具调用。需要给模型足够的max_tokens来同时覆盖推理与调用本身;耗尽预算时 LocalAI 会报告finish_reason: "length"。
延伸阅读
本文的机制与 OpenAI Functions and Tools 中各后端的工具调用提取方式、Text Generation (GPT) 的 chat completions 基础,以及 Advanced model configuration 中所述的模型options机制配合阅读;本特性的原始文档见 interleaved-thinking.md。
【免费下载链接】LocalAILocalAI is the open-source AI engine. Run any model - LLMs, vision, voice, image, video - on any hardware. No GPU required.项目地址: https://gitcode.com/GitHub_Trending/lo/LocalAI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考