跳至主要内容

如何微调 FunctionGemma:分步指南

了解如何在 10 分钟内利用 Kaggle 的免费 GPU 微调 FunctionGemma——从数据集准备、基线评估到训练与训练后验证。
· 13分钟 阅读

使用 AI 探索

在本教程中,我们将探索来自 Google DeepMind 的轻量级函数调用语言模型 FunctionGemma,并解释为何微调对于实现可靠且符合模式(schema)的工具使用至关重要。

我们将先设置启用 GPU 的 Kaggle 环境,然后加载并准备数据集和基础 FunctionGemma 模型。

接下来,我们会在微调前进行评估,以建立工具选择与函数调用准确性的基线。随后使用监督训练对 FunctionGemma 进行微调,并在训练后评估其表现,以验证微调是否正确生效。

如果您想通过动手练习加深对微调的理解,建议查看 Fine-Tuning with Llama 3 课程。

什么是 FunctionGemma?

FunctionGemma 是 Google 的 Gemma 3 270M 开源模型的一个专用版本,专为函数调用与工具使用而设计,而非通用对话。

它采用与 Gemma 3 相同的架构,但加入了专门的格式与训练重点,使其能够生成表示函数调用的结构化输出。

FunctionGemma 以基础模型形式发布,开发者可针对特定用例进行微调。其规模与设计使之轻量高效,可部署在资源受限的设备上,如笔记本与边缘硬件。

为何要微调 FunctionGemma?

尽管 FunctionGemma 已针对函数调用进行训练,但这类规模的模型在通过任务特定数据进行微调、实现专门化后,表现更佳。

微调有助于模型学习稳定的模式,以便从一组工具中选择正确的函数,并为真实用例正确格式化相应参数。

这一过程会带来更一致、可预测的结构化输出,从而提高在实际工作流中的可靠性。

微调 FunctionGemma:分步指南

让我们逐步完成微调 FunctionGemma 所需的步骤。

1. 配置 Kaggle 环境

首先访问 Kaggle 并创建一个新笔记本。打开笔记本后,在右侧找到 Session options 面板,将 Accelerator 设置为 GPU (T4 ×2)。

这将为会话启用 GPU 加速,使您可以使用 Kaggle 的免费算力资源对模型进行微调。

Selecting GPU Kaggle session.

启用 GPU 后,新建一个代码单元并运行以下命令安装所需的 Python 依赖。内核可能需要稍等片刻启动,随后将自动安装这些软件包。

%pip -q install -U datasets accelerate trl kagglehub sentencepiece huggingface_hub tqdm evaluate jiwer

接下来,安全地添加您的 Hugging Face 访问令牌。

在顶部菜单中打开 Add-ons → Secrets,然后点击 Add Secret。将密钥名称设为 HUGGINGFACE_TOKEN,并将您的 Hugging Face API Key 粘贴为其值。

使用 Kaggle Secrets 比在笔记本中直接硬编码环境变量或令牌更安全。

Setting the Hugging Face secret in the Kaggle.

密钥保存后,Kaggle 会提供一段代码片段,便于以编程方式访问。

使用以下代码在 Hugging Face Hub 上进行认证:

from kaggle_secrets import UserSecretsClient
from huggingface_hub import login

hf_token = UserSecretsClient().get_secret("HUGGINGFACE_TOKEN")
if not hf_token:
    raise ValueError("HUGGINGFACE_TOKEN not found in Kaggle Secrets.")
login(token=hf_token)

认证成功后,您将能够访问受限模型与数据集,并可将微调后的 FunctionGemma 模型推送到 Hugging Face Hub 以供分享与复用。

2. 加载 Hermes Reasoning Tool-Use 数据集

Hermes Reasoning Tool-Use 数据集是一个开源英文数据集,采用工具使用推理格式,提供结构化示例,用于训练与评估模型在工具选择与类 JSON 函数调用任务上的表现。

其中包含数以万计的工具使用推理样本,包含自然语言提示与工具模式(schema),格式适合用于监督微调。

本教程中,我们将使用该数据集的一个子集,以便更快速地进行实验。

首先,定义随机种子与使用的样本总数。然后将数据划分为训练与评估集:

from datasets import load_dataset

SEED = 40
N_TOTAL = 3000
N_EVAL  = 300
N_TRAIN = N_TOTAL - N_EVAL

接着,加载数据集、打乱顺序,并创建训练与评估子集:

raw = load_dataset("interstellarninja/hermes_reasoning_tool_use", split="train")
raw = raw.shuffle(seed=SEED).select(range(N_TOTAL))
train_ds = raw.select(range(N_TRAIN))
eval_ds  = raw.select(range(N_TRAIN, N_TOTAL))

print("train:", len(train_ds), "eval:", len(eval_ds))

Downloading the dataset from Hugging Face

这样我们得到 2,700 个训练样本和 300 个评估样本,足以演示微调流程,同时保持计算资源可控。

train: 2700 eval: 300

3. 加载 FunctionGemma 模型与处理器

在本教程中,我们使用 KaggleHub 将 FunctionGemma 模型直接加载到 Kaggle 笔记本中。

KaggleHub 会自动解析模型位置并在笔记本环境中提供模型文件,无需手动从 Hugging Face 下载。

import kagglehub
from transformers import AutoProcessor, AutoModelForCausalLM

model_path = kagglehub.model_download("google/functiongemma/transformers/functiongemma-270m-it")

运行上述单元后,模型文件会出现在 Kaggle 笔记本的 Input 区域,表示模型已成功加载且可在本地使用。

Using the Kaggle Hub library to access the functiongemma model.

模型就绪后,我们将 processor(处理器) 与 model(模型) 一并加载到 GPU 内存。

处理器负责聊天模板与工具模式的格式化,模型负责生成结构化的函数调用。

设置 device_map="auto" 可确保模型被放置在可用的 GPU 上,而 dtype="auto" 会选择硬件支持的高效精度。

processor = AutoProcessor.from_pretrained(model_path, device_map="auto")
model = AutoModelForCausalLM.from_pretrained(model_path, dtype="auto", device_map="auto")
tokenizer = processor.tokenizer if hasattr(processor, "tokenizer") else processor

print("dtype:", model.dtype, "| device:", model.device)

这确认了 FunctionGemma 已正确加载、放置于 GPU 上,并已准备好进行评估与微调。

dtype: torch.bfloat16 | device: cuda:1

4. 规范化工具定义并提取黄金函数调用

在微调 FunctionGemma 之前,我们需要标准化工具的表示方式,并可靠地从数据集中提取正确(黄金)的函数调用。

Hermes Reasoning Tool-Use 数据集可能以多种格式存储工具定义,因此此步骤会将所有内容转换为一致的结构,便于进行监督微调。

规范化 tools 字段

我们先规范化 tools 字段。在数据集中,tools 可能以 Python 列表、JSON 字符串出现,或完全缺失。

下面的辅助函数将所有有效表示转换为干净的 Python 列表,并安全地忽略格式错误或空条目。

import re, json

def normalize_tools_field(tools):
    if tools is None:
        return []
    if isinstance(tools, list):
        return tools
    if isinstance(tools, str):
        s = tools.strip()
        if not s:
            return []
        try:
            parsed = json.loads(s)
            return parsed if isinstance(parsed, list) else []
        except Exception:
            return []
    return []

规范化参数类型

接下来规范化参数类型。Hermes 的工具定义常使用非正式类型标注,如 str、int,或如 List[str] 这类容器类型。下面的函数将这些类型转换为与 Hugging Face 函数调用兼容的有效 JSON Schema 类型。

def _parse_hermes_type(t) -> dict:
    if t is None:
        return {"type": "string"}
    if isinstance(t, dict):
        return t if "type" in t else {"type": "object"}
    if isinstance(t, list):
        return {"type": "array"}
    if not isinstance(t, str):
        return {"type": "string"}

    t = t.strip()
    prim = {
        "str": "string", "string": "string",
        "int": "integer", "integer": "integer",
        "float": "number", "number": "number",
        "bool": "boolean", "boolean": "boolean",
        "dict": "object", "object": "object",
        "list": "array", "array": "array",
    }
    if t.lower() in prim:
        return {"type": prim[t.lower()]}

    m = re.match(r"List\[(.+)\]$", t)
    if m:
        return {"type": "array", "items": _parse_hermes_type(m.group(1).strip())}

    m = re.match(r"Dict\[(.+),\s*(.+)\]$", t)
    if m:
        return {"type": "object", "additionalProperties": _parse_hermes_type(m.group(2).strip())}

    return {"type": "string"}

将 Hermes 风格工具转换为 Hugging Face 函数模式

利用这些工具函数,我们将 Hermes 风格的工具定义转换为 Hugging Face 的 {"type": "function", "function": {...}} 模式。该函数支持两种格式:

  • Hermes 按参数粒度的风格(每个参数都有独立的小规格说明)
  • 已接近 JSON Schema 的工具定义

对于按参数粒度的格式,如果未显式提供 required 列表,则将列出的参数视为必填。

def hermes_tools_to_hf_schema(tools_field):
    """
    Handles both:
    - Hermes per-arg style: {"parameters": {"x":{"type":"str"}, ...}}
    - Already-JSON-schema style: {"parameters":{"type":"object","properties":...,"required":[...]}}
    """
    hermes_tools = normalize_tools_field(tools_field)
    out = []

    for tool in hermes_tools:
        if not isinstance(tool, dict):
            continue

        name = tool.get("name")
        desc = tool.get("description", "")
        params = tool.get("parameters", {}) or {}

        # If params already look like JSON schema (best case)
        if isinstance(params, dict) and "type" in params and "properties" in params:
            json_schema_params = params
            if "required" not in json_schema_params:
                json_schema_params["required"] = []
        else:
            props, req = {}, []
            if isinstance(params, dict):
                for p_name, p_spec in params.items():
                    p_spec = p_spec or {}
                    if isinstance(p_spec, dict):
                        p_desc = p_spec.get("description", "")
                        p_type = p_spec.get("type", "str")
                    else:
                        p_desc, p_type = "", "str"

                    frag = dict(_parse_hermes_type(p_type))
                    if p_desc:
                        frag["description"] = p_desc
                    props[p_name] = frag
                    req.append(p_name)

            json_schema_params = {"type": "object", "properties": props, "required": req}

        out.append({
            "type": "function",
            "function": {
                "name": name,
                "description": desc,
                "parameters": json_schema_params,
            }
        })

    return out

辅助函数

我们定义一个小型辅助函数,用于为每个数据集样本获取转换后的工具模式:

def get_tools_hf(ex):
    return hermes_tools_to_hf_schema(ex.get("tools"))

提取黄金函数调用

然后我们从对话中提取黄金工具调用。Hermes 对话可能包含多轮,我们通过解析 <tool_call>{...}</tool_call> 块并提取第一个有效的工具调用对象(名称 + 参数)。这可使监督过程保持简单一致。

TOOL_CALL_RE = re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL)

def extract_first_tool_call_obj(text: str):
    if not text:
        return None
    m = TOOL_CALL_RE.search(text)
    if not m:
        return None
    try:
        obj = json.loads(m.group(1))
        if isinstance(obj, dict) and "name" in obj and "arguments" in obj:
            return obj
    except Exception:
        return None
    return None

将工具调用与用户任务对齐

为使工具调用与正确的用户任务对齐,我们扫描对话历史,找到与数据集 task 匹配的用户消息,然后取其后的助手回复。如果该回复包含工具调用,则将其作为黄金标签返回。

def _role(turn):
    return (turn.get("from") or "").lower().strip()

def get_gold_tool_call_task_aligned(ex):
    task = (ex.get("task") or "").strip()
    conv = ex.get("conversations") or []
    if not conv:
        return None

    idx = None
    if task:
        for i, t in enumerate(conv):
            if _role(t) in ["human", "user"]:
                val = (t.get("value") or "").strip()
                if val == task or task in val or val in task:
                    idx = i
                    break
    if idx is None:
        for i in range(len(conv)-1, -1, -1):
            if _role(conv[i]) in ["human", "user"]:
                idx = i
                break
    if idx is None:
        return None

    for j in range(idx+1, len(conv)):
        if _role(conv[j]) in ["gpt", "assistant", "model"]:
            gold = extract_first_tool_call_obj(conv[j].get("value", ""))
            if gold:
                return gold
        if _role(conv[j]) in ["human", "user"]:
            break
    return None

完成此步骤后,每个可用的数据集样本具备:

  • 一组规范化的工具模式(Hugging Face 格式)
  • 干净、与任务对齐的黄金函数调用
  • 可用于构建监督微调样本的一致结构。

6. 构建用于函数调用的监督微调样本

现在我们需要使用上述辅助函数,将其转换为 监督微调(SFT) 训练样本,格式需完全符合 FunctionGemma 的预期。

目标是创建如下输入内容的示例:

  • 激活函数调用的开发者消息
  • 用户任务
  • 工具声明(模式)

而目标输出是如下格式的单个结构化 FunctionGemma 调用:

<start_function_call>call:TOOL_NAME{args:<escape>{...}<escape>}<end_function_call>

步骤 1:构建干净可用的行

我们从遍历数据集开始,过滤掉无法用于训练的示例。

该函数仅保留以下示例:

  • 存在黄金工具调用
  • 存在有效工具列表
  • 黄金工具名称出现在工具列表中
  • 参数为有效字典
  • 若工具没有必填字段,则将参数强制为 {},以避免嘈杂或不一致的目标
from datasets import Dataset

def build_simple_rows(ds, max_rows=None):
    rows = []
    for ex in ds:
        # 1) task-aligned gold tool call
        gold = get_gold_tool_call_task_aligned(ex)
        if not gold:
            continue

        # 2) tools -> HF schema
        hf_tools = get_tools_hf(ex)
        if not hf_tools:
            continue

        # 3) build required_map: tool_name -> required fields
        required_map = {}
        for t in hf_tools:
            if t.get("type") == "function":
                fn = t.get("function", {})
                name = fn.get("name")
                req = (fn.get("parameters", {}) or {}).get("required", []) or []
                if name:
                    required_map[name] = req

        # 4) guard: gold tool must be in tool list
        tool_names = set(required_map.keys())
        if gold["name"] not in tool_names:
            continue

        # 5) force {} when no required params
        gold_args = gold.get("arguments", {})
        if not isinstance(gold_args, dict):
            gold_args = {}

        req = required_map.get(gold["name"], [])
        if len(req) == 0:
            gold_args = {}  # key fix

        rows.append({
            "user_content": ex.get("task", ""),
            "tool_name": gold["name"],
            "tool_arguments": json.dumps(gold_args, ensure_ascii=False),
            "hf_tools": hf_tools,
        })

        if max_rows and len(rows) >= max_rows:
            break

    return rows

现在生成训练与评估用的行:

simple_train = build_simple_rows(train_ds, max_rows=N_TRAIN)
simple_eval  = build_simple_rows(eval_ds,  max_rows=N_EVAL)
print("usable train:", len(simple_train), "usable eval:", len(simple_eval))

在我们的例子中,最终从最初的 3,000 条中得到 961 个训练样本与 109 个评估样本。这一降幅是预期的,因为过滤条件较为严格。它会移除缺少工具调用、工具定义无效、工具名不匹配或参数格式错误的示例。

usable train: 961 usable eval: 109

清洗步骤至关重要。在应用该步骤前,数据集中存在嘈杂且未对齐的样本,即便微调后也会导致函数调用行为不稳定、结果不佳。过滤至仅保留高质量、任务对齐的示例后,FunctionGemma 的一致性显著提升,微调效果大幅改善。

步骤 2:将每一行转换为单个训练文本样本

接着,我们将每一行格式化为 FunctionGemma 所需的精确「提示 + 目标」结构。我们使用处理器的 apply_chat_template 正确注入工具声明,然后附加黄金函数调用作为目标。

def format_row_as_text(row):
    messages = [
        {"role": "developer", "content": "You are a model that can do function calling with the following functions"},
        {"role": "user", "content": row["user_content"]},
    ]
    prompt = processor.apply_chat_template(
        messages,
        tools=row["hf_tools"],
        add_generation_prompt=True,
        tokenize=False,
    )
    target = (
        f"<start_function_call>call:{row['tool_name']}"
        f"{{args:<escape>{row['tool_arguments']}<escape>}}"
        f"<end_function_call>"
    )
    return prompt + target

现在将这些行转换为 Hugging Face Dataset 对象:

train_text_ds = Dataset.from_list([{"text": format_row_as_text(r)} for r in simple_train])
eval_text_ds  = Dataset.from_list([{"text": format_row_as_text(r)} for r in simple_eval])

步骤 3:确保每个样本均为有效字符串

某些 TRL 版本要求 formatting_func 返回字符串;若任何样本成为列表或非字符串,训练可能会出错。下面的辅助函数会强制 text 始终为干净的字符串。

# ensure text is always a string
def force_text_string(ds):
    def fix(ex):
        t = ex.get("text", "")
        if isinstance(t, list):
            t = "\n".join(map(str, t))
        return {"text": str(t)}
    return ds.map(fix)

train_text_ds = force_text_string(train_text_ds)
eval_text_ds  = force_text_string(eval_text_ds)

最后,打印一个样本以确认格式:

print(train_text_ds[0]["text"])

您应能看到包含工具声明的完整提示,随后是类似如下的黄金函数调用:

<bos><start_of_turn>developer
You are a model that can do function calling with the following functions<start_function_declaration>declaration:get_pollution_levels{description:<escape>Retrieve pollution levels information<escape>,parameters:{properties:{location:{description:<escape>The location for which you want to get the pollution levels (e.g., Beijing, London, New York)<escape>,type:<escape>STRING<escape>},pollutant:{description:<escape>Specify a pollutant for pollution levels (e.g., PM2.5, PM10, ozone)<escape>,type:<escape>STRING<escape>}},required:[<escape>location<escape>],type:<escape>DICT<escape>}}<end_function_declaration><start_function_declaration>declaration:get_water_quality{description:<escape>Retrieve water quality information<escape>,parameters:{properties:{location:{description:<escape>The location for which you want to get the water quality (e.g., river, lake, beach)<escape>,type:<escape>STRING<escape>}},required:[<escape>location<escape>],type:<escape>DICT<escape>}}<end_function_declaration><end_of_turn>
<start_of_turn>user
I'm planning a beach cleanup at Zuma Beach this weekend and need to ensure safety. Can you provide the current water quality and if it's poor, check the pollution level of PM2.5 there?<end_of_turn>
<start_of_turn>model
<start_function_call>call:get_water_quality{args:<escape>{"location": "Zuma Beach"}<escape>}<end_function_call>

7. 微调前评估 FunctionGemma

在训练前,您应当测量基础 FunctionGemma 模型在函数调用任务上的表现。该基线有助于确认微调是否确实提升了工具选择与函数调用格式化能力。

本节我们评估两点:

  • 工具名称准确率:模型是否选择了正确的函数名
  • TC-CER(Tool-Call 字符错误率):生成的函数调用块与黄金调用块的接近程度,越低越好

我们使用两个正则表达式从模型输出中提取预测的工具名与完整的函数调用块,然后与黄金标签进行比较。

import re
import evaluate
import torch
from tqdm.auto import tqdm

cer_metric = evaluate.load("cer")

# Gemma / FunctionGemma-style only
FG_BLOCK_RE = re.compile(r"<start_function_call>.*?<end_function_call>", re.DOTALL)
FG_NAME_RE  = re.compile(r"call:([a-zA-Z0-9_]+)\{", re.DOTALL)

def extract_tool_name(gen: str):
    gen = gen or ""
    m = FG_NAME_RE.search(gen)
    return m.group(1) if m else None

def extract_call_block(gen: str):
    gen = gen or ""
    m = FG_BLOCK_RE.search(gen)
    return m.group(0) if m else ""

def gold_call_block(r):
    return (
        f"<start_function_call>call:{r['tool_name']}"
        f"{{args:<escape>{r['tool_arguments']}<escape>}}"
        f"<end_function_call>"
    )

基线评估循环

下面的函数在评估子集上运行推理并计算两项指标。我们禁用采样 (do_sample=False)以使结果确定,便于在微调前后对比。

@torch.inference_mode()
def eval_tool_and_cer(proc, mdl, rows, n=50, max_new_tokens=128):
    mdl.eval()
    n = min(n, len(rows))
    tool_ok = 0
    preds, refs = [], []

    for i in tqdm(range(n)):
        r = rows[i]
        messages = [
            {"role": "developer", "content": "You are a model that can do function calling with the following functions"},
            {"role": "user", "content": r["user_content"]},
        ]

        inputs = proc.apply_chat_template(
            messages,
            tools=r["hf_tools"],
            add_generation_prompt=True,
            return_dict=True,
            return_tensors="pt",
        )
        inputs = {k: v.to(mdl.device) for k, v in inputs.items()}

        out = mdl.generate(
            **inputs,
            do_sample=False,
            max_new_tokens=max_new_tokens,
            pad_token_id=proc.eos_token_id,
        )

        gen = proc.decode(out[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True).strip()

        if extract_tool_name(gen) == r["tool_name"]:
            tool_ok += 1

        preds.append(extract_call_block(gen))
        refs.append(gold_call_block(r))

    return {
        "n_eval": n,
        "tool_accuracy": tool_ok / n,
        "TC-CER (lower is better)": cer_metric.compute(predictions=preds, references=refs),
    }

运行基线评估:

pre_metrics = eval_tool_and_cer(processor, model, simple_eval, n=50, max_new_tokens=128)
print("PRE metrics:", pre_metrics)

即使在微调前,FunctionGemma 在该任务上的基线表现也相当强劲。

在 50 个评估样本的子集上,模型的工具名称准确率达到 88%,即多数情况下能选对函数。

此外,完整函数调用块的字符错误率(TC-CER)约为 33%,说明尽管经常能选对工具,但生成的参数与格式仍与黄金目标存在偏差。

PRE metrics: {'n_eval': 50, 'tool_accuracy': 0.88, 'TC-CER (lower is better)': 0.33399307273626916}

这些结果表明基础版 FunctionGemma 已理解函数调用的机制。

然而,较高的字符错误率凸显了微调的必要性,以改进参数完整性、格式一致性,以及生成函数调用的整体结构正确性。

查看单条预测

指标有用,但查看定性输出也很重要。下面的辅助函数会对一个示例运行推理,并将预测的函数调用与黄金参考并排打印。

import torch

@torch.inference_mode()
def infer_one(proc, mdl, rows, idx=0, max_new_tokens=128):
    r = rows[idx]

    messages = [
        {"role": "developer", "content": "You are a model that can do function calling with the following functions"},
        {"role": "user", "content": r["user_content"]},
    ]

    inputs = proc.apply_chat_template(
        messages,
        tools=r["hf_tools"],
        add_generation_prompt=True,
        return_dict=True,
        return_tensors="pt",
    )
    inputs = {k: v.to(mdl.device) for k, v in inputs.items()}

    out = mdl.generate(
        **inputs,
        do_sample=False,
        max_new_tokens=max_new_tokens,
        pad_token_id=proc.eos_token_id,
    )

    gen = proc.decode(
        out[0][inputs["input_ids"].shape[-1]:],
        skip_special_tokens=True
    ).strip()

    pred_tool = extract_tool_name(gen)

    return {
        "task": r["user_content"],
        "tool_match": (pred_tool == r["tool_name"]),
        "predicted": extract_call_block(gen),
        "gold": gold_call_block(r),
    }

对一个示例运行:

pre = infer_one(processor, model, simple_eval, idx=15)


print(f"""
TASK:
{pre['task']}

TOOL MATCH: {pre['tool_match']}

--- PREDICTED ---
{pre['predicted']}

--- GOLD ---
{pre['gold']}
""")

输出:

TASK:
I'm reviewing the schedule in the surgical unit. Can you fetch the surgical nursing details for patient ID 12345 undergoing a 'Cardiac Bypass' today? If the procedure is handled by Nurse Ratched, let's record a post-operative care task for this evening.

TOOL MATCH: True

--- PREDICTED ---
<start_function_call>call:get_surgical_nursing_information{nurse_id:<escape>Ratched<escape>,patient_id:<escape>12345<escape>,procedure_type:<escape>Cardiac Bypass<escape>}<end_function_call>

--- GOLD ---
<start_function_call>call:get_surgical_nursing_information{args:<escape>{"patient_id": "12345", "procedure_type": "Cardiac Bypass"}<escape>}<end_function_call>

在此示例中,模型正确选择了合适的函数,因此工具匹配为 True。

但生成的调用包含额外参数,且未遵循训练目标要求的精确结构。

这些参数在语义上虽合理,但与黄金函数调用格式不一致,导致字符错误率更高。这说明微调不仅要改进工具选择,也需提升结构一致性与模式合规性。

8. 配置用于微调的训练器

既然我们已有干净的 SFT 数据集,下一步是配置用于微调 FunctionGemma 的训练器。我们使用 TRL 的 SFTTrainer,这是一种对聊天风格模型进行文本样本微调的轻量、可靠方式。

我们还设置输出目录,使 Kaggle 将微调后的检查点保存至持久位置。

from trl import SFTConfig, SFTTrainer

OUT_DIR = "/kaggle/working/functiongemma-hermes-ft"

为降低训练期间的显存占用,我们启用梯度检查点并禁用 KV 缓存。

# VRAM savings
model.gradient_checkpointing_enable()
model.config.use_cache = False

训练器配置

我们使用 SFTConfig 定义训练配置。这些设置在 Kaggle GPU 上兼顾稳定性与效率,同时保持训练时间较短。

cfg = SFTConfig(
    output_dir=OUT_DIR,
    max_length=512,
    packing=False,
    num_train_epochs=1,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    learning_rate=5e-5,
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    logging_steps=10,
    eval_strategy="steps",
    eval_steps=10,
    report_to="none",
    fp16=(model.dtype == torch.float16),
    bf16=(model.dtype == torch.bfloat16),
    optim="adamw_torch_fused",
)

格式化函数确保每个训练样本以单个字符串返回,这是当前 TRL 版本的要求。

def formatting_func(example):
    # MUST return a STRING (not list) for your TRL version
    t = example.get("text", "")
    if isinstance(t, list):
        t = "\n".join(map(str, t))
    return str(t)

初始化训练器

最后,我们用模型、配置、数据集与分词器初始化 SFTTrainer。

trainer = SFTTrainer(
    model=model,
    args=cfg,
    train_dataset=train_text_ds,
    eval_dataset=eval_text_ds,
    processing_class=tokenizer,
    formatting_func=formatting_func,
)

9. 微调 FunctionGemma

训练器配置完成后,即可开始微调模型。该步骤会在准备好的数据集上进行监督训练,并在训练期间定期评估模型。

trainer.train()
trainer.save_model(OUT_DIR)
processor.save_pretrained(OUT_DIR)
print("Saved to:", OUT_DIR)

训练完成后,我们将微调后的模型与处理器保存到输出目录,以便后续推理或上传到 Hugging Face Hub。

Model training and validation loss

训练过程中,训练与验证损失均稳步下降。平均 token 准确率也随时间提升,表明模型已学会更准确、一致地生成函数调用输出。

将微调后的 FunctionGemma 发布到 Hugging Face

微调完成后,我们可以发布模型,方便复用、分享或部署。将模型推送到 Hugging Face Hub 后,他人即可使用标准 Transformers API 直接加载。

HF_REPO_ID = "kingabzpro/functiongemma-hermes-3k-ft"
model.push_to_hub(HF_REPO_ID)
processor.push_to_hub(HF_REPO_ID)
print("Pushed to:", HF_REPO_ID)

上传完成后,微调模型将公开可用:

Pushed to: kingabzpro/functiongemma-hermes-3k-ft

Fine-tuned model published on the Hugging Face Hub.

来源: kingabzpro/functiongemma-hermes-3k-ft · Hugging Face

微调后评估与误差分析

微调完成后,我们从磁盘重新加载保存的模型与处理器,并运行与训练前相同的评估流程。这样可确保微调前后对比公平、一致。

from transformers import AutoProcessor, AutoModelForCausalLM

ft_processor = AutoProcessor.from_pretrained(OUT_DIR, device_map="auto")
ft_model = AutoModelForCausalLM.from_pretrained(OUT_DIR, dtype="auto", device_map="auto")

随后,我们在评估集上用工具名称准确率与 TC-CER 对微调模型进行评估。

post_metrics = eval_tool_and_cer(ft_processor, ft_model, simple_eval, n=50, max_new_tokens=64)
print("POST metrics:", post_metrics)

结果显示相较基线有明显提升:

POST metrics: {'n_eval': 50, 'tool_accuracy': 0.98, 'TC-CER (lower is better)': 0.1454725383473528}

工具名称准确率从 88% 提升至 98%,完整函数调用块的字符错误率降低超过一半。这表明模型不仅更可靠地选择正确工具,还能生成更接近目标模式的输出。

为更好理解改进点,我们查看单条预测。下面的示例中,模型既选对了函数,也生成了结构良好、参数更完整的调用。

post = infer_one(ft_processor, ft_model, simple_eval, idx=15)

print(f"""
TASK:
{post['task']}

TOOL MATCH: {pre['tool_match']}

--- PREDICTED ---
{post['predicted']}

--- GOLD ---
{post['gold']}
""")

尽管预测可能包含超出黄金目标的额外参数,但总体结构与参数格式较微调前明显更一致。这反映出更好的模式感知与更强的用户意图—函数调用对齐能力。

TASK:
I'm reviewing the schedule in the surgical unit. Can you fetch the surgical nursing details for patient ID 12345 undergoing a 'Cardiac Bypass' today? If the procedure is handled by Nurse Ratched, let's record a post-operative care task for this evening.

TOOL MATCH: True

--- PREDICTED ---
<start_function_call>call:get_surgical_nursing_information{args:<escape>{"patient_id": "12345", "procedure_type": "Cardiac Bypass", "nurse_id": "Ratched", "task": "Post-operative care"}<escape>}<end_function_call>

--- GOLD ---
<start_function_call>call:get_surgical_nursing_information{args:<escape>{"patient_id": "12345", "procedure_type": "Cardiac Bypass"}<escape>}<end_function_call>

我们再看另一个样本。

post = infer_one(ft_processor, ft_model, simple_eval, idx=25)

print(f"""
TASK:
{post['task']}

TOOL MATCH: {pre['tool_match']}

--- PREDICTED ---
{post['predicted']}

--- GOLD ---
{post['gold']}
""")

第二个示例展示了一个干净、正确的 URL 提取任务函数调用,说明微调后的模型能在不同工具间良好泛化。

TASK:
I would like to extract details from a LinkedIn company page. Could you assist me in fetching the information from this URL: https://www.linkedin.com/company/abc-corporation?

TOOL MATCH: True

--- PREDICTED ---
<start_function_call>call:extract{args:<escape>{"url": "https://www.linkedin.com/company/abc-corporation"}<escape>}<end_function_call>

--- GOLD ---
<start_function_call>call:extract{args:<escape>{"url": "https://www.linkedin.com/company/abc-corporation", "html": true}<escape>}<end_function_call>

总体而言,结果证实微调成功提升了函数调用任务的准确性与结构正确性。

本教程的完整 Kaggle 笔记本见:https://www.kaggle.com/code/kingabzpro/finetuning-functiongemma

如遇问题,您可克隆笔记本、添加 Hugging Face 密钥并端到端运行。

结语

如果您尝试用通用文本生成数据集来训练 FunctionGemma,最好尽早停止。FunctionGemma 并非为通用型语言模型行为而设计。

对于开放式文本生成,Gemma 3 模型家族是更佳选择。

FunctionGemma 为函数调用而生,其目标是选择正确工具、生成符合模式的参数,并与 API、数据库与服务等外部系统可靠交互。

FunctionGemma 的小尺寸是刻意设计之举。其 2.7 亿参数针对低时延、资源高效的部署进行了优化,包括本地机器、边缘设备与私有基础设施。这使其十分适合实时函数调用与智能体工作流,在这些场景中,正确性与结构比流畅表述更重要。

在本教程中,我们学习了如何在 Hermes Reasoning Tool-Use 数据集上微调 FunctionGemma。一个关键结论是:数据准备与评估比生硬堆叠训练时长更重要。

通过认真清洗数据集、将任务与黄金工具调用对齐,并严格执行模式一致性,我们仅用相对较少的高质量样本就能显著改善模型行为。

结果清晰表明微调的影响:工具名称准确率从 88% 提高至 98%,完整函数调用块的字符错误率降低超过一半。更重要的是,模型在生成结构良好、可预测且贴近真实使用的函数调用方面,一致性显著提升。

FunctionGemma 常见问题

FunctionGemma 是什么?它与 Gemma 3 有何不同?

FunctionGemma 是 Gemma 3 270M 开源模型的专用版本。标准 Gemma 3 面向文本生成,而 FunctionGemma 明确训练用于将自然语言翻译为结构化函数调用(API 请求),更像是面向智能体工作流的专用“路由器”,而非对话聊天机器人。

hat hardware do I need to run FunctionGemma?

由于其是一个 2.7 亿参数 的模型,FunctionGemma 极其轻量(量化后约 ~300MB)。它可在笔记本电脑、手机、嵌入式硬件(如 NVIDIA Jetson Nano)等边缘设备上高效运行,而无需专用云端 GPU。

如果 FunctionGemma 已经是专用模型,为什么还需要微调?

虽然基础模型理解工具使用的概念,但仍需通过微调来教授其您的特定 API 模式与业务逻辑。Google 的基准测试显示,在领域特定任务上,微调可将函数调用准确率从约 58%(基础)提升至超过 85%。

FunctionGemma 能处理多轮对话吗?

FunctionGemma 主要针对单轮指令执行(用户指令 → 工具调用)进行优化。对于复杂的多轮推理或维护长对话历史,最佳用法是将其作为“交通指挥员”,将困难问题委派给更大的模型(如 Gemma 3 27B),而在本地处理简单操作。

Abid Ali Awan

作者

Abid Ali Awan

作为一名持证的数据科学家,我热衷于利用前沿技术打造创新的机器学习应用。凭借在语音识别、数据分析与报告、MLOps、对话式人工智能以及自然语言处理方面的扎实背景,我不断打磨构建智能系统的能力,力求带来切实影响。除技术专长外,我也擅长沟通,能够将复杂概念提炼为清晰、简明的表述。因此,我成为数据科学领域备受关注的博主,与不断壮大的数据专业人士社区分享洞见与实践经验。目前,我专注于内容创作与编辑,借助大语言模型打造有力且吸引人的内容,帮助企业与个人更好地发挥数据价值。

主题