本地模型不遵守 tool calling 格式

本地 LLM 返回纯文本描述而不是 JSON 工具调用,或格式错误导致解析失败。用支持 tool calling 的模型、llama-server 的 --jinja 参数和 Ollama 的 format JSON Schema 约束彻底修好。

你向本地的 Ollama 或 llama-server 接口发送一个带 tools 列表的请求(OpenAI /v1/chat/completions 格式),prompt 明确需要用到其中某个工具。但模型没有返回结构化的 tool_calls 数组(比如 {"tool_calls": [{"function": {"name": "search_web", "arguments": {"query": "..."}}}]}),而是用纯文本写出 "我需要查询天气,请调用 get_weather 函数,参数为 location: '北京'",然后开始编造答案。或者返回的 JSON 缺括号、键名拼错,客户端解析直接失败。

最快的修复(覆盖大多数情况):换一个真正为 tool calling 微调过的模型(截至 2026 年 6 月,本地最稳的是 Qwen3,其次是 GPT-OSS 和 Llama 3.3),跑在当前版本的运行时上(Ollama v0.30.x 或较新的 llama-server 构建),并且启动 llama-server 时一定要带 --jinja 参数。最常见的原因就是 llama-server 启动时没加 --jinja,这会静默关闭 tool-call 解析;其次是用了一个根本没经过 function calling 微调的模型。

这几乎从来不是「模型不够聪明」的问题。真正的原因是 chat template 里的 tool-call 格式和模型微调时学到的不一致,或者运行时根本没有约束输出格式。

先判断你属于哪一类

症状最可能的原因跳转
每次都是纯文本,从不出现 JSONllama-server--jinja,或模型没被 tool 微调原因 1、3
有时是 JSON,有时是纯文本temperature 太高 / 没约束原因 5、3
出现了 JSON 但键名/括号错乱量化太低,或 schema 没被强制原因 3、7
工具永远被忽略,连明显的 prompt 也不触发tools 没注入进 prompt原因 4
格式对了,但调错工具或参数错误schema 对小模型太复杂原因 6

常见原因

按命中率从高到低排列。

1. llama-server 启动时没加 --jinja

截至 2026 年中,llama.cppllama-server 只有在传入 --jinja 时才会执行 OpenAI 风格的 tool calling。不加这个参数,服务器会忽略 tools 数组、套用普通 chat template,模型就只会用纯文本回答。这是 llama-server 用户最常见的原因。

怎么判断:看启动 llama-server 的完整命令。如果没有 --jinja,问题就在这。(Ollama 会自动套用 tool template,所以这条只针对 llama-server。)

2. 模型本身没经过 function calling 微调

不是每个 instruct 模型都支持 tool calling。基础模型和很多通用 instruct 微调版本根本没有「输出结构化 tool-call JSON」的概念。只有显式用 function-calling 数据训练过的模型才能稳定产出。

截至 2026 年 6 月,本地表现强的 tool-caller 包括:Qwen3(第三方 benchmark 中丢调用率最低)、GPT-OSS(20B 已经很稳,120B 顶级)、Llama 3.3Gemma 4(function calling 直接训进了权重),以及偏老但仍然好用的 Llama 3.1、Mistral Nemo、Qwen2.5、Hermes 3、Functionary v3.x。

怎么判断:在 Ollama 上看支持工具的模型列表,如果你的模型没有 tools 标签,它就不会遵守格式。在 HuggingFace 上则看模型卡里有没有 “function calling”、“tool use” 或 “tools”。

3. 套用了错误的 tool-calling template

每个模型家族编码 tool call 的方式都不同:Mistral 用 [TOOL_CALLS] token,Llama 3.x 用 <|python_tag|> 加一段 JSON,Qwen 用 <tool_call> 标签,Hermes 有自己的包裹格式。如果运行时套错了 template(比如在 Mistral 模型上用了 Llama 2 的 template),模型根本看不到自己训练过的 tool-call token,就退回纯文本。

llama-server 自带一个基于 PEG 的 autoparser,能识别 Llama 3.1/3.2/3.3、Functionary v3.1/v3.2、Hermes 2/3、Qwen 2.5、Mistral Nemo、FireFunction v2、Command R7B 和 DeepSeek R1(WIP)的原生格式。对它识别不了的模型,会退回一个通用 JSON 格式,能用但更费 token。少数 template(如 DeepSeek R1)需要显式指定覆盖文件。

怎么判断:Ollama 用 ollama show modelname --modelfile | grep -A20 TEMPLATE,把 tool-call token 格式和模型 HuggingFace 仓库 tokenizer_config.json 里的 chat_template 对比。llama-server 这边,如果模型不在上面那张识别列表里,就用 --chat-template-file 指定一个支持 tool 的 Jinja template。

4. tools 列表没注入进 prompt

有些服务栈(旧构建、自定义封装)接受了 API 调用里的 tools 参数,却从来没把 tool 定义渲染进真正的 prompt。模型压根没见过可用工具,自然没法调用。

怎么判断:打开 debug 日志,读出完整渲染后的 prompt。llama-server--verbose 启动;Ollama 用 OLLAMA_DEBUG=1 ollama serve。如果日志里的 prompt 中没有 tool 名称、描述和参数 schema,就说明没注入。

5. temperature 太高,采样偏离结构化输出

在 temperature 0.8 以上,即便是配置正确的 tool-calling 模型也可能偏离合法 JSON,因为高熵采样偶尔会选到破坏语法的 token。tool calling 需要低温或受约束的采样。

怎么判断:把 temperature 设为 0 重跑同一个 prompt。如果 0 温下能稳定输出结构化结果,原因就是 temperature。

6. tool schema 对小模型太复杂

小模型(Llama 3.1 8B、Qwen3 4B)能处理简单 schema(2-3 个参数),但碰到深层嵌套或单次超过 8-10 个工具就吃力。输出看着语法没问题,却选错工具或填错参数。

怎么判断:把 schema 砍到只有一个工具、一个必填参数再测。如果这时调用正常,就是 schema 复杂度问题——换 14B+ 模型,或把工具集拆开。

7. 量化太低,JSON 生成不稳定

tool calling 要求精确的 token 序列("function"{}、引号)。在激进量化(IQ3 及以下)中,这些位置的概率分布更平,括号和键名更容易出错,JSON 解析失败。

怎么判断:重新拉同一模型的 Q5_K_MQ8_0 版本再测。成功率明显上升,就是量化精度问题。

最短修复路径

Step 1:确认模型支持 tool calling

# Ollama:支持工具的模型带 "tools" 标签
ollama show llama3.3 --modelfile | grep -i tool

# 截至 2026-06 调用工具较稳的模型:
# - qwen3              (最稳;丢调用率最低)
# - gpt-oss:20b        (tool calling 干净,为 agent 调过)
# - llama3.3           (70B 需要 48GB+ 显存)
# - gemma4 / gemma     (function calling 训进了权重)
# - llama3.1:8b, qwen2.5, mistral-nemo, hermes3

Step 2(llama-server):启动时加 --jinja

这是大多数 llama-server 用户的解药。不加 --jinja,tool calling 就是关闭的。

# --jinja 开启 OpenAI 风格的 tool calling 和 PEG tool-call autoparser
llama-server --jinja -fa \
  -hf bartowski/Qwen2.5-7B-Instruct-GGUF:Q5_K_M \
  --port 8080

# 如果模型的 template 不在 autoparser 识别列表里,
# 用一个支持 tool 的 Jinja template 文件:
llama-server --jinja -fa -m model.gguf \
  --chat-template-file models/templates/llama-cpp-deepseek-r1.jinja \
  --port 8080

之后 /v1/chat/completions 接口就会解析并返回真正的 tool_calls 数组:

response = requests.post("http://localhost:8080/v1/chat/completions", json={
    "model": "local",
    "messages": [{"role": "user", "content": "What's the weather in Paris?"}],
    "tools": [{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"}
                },
                "required": ["city"]
            }
        }
    }],
    "tool_choice": "auto",
    "temperature": 0.1,
})

Step 2(Ollama):用带 tool 标签的模型和正确的接口

Ollama 会替你套用 tool template,没有 --jinja 这个参数。用当前版本的 Ollama(截至 2026 年 6 月是 v0.30.x):tool calling 从 Llama 3.1 发布时就支持了,流式 tool call 在 v0.8.0 落地,之后的版本持续完善 thinking 过程中的 tool-call 解析。接口选 /api/chat(原生)或 /v1/chat/completions(OpenAI 兼容)都行。

import openai

client = openai.OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

resp = client.chat.completions.create(
    model="qwen3",
    messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
    tools=tools,
    tool_choice="auto",
    temperature=0.1,
)
msg = resp.choices[0].message
print(msg.tool_calls[0].function if msg.tool_calls else msg.content)

Step 3:约束输出结构

要想摆脱「靠运气出 JSON」,就约束解码器。Ollama 通过 format 参数暴露这个能力:它接受一个完整的 JSON Schema,强制解码结果匹配该 schema(自 Ollama 0.3.0 起)。llama-server--jinja 打开时,内部会从 tool schema 推导出 GBNF grammar 做同样的事。

# Ollama 原生 /api/chat:用 format 强制 JSON 结构
import ollama

resp = ollama.chat(
    model="qwen3",
    messages=[{"role": "user", "content": "查询北京天气,只输出 JSON。"}],
    format={
        "type": "object",
        "properties": {
            "city": {"type": "string"},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
        },
        "required": ["city"],
    },
    options={"temperature": 0},
)
print(resp["message"]["content"])  # 保证匹配 schema

约束只保证 JSON 在「语法」上合法,并不保证模型选对了工具或填对了参数值——那仍然取决于模型是否经过 tool 训练(Step 1)。

Step 4:tool 请求把 temperature 降下来

response = client.chat.completions.create(
    model="qwen3",
    messages=messages,
    tools=tools,
    tool_choice="auto",
    temperature=0,    # 确定性;0.1 也可以
    max_tokens=256,   # tool call 很短
)

Step 5:打印原始 prompt,确认 tools 被注入

# llama-server:导出渲染后的 prompt
llama-server --jinja --verbose -m model.gguf 2>&1 | grep -A 50 "prompt"

# Ollama:给服务进程开 debug 日志
OLLAMA_DEBUG=1 ollama serve

确认日志里的 prompt 出现了 tool 名称和 JSON schema。如果没有,就是运行时没注入工具,再怎么调模型参数都没用。

Step 6:先用最小的单工具 schema 测试

minimal_tool = {
    "type": "function",
    "function": {
        "name": "echo",
        "description": "Echo back the input string",
        "parameters": {
            "type": "object",
            "properties": {"text": {"type": "string"}},
            "required": ["text"],
        },
    },
}

# 一个毫无歧义、必须用到该工具的 prompt
messages = [{"role": "user", "content": "Echo the phrase 'hello world' using the echo tool."}]

如果最小用例能跑通、真实 schema 跑不通,原因就是 schema 复杂度(原因 6)——简化、拆分工具,或换更大的模型。

如何确认已修好

把同一个请求跑 5-10 次,检查每次响应都带有非空的 tool_calls 数组(或在用了 format 时是符合 schema 的 JSON),且周围没有多余文本。真正修好的模型在 temperature=0 下应该每次都命中结构。如果 10 次里仍有 1-2 次回到纯文本,说明模型的 tool 训练不足——换模型,而不是死磕 prompt。

预防建议

  • agent 场景一律从支持工具的列表里选模型,别假设通用 instruct 模型支持工具。截至 2026 年 6 月,Qwen3 和 GPT-OSS 是稳妥默认。
  • llama-server--jinja 写进启动脚本,缺了就当 bug 处理。
  • 任何带 tools 参数的请求都把 temperature 设为 0 或 0.1。
  • 跑当前版本的运行时:Ollama v0.30.x+ 或较新的 llama-server 构建。旧版本没有 autoparser 和流式 tool call 支持。
  • tool calling 场景优先用 Q5_K_M 及以上量化,低于 Q4 的量化更容易把 JSON 弄坏。
  • 发送前用 JSON Schema 校验 tool 定义:所有 type 值小写("string" 而非 "String"),properties 结构完整。
  • 先用一个简单工具测通,再上复杂的 multi-tool schema。
  • 生产环境加一个校验器:检查响应里有没有 tool_calls,模型忽略格式时优雅降级(先用更强约束重试一次,再走文本路径)。

常见问答 (FAQ)

Q:Ollama 原生支持 OpenAI 的 tool-calling API 吗? A:支持。用 /api/chat(原生)或 /v1/chat/completions(OpenAI 兼容),带上 tools 数组即可。但模型本身得经过 tool 训练——在它的 Ollama 模型页上看有没有 tools 标签。当前版本里流式 tool call 和 think 都支持。

Q:我的 llama-server 完全忽略了 tools 数组,为什么? A:你几乎可以肯定是启动时没加 --jinja。这个参数才是开启 OpenAI 风格 tool calling 和 tool-call autoparser 的开关。用 llama-server --jinja ... 重启再测。

Q:grammar 或 format 约束能强迫任何模型输出 tool-call JSON 吗? A:它能强制「语法」合法——合法且匹配 schema 的 JSON。但它无法让一个没经过 tool 训练的模型选对工具或填出合理参数。约束 + tool 训练过的模型才是可靠组合;只在基础模型上加约束,会得到看着像样、实则胡乱的调用。

Q:为什么模型有时给出正确的 tool call,有时又是纯文本? A:采样的随机性。温度高时模型会走到一个岔口,“I’ll call”(纯文本)和 {"tool_calls"(JSON)都是可信的下一个 token。把 temperature 设为 0,或用 --jinja / Ollama 的 format 约束输出,就能排除纯文本这条路。

Q:为什么 tool calling 在 GPT-5.5 上是 100%,本地 7B 只有 60-70%? A:前沿模型有多得多的 function-calling 微调,API 背后还有受约束解码。小型开源模型这方面少很多。换成 Qwen3 或 GPT-OSS、提高量化、降低 temperature,并在应用层加「带约束重试」的降级逻辑。

Q:本地模型支持 parallel tool calls(一次调用多个工具)吗? A:主流本地模型(Qwen3、Llama 3.3)支持,但稳定性低于单工具。先确认单工具稳定,再开 parallel calls,并且对每个 tool call 都做防御式解析。

相关阅读

标签: #local-llm #ollama #排查