用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 多卡编排的复杂度。你牺牲的是极限吞吐,换来的是十分钟内从零跑通。
二、安装与拉模型
text
# 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:
text
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:
python
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 循环
函数调用的本质是"模型出计划 → 本地执行 → 结果回灌 → 模型再决策"。一个最小循环:
python
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 结果",成对出现。
五、踩坑实录
- 返回非法 JSON :Q4 量化偶发把参数写成单引号或中文引号。务必
json.loads加 try/except,失败就让模型"重新输出严格 JSON"。 - ctx 爆了却不报错 :
num_ctx太小,长工具结果被静默截断,模型拿到的数据不全、开始瞎编。比如返回 5 千字日志时,先把文本做摘要再回灌更稳。 - 循环停不下来 :模型反复调用同一个工具。加
max_steps硬上限,并在 system prompt 里写清"拿到结果就直接回答,不要再次调用"。 - 首 token 慢 :27B Q4 在 CPU offload 下 prefill 极慢,确认
nvidia-smi里显存真的吃满,而不是在吃内存。
六、辩证:小模型 Agent 的天花板
本地 27B 不是银弹。比如需要跨文档长程推理、或调用几十个复杂工具时,小模型的函数选择准确率会明显下降,这时该上云端大模型而非硬撑。另一个常被忽视的点:本地化意味着你放弃了公有 API 那层"安全护栏",模型若被诱导调用危险工具(比如 rm -rf 类),后果完全自己扛。所以本地 Agent 一定要在工具层做白名单------只允许明确无害的函数进入循环,而不是把 shell 直接交给模型。轻量与自主,从来都要用一道护栏来交换。
互动提问
- 你会在生产里用 27B 本地 Agent,还是直接上云端大模型,欢迎在评论区聊聊取舍?
- 本地函数调用最让你头疼的坑是什么,是 JSON 解析还是上下文截断?
- 把 shell 这类危险工具交给本地模型,你觉得该不该做白名单限制?
数据与事件来源
- Ollama 官方文档:模型拉取、Modelfile 参数、tools 调用协议
- Qwen 官方模型卡与 GGUF 量化发布说明:27B 规格与 Q4 量化档
- 多平台函数调用(tool-call)评测榜单交叉验证:27B 级别中文 tool-call 合规率