使用LLaMA-Factory微调大模型的function calling能力

发布时间:2025-08-10 10:29:49编辑:Run阅读(179)

    引言:

    Agent(智能体)是当今 LLM(大模型)应用的热门话题[1],通过任务分解(task planning)、工具调用(tool using)和多智能体协作(multi-agent cooperation)等途径,LLM Agent 有望突破传统语言模型能力界限,体现出更强的智能水平。在这之中,调用外部工具解决问题成为 LLM Agent 必不可缺的一项技能,模型根据用户问题从工具列表中选择恰当的工具,同时生成工具调用参数,综合工具返回结果和上下文信息总结出答案。通过调用外部工具,LLM 能够获取到实时、准确的知识,大大降低了生成中的幻觉(hallucination)现象,使 LLM 的任务解决能力得到长足的提升。工具调用能力的获得离不开模型微调,尽管使用 ReAct 提示或其他预训练模型也能实现类似效果,但对于定制化或更加广泛的工具,对模型做进一步微调能有效地提升工具使用能力。本文将会使用 LLaMA Factory 的 Agent Tuning 功能训练出自己专属的 LLM Agent。

    如何使用大模型的函数调用(function calling)功能,这样就能让大模型调用成千上万的工具API,赋予大模型更多的外部知识,使得大模型能力变得更加强大。


    训练框架:

    LLaMA Factory 是一个涵盖预训练、指令微调到 RLHF 阶段的开源全栈大模型微调框架,具备高效、易用、可扩展的优点,配备有零代码可视化的一站式网页微调界面 LLaMA Board。


    模型与数据:

    基座模型选择的是:通义千问3-4B, 数据集可以在HuggingFace找到function calling相关的数据集,盖数据集包含用户(human)、模型(gpt)、工具调用(function_call)和工具调用结果(observation)四种不同角色,以及工具列表(tools)字段。下面是数据集中的一个样本示例:

    {
      "conversations": [
        {
          "from": "human",
          "value": "I saw a dress that I liked. It was originally priced at $200 but it's on sale for 20% off. Can you tell me how much it will cost after the discount?"
        },
        {
          "from": "function_call",
          "value": "{\"name\": \"calculate_discount\", \"arguments\": {\"original_price\": 200, \"discount_percentage\": 20}}"
        },
        {
          "from": "observation",
          "value": "{\"discounted_price\": 160}"
        },
        {
          "from": "gpt",
          "value": "The dress will cost you $160 after the 20% discount."
        }
      ],
      "tools": "[{\"name\": \"calculate_discount\", \"description\": \"Calculate the discounted price\", \"parameters\": {\"type\": \"object\", \"properties\": {\"original_price\": {\"type\": \"number\", \"description\": \"The original price of the item\"}, \"discount_percentage\": {\"type\": \"number\", \"description\": \"The percentage of discount\"}}, \"required\": [\"original_price\", \"discount_percentage\"]}}]"
    }

    模型下载:git clone https://www.modelscope.cn/Qwen/Qwen3-4B.git

    image.png

    数据集下载:

    https://huggingface.co/datasets/llamafactory/glaive_toolcall_zh

    https://huggingface.co/datasets/llamafactory/alpaca_gpt4_zh

    https://huggingface.co/datasets/SDUIRLab/fuzi-mingcha-v1_0-data/blob/main/oaast_sft_zh.json

    image.png


    环境准备:

    LLaMA Factory的环境搭建参考文章:

    LLaMA Factory本地部署https://py3study.com/Article/details/id/20107.html


    训练流程:

    1 启动LLaMA-Factory webui,执行命令:llamafactory-cli webui

    image.png

    2 打开浏览器,在地址栏输入 http://127.0.0.1:7860/  进入 LLaMA Board

    image.png


    3 输入本地下载模型的路径,选择模型

    image.png

    4 选择加速方式:点击高级设置-加速方式提升训练速度,其中 Flash Attention-2[9]可提升至 120% 左右的速度,Unsloth可提升至 170% 左右的速度。

    image.png

    5 点击数据集,选择此次要使用的三个数据集 glaive_toolcall、alpaca_gpt4_zh 和 oaast_sft_zh,如果数据集下拉框为空白,请检查数据路径是否正确。选择后点击预览数据集按钮可预览数据集.

    创建数据集配置文件,内容如下:

    vim /home/sam_admin/LLaMA-Factory/web_dataset/dataset_info.json

    {
    "glaive_toolcall_zh_1k": {
      "file_name": "glaive_toolcall_zh_1k.json",
      "formatting": "sharegpt",
      "columns": {
      "messages": "conversations",
         "tools": "tools"
       }
     },
    "alpaca_gpt4_data_zh": {
       "file_name": "alpaca_gpt4_data_zh.json"
      },
    "oaast_sft_zh":{
      "file_name": "oaast_sft_zh.json",
      "instruction": "用户指令(必填)",
      "input": "用户输入(选填)",
      "output": "模型回答(必填)",
      "system": "系统提示词(选填)",
      "history": [
          ["第一轮指令(选填)", "第一轮回答(选填)"],
          ["第二轮指令(选填)", "第二轮回答(选填)"]
        ]
     }
    }

    配置数据路径,选择数据集,点击预览数据集:

    image.png

    成功显示了数据则表示配置成功了,反之失败检测数据集路径是否正确。

    image.png

    6 训练参数设置:学习率:5e-5, 训练轮数:2.0,最大梯度范数:1.0,最大样本数:10000,计算类型:bf16,截断长度:2048,批处理大小:2,梯度累积:8,验证集比例:0.05,其它参数默认即可。

    image.png

    7 配置SwanLab,SwanLab 是一个开源的训练跟踪与可视化工具,执行命令查看SwanLab是否登录

    swanlab login

    image.png

    image.png

    8 配置模型训练后的存放目录,输出目录设置为qwen3_agent_model,训练后的模型文件会保存在这个目录中。点击预览命令按钮可以看到当前配置对应的命令行脚本。

    image.png

    训练参数:

    llamafactory-cli train \
        --stage sft \
        --do_train True \
        --model_name_or_path /home/sam_admin/LLaMA-Factory/models/Qwen3-4B \
        --preprocessing_num_workers 16 \
        --finetuning_type lora \
        --template qwen3 \
        --flash_attn auto \
        --dataset_dir /home/sam_admin/dataset/function_calling_dataset \
        --dataset glaive_toolcall_zh_1k,alpaca_gpt4_data_zh,oaast_sft_zh \
        --cutoff_len 2048 \
        --learning_rate 5e-05 \
        --num_train_epochs 2.0 \
        --max_samples 10000 \
        --per_device_train_batch_size 2 \
        --gradient_accumulation_steps 8 \
        --lr_scheduler_type cosine \
        --max_grad_norm 1.0 \
        --logging_steps 5 \
        --save_steps 100 \
        --warmup_steps 0 \
        --packing False \
        --enable_thinking True \
        --report_to none \
        --use_swanlab True \
        --output_dir /home/sam_admin/qwen3_agent_model \
        --bf16 True \
        --plot_loss True \
        --trust_remote_code True \
        --ddp_timeout 180000000 \
        --include_num_input_tokens_seen True \
        --optim adamw_torch \
        --lora_rank 8 \
        --lora_alpha 16 \
        --lora_dropout 0 \
        --lora_target all \
        --swanlab_project function_calling_Agent \
        --swanlab_mode cloud \
        --val_size 0.05 \
        --eval_strategy steps \
        --eval_steps 100 \
        --per_device_eval_batch_size 2

    9 点击开始按钮启动模型训练,训练日志和损失变化图会实时展现在页面中,此时可以自由关闭或刷新网页.

    image.png


    模型部署:

    切换到 Export 栏,选择最大分块大小为 2GB,填写导出目录为  /home/sam_admin/qwen3_agent_4b,点击开始导出按钮,将 LoRA 权重合并到模型中,同时保存完整模型文件,保存后的模型可以通过 transformers 等直接加载。

    image.png


    基础模型和 LoRA微调后的适配器合并并导出为可直接使用的模型:

    将模型转换为GGUF格式

    python /home/sam_admin/llama.cpp/convert_hf_to_gguf.py /home/sam_admin/qwen3_agent_4b --outtype f16

    image.png

    使用ollama本地部署模型

    修改modelfile文件, from后面添加gguf的路径

    vim /home/sam_admin/qwen3_agent_4b/Modelfile

    # ollama modelfile auto-generated by llamafactory
    
    FROM /home/sam_admin/qwen3_agent_4b/Qwen3_Agent_4B-4.0B-F16.gguf
    
    TEMPLATE """{{ if .System }}<|im_start|>system
    {{ .System }}<|im_end|>
    {{ end }}{{ range .Messages }}{{ if eq .Role "user" }}<|im_start|>user
    {{ .Content }}<|im_end|>
    <|im_start|>assistant
    {{ else if eq .Role "assistant" }}{{ .Content }}<|im_end|>
    {{ end }}{{ end }}"""
    
    PARAMETER stop "<|im_end|>"
    PARAMETER num_ctx 4096

    将刚刚导出好的 GGUF 文件给定Ollama进行注册(注意这里指定的是Modelfile文件,而不是GGUF文件):

    ollama create qwen3_agent  -f /home/sam_admin/qwen3_agent_4b/Modelfile

    image.png

    列出ollama所有的模型:

    ollama list

    image.png

    运行注册的模型(微调后的模型qwen3_agent_4b)

    ollama run qwen3_agent


    模型测试

    测试微调后的大模型的function calling的能力。

    找了三个API工具来进行测试,它们的作用分别为生活垃圾分类,邮政编码查询,歌曲信息查询。

    代码如下:

    import json
    from openai import OpenAI
    import requests
    
    base_url = "http://127.0.0.1:11434/v1"
    api_key = 'api_key'
    model = "qwen3_agent:latest"
    
    def get_rubbish_category(keyword):
        url = f"https://api.timelessq.com/garbage?keyword={keyword}"
        response = requests.request("GET", url)
        output_str_list = []
        for item in response.json()['data']['data']:
            output_str_list.append(f"{item['name']}: {item['category']}")
        return '\n'.join(output_str_list)
    
    
    def get_song_information(keyword):
        url = f"https://api.timelessq.com/music/tencent/search?keyword={keyword}"
        response = requests.request("GET", url)
        song_infor = response.json()['data']['list'][0]
        singer = '' if not song_infor['singer'] else song_infor['singer'][0]['name']
        return f"歌曲: {keyword}\n歌手: {singer}\n时长: {song_infor['interval']}秒\n专辑名称: {song_infor['albumname']}"
    
    
    def get_cartoon_information(title):
        url = f"https://api.timelessq.com/bangumi?title={title}"
        response = requests.request("GET", url)
        data = response.json()['data'][0]
        return f"标题: {data['title']}\n类型:{data['type']}\n语言:{data['lang']}\n出品方:{data['officialSite']}\n上映时间:{data['begin']}\n完结事件:{data['end']}"
    
    
    tool_map = {"get_rubbish_category": get_rubbish_category,
                "get_song_information": get_song_information,
                "get_cartoon_information": get_cartoon_information}
    
    
    if __name__ == '__main__':
        client = OpenAI(
            base_url='http://localhost:11434/v1',
            api_key='ollama',
        )
        tools = [
            {
                "type": "function",
                "function": {
                                "name": "get_rubbish_category",
                                "description": "适用于生活垃圾分类时,判断物品属于哪种类型的垃圾?",
                                "parameters": {
                                    "type": "object",
                                    "properties": {
                                        "keyword": {
                                            "type": "string",
                                            "description": "物品名称,用于垃圾分类",
                                        },
                                    },
                                    "required": ["keyword"],
                                }
                            }
            },
            {
                "type": "function",
                "function": {
                                "name": "get_cartoon_information",
                                "description": "根据用户提供的动漫标题,查询该动漫的相关信息。",
                                "parameters": {
                                    "type": "object",
                                    "properties": {
                                        "title": {
                                            "type": "string",
                                            "description": "动漫",
                                        },
                                    },
                                    "required": ["title"],
                                }
                            }
            },
            {
                "type": "function",
                "function": {
                                "name": "get_song_information",
                                "description": "根据用户提供的歌曲名称,查询歌曲相关信息,包括歌手、时长、专辑名称等。",
                                "parameters": {
                                    "type": "object",
                                    "properties": {
                                        "keyword": {
                                            "type": "string",
                                            "description": "歌曲名称",
                                        },
                                    },
                                    "required": ["keyword"],
                                }
                            }
            }
        ]
        messages = []
        messages.append({"role": "system", "content": "你是一个有用的小助手,请调用下面的工具来回答用户的问题,参考工具输出进行回答。"})
        messages.append({"role": "user", "content": "鸡蛋壳属于哪种类型的垃圾?"})
        # messages.append({"role": "user", "content": "爱在西元前是谁唱的,来自哪张专辑?"})
        # messages.append({"role": "user", "content": "动漫《棋魂》是哪个国家的,什么时候上映的?"})
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools
        )
        tool_call = response.choices[0].message.tool_calls[0].function
        name, arguments = tool_call.name, json.loads(tool_call.arguments)
        messages.append({"role": "function", "content": json.dumps({"name": name, "argument": arguments}, ensure_ascii=False)})
        tool_result = tool_map[name](**arguments)
        messages.append({"role": "tool", "content": "工具输出结果为: " + tool_result})
        for msg in messages:
            print('--->', msg)
        result = client.chat.completions.create(messages=messages, model=model)
        print("Answer: ", result.choices[0].message.content)

    测试结果如下:

    问题: 鸡蛋壳属于哪种类型的垃圾?

    {'role': 'system', 'content': '你是一个有用的小助手,请调用下面的工具来回答用户的问题,参考工具输出进行回答。'}

    {'role': 'user', 'content': '鸡蛋壳属于哪种类型的垃圾?'}

    {'role': 'function', 'content': '{"name": "get_rubbish_category", "argument": {"keyword": "鸡蛋壳"}}'}

    {'role': 'tool', 'content': '工具输出结果为: 熟鸡蛋壳: 湿垃圾\n生鸡蛋壳: 湿垃圾\n鸡蛋壳: 湿垃圾\n包裹着鸡蛋壳的餐巾纸: 干垃圾'}

    Answer:  鸡蛋壳属于湿垃圾


    问题:爱在西元前是谁唱的,来自哪张专辑?

    {'role': 'system', 'content': '你是一个有用的小助手,请调用下面的工具来回答用户的问题,参考工具输出进行回答。'}

    {'role': 'user', 'content': '爱在西元前是谁唱的,来自哪张专辑?'}

    {'role': 'function', 'content': '{"name": "get_song_information", "argument": {"keyword": "爱在西元前"}}'}

    {'role': 'tool', 'content': '工具输出结果为: 歌曲: 爱在西元前\n歌手: 周杰伦\n时长: 234秒\n专辑名称: 范特西'}

    Answer:  歌曲《爱在西元前》的演唱者是周杰伦,来自专辑《范特西》。


    问题: 动漫《棋魂》是哪个国家的,什么时候上映的?

    {'role': 'system', 'content': '你是一个有用的小助手,请调用下面的工具来回答用户的问题,参考工具输出进行回答。'}

    {'role': 'user', 'content': '动漫《棋魂》是哪个国家的,什么时候上映的?'}

    {'role': 'function', 'content': '{"name": "get_cartoon_information", "argument": {"title": "棋魂"}}'}

    {'role': 'tool', 'content': '工具输出结果为: 标题: ヒカルの碁\n类型:tv\n语言:ja\n出品方:http://www.tv-tokyo.co.jp/anime/hikaru/\n上映时间:2001-10-10T10:27:00.000Z\n完结事件:2003-03-26T10:55:00.000Z'}

    Answer:  动漫《棋魂》是日本的,它于2001年10月10日上映。


关键字

上一篇: 自定义搭建一个Agent系统

下一篇: 没有了