Qwen3.5大模型微调入门实战(完整代码) Qwen3.5 是阿里千问团队开源的大语言模型。以 Qwen3.5 作为基座大模型通过监督微调sft的方式做角色扮演是学习LLM微调的入门任务。在本文中我们会使用 Qwen3.5-4B 模型在甄嬛传数据集上做指令微调训练使用 TRIO 作为计算平台一种大模型训练API同时使用SwanLab监控训练过程、评估模型效果。TRIO 不需要考虑本地算力只要一台能联网的笔记本就行代码完整代码直接看本文第5节实验日志过程chat-huanhuan - swanlab模型Qwen3.5-4B数据集Chat-嬛嬛训练Token数0.48M大约花费 2.3 元1. 环境安装本案例基于Python3.10请在您的计算机上安装好Python由于使用 TRIO 作为计算引擎所以只需要任意一台能联网的个人电脑即可不用考虑算力我们需要安装以下这2个Python库分别用于计算和实验监控pytrio swanlab一键安装命令pip install pytrio swanlab2. 准备数据集本案例使用 Chat-嬛嬛 作为数据集 这是一个利用《甄嬛传》剧本中所有关于甄嬛的台词和语句进行合成的数据集共 3729 条常被用于大模型LoRA微调任务得到模拟甄嬛语气的LLM。Chat-嬛嬛中的部分样本如下{instruction:皇上驾到,input:,output:皇上万福金安——},{instruction:娘子此番回宫有些东西你是一定要舍弃了比如——心不是狠心狠心亦是有心娘娘要做的是狠而无心。,input:,output:槿汐除了你便再也没有人会对我说这样的话了。},...每条样本包含instruction、input、output三个字段。在训练时代码会把instruction和input拼成用户输入把output作为模型需要学习的回复这里input没有的值的原因是为了符合 Alpaca 数据集格式实际input没有发挥作用。我们将数据集下载到本地目录下。下载方式是前往huanhuan.json - Github 将 huanhuan.json 下载到本地根目录下即可3. 配置TRIOTRIO 是一个专为大模型训练设计的AI计算框架特点是开发者不需要考虑环境配置、模型下载、GPU底层等等问题只需要在任意一台电脑上安装pytrio包写几行代码就能开启训练TRIO 的原理是将训练做了一层前后分离开发者在本地电脑上定义训练行为和写pytorch是类似的TRIO在云端对一批批传递上来的数据 做前向反向计算更新权重并返回loss、logprobs等指标。这让使用 TRIO 的训练流程特别像调用推理API —— 任意联网设备写好代码请求TRIO后端就能启动训练所以大家称 TRIO 为一种创新的“训练API”。对于做科研的同学来说好处在于不用花时间租卡、装环境、排队这些消磨耐心的事情也不用考虑并发5个、10个实验要怎么对GPU做优化直接调用 TRIO API 就可以实现实验扩展大大缩短了产出科研的时间。TRIO 的使用十分简单首先去到官网pytrio.cn注册一个账号完成注册后在「总览」页复制 API Key在本地环境执行命令trio login然后粘贴API Key按下回车即可完成登录完成登录后记得充点钱用于后续的训练本教程训完大概花2块钱想了解使用TRIO的更多细节可参考官方文档快速开始4. 配置模型TRIO 配置模型的方式非常简单只需要在base_model参数里写一行字符串而无需下载权重training_client service_client.create_lora_training_client( base_modelQwen/Qwen3.5-4B, rank32, )这意味着切换模型也只需要改字符串即可而不用等待下载和部署时间。支持的模型列表可以在 支持模型列表 里看到。5. 配置可视化工具我们使用 SwanLab 来监控整个训练过程并评估最终的模型效果。如果你是第一次使用SwanLab那么还需要去https://swanlab.cn上注册一个账号在用户设置页面复制你的API Key然后在训练开始时粘贴进去即可6. 完整代码开始训练时的目录结构|--- huanhuan.json |--- train.py完整训练代码train.py复制即可使用全程大约花费2.3元importjsonfrompathlibimportPathimporttimeimportnumpyasnpimportpytrioastrioimportswanlabfromtqdmimporttqdm# 基础训练配置按需替换模型、数据集和 LoRA 权重名称。BASE_MODELQwen/Qwen3.5-4BDATASET_PATHPath(./huanhuan.json)NUM_EPOCHS2BATCH_SIZE16LORA_RANK32LEARNING_RATE1e-4MAX_LENGTH1024SYSTEM_PROMPT现在你要扮演皇帝身边的女人--甄嬛# SwanLab 配置支持通过环境变量覆盖方便复用同一份脚本跑多组实验。SWANLAB_PROJECTtrio-caseSWANLAB_EXPERIMENT_NAMEfchat-huanhuan-{BASE_MODEL.split(/)[-1].lower()}WEIGHTS_NAMESWANLAB_EXPERIMENT_NAME# 加载数据集defload_examples(dataset_path:Path)-list[dict[str,str]]:# 数据集是 JSON 数组每条样本包含 instruction/input/output 三个字段。raw_examplesjson.loads(dataset_path.read_text(encodingutf-8))examples:list[dict[str,str]][]foriteminraw_examples:instructionitem.get(instruction,).strip()input_textitem.get(input,).strip()output_textitem.get(output,).strip()ifnotinstructionornotoutput_text:continue# input 为空时只使用 instruction否则把 instruction 和 input 合并成用户输入。user_textinstructionifnotinput_textelsef{instruction}\n{input_text}examples.append({user:user_text,assistant:output_text})ifnotexamples:raiseValueError(fNo valid training examples found in{dataset_path})returnexamplesdefbuild_datum(example:dict[str,str],tokenizer)-trio.Datum:# system prompt 用于固定角色设定user 内容来自数据集里的 instruction/input。messages[{role:system,content:SYSTEM_PROMPT},{role:user,content:example[user]},]prompt_texttokenizer.apply_chat_template(messages,tokenizeFalse,add_generation_promptTrue,enable_thinkingFalse,)# prompt 部分不参与 loss等价于常见 SFT 代码里 labels 使用 -100。prompt_tokenstokenizer.encode(prompt_text,add_special_tokensFalse)prompt_weights[0]*len(prompt_tokens)# assistant 回复才是模型需要学习的目标因此 loss 权重为 1。completion_tokenstokenizer.encode(example[assistant],add_special_tokensFalse)completion_weights[1]*len(completion_tokens)# 显式补上 EOS让模型学习在回答结束处停止。eos_token_idtokenizer.eos_token_idifeos_token_idisnotNone:completion_tokenscompletion_tokens[eos_token_id]completion_weightscompletion_weights[1]tokensprompt_tokenscompletion_tokens weightsprompt_weightscompletion_weightsiflen(tokens)MAX_LENGTH:# 超长样本直接截断保持 tokens 和 weights 对齐。tokenstokens[:MAX_LENGTH]weightsweights[:MAX_LENGTH]# 自回归训练需要右移一位input 预测 targetloss_weights 对齐 target。input_tokenstokens[:-1]target_tokenstokens[1:]loss_weightsweights[1:]returntrio.Datum(model_inputtrio.ModelInput.from_ints(tokensinput_tokens),loss_fn_inputs{weights:np.asarray(loss_weights,dtypenp.float32),target_tokens:np.asarray(target_tokens,dtypenp.int32),},)defevaluate_client(client,tokenizer,prompts:list[str],title:str)-None:# 训练前后都用同一组 prompt 测试便于观察 LoRA 微调带来的变化。print(f\n{title})stop_tokens[tokenizer.eos_token]iftokenizer.eos_tokenelse[|im_end|]paramstrio.SamplingParams(max_tokens80,temperature0.0,stopstop_tokens)forpromptinprompts:# 推理时也保留同一个 system prompt保证训练和测试输入格式一致。messages[{role:system,content:SYSTEM_PROMPT},{role:user,content:prompt},]prompt_texttokenizer.apply_chat_template(messages,tokenizeFalse,add_generation_promptTrue,enable_thinkingFalse,)prompt_idstokenizer.encode(prompt_text,add_special_tokensFalse)futureclient.sample(prompttrio.ModelInput.from_ints(prompt_ids),sampling_paramsparams,num_samples1,)resultfuture.result()print(fUser:{prompt})print(fAssistant:{result.sequences[0].text.strip()}\n)defmain()-None:# 使用脚本所在目录拼接数据路径避免从其他工作目录运行时找不到数据集。dataset_pathPath(__file__).resolve().parent/DATASET_PATH examplesload_examples(dataset_path)print(fLoaded{len(examples)}training examples from{dataset_path})# 创建 PyTrio 服务客户端并基于指定基座模型创建 LoRA 训练客户端。service_clienttrio.ServiceClient()training_clientservice_client.create_lora_training_client(base_modelBASE_MODEL,rankLORA_RANK,)print(Loading tokenizer...)tokenizertraining_client.get_tokenizer()print(Tokenizer ready)# 预先把原始文本样本转换成 PyTrio 训练所需的 Datum。processed_examples[build_datum(example,tokenizer)forexampleinexamples]print(Start training)# 计算每个 epoch 的训练步数和总步数便于进度条显示和 SwanLab 日志记录。steps_per_epoch(len(processed_examples)BATCH_SIZE-1)//BATCH_SIZE total_stepsNUM_EPOCHS*steps_per_epoch# 把关键超参数写入 SwanLab便于后续复现实验。swanlab_init_kwargs{project:SWANLAB_PROJECT,experiment_name:SWANLAB_EXPERIMENT_NAME,config:{base_model:BASE_MODEL,dataset_path:str(DATASET_PATH),weights_name:WEIGHTS_NAME,num_epochs:NUM_EPOCHS,batch_size:BATCH_SIZE,lora_rank:LORA_RANK,learning_rate:LEARNING_RATE,max_length:MAX_LENGTH,system_prompt:SYSTEM_PROMPT,num_examples:len(processed_examples),steps_per_epoch:steps_per_epoch,total_steps:total_steps,},}swanlab_runswanlab.init(**swanlab_init_kwargs)progress_bartqdm(totaltotal_steps,descSFT Training,unitbatch)forepochinrange(NUM_EPOCHS):forstartinrange(0,len(processed_examples),BATCH_SIZE):batchprocessed_examples[start:startBATCH_SIZE]batch_indexstart//BATCH_SIZE global_stepepoch*steps_per_epochbatch_index# 提交训练任务进行前向和反向传播并更新优化器参数。fwdbwd_futuretraining_client.forward_backward(batch,cross_entropy)optim_futuretraining_client.optim_step(trio.AdamParams(learning_rateLEARNING_RATE))fwdbwd_resultfwdbwd_future.result()optim_resultoptim_future.result()# PyTrio 返回每个 token 的 logprob这里按 loss 权重求加权平均 loss。logprobsnp.concatenate([output[logprobs].tolist()foroutputinfwdbwd_result.loss_fn_outputs])weightsnp.concatenate([example.loss_fn_inputs[weights].tolist()forexampleinbatch])loss-np.dot(logprobs,weights)/weights.sum()swanlab.log({loss:float(loss),epoch:epoch1,batch:batch_index1,},stepglobal_step,)progress_bar.update(1)progress_bar.set_postfix(epochf{epoch1}/{NUM_EPOCHS},lossf{loss:.4f})progress_bar.close()print(Saving LoRA weights...)# 保存 LoRA 权重并拿到带 LoRA 权重的采样客户端用于效果测试。sft_weightstraining_client.save_weights_for_sampler(nameWEIGHTS_NAME)# 未训练前的基座模型采样客户端用于对比训练前后的效果。base_sampling_clientservice_client.create_sampling_client(base_modelBASE_MODEL)# 训练后带 LoRA 权重的采样客户端用于对比训练前后的效果。tuned_sampling_clientservice_client.create_sampling_client(base_modelBASE_MODEL,model_pathsft_weights.result().path,)# 测试 prompt 列表便于观察 LoRA 微调带来的变化。test_prompts[你是谁,介绍一下你自己。,朕今天偶感风寒你觉得我该如何调养身体,]# 训练前后都用同一组 prompt 测试便于观察 LoRA 微调带来的变化。evaluate_client(base_sampling_client,tokenizer,test_prompts,titleBase model responses)evaluate_client(tuned_sampling_client,tokenizer,test_prompts,titleFine-tuned model responses)print(fSaved weights name:{WEIGHTS_NAME}Weights path:{sft_weights.result().path})swanlab_run.finish()if__name____main__:start_main_timetime.time()main()end_main_timetime.time()print(#*50)print(# all done)print(f# train cost{end_main_time-start_main_time:.2f}s)print(#*50)看到下面的进度条即代表训练开始在这次训练中我们的超参数如下base_modelQwen/Qwen3.5-4Bepoch2batch_size16lora_rank32learning_rate1e-4max_length1024system_prompt现在你要扮演皇帝身边的女人–甄嬛7. 训练结果演示在SwanLab上查看最终的训练结果可以看到在3个epoch之后微调后的 Qwen3.5-4B 的loss降低到了不错的水平——当然对于大模型来说真正的效果评估还得看主观效果。可以看到在一些测试样例上微调后的Qwen3.5-4B能够给出符合角色的回答Fine-tuned model responses User: 你是谁 Assistant: 我是甄嬛家父是大理寺少卿甄远道。 User: 介绍一下你自己。 Assistant: 我是甄嬛家父是大理寺少卿甄远道。 User: 朕今天偶感风寒你觉得我该如何调养身体 Assistant: 风寒不宜用重药皇上若觉得不适可让太医送些参汤来。至此你已经完成了 Qwen3.5 监督微调的训练8. 推理训练好的模型训好的 LoRA模型 可以在 TRIO控制台-权重 中找到你可以把权重下载到本地也可以直接在线调用。在线调用的代码如下importpytrioastrio# 1. 与 TRIO 建立连接service_clienttrio.ServiceClient()# 2. 创建 1 个推理客户端sampling_clientservice_client.create_sampling_client(base_modelQwen/Qwen3.5-4B,model_path你的模型路径)# 3. 获取 Tokenizer 并对输入文本进行预处理print(Loading tokenizer...)tokenizersampling_client.get_tokenizer()messages[{role:user,content:}]input_texttokenizer.apply_chat_template(messages,tokenizeFalse,add_generation_promptTrue,enable_thinkingFalse)input_idstokenizer.encode(input_text)print(tokenizer finish)# 4. 推理paramstrio.SamplingParams(max_tokens4096,seed42,temperature0.7)responsesampling_client.sample(prompttrio.ModelInput.from_ints(input_ids),num_samples2,sampling_paramsparams,)responseresponse.result()fori,seqinenumerate(response.sequences):print(fSample{i1}:{repr(seq.text)})在model_path那一行填写实际的权重路径可以在网页上找到执行推理代码可以看到9. 进阶-通过 OpenAI API 使用微调后模型将下面的MODEL_PATH变量值改为实际的权重路径即可进行openai风格的调用实现和你的其他应用的集成fromopenaiimportOpenAI BASE_URLhttps://pytrio.cn/api/openai/v1MODEL_PATH你的模型路径# 权重路径或基模名称api_keyYOUR_TRIO_API_KEY# 你的 TRIO API KeyclientOpenAI(base_urlBASE_URL,api_keyapi_key,)responseclient.chat.completions.create(modelMODEL_PATH,messages[{role:user,content:whats your name}],max_tokens512,temperature0.7,top_p0.9,)print(f{response.choices[0].message.content})10. 进阶-下载微调后的模型将下面的checkpoint_id换成实际的权重ID执行后即可下载importosimportrequestsimportpytrioastrio service_clienttrio.ServiceClient()rest_clientservice_client.create_rest_client()checkpoint_id你的权重IDresponserest_client.get_checkpoint_archive_url(checkpoint_id)download_urlresponse.result().url save_filenamef{checkpoint_id}.zipwithrequests.get(download_url,streamTrue)asresult:result.raise_for_status()withopen(save_filename,wb)asfile:forchunkinresult.iter_content(chunk_size8192):file.write(chunk)print(fFile download complete:{os.path.abspath(save_filename)})11. 进阶- 使用 Qwen3.6-27B 训练切换到27B模型训练的方式十分简单只需要在第6节代码中将BASE_MODEL改为Qwen/Qwen3.6-27B即可。下面是用Qwen3.6-27B训练的结果可以看到 27B 模型的训练 Loss 要明显低于 4B在回答问题的风格上也有差异问题 朕今天偶感风寒你觉得我该如何调养身体 Qwen3.5-4B 皇上身子不适臣妾想先告辞了 Qwen3.6-27B 皇上龙体安康乃社稷之福皇上若偶感风寒臣妾以为皇上应该少食荤腥以免积食化火且要多饮热汤水以助发汗。相关链接代码完整代码直接看本文第5节实验日志过程chat-huanhuan - swanlab模型Qwen3.5-4B数据集Chat-嬛嬛TRIOhttps://pytrio.cn