Ollama 函数调用怎么配:七个不报错的坑

Ollama 的函数调用能跑通,但七处出错时它不报错,只是安静地少做一次调用。下面的数字来自 ollama 0.40.14 配 qwen3:8b,全部在一台 M4 16GB 的机器上逐条复现,每条都附了判断方法。

先说结论:单轮、单工具、参数简单的场景可以直接用。工具数量上去、上下文收紧、或者你要把它塞进 OpenAI 兼容的客户端时,必须先按下面七条过一遍,否则线上会出现查不到数据却回复得很自信的情况。

先把最小请求跑通

原生端点 /api/chat 的请求体长这样,注意 tools 是顶层字段,不在 options 里面。

curl -s http://localhost:11434/api/chat -H 'Content-Type: application/json' -d '{
  "model": "qwen3:8b",
  "messages": [{"role": "user", "content": "查一下根目录还剩多少空间"}],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_disk_free",
      "description": "查询指定挂载点的剩余磁盘空间,单位 GB",
      "parameters": {
        "type": "object",
        "properties": {"mount": {"type": "string"}},
        "required": ["mount"]
      }
    }
  }],
  "stream": false,
  "think": false,
  "options": {"num_ctx": 4096}
}'

返回体里工具调用在 message.tool_calls,arguments 已经是对象,不需要再解析 JSON。这一条与 OpenAI 的格式不同,第五个坑会细说。

{
  "message": {
    "role": "assistant",
    "content": "",
    "tool_calls": [
      {"id": "call_e8npmy17",
        "function": {"index": 0, "name": "get_disk_free",
                      "arguments": {"mount": "/"}}}
    ]
  },
  "done_reason": "stop",
  "prompt_eval_count": 164,
  "eval_count": 20
}

拿到调用之后,把结果按 role 为 tool 的消息塞回 messages,再发一次就有了最终回答。这一来一回大约 1.7 秒,比同机云端 API 慢不了多少,本地跑工具链是够用的。

坑一:format 与 tools 同时给,调用会消失

想让它既走工具又输出结构化 JSON,很自然会两个都传。实测三次三次都是同样的结果:工具调用被完整吞掉,返回 200,content 里是一段解释文字,被 format 的 schema 包成 JSON。

# 同一句话,只差一个 format 字段
# 不带 format:tool_calls 有,content 为空
# 带 format:tool_calls 为 null,content 是 {"note": "调用 get_disk_free 工具来查询 / 挂载点的磁盘空间。"}

两次请求的 prompt_eval_count 都是 162,说明工具定义照样进了上下文,只是输出被 format 的约束接管了。判断方法很直接:只要 response 的 content 里出现了工具名的拼音化描述,而 tool_calls 是 null,就是踩了这个。

做法是二选一。要么这一轮只给 tools,让模型决定调用;要么这一轮只给 format,把已经拿到的工具结果整理成结构。两个约束不要同轮叠加。

坑二:num_ctx 不够时会选错工具

工具定义是吃上下文的。20 个工具、每个带一段中文描述和参数说明,prompt_eval_count 直接到 1027。用同一句话去测不同 num_ctx,结果三档完全不同。

num_ctx=512   prompt=258   无任何调用,content 为空
num_ctx=1024  prompt=514   调用了 read_smart(错的那个)
num_ctx=1536  prompt=1027  调用 get_disk_free(正确)

1024 那一档最危险。模型没有拒绝,也没有报错,它从被截断的工具列表里挑了一个还看得见的,read_smart 是读磁盘健康信息的,跟剩余空间差得很远,但回复的语气一样笃定。512 那一档更彻底,连工具调用都不生成,content 空串,done_reason 还是 stop。

可用预算比 num_ctx 小,16K 以内大致只给一半,这个规律我此前单独测过,见 Ollama 上下文怎么设,1024 档只放得下 514 个 token 就是这条规律在起作用。所以装 20 个以上工具的 agent,num_ctx 起步给 8192,别用默认值。

坑三:num_predict 太小,调用变成空回答

把 num_predict 压到 12,返回体是 content 空、tool_calls 为 null、done_reason 为 length。客户端如果只看 tool_calls 有没有值,会认为模型主动选择不调工具。

num_predict=6    done_reason=length  eval=6   tool_calls=null
num_predict=12   done_reason=length  eval=12  tool_calls=null
num_predict=20   done_reason=length  eval=20  tool_calls 完整
num_predict=30   done_reason=stop    eval=22  tool_calls 完整

临界点在 20 附近,因为一次 tool_calls 的完整信封本身就要 20 个 token 左右。20 那一档 done_reason 仍是 length,但信封刚好闭合,所以能被解析出来,这说明 length 不等于废输出,判据要看 tool_calls 是否为空。

顺带一个同类现象:思考链会把预算吃光,正文一个字不剩,见 Qwen3-VL 读表格实测 里的 max_tokens 一节。工具调用这一侧的表现就是上面的 null,处理方式一样,把预算给到 6000 以上,或者关掉思考。

坑四:tool_choice 三个值都没生效

在 /v1/chat/completions 上分别传 none、required、auto,三次全部返回了工具调用,耗时 13.17 秒、12.91 秒、13.33 秒,行为完全一致。

# 全都照样调用,none 没能禁用
{"tools": [...], "tool_choice": "none"}

这意味着你没有办法在不删掉 tools 字段的前提下临时禁止调用。想禁用只能在客户端把 tools 整个拿掉,然后按普通对话发一次。工程上更稳的写法是维护两组请求模板,一组带工具一组不带,不要指望服务端帮你切换。

坑五:两个端点的 arguments 类型不一样

同一份工具声明,/api/chat 返回的 arguments 是对象,/v1/chat/completions 返回的是 JSON 字符串。类型不同,客户端代码不能一套通吃。

# /api/chat
"arguments": {"mount": "/"}

# /v1/chat/completions
"arguments": "{\"mount\":\"/\"}"

两边还有三处细节差。原生端点没有 finish_reason,/v1 给的是 tool_calls。原生端点的 tool_calls 元素没有 type 字段,/v1 有。原生端点把序号放在 function.index,/v1 把 index 放在元素顶层。

写适配层的办法是拿到参数后先判类型,是字符串就 json.loads,是字典就直接用。见过太多人在这里抛 TypeError 然后去换模型、重下权重,白折腾半天。同类适配问题在 aider 接本地 Ollama 里也出现过一次,那边踩的是 num_ctx 每轮重算。

坑六:/v1 的 tool 消息 content 必须是字符串

把工具执行结果直接塞字典进去,服务端立刻返 400,报错文本是 invalid message content type: map[string]interface {}。这个报错还算友好,至少指到了类型。

# 错:直接给字典
{"role": "tool", "tool_call_id": "call_abc", "content": {"free_gb": 142.7}}

# 对:先序列化
{"role": "tool", "tool_call_id": "call_abc", "content": "根目录 / 剩余 142.7 GB"}

更隐蔽的是漏掉 assistant 那一轮。把 user 和 tool 两条消息直接发过去,/v1 不报错,还是 200,prompt_tokens 从 202 掉到 179,模型自己把上下文补全了,回答看着也对。少掉的 23 个 token 就是被丢掉的那条调用记录,它不会提醒你。

所以历史必须按顺序保留完整三段:user、带 tool_calls 的 assistant、role 为 tool 的结果。原生端点在 tool 消息上用 tool_name 字段也能通,/v1 认的是 tool_call_id。

坑七:/v1 关思考只有 reasoning_effort 管用

qwen3 默认带思考,一次工具调用前它先写几百字推理。想省这部分开销,四个写法实测差别很大。

不传开关                 思考 300 字  completion_tokens=206  14.0s
"think": false           思考 184 字  completion_tokens=139   7.2s
"reasoning_effort":"none" 思考 0 字   completion_tokens=20    1.1s
chat_template_kwargs     思考 416 字  completion_tokens=269  14.0s

只有 reasoning_effort 传 none 能真正压到零,耗时从 14 秒降到 1.1 秒,差了 12.7 倍。think 字段在 /v1 上只减了一部分,chat_template_kwargs 那套写法则完全没被解析,思考反而变长了。原生 /api/chat 认的是顶层 think 布尔值,同一个名字在两个端点上语义不同,这一点与 LiteLLM 代理 Ollama 的五处失败 里记的 think 差异是同一个根因。

工具定义的 token 成本

工具越多越贵,这个成本是固定的,跟问什么无关。同一句话、同一模型,只改工具数量,prompt_eval_count 的变化如下。

工具数 prompt token 单个工具均摊 请求体大小
1 157 — 335 字节
5 342 46.3 1480 字节
10 574 46.4 2851 字节
20 1027 45.3 5561 字节

每个工具稳定在 46 个 token 上下,与工具数量无关,是线性叠加。换句话说,一个接了 20 个工具的 MCP 服务,每轮对话光是工具清单就固定花掉 870 个 token,这部分预算不随上下文压缩而消失。

按这个系数倒推,工具清单占用不要超过 num_ctx 的四分之一。8192 的上下文最多挂 40 个工具,4096 的上下文建议控制在 15 个以内。超过这个比例,第二个坑就会开始出现,模型会挑错工具而不报错。

踩坑:三个真实的失败现场

第一个是工具声明根本没被校验。空数组、缺 type 字段、缺 parameters 字段,三种畸形写法全部返回 200,一个错误都没有,接口静默接受然后按没有工具处理。如果你的工具列表是拼接出来的,拼错了不会有人告诉你,只能靠 prompt_eval_count 是否明显偏小来发现。

第二个是能力徽标不可信。ollama show 给 qwen3-embedding:0.6b 列出了 tools 能力,但真喂 tools 过去,两个端点都返 400,报 does not support chat。这条徽标只说明训练数据里有工具调用样本,不代表这个模型能进对话链路。判断能不能用,看的是 chat 能力,不是 tools。

第三个是 response_format 的偶发 500。/v1 上带 json_object 连测三次,第一次跑了 31.29 秒后返 500,报错是 prediction aborted, token repeat limit reached,后两次正常。同类请求在原生端点加 format 从来没复现过。要稳定拿 JSON,用 format 字段走原生端点,别用 OpenAI 那套 response_format。

另外有两个正面结论值得记下。并行调用是支持的,一次要三个结果时它能在一条消息里返回三个 tool_calls,顺序和参数都对,实测 3.89 秒。流式也支持,tool_calls 完整落在第一帧,不会截断。

常见问题

模型明明支持工具,为什么一次都不调用?
先看 prompt_eval_count。如果只有一两百而不是上千,说明工具定义没进上下文,检查是不是写进了 options 里,tools 必须在顶层。再看 done_reason,是 length 就是被 num_predict 掐断了,放宽到 6000 以上再看。这两个都不是,才去怀疑模型本身。

num_ctx 设多大才够用?
把工具数乘以 46,加上你平时提示词的 token 数,再乘以二,就是下限。20 个工具加一段 500 token 的系统提示,算下来要 4000 上下,实际给 8192 才稳妥。低于这个值不会报错,只会选错工具或者干脆不调。

本地小模型的工具调用能直接上生产吗?
简单 schema、单工具、参数只有两三个字符串字段的场景,qwen3:8b 十次连测全部合规,可以直接用。带数组、带枚举、带嵌套对象的复杂 schema 也能过,但前提是上下文给足、format 不与 tools 同轮出现。真要上生产,客户端必须自己校验参数类型再执行,别把工具里抛的异常当成模型能力问题。

把工具链搭起来之后,下一步通常是把它接到客户端里。这一层协议怎么起服务,见 FastMCP 改名后 MCP 服务怎么起,本地工具打包成服务端之后,上面七个坑全部照样存在。

全部文章归档与模型专题