Ollama本地函数调用发布72小时,我替你试完了

14 阅读5分钟

用Ollama跑Qwen3.8-27B做本地函数调用

面向想把"轻量模型 + 工具调用"跑在自己机器上的工程师。全文命令可复现,显卡门槛低至单张 24GB。

一、为什么用 27B 而不是 70B/744B

不是所有 Agent 都需要旗舰模型。比如内部工单分类、日志初筛、表单自动填充这类任务,延迟比"智商上限"重要得多。Qwen3.8-27B 在 Q4 量化后权重约 16–18 GB,一张 24GB 的 4090/3090 就能常驻,推理成本接近零。据 multiple 模型榜单交叉验证,27B 级别在中文函数调用(tool-call)评测上已经能稳定输出合规 JSON,足够撑起大部分本地自动化。

Ollama 的价值在于:它把"下载—量化—起服务—函数调用"收敛成几条命令,省去手搓 vLLM 多卡编排的复杂度。你牺牲的是极限吞吐,换来的是十分钟内从零跑通。

二、安装与拉模型

# macOS / Linux 一键安装
curl -fsSL https://ollama.com/install.sh | sh

# 拉取 Qwen3.8-27B 的 Q4 量化版本(约 17GB)
ollama pull qwen3.8:27b-instruct-q4_K_M

# 后台常驻服务(默认 11434 端口,OpenAI 兼容)
ollama serve

如果官方 tag 里没有你想要的量化档,可以用 Modelfile 自己指 GGUF:

FROM ./qwen3.8-27b-q4.gguf
PARAMETER num_ctx 8192
PARAMETER num_gpu 99
TEMPLATE """{{ if .System }}<|im_start|>system\n{{ .System }}<|im_end|>\n{{ end }}{{ if .Prompt }}<|im_start|>user\n{{ .Prompt }}<|im_end|>\n{{ end }}<|im_start|>assistant\n{{ .Response }}"""

num_gpu 99 是把层数尽量塞进 GPU,避免 CPU offload 拖慢首 token;num_ctx 8192 给函数调用留足上下文,比如工具返回一长串 JSON 时不会截断。

三、定义工具并跑通第一轮调用

Ollama 的 OpenAI 兼容接口原生支持 tools。下面用官方 ollama Python 库演示一个"查天气 + 写日历"的最小 Agent:

import ollama, json, datetime

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询指定城市的当前天气",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    }
}]

messages = [{"role": "user", "content": "上海今天适合户外团建吗?"}]

resp = ollama.chat(
    model="qwen3.8:27b-instruct-q4_K_M",
    messages=messages,
    tools=tools,
)
msg = resp["message"]
print(msg.get("tool_calls"))   # 模型会输出结构化调用

如果 tool_calls 为空,先别怀疑模型——八成是 description 写得太含糊,或者参数 required 没标全。比如把"查询天气"和"查温度"拆成两个函数时,模型容易乱选,明确每个工具的边界能让命中率明显提升。

四、把工具结果回灌,凑成 Agent 循环

函数调用的本质是"模型出计划 → 本地执行 → 结果回灌 → 模型再决策"。一个最小循环:

def run_agent(prompt, max_steps=5):
    messages = [{"role": "user", "content": prompt}]
    for _ in range(max_steps):
        r = ollama.chat(model="qwen3.8:27b-instruct-q4_K_M",
                        messages=messages, tools=tools)
        m = r["message"]
        if not m.get("tool_calls"):
            return m["content"]          # 没有工具调用 = 最终回答
        for call in m["tool_calls"]:
            fn = call["function"]
            result = dispatch(fn["name"], fn["arguments"])
            messages.append(m)            # 先把模型的 tool_call 消息留下
            messages.append({            # 再把执行结果以 tool 角色回灌
                "role": "tool",
                "content": json.dumps(result, ensure_ascii=False),
                "name": fn["name"],
            })
    return "超过最大步数"

注意 messages.append(m) 这一步不能省:很多人在回灌时只 append 了 tool 结果,漏掉模型的 tool_call 消息,Ollama 会因上下文结构不连续直接报 tool_calls 格式错误。回灌顺序永远是"模型的 assistant(tool_calls) → 你的 tool 结果",成对出现。

五、踩坑实录

  1. 返回非法 JSON:Q4 量化偶发把参数写成单引号或中文引号。务必 json.loads 加 try/except,失败就让模型"重新输出严格 JSON"。
  2. ctx 爆了却不报错:num_ctx 太小,长工具结果被静默截断,模型拿到的数据不全、开始瞎编。比如返回 5 千字日志时,先把文本做摘要再回灌更稳。
  3. 循环停不下来:模型反复调用同一个工具。加 max_steps 硬上限,并在 system prompt 里写清"拿到结果就直接回答,不要再次调用"。
  4. 首 token 慢:27B Q4 在 CPU offload 下 prefill 极慢,确认 nvidia-smi 里显存真的吃满,而不是在吃内存。

六、辩证:小模型 Agent 的天花板

本地 27B 不是银弹。比如需要跨文档长程推理、或调用几十个复杂工具时,小模型的函数选择准确率会明显下降,这时该上云端大模型而非硬撑。另一个常被忽视的点:本地化意味着你放弃了公有 API 那层"安全护栏",模型若被诱导调用危险工具(比如 rm -rf 类),后果完全自己扛。所以本地 Agent 一定要在工具层做白名单——只允许明确无害的函数进入循环,而不是把 shell 直接交给模型。轻量与自主,从来都要用一道护栏来交换。

互动提问

  1. 你会在生产里用 27B 本地 Agent,还是直接上云端大模型,欢迎在评论区聊聊取舍?
  2. 本地函数调用最让你头疼的坑是什么,是 JSON 解析还是上下文截断?
  3. 把 shell 这类危险工具交给本地模型,你觉得该不该做白名单限制?

数据与事件来源

  • Ollama 官方文档:模型拉取、Modelfile 参数、tools 调用协议
  • Qwen 官方模型卡与 GGUF 量化发布说明:27B 规格与 Q4 量化档
  • 多平台函数调用(tool-call)评测榜单交叉验证:27B 级别中文 tool-call 合规率