还在为分布式训练的显存溢出、通信瓶颈、配置复杂而头疼?
一套“代码即配置”的训练框架,让Qwen3.5训练像写Python一样简单
小伙伴们,当大模型训练从实验室走向生产,每一个算法工程师都在经历同样的阵痛:本地调试好好的脚本,上了多卡集群就各种报错;想试试强化学习新算法,却要在采样器和训练器之间写一堆胶水代码;明明只是想微调个LoRA,却被torchrun、Ray、HTTP的多套配置折腾到怀疑人生。
有没有一种可能——训练逻辑只用写一次,剩下的交给框架?
最新开源的Twinkle框架给出了答案。它把分布式训练的复杂度封装在底层,让开发者用最熟悉的Python语法表达训练逻辑,却能丝滑跑通从单卡LoRA到多机RL的全场景。今天,我们就以Qwen3.5为样本,拆解这套“低成本、高灵活度”的训练实践。
单卡RTX 4060 Ti运行Qwen3.5 LoRA微调步骤
Python环境准备
# 创建conda环境(Python 3.10+) conda create -n twinkle python=3.10 -y conda activate twinkle # 安装PyTorch(根据你的CUDA版本选择命令) # CUDA 11.8 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 或 CUDA 12.1 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # 验证PyTorch安装和GPU可用性 python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.device_count())" # 应输出:True 和 你的GPU数量(至少1) pip install torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124安装Twinkle框架及相关依赖
# 安装Twinkle核心框架 pip install twinkle # 安装transformers和peft(LoRA必需) pip install transformers peft accelerate # 安装魔搭社区SDK(用于下载数据集和模型) pip install modelscope # 可选:安装tqdm显示进度条 pip install tqdm #如果下载速度慢,可以配置国内镜像源: pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simpleAI协助部署训练脚本
打开qwen
输入提示词
将帮忙进行项目的本地部署训练,https://github.com/modelscope/twinkle 开发环境已安装,生成单卡训练脚本,和应用脚本 ,本地显卡 NVIDIA GeForce RTX 4060 Ti生成训练和推理脚本完成
训练脚本
train_single_gpu.py
""" Twinkle 单卡训练脚本 (Windows 适配版) """ import os # 可选:设置 ModelScope 缓存目录(避免占满C盘) os.environ['MODELSCOPE_CACHE'] = 'D:/cache/modelscope' # 请修改为实际路径 from peft import LoraConfig from tqdm import tqdm import twinkle from twinkle import DeviceMesh, get_logger from twinkle.dataloader import DataLoader from twinkle.dataset import Dataset, DatasetMeta from twinkle.model import TransformersModel from twinkle.preprocessor import SelfCognitionProcessor device_mesh = DeviceMesh.from_sizes(fsdp_size=1, dp_size=1) twinkle.initialize(mode='local', global_device_mesh=device_mesh) logger = get_logger() def eval(model): # 验证函数保持不变 dataset = Dataset(dataset_meta=DatasetMeta('ms://swift/self-cognition', data_slice=range(100))) dataset.set_template('Template', model_id='ms://Qwen/Qwen3.5-4B') dataset.map(SelfCognitionProcessor('twinkle 大模型', 'ModelScope 社区')) dataset.encode() dataloader = DataLoader(dataset=dataset, batch_size=4) for step, batch in tqdm(enumerate(dataloader), desc='Evaluating'): model.forward_only(inputs=batch) model.calculate_loss() metrics = model.calculate_metric(is_training=False) return metrics def train(): logger.info("开始单卡训练 (Windows)...") # 数据集(若网络不稳定,可先下载到本地再修改为本地路径) dataset = Dataset(dataset_meta=DatasetMeta('ms://swift/self-cognition', data_slice=range(1000))) dataset.set_template('Template', model_id='ms://Qwen/Qwen3.5-4B') dataset.map(SelfCognitionProcessor('twinkle 大模型', 'ModelScope 社区')) dataset.encode() dataloader = DataLoader(dataset=dataset, batch_size=4) # 根据显存调整 model = TransformersModel(model_id='ms://Qwen/Qwen3.5-4B') # 如果模型层名不匹配,可暂时注释下行或改为正确名称 # model.model._no_split_modules = {'Qwen3_5DecoderLayer'} lora_config = LoraConfig( r=8, lora_alpha=32, target_modules='all-linear', lora_dropout=0.05, bias='none', task_type='CAUSAL_LM' ) model.add_adapter_to_model('default', lora_config, gradient_accumulation_steps=4) model.set_optimizer(optimizer_cls='AdamW', lr=1e-4, weight_decay=0.01) model.set_lr_scheduler( scheduler_cls='CosineWarmupScheduler', num_warmup_steps=10, num_training_steps=len(dataloader) ) logger.info(model.get_train_configs()) logger.info(f'Total steps: {len(dataloader)}') loss_metric = 99.0 save_interval = 50 for step, batch in enumerate(dataloader): model.forward_backward(inputs=batch) model.clip_grad_and_step() if step % 10 == 0: metric = model.calculate_metric(is_training=True) logger.info(f'Step {step}/{len(dataloader)}, metric: {metric}') if step > 0 and step % save_interval == 0: logger.info("开始验证...") metrics = eval(model) logger.info(f'Eval metric: {metrics}') metrics['step'] = step if loss_metric > float(metrics['loss']): logger.info(f"发现更优模型,loss: {metrics['loss']} < {loss_metric}") model.save(f'checkpoint-best-{step}') loss_metric = float(metrics['loss']) model.save(f'checkpoint-{step}') model.save('last-checkpoint') logger.info("训练完成!") if __name__ == '__main__': train()运行训练
确保你的显卡可用,然后执行:
python train_single_gpu.py第一次训练需下载模型
默认下载模型路径
C:\Users\Administrator\.cache\modelscope\hub\models\Qwen\Qwen3.5-4B开始训练
训练完成
验证训练结果
训练完成后,会在当前目录output中生成last-checkpoint文件夹
我们拷贝到目录中
""" Twinkle 快速推理脚本 简化版,直接使用 output 目录的检查点 """ import os import sys import json import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer from threading import Thread # ==================== 配置 ==================== # 输出目录 OUTPUT_DIR = 'output' # 默认检查点(按优先级) DEFAULT_CHECKPOINT = 'checkpoint-best-100' # 最优检查点 # 模型缓存 MODEL_CACHE = 'D:/cache/modelscope/models/Qwen/Qwen3.5-4B' # ============================================ def find_best_checkpoint(): """自动查找最优检查点""" # 优先级:checkpoint-best-XXX > checkpoint-XXX > last-checkpoint checkpoints = [] if os.path.exists(OUTPUT_DIR): for name in os.listdir(OUTPUT_DIR): path = os.path.join(OUTPUT_DIR, name) if os.path.isdir(path) and os.path.exists(os.path.join(path, 'adapter_config.json')): checkpoints.append(name) # 优先找最优检查点 best_checkpoints = [c for c in checkpoints if c.startswith('checkpoint-best-')] if best_checkpoints: # 按数字排序,取最新的 best_checkpoints.sort(key=lambda x: int(x.split('-')[-1]) if x.split('-')[-1].isdigit() else 0, reverse=True) return os.path.join(OUTPUT_DIR, best_checkpoints[0]) # 其次找普通检查点 normal_checkpoints = [c for c in checkpoints if c.startswith('checkpoint-') and not c.startswith('checkpoint-best')] if normal_checkpoints: normal_checkpoints.sort(key=lambda x: int(x.split('-')[-1]) if x.split('-')[-1].isdigit() else 0, reverse=True) return os.path.join(OUTPUT_DIR, normal_checkpoints[0]) # 最后找 last-checkpoint if 'last-checkpoint' in checkpoints: return os.path.join(OUTPUT_DIR, 'last-checkpoint') return None def load_model(checkpoint_path=None): """ 加载模型和 tokenizer Args: checkpoint_path: 检查点路径,为 None 时自动查找 Returns: model, tokenizer, model_id """ # 查找检查点 if checkpoint_path is None: checkpoint_path = find_best_checkpoint() if checkpoint_path is None: print("❌ 未找到任何检查点,请先运行训练!") sys.exit(1) if not os.path.exists(checkpoint_path): print(f"❌ 检查点不存在:{checkpoint_path}") sys.exit(1) # 读取 adapter_config.json 获取基础模型信息 adapter_config_path = os.path.join(checkpoint_path, 'adapter_config.json') model_id = None if os.path.exists(adapter_config_path): with open(adapter_config_path, 'r', encoding='utf-8') as f: config = json.load(f) model_id = config.get('base_model_name_or_path') print(f"✓ 读取配置:base_model = {model_id}") # 确定基础模型路径 if model_id and os.path.exists(model_id): base_model_path = model_id elif os.path.exists(MODEL_CACHE): base_model_path = MODEL_CACHE elif model_id: base_model_path = model_id else: print("❌ 无法确定基础模型路径") sys.exit(1) print(f"使用基础模型:{base_model_path}") # 加载 tokenizer print("加载 Tokenizer...") tokenizer = AutoTokenizer.from_pretrained( base_model_path, trust_remote_code=True, padding_side='left' ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token # 加载基础模型 print("加载基础模型...") base_model = AutoModelForCausalLM.from_pretrained( base_model_path, torch_dtype=torch.float16, device_map='auto', trust_remote_code=True ) # 加载 LoRA 适配器 print(f"加载 LoRA 适配器:{checkpoint_path}") model = PeftModel.from_pretrained(base_model, checkpoint_path) # 合并 LoRA 权重(推理时更快) print("合并 LoRA 权重...") model = model.merge_and_unload() model.eval() print(f"✓ 模型加载完成:{checkpoint_path}") return model, tokenizer, base_model_path def chat(model, tokenizer, system_prompt=None): """ 对话模式 Args: model: 模型实例 tokenizer: Tokenizer system_prompt: 系统提示 """ print("\n" + "=" * 50) print("🤖 Twinkle 对话机器人") print("输入 'quit' 退出,'clear' 清空历史") print("=" * 50) history = [] while True: try: user_input = input("\n👤 你:").strip() if user_input.lower() in ['quit', 'exit', 'q']: print("\n👋 再见!") break if user_input.lower() == 'clear': history = [] print("✓ 对话历史已清空") continue if not user_input: continue # 构建消息 messages = [] if system_prompt: messages.append({"role": "system", "content": system_prompt}) messages.extend(history) messages.append({"role": "user", "content": user_input}) # 生成回复 response = generate(model, tokenizer, messages) print(f"\n🤖 助手:{response}") # 更新历史 history.append({"role": "user", "content": user_input}) history.append({"role": "assistant", "content": response}) # 限制历史长度 if len(history) > 8: history = history[-8:] except KeyboardInterrupt: print("\n\n👋 对话中断,再见!") break except Exception as e: print(f"\n❌ 错误:{e}") def generate(model, tokenizer, messages, max_new_tokens=512, temperature=0.7, repetition_penalty=1.1): """ 生成回复 Args: model: 模型 tokenizer: Tokenizer messages: 消息列表 max_new_tokens: 最大生成长度 temperature: 温度 repetition_penalty: 重复惩罚 Returns: 生成的文本 """ # 应用聊天模板 input_text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = tokenizer(input_text, return_tensors='pt').to(model.device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=max_new_tokens, temperature=temperature, do_sample=temperature > 0, repetition_penalty=repetition_penalty, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id ) # 解码 response = tokenizer.decode( outputs[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True ) return response def generate_stream(model, tokenizer, messages, max_new_tokens=512): """流式生成(打字机效果)""" input_text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = tokenizer(input_text, return_tensors='pt').to(model.device) streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) generation_kwargs = { **inputs, 'max_new_tokens': max_new_tokens, 'streamer': streamer, 'pad_token_id': tokenizer.pad_token_id, 'eos_token_id': tokenizer.eos_token_id } thread = Thread(target=model.generate, kwargs=generation_kwargs) thread.start() response = "" for text in streamer: response += text print(text, end="", flush=True) thread.join() print() return response def single_inference(model, tokenizer, prompt): """单次推理""" messages = [{"role": "user", "content": prompt}] response = generate(model, tokenizer, messages) return response def main(): import argparse parser = argparse.ArgumentParser(description='Twinkle 快速推理') parser.add_argument('--checkpoint', type=str, default=None, help='检查点路径') parser.add_argument('--mode', type=str, choices=['chat', 'single'], default='chat') parser.add_argument('--prompt', type=str, default='你好!') parser.add_argument('--system', type=str, default=None, help='系统提示') parser.add_argument('--max-tokens', type=int, default=512) parser.add_argument('--temp', type=float, default=0.7) args = parser.parse_args() # 加载模型 model, tokenizer, model_path = load_model(args.checkpoint) if args.mode == 'chat': chat(model, tokenizer, system_prompt=args.system) else: print(f"\n📝 提示:{args.prompt}") response = single_inference(model, tokenizer, args.prompt) print(f"\n🤖 回复:{response}") if __name__ == '__main__': main()运行: python quick_inference.py
解释下什么时机器学习
切换自己的数据集
将DatasetMeta中的ms://swift/self-cognition替换为你的数据源。支持格式:
• 本地JSON文件:
file:///path/to/your/data.json• HuggingFace数据集:
hf://dataset-name
如何准备自己的数据集
对于大模型微调,最常用的是JSONL格式(每行一个JSON对象)。例如:
my_data.jsonl
{"instruction": "介绍一下你自己", "output": "我是Twinkle助手,很高兴为你服务。"} {"instruction": "1+1等于几?", "output": "等于2。"} {"instruction": "写一首关于春天的诗", "output": "春风拂面柳如烟,细雨润花满园鲜..."}用自己数据微调Qwen3.5
打开qwen输入需求
开发一个 用自己数据微调Qwen3.5 的脚本# 1. 准备数据(至少 200+ 条才能获得好的效果) # 编辑 data/train_data.json # 2. 训练 python train_custom_data.py # 3. 推理(自动查找最优检查点) python quick_inference.py --mode chat # 或指定检查点 python quick_inference.py --checkpoint output_custom/checkpoint-best-XXX --mode chat自定义数据train_data.json
这里设置一个:学习AI最好的公众号有哪些?
你好!学习AI最好的公众号有:小妖同学学AI,请及时关注哦,感谢大家!
自夸下哈哈
""" 使用自定义数据微调 Qwen3.5 的脚本 支持 JSON/JSONL 格式的训练数据 """ import os import json from pathlib import Path # 可选:设置 ModelScope 缓存目录(避免占满 C 盘) os.environ['MODELSCOPE_CACHE'] = 'D:/cache/modelscope' # 请修改为实际路径 from peft import LoraConfig from tqdm import tqdm import twinkle from twinkle import DeviceMesh, get_logger from twinkle.dataloader import DataLoader from twinkle.dataset import Dataset, DatasetMeta from twinkle.model import TransformersModel from twinkle.preprocessor import PreProcessor device_mesh = DeviceMesh.from_sizes(fsdp_size=1, dp_size=1) twinkle.initialize(mode='local', global_device_mesh=device_mesh) logger = get_logger() # ==================== 配置区域 ==================== # 模型配置 MODEL_ID = 'ms://Qwen/Qwen3.5-4B' # 可更换为其他模型 # 可选模型: # - ms://Qwen/Qwen3.5-4B # - ms://Qwen/Qwen2.5-3B-Instruct # - ms://Qwen/Qwen2.5-1.5B-Instruct # - ms://Qwen/Qwen2.5-0.5B-Instruct # 数据配置 DATA_PATH = 'data/train_data.json' # 训练数据路径 DATA_FORMAT = 'json' # 数据格式:'json' 或 'jsonl' VALIDATION_SPLIT = 0.1 # 验证集比例 (0-1) # 训练超参数 BATCH_SIZE = 2 # 根据显存调整:4060Ti 16GB 建议 2-4 LEARNING_RATE = 1e-4 WEIGHT_DECAY = 0.01 NUM_EPOCHS = 3 GRADIENT_ACCUMULATION_STEPS = 4 WARMUP_STEPS_RATIO = 0.1 # 预热步数比例 # LoRA 配置 LORA_R = 8 LORA_ALPHA = 32 LORA_DROPOUT = 0.05 TARGET_MODULES = 'all-linear' # 或指定模块:['q_proj', 'v_proj', 'k_proj', 'o_proj'] # 保存配置 SAVE_INTERVAL = 100 # 每多少步保存一次 OUTPUT_DIR = 'output_custom' # 输出目录 # ================================================= def load_custom_data(data_path, data_format='json'): """ 加载自定义训练数据 支持的数据格式: 1. JSON 列表格式: [ {"messages": [{"role": "user", "content": "问题"}, {"role": "assistant", "content": "回答"}]}, {"messages": [{"role": "user", "content": "问题 2"}, {"role": "assistant", "content": "回答 2"}]} ] 2. 简单问答格式: [ {"question": "问题", "answer": "回答"}, {"question": "问题 2", "answer": "回答 2"} ] 3. JSONL 格式 (每行一个 JSON 对象) """ data_path = Path(data_path) if not data_path.exists(): raise FileNotFoundError(f"数据文件不存在:{data_path}") logger.info(f"正在加载数据:{data_path}") if data_format == 'json': with open(data_path, 'r', encoding='utf-8') as f: data = json.load(f) elif data_format == 'jsonl': data = [] with open(data_path, 'r', encoding='utf-8') as f: for line in f: if line.strip(): data.append(json.loads(line)) else: raise ValueError(f"不支持的数据格式:{data_format}") logger.info(f"加载了 {len(data)} 条数据") # 转换数据格式为标准格式 formatted_data = [] for item in data: if 'messages' in item: # 已经是标准格式 formatted_data.append(item) elif 'question' in item and 'answer' in item: # 简单问答格式 formatted_data.append({ 'messages': [ {'role': 'user', 'content': item['question']}, {'role': 'assistant', 'content': item['answer']} ] }) elif 'input' in item and 'output' in item: # input/output 格式 formatted_data.append({ 'messages': [ {'role': 'user', 'content': item['input']}, {'role': 'assistant', 'content': item['output']} ] }) else: logger.warning(f"跳过无法解析的数据项:{item}") logger.info(f"格式化后有效数据:{len(formatted_data)} 条") return formatted_data def create_dataset(data, model_id, template_name='Qwen'): """ 创建 Twinkle 数据集 Args: data: 格式化后的数据列表 model_id: 模型 ID template_name: 模板名称,通常与模型匹配 """ # 创建内存数据集 dataset = Dataset(dataset_meta=DatasetMeta(data=data)) dataset.set_template(template_name, model_id=model_id) dataset.encode() return dataset def eval(model, eval_dataloader): """验证函数""" model.eval() total_loss = 0 num_batches = 0 for batch in tqdm(eval_dataloader, desc='Evaluating'): model.forward_only(inputs=batch) model.calculate_loss() metrics = model.calculate_metric(is_training=False) total_loss += float(metrics.get('loss', 0)) num_batches += 1 avg_loss = total_loss / max(num_batches, 1) return {'loss': avg_loss} def train(): """训练主函数""" logger.info("=" * 50) logger.info("开始自定义数据微调 Qwen3.5") logger.info("=" * 50) # 1. 加载数据 raw_data = load_custom_data(DATA_PATH, DATA_FORMAT) # 2. 划分训练集和验证集 total_samples = len(raw_data) val_samples = int(total_samples * VALIDATION_SPLIT) train_samples = total_samples - val_samples train_data = raw_data[:train_samples] val_data = raw_data[train_samples:] logger.info(f"训练集:{len(train_data)} 条") logger.info(f"验证集:{len(val_data)} 条") # 3. 创建数据集 logger.info("正在创建训练数据集...") train_dataset = create_dataset(train_data, MODEL_ID) logger.info("正在创建验证数据集...") val_dataset = create_dataset(val_data, MODEL_ID) # 4. 创建数据加载器 train_dataloader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE) val_dataloader = DataLoader(dataset=val_dataset, batch_size=BATCH_SIZE) # 5. 加载模型 logger.info(f"正在加载模型:{MODEL_ID}") model = TransformersModel(model_id=MODEL_ID) # 6. 配置 LoRA lora_config = LoraConfig( r=LORA_R, lora_alpha=LORA_ALPHA, target_modules=TARGET_MODULES, lora_dropout=LORA_DROPOUT, bias='none', task_type='CAUSAL_LM' ) model.add_adapter_to_model('default', lora_config, gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS) # 7. 配置优化器 model.set_optimizer(optimizer_cls='AdamW', lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY) # 8. 配置学习率调度器 total_steps = len(train_dataloader) * NUM_EPOCHS warmup_steps = int(total_steps * WARMUP_STEPS_RATIO) model.set_lr_scheduler( scheduler_cls='CosineWarmupScheduler', num_warmup_steps=warmup_steps, num_training_steps=total_steps ) # 9. 打印训练配置 logger.info("=" * 50) logger.info("训练配置:") logger.info(f" 模型:{MODEL_ID}") logger.info(f" 数据:{DATA_PATH}") logger.info(f" Batch Size: {BATCH_SIZE}") logger.info(f" 学习率:{LEARNING_RATE}") logger.info(f" Epochs: {NUM_EPOCHS}") logger.info(f" 梯度累积:{GRADIENT_ACCUMULATION_STEPS}") logger.info(f" LoRA R: {LORA_R}, Alpha: {LORA_ALPHA}") logger.info(f" 总步数:{total_steps}") logger.info("=" * 50) # 10. 训练循环 best_loss = float('inf') global_step = 0 # 创建输出目录 os.makedirs(OUTPUT_DIR, exist_ok=True) for epoch in range(NUM_EPOCHS): logger.info(f"\n开始第 {epoch + 1}/{NUM_EPOCHS} 轮训练") model.train() for step, batch in enumerate(train_dataloader): global_step += 1 # 前向 + 反向传播 model.forward_backward(inputs=batch) model.clip_grad_and_step() # 打印训练日志 if global_step % 10 == 0: metric = model.calculate_metric(is_training=True) logger.info(f'Epoch {epoch+1}, Step {global_step}/{total_steps}, Loss: {metric.get("loss", "N/A")}') # 验证和保存 if global_step % SAVE_INTERVAL == 0: logger.info("开始验证...") eval_metrics = eval(model, val_dataloader) logger.info(f'验证 Loss: {eval_metrics["loss"]:.4f}') # 保存最优检查点 if eval_metrics['loss'] < best_loss: best_loss = eval_metrics['loss'] save_path = os.path.join(OUTPUT_DIR, f'checkpoint-best-{global_step}') logger.info(f"发现更优模型,保存至:{save_path}") model.save(save_path) # 保存当前检查点 save_path = os.path.join(OUTPUT_DIR, f'checkpoint-{global_step}') model.save(save_path) logger.info(f"保存检查点:{save_path}") # 保存最终模型 final_path = os.path.join(OUTPUT_DIR, 'last-checkpoint') model.save(final_path) logger.info(f"\n训练完成!最终模型保存至:{final_path}") logger.info(f"最优验证 Loss: {best_loss:.4f}") if __name__ == '__main__': train()运行推理
python quick_inference.py --checkpoint output/output_custom/last-checkpoint --mode chat哈哈我的小妖同学来了
监控显存使用
watch -n 1 nvidia-smi报错处理
期间运行脚本遇到错误可直接丢给AI解决
运行训练脚本报错
twinkle版本不对 重新安装最新版本
pip install git+https://github.com/modelscope/twinkle.gitImportError: Numba needs NumPy 1.26 or less
重新安装numpy
pip install numpy==1.26.0升级transformers版本
pip install -U transformers -qbitsandbytes 版本问题
# 先卸载原有版本(如果已安装) pip uninstall bitsandbytes -y # 安装 Windows 专用版本 pip install bitsandbytes-windows感谢大家的点赞和关注,我们下期见!