HuggingFace 模型转为 Ollama 格式后工具调用失效如何处理?
问题分析
将 HuggingFace 格式的模型转换为 Ollama 支持的 GGUF 格式后,开发者常发现模型的工具调用能力消失。原本能正确输出 JSON 格式函数调用的模型,转换后只生成自然语言描述而非结构化输出。
问题根源在于 GGUF 格式转换过程中,模型配置文件的关键字段丢失。HuggingFace 模型的 tokenizer_config.json 中包含 chat_template 字段,定义了模型期望的对话格式。对于支持 Function Calling 的模型,这个模板包含特殊的工具调用标签。
另一个关键配置是 tokenizer.json 中的特殊 token 定义。工具调用模型通常定义了专用的开始/结束 token,如果转换过程中这些 token 映射丢失,模型无法正确识别工具调用的边界。
即使模型权重正确转换,Ollama 的 Modelfile 配置也影响工具调用行为。如果不指定正确的 template 参数,Ollama 会使用默认模板,可能与原模型不兼容。
解决原理
解决工具调用失效需要确保三个层面的配置正确:
第一层:转换脚本配置
使用 convert-hf-to-gguf.py 时,需要确保正确识别模型的架构和 tokenizer 类型。对于 Llama-3 派生模型,需要指定正确的模型类型。
第二层:GGUF 元数据验证
转换后,使用 gguf-dump 工具检查生成的 GGUF 文件元数据。关键字段包括 tokenizer.chat_template、tokenizer.ggml.added_tokens_json 等。
第三层:Ollama Modelfile 配置
创建 Modelfile 时,需要显式指定 TEMPLATE(对话模板)、SYSTEM(系统提示词)和 PARAMETER stop(停止序列)。
程序实现与说明
"""
HuggingFace 模型转 Ollama GGUF 格式并保持工具调用能力
"""
import json
import subprocess
from pathlib import Path
from typing import Dict, List, Optional
class HFToOllamaConverter:
"""HF 模型转 Ollama 格式,保持工具调用能力"""
def __init__(self, hf_model_path: str, output_dir: str):
self.hf_model_path = Path(hf_model_path)
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
# 加载原始配置
self.tokenizer_config = self._load_json("tokenizer_config.json")
self.config = self._load_json("config.json")
def _load_json(self, filename: str) -> dict:
"""加载 HuggingFace 配置文件"""
filepath = self.hf_model_path / filename
if filepath.exists():
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
def get_chat_template(self) -> str:
"""提取对话模板"""
# 从配置中获取原始模板
chat_template = self.tokenizer_config.get('chat_template', '')
# 如果没有模板,根据模型类型推断
if not chat_template:
model_type = self.config.get('model_type', '').lower()
# Llama-3 系列模板
if 'llama' in model_type:
chat_template = """{% for message in messages %}
<|start_header_id|>{{ message['role'] }}<|end_header_id|>
{{ message['content'] }}<|eot_id|>
{% endfor %}
{% if add_generation_prompt %}
<|start_header_id|>assistant<|end_header_id|>
{% endif %}"""
# Qwen 系列模板
elif 'qwen' in model_type:
chat_template = """{% for message in messages %}
<|im_start|>{{ message['role'] }}
{{ message['content'] }}<|im_end|>
{% endfor %}
{% if add_generation_prompt %}
<|im_start|>assistant
{% endif %}"""
return chat_template
def build_modelfile(self, gguf_path: Path) -> str:
"""
构建 Modelfile
关键:正确配置模板和停止序列
"""
# 获取对话模板
template = self.get_chat_template()
# 系统提示词(定义工具使用规则)
system_prompt = """你是一个有帮助的助手,可以使用以下工具。
当需要调用工具时,请输出 JSON 格式:
{"name": "函数名", "arguments": {"参数": "值"}}
可用工具:
- get_weather(location): 获取指定地点的天气
- calculate(expression): 计算数学表达式
- search_web(query): 搜索互联网
只在必要时调用工具,其他时候用自然语言回答。"""
# 构建 Modelfile
modelfile = f"""# Modelfile - 转换自 {self.hf_model_path.name}
# 保持工具调用能力
FROM {gguf_path.name}
# 设置参数
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER num_ctx 4096
# 停止序列(关键:防止工具调用后继续生成)
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop ""
PARAMETER stop "<|im_end|>"
# 对话模板(使用 Jinja2 格式)
TEMPLATE \"\"\"{template}\"\"\"
# 系统提示词
SYSTEM \"\"\"{system_prompt}\"\"\"
"""
return modelfile
def convert(self, quantization: str = "q4_k_m") -> Path:
"""
执行完整的转换流程
"""
# 假设 GGUF 文件已通过 llama.cpp 转换
gguf_file = self.output_dir / f"{self.hf_model_path.name}-{quantization}.gguf"
# 生成 Modelfile
modelfile_content = self.build_modelfile(gguf_file)
modelfile_path = self.output_dir / "Modelfile"
with open(modelfile_path, 'w', encoding='utf-8') as f:
f.write(modelfile_content)
print(f"[成功] Modelfile 已生成: {modelfile_path}")
print(f"[提示] 执行以下命令创建模型:")
print(f" ollama create my-model -f {modelfile_path}")
return modelfile_path
def test_tool_calling(model: str):
"""
测试工具调用是否正常工作
"""
import ollama
# 定义工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称"
}
},
"required": ["location"]
}
}
}
]
# 发送带工具的请求
response = ollama.chat(
model=model,
messages=[
{"role": "user", "content": "北京今天天气怎么样?"}
],
tools=tools
)
# 检查响应
message = response.get('message', {})
if 'tool_calls' in message:
print("[成功] 模型正确调用了工具:")
for tool_call in message['tool_calls']:
print(f" 函数: {tool_call['function']['name']}")
print(f" 参数: {tool_call['function']['arguments']}")
else:
print("[失败] 模型未调用工具,返回内容:")
print(f" {message.get('content', '')}")
if __name__ == "__main__":
# 使用示例
converter = HFToOllamaConverter(
hf_model_path=r"D:\models\Llama-3-8B-Instruct",
output_dir=r"D:\models\ollama_models"
)
# 执行转换
modelfile = converter.convert(quantization="q4_k_m")
# 创建模型后测试
# test_tool_calling("my-model")
关键代码行解析:
chat_template = self.tokenizer_config.get('chat_template', ''):从配置中提取原始对话模板,这是工具调用能力的关键载体。
PARAMETER stop "<|eot_id|>":设置停止序列,确保模型在生成工具调用 JSON 后正确停止,而非继续生成无关内容。
if 'tool_calls' in message:验证工具调用成功的核心检查。成功时,响应包含结构化的函数调用信息。
修复工具调用失效的检查清单:
- 检查 tokenizer_config.json 是否包含 chat_template
- 验证 GGUF 文件元数据中的 tokenizer.chat_template 字段
- Modelfile 中设置正确的停止序列
- 系统提示词定义工具使用规则
- 使用 ollama show model 命令查看实际加载的模板