你向本地的 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 格式和模型微调时学到的不一致,或者运行时根本没有约束输出格式。
先判断你属于哪一类
| 症状 | 最可能的原因 | 跳转 |
|---|---|---|
| 每次都是纯文本,从不出现 JSON | llama-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.cpp 的 llama-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.3、Gemma 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_M 或 Q8_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 都做防御式解析。
相关阅读
- Chat template 不匹配导致输出全是乱码
- Ollama Modelfile 里的 SYSTEM prompt 被忽略
- 本地模型输出在 token 中间被截断
- tokenizer 漂移导致 token 计数不一致
- Cursor MCP server 连接不上
标签: #local-llm #ollama #排查