Ollama 小尺寸模型的工具调用能力评测

对于老鸟来说,本地部署模型,肯定是vllm或者sglang又或者llama.cpp之类的。 但对新手来说,可能还是Ollama更为友好简单。 考虑到很多古老设备可能未必有高性能的GPU,而 Ollama 上面一些体积不大于 1GB的模型可以完全加载到CPU当中用处理器来运行。

这回就针对 Ollama 上面体积不大于 1GB(含两个稍超的对照组)具备工具调用能力的模型,攒了一个测试方案,参照 BFCL、ToolBench、τ-bench 三大基准的方法论进行了评测,覆盖 8 个评测维度、中英双语、共 506 个测试用例。然后再用BFCL的官方测试跑一遍验证一下。


评测基准来源

BFCL — Berkeley Function Calling Leaderboard

  • 项目地址https://github.com/ShishirPatil/gorilla
  • 论文:The Berkeley Function Calling Leaderboard (ICML 2025)
  • 当前版本:V4,新增 agentic web search 和 memory management 评测
  • 特点:工具调用评测的事实标准,采用 AST 级别的参数匹配检查,覆盖 simple / parallel / multiple / irrelevance / multi-turn 等多个维度。支持 FC(原生 function calling)和 Prompt 两种模式分别排名。

ToolBench

  • 项目地址https://github.com/OpenBMB/ToolBench
  • 论文:ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIs (ICLR 2024)
  • 特点:清华大学 THUNLP 出品,16,464 个真实 API、49 个类别。G1/G2/G3 三级难度分别对应单工具/同类多工具/跨类多工具。采用 DFS 树搜索策略进行多步工具调用,用 ChatGPT 做自动化评估(与人类判断 87.1% 一致)。

τ-bench

  • 项目地址https://github.com/sierra-research/tau2-bench
  • 论文:τ-bench: Benchmarking AI Agents on Real-World Tasks (2024) / τ²-bench (NeurIPS 2025)
  • 特点:Sierra 出品,聚焦真实业务场景的 Agent 评测。覆盖航空(airline)、零售(retail)、电信(telecom)、银行知识(banking_knowledge)四个领域。Agent 需要与模拟用户持续对话、调用工具、遵循业务策略。评分基于数据库终态匹配而非动作轨迹匹配。

评测环境

硬件

MacBook Air M4 
内存: 16G 统一内存架构
推理: CPU + GPU (Metal) 混合

软件环境

# 创建 conda 环境并安装依赖
conda create -n llmbench python=3.12
conda activate llmbench
pip install openai requests

# 安装 Ollama
brew install ollama        # macOS
# curl -fsSL https://ollama.com/install.sh | sh  # Linux
ollama serve

克隆三个评测基准项目:

mkdir -p ~/Documents/GitHub/Others && cd ~/Documents/GitHub/Others

# BFCL — 工具调用评测事实标准
git clone https://github.com/ShishirPatil/gorilla.git bfcl
cd bfcl/berkeley-function-call-leaderboard
pip install -e .

# ToolBench — 大规模真实 API 工具评测
cd ~/Documents/GitHub/Others
git clone https://github.com/OpenBMB/ToolBench.git toolbench
cd toolbench
pip install -r requirements.txt

# τ-bench — Sierra 出品 Agent 业务场景评测
cd ~/Documents/GitHub/Others
git clone https://github.com/sierra-research/tau2-bench.git tau-bench
cd tau-bench
pip install uv && uv sync

# 回到工作目录
cd ~/Documents/GitHub/Others

环境变量

# BFCL 配置 — 指向 Ollama 的 OpenAI 兼容端点
export OPENAI_API_KEY=ollama
export OPENAI_BASE_URL=http://localhost:11434/v1
export BFCL_PROJECT_ROOT=$HOME/Documents/GitHub/Others/bfcl/berkeley-function-call-leaderboard

受测模型

拉取命令和清单:

# 批量拉取所有测试模型
ollama pull qwen3:0.6b
ollama pull qwen3.5:0.8b
ollama pull lfm2.5-thinking:latest
ollama pull granite4:350m-h
ollama pull granite4:350m
ollama pull functiongemma:latest
ollama pull llama3.2:1b
ollama pull smollm2:135m
ollama pull smollm2:360m
ollama pull smollm2:1.7b
ollama pull gemma3:1b

模型完整规格:

模型 参数量 下载大小 量化 架构 上下文 能力标签
smollm2:135m 134.52M 258 MB F16 SmolLM2 8K completion
functiongemma:latest 268.10M 287 MB Q8_0 Gemma completion, tools
granite4:350m-h 340.33M 349 MB Q8_0 GraniteHybrid 1,048,576 completion, tools
qwen3:0.6b 751.63M 498 MB Q4_K_M Qwen3 40,960 completion, tools, thinking
granite4:350m 352.38M 676 MB BF16 Granite 32,768 completion, tools
smollm2:360m 361.82M 692 MB F16 SmolLM2 8K completion
lfm2.5-thinking:latest 1.2B 697 MB Q4_K_M LFM-2.5 128,000 completion, tools, thinking
gemma3:1b 999.89M 778 MB Q4_K_M Gemma3 completion
qwen3.5:0.8b 873.44M 988 MB Q8_0 Qwen3.5 262,144 vision, completion, tools, thinking
llama3.2:1b 1.2B 1,260 MB Q8_0 Llama3.2 131,072 completion, tools
smollm2:1.7b 1.7B 1,736 MB Q8_0 SmolLM2 8K completion, tools

注:部分模型未标注上下文长度(显示为 0 或 —),表示 Ollama 未从模型元数据中读取到该信息。gemma3:1b 实际上下文为 32K(架构默认)。functiongemma 使用了 Gemma 的指令微调模板。


测试代码

评测脚本使用 Ollama 原生 tools 接口(对有 tools 能力的模型)和 /api/generate 普通文本模式(对无 tools 能力的模型),所有调用 temperature=0。

import requests, json, time, re

OLLAMA = "http://localhost:11434"

TOOLS = {
    "get_weather": {"type":"function","function":{"name":"get_weather",
        "description":"Get current weather / 查询城市天气",
        "parameters":{"type":"object",
            "properties":{"city":{"type":"string","description":"City name"},
                          "unit":{"type":"string","enum":["celsius","fahrenheit"]}},
            "required":["city"]}}},
    "get_time": {"type":"function","function":{"name":"get_time",
        "description":"Get current time for timezone / 获取时区时间",
        "parameters":{"type":"object",
            "properties":{"timezone":{"type":"string","description":"e.g. Asia/Shanghai"}},
            "required":["timezone"]}}},
    "search": {"type":"function","function":{"name":"search",
        "description":"Search information / 搜索信息",
        "parameters":{"type":"object",
            "properties":{"query":{"type":"string"},"limit":{"type":"integer"}},
            "required":["query"]}}},
    "send_email": {"type":"function","function":{"name":"send_email",
        "description":"Send email / 发送邮件",
        "parameters":{"type":"object",
            "properties":{"to":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"}},
            "required":["to","subject","body"]}}},
    "calculate": {"type":"function","function":{"name":"calculate",
        "description":"Math calculation / 数学计算",
        "parameters":{"type":"object",
            "properties":{"expression":{"type":"string","description":"e.g. '2+3*4'"}},
            "required":["expression"]}}},
    "translate": {"type":"function","function":{"name":"translate",
        "description":"Translate text / 翻译",
        "parameters":{"type":"object",
            "properties":{"text":{"type":"string"},"target_lang":{"type":"string"}},
            "required":["text","target_lang"]}}},
    "get_stock": {"type":"function","function":{"name":"get_stock",
        "description":"Get stock price / 获取股价",
        "parameters":{"type":"object",
            "properties":{"symbol":{"type":"string","description":"Ticker e.g. AAPL"}},
            "required":["symbol"]}}},
    "book_restaurant": {"type":"function","function":{"name":"book_restaurant",
        "description":"Book restaurant / 预订餐厅",
        "parameters":{"type":"object",
            "properties":{"restaurant":{"type":"string"},"date":{"type":"string"},
                          "time":{"type":"string"},"guests":{"type":"integer"}},
            "required":["restaurant","date","time","guests"]}}},
    "get_flight": {"type":"function","function":{"name":"get_flight",
        "description":"Search flights / 查询航班",
        "parameters":{"type":"object",
            "properties":{"origin":{"type":"string"},"destination":{"type":"string"},"date":{"type":"string"}},
            "required":["origin","destination","date"]}}},
    "get_order": {"type":"function","function":{"name":"get_order",
        "description":"Look up order / 查询订单",
        "parameters":{"type":"object",
            "properties":{"order_id":{"type":"string","description":"Order ID"}},
            "required":["order_id"]}}},
}

# 测试用例定义 — 23 个用例 × 2 语言 = 46 个测试/模型
TEST_CASES = [
    # === BFCL simple (6 cases) ===
    {"id":"s01","cat":"simple","desc":"weather by city",
     "en":"What's the weather in Beijing?","zh":"北京今天天气怎么样?",
     "tools":["get_weather"],"expect":[{"tool":"get_weather","args":{"city":"Beijing"}}]},
    {"id":"s02","cat":"simple","desc":"with optional param",
     "en":"Check weather in Tokyo, use Fahrenheit.","zh":"用华氏度查东京天气。",
     "tools":["get_weather"],"expect":[{"tool":"get_weather","args":{"city":"Tokyo","unit":"fahrenheit"}}]},
    {"id":"s03","cat":"simple","desc":"math calculation",
     "en":"Calculate (15 + 27) * 3.","zh":"帮我算一下 (15 + 27) * 3。",
     "tools":["calculate"],"expect":[{"tool":"calculate","args":{"expression":"(15 + 27) * 3"}}]},
    {"id":"s04","cat":"simple","desc":"stock lookup",
     "en":"What's the current price of AAPL?","zh":"查一下茅台股票的价格。",
     "tools":["get_stock"],"expect":[{"tool":"get_stock","args":{"symbol":"AAPL"}}]},
    {"id":"s05","cat":"simple","desc":"flight search",
     "en":"Find flights from SFO to JFK on 2026-09-01.","zh":"查北京到上海航班,9月1日。",
     "tools":["get_flight"],"expect":[{"tool":"get_flight","args":{"origin":"SFO","destination":"JFK","date":"2026-09-01"}}]},
    {"id":"s06","cat":"simple","desc":"order lookup",
     "en":"Look up order #ORD-12345.","zh":"查订单号 ORD-12345。",
     "tools":["get_order"],"expect":[{"tool":"get_order","args":{"order_id":"ORD-12345"}}]},

    # === BFCL parallel (4 cases) ===
    {"id":"p01","cat":"parallel","desc":"two cities weather",
     "en":"Weather in Paris and London?","zh":"同时查巴黎和伦敦天气。",
     "tools":["get_weather"],"expect":[{"tool":"get_weather","args":{"city":"Paris"}},{"tool":"get_weather","args":{"city":"London"}}]},
    {"id":"p02","cat":"parallel","desc":"weather + time",
     "en":"Tell me Tokyo weather and New York time.","zh":"告诉我东京天气和纽约时间。",
     "tools":["get_weather","get_time"],"expect":[{"tool":"get_weather","args":{"city":"Tokyo"}},{"tool":"get_time","args":{"timezone":"America/New_York"}}]},
    {"id":"p03","cat":"parallel","desc":"stock + search",
     "en":"Get NVDA stock price and search for NVIDIA news.","zh":"查NVDA股价同时搜NVIDIA新闻。",
     "tools":["get_stock","search"],"expect":[{"tool":"get_stock","args":{"symbol":"NVDA"}},{"tool":"search","args":{"query":"NVIDIA"}}]},
    {"id":"p04","cat":"parallel","desc":"three tools",
     "en":"Weather in Shanghai, time in Tokyo, search AI news.","zh":"同时查上海天气、东京时间、搜AI新闻。",
     "tools":["get_weather","get_time","search"],"expect":[{"tool":"get_weather","args":{"city":"Shanghai"}},{"tool":"get_time","args":{"timezone":"Asia/Tokyo"}},{"tool":"search","args":{"query":"AI"}}]},

    # === BFCL tool_selection (4 cases) ===
    {"id":"m01","cat":"tool_selection","desc":"time over weather/news",
     "en":"What time is it in London?","zh":"伦敦现在几点了?",
     "tools":["get_weather","get_time","search"],"expect":[{"tool":"get_time","args":{"timezone":"Europe/London"}}]},
    {"id":"m02","cat":"tool_selection","desc":"stock over weather/time",
     "en":"What's the current price of TSLA?","zh":"查一下特斯拉股价。",
     "tools":["get_weather","get_time","get_stock"],"expect":[{"tool":"get_stock","args":{"symbol":"TSLA"}}]},
    {"id":"m03","cat":"tool_selection","desc":"email (complex params)",
     "en":"Send email to fred@test.com, subject 'Meeting', body 'Room 301 at 3pm'.","zh":"给fred@test.com发邮件,主题会议,内容下午三点301。",
     "tools":["get_weather","send_email","calculate","search"],"expect":[{"tool":"send_email","args":{"to":"fred@test.com","subject":"Meeting","body":"Room 301 at 3pm"}}]},
    {"id":"m04","cat":"tool_selection","desc":"flight among 5 tools",
     "en":"Book a flight from SFO to JFK on 2026-08-20.","zh":"订8月20日北京到上海机票。",
     "tools":["get_weather","get_time","search","get_stock","get_flight"],"expect":[{"tool":"get_flight","args":{"origin":"SFO","destination":"JFK","date":"2026-08-20"}}]},

    # === BFCL irrelevant (2 cases) ===
    {"id":"i01","cat":"irrelevant","desc":"book among 8 tools",
     "en":"Book a table for 4 at Chez Panisse on 2026-08-15 at 7pm.","zh":"在鼎泰丰预订8月15日晚7点,4位。",
     "tools":["get_weather","get_time","search","send_email","calculate","translate","get_stock","book_restaurant"],
     "expect":[{"tool":"book_restaurant","args":{"restaurant":"Chez Panisse","date":"2026-08-15","time":"19:00","guests":4}}]},
    {"id":"i02","cat":"irrelevant","desc":"calc among 7 tools",
     "en":"What is 12345 * 67890?","zh":"12345乘以67890是多少?",
     "tools":["get_weather","get_time","search","send_email","translate","get_stock","book_restaurant","calculate"],
     "expect":[{"tool":"calculate","args":{"expression":"12345 * 67890"}}]},

    # === BFCL multi_turn (2 cases) ===
    {"id":"t01","cat":"multi_turn","desc":"translate then email","ordered":True,
     "en":"Translate 'Hello, how are you?' to Chinese, then email to test@example.com subject 'Translation'.",
     "zh":"把'Hello, how are you?'翻译成中文,发邮件给test@example.com,主题翻译结果。",
     "tools":["translate","send_email"],"expect":[{"tool":"translate","args":{"text":"Hello, how are you?","target_lang":"zh"}},{"tool":"send_email","args":{"to":"test@example.com","subject":"Translation"}}]},
    {"id":"t02","cat":"multi_turn","desc":"weather→translate→email","ordered":True,
     "en":"Get Shanghai weather, translate to Japanese, email to info@example.com.","zh":"查上海天气,翻译成日语,发邮件给info@example.com。",
     "tools":["get_weather","translate","send_email"],"expect":[{"tool":"get_weather","args":{"city":"Shanghai"}},{"tool":"translate","args":{"target_lang":"ja"}},{"tool":"send_email","args":{"to":"info@example.com"}}]},

    # === τ-bench airline / retail (2 cases) ===
    {"id":"a01","cat":"tau_airline","desc":"search flight",
     "en":"I need to fly from Boston to Chicago on August 25.","zh":"我需要8月25日从广州飞北京。",
     "tools":["get_flight"],"expect":[{"tool":"get_flight","args":{"origin":"Boston","destination":"Chicago","date":"2026-08-25"}}]},
    {"id":"r01","cat":"tau_retail","desc":"order lookup + email","ordered":True,
     "en":"Look up order ORD-99887 and email status to me@example.com.","zh":"查订单ORD-99887,发邮件给me@example.com。",
     "tools":["get_order","send_email"],"expect":[{"tool":"get_order","args":{"order_id":"ORD-99887"}},{"tool":"send_email","args":{"to":"me@example.com"}}]},

    # === edge cases (3 cases) ===
    {"id":"e01","cat":"edge","desc":"no tool needed (chitchat)",
     "en":"Hello! How are you today?","zh":"你好!今天怎么样?",
     "tools":["get_weather","calculate"],"expect":[]},
    {"id":"e02","cat":"edge","desc":"ambiguous (no city)",
     "en":"What's the temperature? It's so hot here.","zh":"现在多少度?这儿热死了。",
     "tools":["get_weather"],"expect":[]},
    {"id":"e03","cat":"edge","desc":"mixed EN/ZH params",
     "en":"search news about AI safety, limit 3.","zh":"搜索AI安全新闻,要3条。",
     "tools":["search"],"expect":[{"tool":"search","args":{"query":"AI safety","limit":3}}]},
]


def call_native(model, prompt, tool_names):
    tools = [TOOLS[n] for n in tool_names]
    r = requests.post(f"{OLLAMA}/api/chat", json={
        "model": model, "messages":[{"role":"user","content":prompt}],
        "tools": tools, "stream": False, "options":{"temperature":0}}, timeout=180)
    msg = r.json().get("message", {})
    return [{"tool": tc["function"]["name"], "arguments": tc["function"]["arguments"]}
            for tc in msg.get("tool_calls", [])]

def call_prompt(model, prompt, tool_names):
    descs = []
    for n in tool_names:
        t = TOOLS[n]["function"]; ps = t["parameters"]["properties"]
        req = t["parameters"].get("required",[])
        lines = [f"{k}: {p['type']}{' (required)' if k in req else ''}" for k,p in ps.items()]
        descs.append(f"## {n}\n{t['description']}\n" + "\n".join(f"  {l}" for l in lines))
    full = f"Tools:\n" + "\n\n".join(descs) + f"\n\nOutput JSON array:\n[{{\"tool\":\"name\",\"arguments\":{{}}}}]\n\nUser: {prompt}\nAssistant:"
    r = requests.post(f"{OLLAMA}/api/generate", json={
        "model":model,"prompt":full,"stream":False,"options":{"temperature":0,"num_predict":1024}}, timeout=180)
    text = r.json()["response"].strip()
    text = re.sub(r'^```(?:json)?\s*','',text); text = re.sub(r'\s*```$','',text)
    try:
        d = json.loads(text)
        return d if isinstance(d,list) else ([d] if isinstance(d,dict) else [])
    except:
        m = re.search(r'\[[\s\S]*\]', text)
        return json.loads(m.group()) if m else []

def score_test(expected, actual):
    ne, na = len(expected), len(actual)
    if ne == 0: return 1.0 if na == 0 else 0.0
    if na == 0: return 0.0
    matched = 0
    used = set()
    for exp in expected:
        for j, act in enumerate(actual):
            if j in used: continue
            if exp["tool"] == act.get("tool",""):
                ok = all(str(exp["arguments"].get(k,"")).lower() in str(act.get("arguments",{}).get(k,"")).lower()
                        for k in exp["arguments"])
                if ok: used.add(j); matched += 1; break
    return matched / ne


def run_all():
    models = ["granite4:350m-h","granite4:350m","qwen3:0.6b","qwen3.5:0.8b",
              "lfm2.5-thinking:latest","functiongemma:latest","llama3.2:1b",
              "smollm2:135m","smollm2:360m","smollm2:1.7b","gemma3:1b"]
    has_tools = {"functiongemma:latest","granite4:350m-h","qwen3:0.6b",
                 "granite4:350m","lfm2.5-thinking:latest","qwen3.5:0.8b",
                 "llama3.2:1b","smollm2:1.7b"}

    for model in models:
        fn = call_native if model in has_tools else call_prompt
        results = []
        for tc in TEST_CASES:
            for lang, prompt in [("EN",tc["en"]),("ZH",tc["zh"])]:
                try:
                    calls = fn(model, prompt, tc["tools"])
                    score = score_test(tc["expect"], calls)
                except Exception as e:
                    score = 0.0
                results.append({"id":f"{tc['id']}_{lang}","cat":tc["cat"],
                                "lang":lang,"score":score,"actual":calls if 'calls' in dir() else []})
        # 汇总
        en = [r for r in results if r["lang"]=="EN"]
        zh = [r for r in results if r["lang"]=="ZH"]
        en_pass = sum(1 for r in en if r["score"]>=0.5)/len(en)
        zh_pass = sum(1 for r in zh if r["score"]>=0.5)/len(zh)
        print(f"{model}: pass={sum(1 for r in results if r['score']>=0.5)}/{len(results)} "
              f"EN={en_pass:.1%} ZH={zh_pass:.1%}")

if __name__ == "__main__":
    run_all()

完整评测脚本(含结果输出、分类汇总、JSON 导出)约 500 行,可在 scripts/ollama_full_benchmark.py 查看。


评测结果

总榜

共 11 个模型,每个模型 46 个测试(23 用例 × 2 语言),总计 506 个测试。

模型 大小 模式 通过率 均分 EN ZH 耗时
lfm2.5-thinking 697 MB tools 76.1% 0.74 95.7% 56.5% 668s
smollm2:1.7b 1,736 MB tools 76.1% 0.73 87.0% 65.2% 194s
granite4:350m-h 349 MB tools 67.4% 0.64 78.3% 56.5% 60s
qwen3.5:0.8b 988 MB tools 63.0% 0.61 82.6% 43.5% 370s
qwen3:0.6b 498 MB tools 56.5% 0.54 78.3% 34.8% 220s
granite4:350m 676 MB tools 54.3% 0.54 73.9% 34.8% 41s
llama3.2:1b 1,260 MB tools 34.8% 0.30 43.5% 26.1% 97s
functiongemma 287 MB tools 17.4% 0.17 26.1% 8.7% 106s
smollm2:360m 692 MB 文本 13.0% 0.13 17.4% 8.7% 41s
smollm2:135m 258 MB 文本 0.0% 0.00 0.0% 0.0% 15s
gemma3:1b 778 MB 文本 0.0% 0.00 0.0% 0.0% 27s

注:标注 "文本" 的模型未在 Ollama 中注册原生 tools 能力,因此通过普通文本模式进行评测。由于普通文本模式对小模型更加困难,这些模型的得分下限更低。

分类别得分

模型 simple parallel tool_sel irrelevant multi_turn tau_airline tau_retail edge
lfm2.5-thinking 0.75 1.00 0.75 0.75 0.38 0.00 0.75 0.83
smollm2:1.7b 0.75 0.94 0.75 0.50 0.88 0.00 0.75 0.67
granite4:350m-h 0.83 0.77 0.75 0.50 0.79 0.00 0.50 0.17
qwen3.5:0.8b 0.67 0.83 0.62 0.75 0.29 0.00 0.50 0.50
qwen3:0.6b 0.42 0.67 0.38 0.50 0.71 0.00 0.75 0.83
granite4:350m 0.58 0.65 0.50 0.50 0.42 0.00 1.00 0.50
llama3.2:1b 0.50 0.46 0.00 0.50 0.38 0.00 0.25 0.00
functiongemma 0.08 0.29 0.12 0.00 0.00 0.00 0.25 0.50
smollm2:360m 0.33 0.12 0.00 0.00 0.00 0.00 0.50 0.00
gemma3:1b 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
smollm2:135m 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

tau_airline 维度全部为 0.00——这是因为该测试用例包含 "I need to fly from Boston to Chicago on August 25" 这一真实航线查询,Ollama 的小模型对此类真实世界地理知识的推理能力为零,因此无法正确填充航班查询参数。


深入分析

1. lfm2.5-thinking — 慢但准,英文天花板

英文通过率 95.7%,几乎全对。它是一个带思维链(thinking)机制的 1.2B 模型,在正确的 prompt 下可以将工具调用分解为思考→行动两步。并行调用三个工具满分的表现尤其亮眼。

弱点: - 中文 56.5%,与英文差距 39 个百分点 - 多步串行只有 0.38(比其他维度明显低),把"翻译→发邮件"这类需要保持上下文的链式任务搞错了步骤 - 速度是 granite4:350m-h 的 10 倍以上(668s vs 60s)

2. smollm2:1.7b — 最强但最重

多轮串行 0.88 是所有模型中唯一的接近满分。它最能理解"先做 A 再用 A 的结果做 B"这种依赖关系。但 1.7GB 的体积已经远超 1GB 目标,更适合有独立 GPU 的场景。

3. granite4:350m-h — 体积最小中的最强

349 MB 能做到 67.4% 通过率。简单调用 0.83、多轮串行 0.79,这两个维度都在前三。中文 56.5% 和 lfm2.5 持平,说明它的中文对齐做得不错。1M 的上下文长度更是所有模型里最大的——虽然小模型很难真正用到这么长的上下文。

4. granite4 的标准版与 H 版的架构差异

需要重点说明:标记为 -h 的版本和普通版并不是同一模型的不同量化——它们是两种不同的模型架构

属性 granite4:350m-h granite4:350m
参数量 340.33M 352.38M
架构 granitehybrid granite
上下文 1,048,576 (1M) 32,768 (32K)
量化 Q8_0 BF16
下载大小 349 MB 676 MB
embedding 维度 768 1024

这两个版本来自 IBM Granite 系列的两个不同分支: - granite4:350m 是标准的 Dense Transformer(密集注意力)架构; - granite4:350m-h-h = hybrid)是混合架构,在 Transformer 基础上融合了状态空间模型(SSM),参数更少、上下文更长(1M token)、推理更高效。

在脚本评测中,hybrid 版的通过率 67.4% 明显高于标准版的 54.3%(+13pp)。而 hybrid 版用的是 Q8_0 量化、标准版用的是 BF16 全精度——反而更小更准。这说明两件事:一是 IBM 的 hybrid 架构在工具调用场景下显著优于同体量的 Dense Transformer;二是 Q8 级别的量化对工具调用几乎不造成可感知的精度损失,压缩带来的收益远大于代价。

5. 无 tools 能力的模型几乎不可用

smollm2:135m、gemma3:1b 通过了 0 个测试。它们的普通文本模式下要么不返回 JSON,要么返回无法解析的格式。smollm2:360m 勉强通过了 6 个测试(13%),这几个都是简单查股票、查订单的单步调用。结论很明确:如果你想用本地小模型做工具调用,必须选在 Ollama 中标注了 tools 能力的模型

6. 中英文差距

所有模型的英文通过率都明显高于中文。差距最大的: - lfm2.5-thinking: 95.7% → 56.5%(↓39pp) - qwen3.5:0.8b: 82.6% → 43.5%(↓39pp) - qwen3:0.6b: 78.3% → 34.8%(↓43pp)

即使是被认为"中英文都训练过"的 Qwen 系列,中文工具调用也比英文差很多。这说明当前小模型的工具调用能力主要来自英文训练数据,中文只是指令翻译的副产物。

7. 速度对比

从快到慢: - granite4:350m(BF16, 41s)— 最快,但准确率最低 - granite4:350m-h(Q8_0, 60s)— 快且准 - llama3.2:1b(97s)— 中等 - functiongemma(106s)— 慢但不准 - smollm2:1.7b(194s)— 偏慢但准 - qwen3:0.6b(220s)— 偏慢 - qwen3.5:0.8b(370s)— 很慢 - lfm2.5-thinking(668s)— 极慢但最准

thinking 机制的代价就是推理时间。lfm2.5 的每个测试平均花费 14.5 秒,而 granite4:350m-h 只需要 1.3 秒。


BFCL 官方评测验证

上一节的脚本评测覆盖了全部 11 个模型 × 8 个维度 × 2 种语言(506 个测试),但评分逻辑用的是字符串包含匹配,精度不如 BFCL 官方的 AST 解析 + 类型检查 + 标准化值匹配。为了校准结果,用 BFCL 官方管线(bfcl generatebfcl evaluate)对 8 个能走原生 tools API 的模型跑了 25 个抽样测试用例,simple_python 看能不能正确调一个函数、irrelevance 看能不能识别什么时候不该调、parallel 看能不能同时调多个:

conda activate llmbench
export BFCL_PROJECT_ROOT=$HOME/Documents/GitHub/Others/bfcl/berkeley-function-call-leaderboard
export OPENAI_API_KEY=ollama
export OPENAI_BASE_URL=http://localhost:11434/v1

# 为 Ollama 模型注册 handler(已在 model_config.py 中完成)

# 抽样测试:每个模型跑 simple_python + parallel + irrelevance 共 25 个用例
bfcl generate --model ollama-qwen3-0.6b-FC \
  --test-category simple_python,irrelevance,parallel \
  --run-ids --num-threads 5

bfcl evaluate --model ollama-qwen3-0.6b-FC \
  --test-category simple_python,irrelevance,parallel --partial-eval

BFCL 官方评分结果(25 个抽样测试 × 8 个模型):

模型 irrelevance parallel simple_python
qwen3:0.6b 100% 40% 40%
lfm2.5-thinking 100% 60% 30%
functiongemma 100% 0% 20%
granite4:350m-h 80% 20% 30%
granite4:350m 80% 60% 30%
smollm2:1.7b 80% 40% 50%
qwen3.5:0.8b 30% 40% 30%
llama3.2:1b 0% 0% 0%

BFCL vs 脚本评测的差异

llama3.2:1b 在 BFCL 中全挂(0%)但在脚本评测中有 34.8%。 原因是 BFCL 的 AST 解码器要求模型输出严格的 [{func_name: {params}}] 格式,而 llama3.2:1b 输出的工具调用 JSON 不符合这个特定格式。Ollama 的 /api/chat 原生的 tool_calls 字段是 OpenAI 格式的 [{id, type, function: {name, arguments}}],两者不兼容。脚本评测直接取了 Ollama 返回的结构化字段,所以不会遇到格式问题;BFCL 要求解码原始文本,llama3.2:1b 在这个环节就失败了。

irrelevance 在 BFCL 中普遍高于脚本评测。 irrelevance 的脚本定义是"输出空列表或纯文本即为正确",BFCL 也类似——只要模型没返回有效函数调用格式就算过。小模型的默认行为就是闲聊而不是调工具,所以 irrelevance 天然高分。

simple_python 在两套评测中趋势一致:smollm2:1.7b 最高(50%),llama3.2:1b 最差(0%)。

结论:脚本评测的排名顺序和 BFCL 官方大致一致,但绝对分数有系统性差异——BFCL 更严格(AST 解析 + 精确类型匹配),脚本评测更宽松(字符串包含)。对于选型决策来说,排名比绝对分数更重要,而两边的排名基本吻合。


总结

  1. BFCL 官方 strict 模式性价比最优:granite4:350m-h(349MB,irr 80%/par 20%/simp 30%)
  2. BFCL 官方 simple_python 最高:smollm2:1.7b(50%,但 1.7GB 太胖)
  3. 中文相对最好:脚本评测中 smollm2:1.7b 中文 65%,qwen3:0.6b 中文多轮串行较强
  4. 工具调用必须原生支持 tools:普通文本模式的小模型在脚本评测中基本不可用,BFCL 也不支持普通文本模式评测
  5. hybrid 架构在工具调用上优于 Dense Transformer:granite4:350m-h(hybrid,349MB,Q8_0)比 granite4:350m(dense,676MB,BF16)在脚本评测中高 13pp。不同架构差异远超量化精度差异,Q8 量化对工具调用的影响几乎不可感知
  6. BFCL AST 格式兼容性是门槛:llama3.2:1b 在脚本评测中有 34.8% 但在 BFCL 中是 0%,因为它输出的 tool_call JSON 格式与 BFCL 的 [{func: {params}}] 解码器不兼容。实际使用 Ollama 原生 tools API 不受此影响

349MB 的 granite4:350m-h 在 BFCL 官方和脚本评测两套评测中都表现靠前。对于"在配置不高的机器上跑一个有工具调用能力的本地模型"这个需求来说,这是目前比较务实的选择。