# 运行微调后的模型：打分、合并，再导入 Ollama

> 要运行微调好的 LoRA 模型，先在留出的测试文件上，拿适配器和基础模型对比打分。再用 merge_and_unload 把它合并进基础权重，用一行 Modelfile 把文件夹导入 Ollama。想要更小的下载文件，就转成量化后的 GGUF 文件。

Mindket 编辑部 · 发布于 2026-11-06 · 更新于 2026-11-06 · https://mindket.com/cn/learn/fine-tuning/run-a-fine-tuned-model

## 要点

- 转换任何东西之前，先在 `test.jsonl` 上拿适配器和基础模型对比打分。
- `merge_and_unload` 返回合并后的模型，所以要接住它的返回值再保存。
- Ollama 用一行 `FROM` 导入 Safetensors 文件夹，导入 GGUF 文件时不会替你量化。
- 要给最终发布的文件打分，因为量化会让答案略有变化。

## 开始之前

你需要第 3 课的 `ticket-title-lora` 文件夹、第 2 课的 `test.jsonl`，以及基础模型的名字。还要装好 Ollama，做法见 [Ollama 教程](https://mindket.com/cn/learn/local-ai/ollama-tutorial)。下面的 Python 用 `pip install peft transformers accelerate` 就够了。走 GGUF 这条路，还需要克隆一份 llama.cpp。

## 拿适配器和基础模型对比打分

OpenAI 的[微调指南](https://developers.openai.com/api/docs/guides/supervised-fine-tuning)说，要先建好评估，再投入微调，因为你需要一个可靠的办法，来判断微调后的模型是否胜过基础模型。`test.jsonl` 就是为此留的。下面的脚本是本课自己写的，它让两个模型在每条留出的提示词上各答一遍，并把答案打印出来。

Qwen3 系列模型默认会先思考再回答，所以脚本用[模型卡](https://huggingface.co/Qwen/Qwen3-0.6B)上的开关把它关掉。它唯一的自动检查是工单标题的规则，最多 12 个词。换成你在第 1 课写下的那些行为行。

````python
import json
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

name = "Qwen/Qwen3-0.6B"
tokenizer = AutoTokenizer.from_pretrained(name)
base = AutoModelForCausalLM.from_pretrained(name, device_map="auto")
tuned = PeftModel.from_pretrained(
    AutoModelForCausalLM.from_pretrained(name, device_map="auto"), "ticket-title-lora"
)

def answer(model, messages):
    text = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
    )
    inputs = tokenizer([text], return_tensors="pt").to(model.device)
    out = model.generate(**inputs, max_new_tokens=40, do_sample=False)
    return tokenizer.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()

rows = [json.loads(line) for line in open("test.jsonl", encoding="utf-8")]
passed = {"base": 0, "tuned": 0}
for row in rows:
    prompt = row["messages"][:-1]  # everything except the reference answer
    for label, model in (("base", base), ("tuned", tuned)):
        title = answer(model, prompt)
        if len(title.split()) <= 12:
            passed[label] += 1
        print(label, "|", title)
print(passed, "of", len(rows), "answers within 12 words")
````

1. 运行它，把每一对答案都读一遍。脚本能数词数，但标题好不好，只有读的人才分得出来。
2. 比较两个计数。适配器没有胜过基础模型，就回到第 2 课去改数据，而不是多加轮数。
3. 数不出来的质量，加一个评判。[LLM 评判课](https://mindket.com/cn/learn/ai-evals/llm-as-a-judge)讲了做法。

## 检查它忘了什么

窄任务的适配器仍可能损失一些通用能力。EleutherAI 的 [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) 能跑 60 多个标准学术基准，在它的模型参数里加上 `peft=PATH` 就能加载适配器。同一个任务，先跑基础模型，再跑适配器，然后比较。

2024 年的研究 [LoRA Learns Less and Forgets Less](https://huggingface.co/papers/2405.09673)用 Llama-2-7B 在代码和数学上发现，比起全量微调，LoRA 更好地保住了基础模型在目标领域之外的表现。所以这里出现大幅下降，就值得查一查。任务名 `hellaswag` 是 README 自己的示例。

````bash
pip install "lm_eval[hf]"
lm_eval --model hf --model_args pretrained=Qwen/Qwen3-0.6B --tasks hellaswag --device cuda:0 --batch_size 8
lm_eval --model hf --model_args pretrained=Qwen/Qwen3-0.6B,peft=ticket-title-lora --tasks hellaswag --device cuda:0 --batch_size 8
````

## 把适配器合并进模型

Ollama 的导入指南和 llama.cpp 的量化 README 都从一个完整的模型文件夹开始，所以本课先合并适配器。llama.cpp 也能用 `--lora` 在基础模型旁边加载一个 LoRA 适配器，它的[服务器 README](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md)列了这个选项，但本课不走这条路。

2021 年的 [LoRA 论文](https://huggingface.co/papers/2106.09685)指出这种方法不增加推理延迟，合并后的模型正是你拿到这一点的办法。在 [PEFT](https://huggingface.co/docs/peft/developer_guides/lora) 里，`merge_and_unload()` 不会原地修改，所以要接住它返回的模型，像脚本那样。它也不会把适配器权重留在内存里，所以以后可能要取消合并的话，改用 `merge_adapter()`。

脚本加载基础模型时没有做任何量化，并把分词器存在权重旁边，让文件夹自成一体。

````python
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

name = "Qwen/Qwen3-0.6B"
base = AutoModelForCausalLM.from_pretrained(name)
model = PeftModel.from_pretrained(base, "ticket-title-lora")

merged = model.merge_and_unload()
merged.save_pretrained("ticket-title-merged")
AutoTokenizer.from_pretrained(name).save_pretrained("ticket-title-merged")
````

## 把文件夹导入 Ollama

Ollama 的[导入指南](https://github.com/ollama/ollama/blob/main/docs/import.mdx)从一个 Modelfile 开始，它的 `FROM` 一行指向装着你的 Safetensors 权重的目录。把 Modelfile 放进 `ticket-title-merged`，写上 `FROM .`，指南里就是这样给这种情况的。它的 [Modelfile 参考](https://github.com/ollama/ollama/blob/main/docs/modelfile.mdx)补充说，目录里必须是受支持架构的 Safetensors 权重，所以先确认你的模型符合。

`FROM` 后面的两行是本课自己写的。它们重复训练时的系统提示词，让模型看到它学过的那段文字，并把温度设低。Modelfile 的其余部分，[Ollama API 课](https://mindket.com/cn/learn/local-ai/ollama-api)里有讲。

````text
FROM .
SYSTEM Write a ticket title of at most 12 words.
PARAMETER temperature 0.2
````

1. 运行 `cd ticket-title-merged`，再运行 `ollama create ticket-titles`。
2. 运行 `ollama run ticket-titles`，贴进 `test.jsonl` 里的一张工单。
3. 答案和 Python 那次不同，就比较提示词格式。Modelfile 参考里有专门的 `TEMPLATE` 指令。

## 用 GGUF 得到更小的文件

[llama.cpp 量化 README](https://github.com/ggml-org/llama.cpp/blob/master/tools/quantize/README.md)描述了两步。先把 Hugging Face 文件夹转成高精度格式的 GGUF 文件，再对它量化。Ollama 的导入指南说它导入 GGUF 文件时不做量化，所以这一步要先做。运行最后一条命令之前，按 llama.cpp 的 [README](https://github.com/ggml-org/llama.cpp)所述先把它构建出来。

README 里 Llama-3.1-8B 的表格展示了其中的取舍。它列出的文本生成速度是 F16 约每秒 29 个 token，Q8_0 约 51 个，Q4_K_M 约 72 个，但没有说明是用什么硬件测的，所以只把它们当作格式之间的对比。量化还可能损失一些准确度，README 说这种损失通常用困惑度和 KL 散度来衡量。它的表格列的是大小和速度，没有这项损失。

````bash
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
python3 -m pip install -r requirements.txt
python convert_hf_to_gguf.py ../ticket-title-merged --outfile ticket-titles-bf16.gguf --outtype bf16
./build/bin/llama-quantize ticket-titles-bf16.gguf ticket-titles-Q4_K_M.gguf Q4_K_M
````

1. 用 `FROM ./ticket-titles-Q4_K_M.gguf` 让 Modelfile 指向量化后的文件，再运行 `ollama create ticket-titles-q4`。
2. 通过 Ollama 的 API 把测试提示词再发给这个模型，把它的答案和全精度那次的比较。

**llama.cpp README 里 Llama-3.1-8B 各格式的文件大小** 大小，单位 GiB。金色的 Q4_K_M 是 README 自己的示例里用的格式。

- F16: 14.96 GiB
- Q8_0: 7.95 GiB
- Q6_K: 6.14 GiB
- Q4_K_M: **4.58 GiB**

来源：[GitHub: llama.cpp quantize README](https://github.com/ggml-org/llama.cpp/blob/master/tools/quantize/README.md) (2026-11-06 查阅)

## 其他发布方式

推送出去的模型带着它从你的样本里学到的东西。分享成果之前，先查一下那些数据的许可和隐私。

| 途径 | 要运行的命令 | 适合 |
| --- | --- | --- |
| Safetensors 文件夹 | 合并后的文件夹里，Modelfile 写 `FROM .` | 最短的路，不涉及 llama.cpp。 |
| llama.cpp GGUF | `convert_hf_to_gguf.py`，再 `llama-quantize` | 更小的量化文件，Ollama 用 `FROM` 导入。 |
| Unsloth 导出 | `model.save_pretrained_gguf("directory", tokenizer, quantization_method="q4_k_m")` | 用 Unsloth 训练的模型。TRL 说它在这一步用的是 llama.cpp。 |
| Hugging Face Hub | `ollama run hf.co/{username}/{repository}` | 分享 GGUF 仓库。`:Q8_0` 这样的标签选择量化方式，有 Q4_K_M 时它是默认。 |
| ollama.com | `ollama cp mymodel myuser/mymodel`，再 `ollama push myuser/mymodel` | 一个公开的模型名。需要 ollama.com 账号和你的 Ollama 公钥。 |

## 常见错误

- 调用了 `merge_and_unload()`，却保存原来的对象。合并后的模型是它的返回值。
- 只保存权重，不保存分词器，文件夹没法自成一体。
- 指望 Ollama 导入 GGUF 文件时替你量化。它不会。
- 给全精度模型打了分，却发布量化后的文件。要给你发布的东西打分。
- 训练用了系统提示词，Modelfile 里却没写。
- 凭一条漂亮的答案下结论。每一行测试数据都要读。

## 自己试一试

- 运行打分脚本，记下基础模型和微调后模型各自的通过数。
- 合并适配器，导入 Ollama，用 `test.jsonl` 里的三张工单问它。
- 量化成 Q4_K_M，把文件大小和合并后的文件夹比一比。
- 把同样的测试提示词发给量化后的模型，写下答案有变化的那些。

## 下一门课：MCP

现在你有了一个用自己的数据训练、在自己机器上运行的模型。[MCP 课](https://mindket.com/cn/learn/mcp/build-mcp-server-python)从用 Python 构建服务器开始，助手就是靠它接触文件、数据库和工具的。

## 自测一下

1. 转换之前，为什么要先拿适配器和基础模型对比打分？

   A. 为了弄清微调有没有用，在它没训练过的行上看
   B. 为了让 GGUF 文件更小
   C. 因为 Ollama 拒绝没打过分的模型
   D. 为了调低学习率

   答案：A。 OpenAI 说要先建好评估，这样才能判断微调后的模型是否胜过基础模型。

2. PEFT 的 merge_and_unload() 做什么？

   A. 原地修改适配器，什么也不返回
   B. 把基础模型从磁盘上删掉
   C. 返回合并后的模型，你接住结果再保存
   D. 把适配器上传到 Hub

   答案：C。 它不会原地修改，合并后的模型是它的返回值。

3. Ollama 的导入指南说它不量化 GGUF 文件。想得到 Q4_K_M 文件该怎么办？

   A. 在 Modelfile 里加一行量化设置
   B. 先运行 ollama push
   C. 什么都不用做，Ollama 会自己选 Q4_K_M
   D. 先用 llama-quantize 之类的工具量化

   答案：D。 指南让你先准备并量化好 GGUF 文件，例如用 llama.cpp 的 llama-quantize。

4. Modelfile 放在 Safetensors 文件夹里面时，哪一行能导入这个文件夹？

   A. FROM /path/to/file.gguf
   B. FROM .
   C. ADAPTER .
   D. IMPORT safetensors

   答案：B。 Ollama 的指南给出，Modelfile 和权重在同一目录时，写 FROM .。

## 常见问题

### 一定要合并 LoRA 适配器吗？

不一定。PEFT 能在你自己的 Python 里把适配器加载到基础模型上，lm-evaluation-harness 用 `peft=` 也一样。本课选择合并，是因为 Ollama 的导入指南讲的是完整的模型，不是适配器。llama.cpp 按它的服务器 README，也能用 `--lora` 加载适配器。

### GGUF 该选哪种量化？

Q4_K_M 是 llama.cpp README 示例里用的格式，也是 Hugging Face 的 Ollama 集成在它存在时默认选的。Q8_0 更大，也更接近原模型。要在你最终发布的文件上看你自己的测试分数。

### 不用 Ollama 能运行这个模型吗？

能。llama.cpp 可以直接运行 GGUF 文件，比如 `llama cli -m ./your-model.gguf`，它的量化 README 对自己的文件就是这样演示的。

### 为什么模型在 Ollama 里的答案和 Python 里不一样？

Python 那次检查是贪心解码，而 Ollama 按它 Modelfile 参考里说的，默认以 0.8 的温度采样。上面的 Modelfile 把它降到了 0.2。提示词格式是另一个要比较的地方。

### 我自己的应用能调用这个模型吗？

能。Ollama 在 `http://localhost:11434/api` 提供它的模型，OpenAI SDK 也能对着 `/v1` 使用，所以你的应用可以用你在 `ollama create` 里起的名字。Ollama API 课两种都讲了。

## 资料来源

- [OpenAI: Supervised fine-tuning](https://developers.openai.com/api/docs/guides/supervised-fine-tuning) (2026-11-06 查阅)
- [Hugging Face PEFT: LoRA](https://huggingface.co/docs/peft/developer_guides/lora) (2026-11-06 查阅)
- [Hugging Face: Qwen/Qwen3-0.6B model card](https://huggingface.co/Qwen/Qwen3-0.6B) (2026-11-06 查阅)
- [GitHub: EleutherAI/lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) (2026-11-06 查阅)
- [GitHub: Ollama docs: Importing a model](https://github.com/ollama/ollama/blob/main/docs/import.mdx) (2026-11-06 查阅)
- [GitHub: Ollama docs: Modelfile reference](https://github.com/ollama/ollama/blob/main/docs/modelfile.mdx) (2026-11-06 查阅)
- [GitHub: llama.cpp quantize README](https://github.com/ggml-org/llama.cpp/blob/master/tools/quantize/README.md) (2026-11-06 查阅)
- [GitHub: ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp) (2026-11-06 查阅)
- [GitHub: llama.cpp server README](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) (2026-11-06 查阅)
- [Hugging Face TRL: Unsloth Integration](https://huggingface.co/docs/trl/unsloth_integration) (2026-11-06 查阅)
- [Hugging Face Hub: Use Ollama with any GGUF model](https://huggingface.co/docs/hub/ollama) (2026-11-06 查阅)
- [Hugging Face Papers: LoRA Learns Less and Forgets Less](https://huggingface.co/papers/2405.09673) (2026-11-06 查阅)
- [Hugging Face Papers: LoRA: Low-Rank Adaptation of Large Language Models](https://huggingface.co/papers/2106.09685) (2026-11-06 查阅)

[浏览自动化](https://mindket.com/cn/automations)
