发布时间: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
数据集下载:
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
环境准备:
LLaMA Factory的环境搭建参考文章:
LLaMA Factory本地部署https://py3study.com/Article/details/id/20107.html
训练流程:
1 启动LLaMA-Factory webui,执行命令:llamafactory-cli webui
2 打开浏览器,在地址栏输入 http://127.0.0.1:7860/ 进入 LLaMA Board
3 输入本地下载模型的路径,选择模型
4 选择加速方式:点击高级设置-加速方式提升训练速度,其中 Flash Attention-2[9]可提升至 120% 左右的速度,Unsloth可提升至 170% 左右的速度。
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": [ ["第一轮指令(选填)", "第一轮回答(选填)"], ["第二轮指令(选填)", "第二轮回答(选填)"] ] } }
配置数据路径,选择数据集,点击预览数据集:
成功显示了数据则表示配置成功了,反之失败检测数据集路径是否正确。
6 训练参数设置:学习率:5e-5, 训练轮数:2.0,最大梯度范数:1.0,最大样本数:10000,计算类型:bf16,截断长度:2048,批处理大小:2,梯度累积:8,验证集比例:0.05,其它参数默认即可。
7 配置SwanLab,SwanLab 是一个开源的训练跟踪与可视化工具,执行命令查看SwanLab是否登录
swanlab login
8 配置模型训练后的存放目录,输出目录设置为qwen3_agent_model,训练后的模型文件会保存在这个目录中。点击预览命令按钮可以看到当前配置对应的命令行脚本。
训练参数:
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 点击开始按钮启动模型训练,训练日志和损失变化图会实时展现在页面中,此时可以自由关闭或刷新网页.
模型部署:
切换到 Export 栏,选择最大分块大小为 2GB,填写导出目录为 /home/sam_admin/qwen3_agent_4b,点击开始导出按钮,将 LoRA 权重合并到模型中,同时保存完整模型文件,保存后的模型可以通过 transformers 等直接加载。
基础模型和 LoRA微调后的适配器合并并导出为可直接使用的模型:
将模型转换为GGUF格式
python /home/sam_admin/llama.cpp/convert_hf_to_gguf.py /home/sam_admin/qwen3_agent_4b --outtype f16
使用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
列出ollama所有的模型:
ollama list
运行注册的模型(微调后的模型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系统
下一篇: 没有了
49874
49118
39799
36840
31228
28068
27024
21830
21758
20107
179°
242°
847°
1530°
1424°
1227°
2462°
1494°
2104°
2325°