大家好,我是刚入门 AI 大模型的小白,最近这段时间踩了无数的坑,终于把大模型从训练到部署再到智能体的整个流程走通了!之前看网上的教程要么太零散,要么全是专业术语看不懂,所以我把整个学习过程整理成了这篇保姆级教程,和大家一起从零开始学,不管你是刚接触 AI 的新手,还是想转大模型开发的同学,跟着这篇,就能走完大模型开发的全流程,做出自己的 AI 应用!全程小白友好,没有看不懂的黑话,手把手带你实操!
最近这两年,AI 大模型真的太火了,从 ChatGPT 到通义千问,从 LLaMA 到 Qwen,越来越多的大模型出来,但是很多同学和我一样,刚开始的时候,看着一堆术语:Deepspeed、LoRA、量化、MCP、Agent,头都大了,不知道从哪下手,觉得大模型开发是大佬才能做的事情,普通人碰不了。
但是真的是这样吗?其实不是的,现在大模型的开发工具已经越来越成熟了,普通人,只要有一张普通的游戏显卡,就能自己微调大模型,部署大模型,甚至做自己的智能体!
我花了一个月的时间,把整个流程走了一遍,从最开始的项目选型,到用 Deepspeed 加速训练,用 LoRA 微调模型,用 GPTQ 量化压缩,用 vLLM 部署服务,再到现在最火的 MCP 协议,做一个能帮你规划行程的智能导游,能查天气的智能助手,整个过程,没有用什么高端的服务器,就是普通的开发机,普通的显卡,就完成了。
所以这篇博客,我就把整个过程完完整整的记录下来,和大家一起学习,我们不仅要会用别人的大模型,还要会自己训练、自己部署、自己做智能体,真正的把大模型变成我们自己的工具!
在开始之前,我们先整体的了解一下,我们这个项目到底要做什么,有一个整体的认知,就像盖房子,先把图纸画好,再动手盖。
1.1 项目说明:解决什么问题?
随着信息爆炸时代的到来,我们每天都会面对大量的文本信息,比如新闻、财报、论文、会议记录,这么多信息,我们一个个看,根本看不完,怎么才能高效的提取关键信息,快速的理解这些内容?
这就是我们这个项目要解决的问题:做一个文本摘要工具,能自动的把长文本,提炼成简洁的摘要,帮我们快速消化信息。
比如,你看一篇 1 万字的财报,它能帮你提炼出核心的指标和风险;你看一篇长新闻,它能帮你总结出要点;你开完会,它能帮你把会议记录整理成待办事项,是不是很实用?
而且,我们不止做这个,我们还要做一个智能导游,能帮你规划出行行程,查路线,查餐厅,查天气,让大模型不再只是聊天,而是能真正的帮你处理实际的任务!
这个应用的场景非常广:
- 新闻媒体:自动生成新闻要点,提升内容分发效率,编辑不用自己写摘要了
- 金融分析:从财报、研报中提取关键指标与风险事件,帮分析师快速看报告
- 医疗健康:解析病历、文献,生成患者病情摘要,帮医生快速了解病情
- 法律文书:快速抽取案件核心信息,辅助法律检索,律师不用看一堆文书了
- 企业知识库:自动化归档会议记录、技术文档,企业的知识管理太方便了
1.2 技术路线:我们用什么技术?
整个项目的技术路线,我们选的是现在最成熟、对小白最友好的方案,从数据到部署,全流程打通:
1.2.1 模型选型:为什么选通义千问(Qwen)?
首先,模型我们选的是通义千问,也就是 Qwen,为什么选它?原因很简单:
- 中文支持最好:Qwen 是阿里的模型,对中文的理解比国外的模型好太多了,做中文的任务,效果比 LLaMA 好很多
- 开源生态完善:Qwen 完全开源,Apache 2.0 协议,商用也没问题,而且有非常完善的工具链,微调、量化、部署都有现成的工具
- 原生支持 MCP:最新的 Qwen3,原生就支持 MCP 协议,工具调用能力非常强,做智能体不用自己折腾
- 硬件要求低:Qwen 的优化做的很好,普通的显卡就能跑,7B 的模型,量化后只要 6G 显存,你的游戏显卡就能用
而且 Qwen 支持最长 1M 的上下文,也就是能处理几十万字的长文本,正好适合我们做长文本摘要,太合适了!
1.2.2 数据准备:怎么准备训练数据?
有了模型,我们需要数据来微调它,让它学会做摘要。
数据来源我们分两种:
- 公开数据集:比如 LCSTS 摘要数据集,还有中文的指令数据集,这些都是现成的,我们可以直接用
- 私有数据:如果是企业的话,还可以用自己脱敏后的内部文档,比如自己的会议记录、财报,训练出来的模型更贴合自己的业务
数据处理的话,我们要做:
- 清洗与标注:把没用的噪声数据去掉,然后标注好,哪个是指令,哪个是输入,哪个是输出
- 数据增强:如果数据不够的话,我们可以用回译、实体替换这些方法,扩充我们的训练样本,让模型泛化能力更强
1.2.3 模型微调:LoRA + DeepSpeed
数据准备好了,我们就要微调模型了,但是直接微调整个大模型,太吃显存了,7B 的模型,全量微调要 40 多 G 显存,普通人根本没有,所以我们用 LoRA+DeepSpeed 的方案:
- LoRA:低秩适配,简单说就是,我们把模型的大部分参数都冻住,只训练一小部分低秩矩阵,这样显存占用直接省了 40% 以上,而且训练速度快很多,小数据集也能训
- DeepSpeed:微软的训练加速工具,用 ZeRO 优化,把模型的参数、梯度、优化器状态,拆分到不同的设备上,进一步省显存,还能加速训练,单卡就能训 7B 的模型!
1.2.4 模型量化:LoraQ + GPTQ
训练完模型,我们要部署,但是原始的模型太大了,7B 的模型,FP16 的话要 14G,部署的时候显存不够,速度也慢,所以我们要量化:
- LoraQ:把我们训练的 LoRA 参数,量化成 4 位,压缩 75% 的体积,精度损失不到 2%
- GPTQ:把整个模型的权重量化成 4 位,这样推理速度能提升 2-3 倍,显存需求降低 60%,7B 的模型量化后只要 3.5G,普通显卡就能跑!
1.2.5 模型部署:vLLM 推理引擎
量化完模型,我们要把它部署成 API 服务,让别人能调用,这里我们用 vLLM,它是现在最快的推理引擎:
- 用 PagedAttention 技术,显存利用率特别高,吞吐量比原生的 Transformers 高 5 倍以上
- 支持 OpenAI 兼容的接口,部署完,你用 OpenAI 的 SDK 就能调用,和调用 GPT 一样,太方便了
- 平均推理延迟不到 500ms,用户用起来感觉不到卡顿
1.2.6 性能指标:我们做到了什么效果?
给大家看一下我们最终的效果对比,是不是很震撼:
你看,我们把显存从 16G 降到了 6G,速度提升了 2 倍多,但是精度几乎没降!这就是优化的力量!
1.3 模型说明:Qwen3 到底强在哪?
很多同学可能对 Qwen 不太了解,我给大家介绍一下,Qwen 现在已经到 3.0 版本了,也就是 Qwen3,这是今年刚出的,真的太强了。
我们先看看 Qwen 三代的演进:
你看,Qwen3 的上下文最长能到 1M tokens,也就是能处理几十万字的长文本,我们做长文本摘要,正好能用!而且它的训练数据是 36 万亿个 token,比 Qwen2.5 多了一倍,能力强了很多。
而且不同大小的模型,适合不同的场景,大家可以根据自己的需求选:
比如你只是做个简单的对话,用 1.8B 的就够了,手机都能跑;如果是做复杂的摘要,用 7B 的就够了,3090 就能跑;如果是要做高精度的代码生成,那就用 72B 的,用多卡。
而且 Qwen3 最厉害的,就是它的思考模式,它能在思考模式和非思考模式之间无缝切换:
- 思考模式:遇到复杂的问题,比如数学题、逻辑题,它会一步步推理,深思熟虑之后再给答案,就像 OpenAI 的 o1 一样
- 非思考模式:遇到简单的问题,比如聊天、摘要,它直接快速给答案,不用思考,速度很快
这就太灵活了,不管你是要速度还是要精度,它都能满足你!而且它原生就支持 MCP 协议,工具调用能力特别强,做智能体太合适了。
1.4 数据集说明:微调的数据长什么样?
很多小白同学,不知道微调大模型的数据要准备成什么格式,其实很简单,就是标准的指令微调格式,每个样本,就三个字段:
1.4.1 instruction(指令)
这个字段,就是告诉模型,你要让它做什么,比如:
“instruction”: “请提取以下内容中的摘要信息”
这个是固定的句式,你训练的时候用这个,以后用户用的时候,哪怕他说 “帮我总结一下这个”,模型也能知道,哦,这就是要做摘要,因为它已经学会了这个任务。
1.4.2 input(输入)
这个就是你要处理的原始文本,比如:
“input”: “冬季护肤五大步骤:
- 使用温和氨基酸洁面乳
- 早晚涂抹含神经酰胺面霜…”
就是你要让模型处理的原始内容,比如你要摘要的原文,就是放在这里。
1.4.3 output(输出)
这个就是你期望模型输出的结果,也就是标准答案,比如:
“output”: “温和洁面、保湿面霜、定期面膜、日常防晒、颈部防护”模型训练的时候,就是学习,看到前面的 instruction 和 input,就要输出这样的 output,多给它几个例子,它就学会这个任务了。
给大家看一个完整的样本例子:
{ “instruction”: “请提取以下内容中的摘要信息”, “input”: “保持身体健康的五个方法: - 每天至少饮用8杯水,促进新陈代谢
- 每周进行150分钟中等强度运动,如快走或游泳
- 保证7-9小时高质量睡眠,避免熬夜
- 饮食中增加蔬菜水果比例,减少油炸食品
- 定期体检,监测血压、血糖等指标”, “output”: “多喝水、规律运动、充足睡眠、均衡饮食、定期体检” }
是不是很简单?你准备数据的时候,就按这个格式准备就行,每个样本都是这样,攒个几千个,就能微调模型了,是不是比你想象的简单多了?
好了,项目我们了解了,接下来,我们进入第一个技术点:Deepspeed,这个是我们训练大模型的核心工具,很多小白刚开始训练大模型,第一个遇到的问题就是:显存不够!跑不动!这时候 Deepspeed 就来救场了!
2.1 什么是 Deepspeed?为什么我们需要它?
Deepspeed 是微软开发的一个开源库,专门为大规模模型训练设计的,简单说,它就是一个训练加速 + 显存优化的工具,能让你用更少的显存,训更大的模型,训练速度还更快!
我刚开始的时候,想训 7B 的 Qwen,我用的 3090 显卡,24G 显存,直接用原生的 PyTorch 训,刚跑起来,直接爆显存,OOM 错误,给我整懵了,后来用了 Deepspeed,直接就能跑了,而且速度还快了不少!
它到底能做到什么程度?比如,你有一张单卡的 V100,16G 显存,原来你最多训 1B 的模型,用了 Deepspeed 的 ZeRO-offload,你能训 130B 的模型!没错,130B!直接把模型的训练门槛降了 10 倍!这就是为什么现在普通人也能训大模型了,就是因为 Deepspeed!
而且它是基于 PyTorch 的,你原来的代码,只要改几行,就能用上 Deepspeed,不用重写,太方便了。
2.2 小白上手:Deepspeed 的安装和使用
2.2.1 安装 Deepspeed
安装特别简单,直接 pip 就行:
pip install deepspeed
安装完,验证一下:
deepspeed –version
如果输出版本号,就说明安装成功了,是不是很简单?
2.2.2 Deepspeed 的配置文件
Deepspeed 的配置,是一个 JSON 文件,你在里面写你的训练参数,比如我们常用的配置:
{ // 全局训练批次大小(所有设备的总和) “train_batch_size”: “auto”, // 单GPU的微批次大小(根据显存自动调整) “train_micro_batch_size_per_gpu”: “auto”, // 梯度累积步数(自动匹配micro_batch配置) “gradient_accumulation_steps”: “auto”, // 梯度裁剪阈值(自动禁用或设置默认1.0) “gradient_clipping”: “auto”, // 允许未经官方测试的优化器(需谨慎开启) “zero_allow_untested_optimizer”: true, // BF16混合精度配置(与FP16二选一) “bf16”: { “enabled”: “auto” // 在支持BF16的GPU上自动启用 }, // ZeRO优化策略(Stage3完整配置) “zero_optimization”: { “stage”: 3, // 最高优化等级(参数/梯度/优化器状态分片) // 优化器状态卸载到CPU “offload_optimizer”: { “device”: “cpu”, // 卸载到CPU内存 “pin_memory”: true // 使用锁页内存加速传输 }, // 模型参数卸载到CPU “offload_param”: { “device”: “cpu”, // 参数存储到CPU内存 “pin_memory”: true // 使用DMA加速数据传输 }, “overlap_comm”: false, // 禁用通信计算重叠(提升稳定性) “contiguous_gradients”: true, // 保持梯度内存连续(优化显存) // 参数分组配置 “sub_group_size”: 1e9, // 单参数组最大尺寸(默认1B防止分组) // 通信缓冲区自动调整 “reduce_bucket_size”: “auto”, // AllReduce缓冲区大小 “stage3_prefetch_bucket_size”: “auto”, // 参数预取缓冲区 // 参数持久化阈值 “stage3_param_persistence_threshold”: “auto”, // 参数驻留GPU的阈值 “stage3_max_live_parameters”: 1e9, // 最大驻留参数数量 “stage3_max_reuse_distance”: 1e9, // 参数重用距离阈值 // 模型保存时收集16位权重 “stage3_gather_16bit_weights_on_model_save”: true } }
很多小白同学,看到这么多配置,头都大了,不用怕,这些参数大部分你都不用改,设成 auto 就行,Deepspeed 会自动帮你调整,你只要改一个:stage,这个是 ZeRO 的阶段,我们后面讲。
2.2.3 怎么把 Deepspeed 集成到你的代码里?
集成也特别简单,只要改几行代码就行,给大家看一个最简单的例子:
# 导入DeepSpeed库 import deepspeed
# 原来的代码,定义你的模型,数据,优化器 model = FashionModel() optimizer = torch.optim.Adam(model.parameters(), lr=0.00015)
# 用deepspeed.initialize包装一下,就搞定了! model, optimizer, _, _ = deepspeed.initialize( args=cmd_args, # 命令行参数 model=model, # 你的模型 model_parameters=model.parameters() # 模型参数 )
# 然后训练的时候,原来的loss.backward(),改成model.backward() # 原来的optimizer.step(),改成model.step() loss = loss_fn(output, y) model.backward(loss) # 反向传播,Deepspeed自动处理 model.step() # 参数更新
就这么简单!你原来的训练代码,几乎不用改,加个初始化,把 backward 和 step 换一下,就用上 Deepspeed 的所有优化了!
然后启动训练的时候,不用 python train.py 了,用 deepspeed 命令启动:
deepspeed train.py –epoch 2 –deepspeed –deepspeed_config ds.json
就搞定了!是不是比你想象的简单太多了?
2.3 核心原理:Deepspeed 是怎么帮你省显存的?
很多同学可能好奇,Deepspeed 到底是怎么做到的,为什么能省这么多显存?其实核心就是两个东西:并行化策略,和 ZeRO 内存优化,我们一个个讲,小白也能看懂。
2.3.1 并行化策略:把大任务拆成小任务
训练大模型,最大的问题就是,模型太大,一个显卡装不下,那怎么办?拆了啊!把大任务拆成小任务,多个显卡一起干,这就是并行化。
Deepspeed 支持两种并行:数据并行和模型并行。
数据并行:把数据拆了
数据并行,最简单,就是把模型复制到每个显卡上,然后把你的训练数据,分成好几份,每个显卡算一份,最后把梯度合起来,更新模型。
比如你有 4 个显卡,你的 batch_size 是 32,那每个显卡算 8 个样本,算完梯度,大家凑一起,更新模型,这样速度就快了 4 倍。
但是这个方法,有个问题,每个显卡都要存一份完整的模型,所以如果模型太大,比如 7B 的模型,每个显卡都要存 14G,那你的显卡显存还是不够,对吧?所以这时候就需要模型并行。
模型并行:把模型拆了
模型并行,就是反过来,数据不用拆,把模型拆了,每个显卡存模型的一部分,这样每个显卡只要存一部分参数,就不用存整个模型了。
模型并行又分两种:流水线并行和张量并行。
流水线并行:按层拆
流水线并行,就是把模型的层,拆成好几份,比如你的模型有 24 层,前 12 层放 GPU0,后 12 层放 GPU1,这样每个显卡只要存 12 层的参数,就省了一半的显存。
但是原来的流水线并行,有个问题,就是 GPU 利用率很低,比如 GPU0 算前 12 层的时候,GPU1 在等着,GPU1 算后 12 层的时候,GPU0 在等着,这样一半的时间显卡都是闲着的,浪费了。
所以 Deepspeed 用了 GPipe 的优化,把 batch 拆成小的 micro-batch,这样就能流水线起来了,比如第一个 micro-batch 到 GPU0 算,算完给 GPU1,这时候 GPU0 就可以算第二个 micro-batch 了,这样两个显卡就能同时工作,利用率就上去了,就能跑满显卡。
张量并行:按矩阵拆
张量并行,就是把矩阵拆了,比如你有一个大的矩阵乘法,Y = X * W,这个 W 太大了,一个显卡装不下,那我就把 W 拆成好几块,每个显卡算一块,最后合起来,这样每个显卡只要存一部分 W,就省显存了。
比如 MLP 层,原来的权重是一个大矩阵,我把它拆成左右两块,每个显卡算一块,最后合起来,这样就搞定了,这个就是张量并行,适合那种特别大的矩阵。
2.3.2 ZeRO 优化:把冗余的东西都去掉
并行化解决了大模型的问题,但是还有个问题,数据并行的时候,每个显卡都要存一份参数、梯度、优化器状态,这就有很多冗余,比如 4 个显卡,就存了 4 份参数,这太浪费了!
ZeRO 就是来解决这个冗余的,Zero Redundancy Optimizer,零冗余优化器,它把这些状态,都拆分到不同的显卡上,每个显卡只存一部分,这样就没有冗余了,显存就省下来了!
ZeRO 分三个阶段,越来越强:
- ZeRO-1:只拆分优化器状态,原来每个显卡都存一份优化器的动量、方差,现在拆分了,每个显卡存一部分,这样能省一半的显存
- ZeRO-2:拆分优化器状态 + 梯度,不仅优化器状态,梯度也拆分,这样又省了一部分,显存又少了
- ZeRO-3:三个都拆分!优化器状态、梯度、模型参数,都拆分,每个显卡只存 1/N 的所有状态,N 是显卡的数量,这样的话,不管你的模型多大,只要显卡够,就能训!
而且 ZeRO 还支持 offload,就是把不用的参数,放到 CPU 内存里,要用的时候再拿过来,这样就算你只有一个显卡,也能训大模型,比如单卡 3090,24G 显存,用了 ZeRO-3+offload,就能训 70B 的模型!我的天,这也太猛了!
这就是为什么 Deepspeed 这么强,原来你要 8 张 A100 才能训的模型,现在你一张 3090 就能训了,门槛直接降没了!
2.4 混合精度训练:更快更小还不失真
除了这些,Deepspeed 还支持混合精度训练,什么意思?就是我们不用 32 位的浮点数来训练,用 16 位的,这样显存又能省一半,速度还能快一倍!
很多小白担心,用 16 位的,精度会不会丢?其实不会,因为混合精度训练,会用 FP32 来存参数,用 FP16 来计算,既保证了精度,又提升了速度,省了显存。
不同的精度,我们来对比一下:
所以现在我们训练大模型,一般都用 BF16,速度快,显存省,精度还够,完美。
好了,Deepspeed 我们了解了,接下来,我们就要真正的训练模型了,很多小白觉得训练大模型很复杂,要写很多代码,其实不用,现在有 LLaMA-Factory 这个工具,一键就能微调大模型,不用写代码,配置一下就行!
3.1 什么是 LLaMA-Factory?为什么用它?
LLaMA-Factory 是现在最火的大模型微调框架,简单说,它把所有的微调方法,都给你封装好了,你不用自己写 LoRA,不用自己写 Deepspeed,不用自己写量化,只要改个配置文件,就能一键训练,支持上百种模型,Qwen、LLaMA、Mistral 都支持,太方便了!
它的特性真的太多了:
- 支持各种训练算法:全量微调、LoRA、QLoRA、DPO、ORPO,你想要的都有
- 支持各种量化:4 位、8 位,GPTQ、AWQ 都支持
- 支持各种加速:FlashAttention、Unsloth,速度拉满
- 支持各种模型:几乎所有的主流大模型,都支持,不用你自己适配
我们用它来微调 Qwen,真的太简单了,几步就搞定。
3.2 安装 LLaMA-Factory
安装也很简单,直接从 GitHub 拉下来:
git clone –depth 1 https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e “.[torch,metrics]” -i https://mirrors.aliyun.com/pypi/simple/
安装完,验证一下:
llamafactory-cli version
如果输出欢迎信息,就说明安装成功了,是不是很简单?
3.3 准备训练数据
数据我们之前说了,就是按指令格式准备,我们准备一个 json 文件,比如叫qwen_dataset.json,内容就是我们之前说的那样:
[ { “instruction”: “请提取以下内容中的摘要信息”, “input”: “保持身体健康的五个方法:
- 每天至少饮用8杯水,促进新陈代谢
- 每周进行150分钟中等强度运动,如快走或游泳
- 保证7-9小时高质量睡眠,避免熬夜
- 饮食中增加蔬菜水果比例,减少油炸食品
- 定期体检,监测血压、血糖等指标”, “output”: “多喝水、规律运动、充足睡眠、均衡饮食、定期体检” }, { “instruction”: “请提取以下内容中的摘要信息”, “input”: “提高学习效率的三个技巧:
- 使用番茄工作法,每25分钟专注后休息5分钟
- 建立思维导图整理知识框架
- 睡前复习重点内容加强记忆”, “output”: “番茄工作法、思维导图、睡前复习” } ]
然后,我们要把这个数据注册到 LLaMA-Factory 里,找到 LLaMA-Factory 下的
data/dataset_info.json,在最后加一行:“qwen_dataset”: ,这样,LLaMA-Factory 就认识我们的数据集了。
3.4 三种微调方式:选哪个适合你?
很多小白问,微调有全量、LoRA、QLoRA,我该选哪个?别急,我一个个给你讲,你就知道了。
3.4.1 全量微调:土豪专属
全量微调,就是把模型的所有参数,都重新训练,这样效果最好,但是最吃显存,7B 的模型,全量微调要 42G 的显存,你得有 A100 才能跑,普通人就别想了,除非你有服务器。
它的配置文件是这样的:
方法配置 method: # 微调阶段:监督式微调 (Supervised Fine-Tuning) stage: sft # 是否执行训练阶段 do_train: true # 微调类型:全参数微调(可选值:full/lora/qlora) finetuning_type: full # DeepSpeed配置文件路径(使用ZeRO Stage 3优化策略) deepspeed: /gemini/code/LLaMA-Factory/examples/deepspeed/ds_z3_config.json 数据集配置 dataset: # 使用的数据集名称(需与data目录下的数据集名称对应) dataset: qwen_dataset # 使用的模板格式(与模型架构匹配) template: qwen # 输入序列最大长度(单位:token) cutoff_len: 1024 # 是否覆盖已有的缓存文件(建议数据集修改后启用) overwrite_cache: true # 数据预处理的并行进程数(建议设置为CPU核心数的50-70%) preprocessing_num_workers: 16 输出配置 output: # 模型和日志的输出目录 output_dir: saves # 每隔多少训练步记录一次日志 logging_steps: 10 # 每隔多少训练步保存一次模型 save_steps: 100 # 是否生成训练损失曲线图 plot_loss: true # 是否覆盖已有输出目录(建议新训练时启用) overwrite_output_dir: true 训练参数 train: # 每个GPU的批次大小(实际batch_size = 此值 * gradient_accumulation_steps * GPU数量) per_device_train_batch_size: 1 # 梯度累积步数(用于模拟更大batch_size) gradient_accumulation_steps: 16 # 初始学习率(适合7B级别模型的典型值) learning_rate: 1.0e-5 # 训练总轮数 num_train_epochs: 1.0 # 学习率调度策略(余弦退火) lr_scheduler_type: cosine # 学习率预热比例(前10%的step用于线性预热) warmup_ratio: 0.1 # 启用BF16混合精度训练(需要Ampere架构以上GPU) bf16: true # 分布式训练超时时间(单位:毫秒) ddp_timeout: # 约50小时 评估配置 eval: # 验证集划分比例(从训练集划分) val_size: 0.1 # 评估时每个GPU的批次大小 per_device_eval_batch_size: 1 # 评估策略(按训练步数间隔评估) eval_strategy: steps # 每隔多少训练步执行一次评估 eval_steps: 500然后启动训练:
FORCE_TORCHRUN=1 llamafactory-cli train qwen2-7b-full-sft.yaml这个就是全量微调,适合有高端显卡的同学,普通人我们看下面的。
3.4.2 LoRA 微调:普通人的首选
LoRA,低秩适配,这个是现在最常用的微调方式,它把模型的大部分参数都冻住,只训练一小部分低秩矩阵,这样显存占用直接降到 20G,你的 3090 就能跑了!而且训练速度快,小数据集也能训,效果还很好。
它的配置文件:
模型配置 model: # 预训练模型的本地路径或HuggingFace模型ID model_name_or_path: Qwen/Qwen2-7B # 必须开启以加载包含自定义代码的模型(如Qwen/ChatGLM等) trust_remote_code: true 训练方法 method: # 训练阶段:监督式微调(Supervised Fine-Tuning) stage: sft # 是否启用训练模式 do_train: true # 微调类型:LoRA(低秩适配) finetuning_type: lora # LoRA作用的目标层(all表示所有线性层) lora_target: all # LoRA的秩(矩阵分解维度) lora_rank: 16 # LoRA的α值(缩放因子,通常等于rank) lora_alpha: 16 # LoRA层的dropout率(防止过拟合) lora_dropout: 0.05 数据集配置 dataset: # 使用的数据集名称(对应data目录下的数据集文件夹) dataset: qwen_dataset # 使用的模板格式(需与模型匹配,如qwen/llama/chatglm) template: qwen # 输入序列最大长度(单位:token) cutoff_len: 1024 # 是否覆盖已有的预处理缓存 overwrite_cache: true # 数据预处理的并行进程数(建议设置为CPU核心数的50-70%) preprocessing_num_workers: 16 输出配置 output: # 模型和日志的输出目录 output_dir: saves/qwen2-7b/lora/sft # 每隔100训练步记录一次日志 logging_steps: 100 # 每隔100训练步保存一次模型 save_steps: 100 # 是否生成训练损失曲线图 plot_loss: true # 是否覆盖已有输出目录(新训练时建议开启) overwrite_output_dir: true 训练参数 train: # 每个GPU的批次大小(实际总batch_size = 此值 * gradient_accumulation_steps * GPU数) per_device_train_batch_size: 1 # 梯度累积步数(用于模拟更大batch_size,此处等效总batch_size=16*GPU数) gradient_accumulation_steps: 16 # 初始学习率(LoRA微调的典型学习率范围:1e-4 ~ 5e-4) learning_rate: 1.0e-4 # 训练总轮数 num_train_epochs: 1.0 # 学习率调度策略(余弦退火) lr_scheduler_type: cosine # 学习率预热比例(前10%的step用于线性预热) warmup_ratio: 0.1 # 启用BF16混合精度(需Ampere架构以上GPU,如A100/3090) bf16: true # 分布式训练超时时间(单位:毫秒,此处约50小时) ddp_timeout: 评估配置 eval: # 验证集划分比例(从训练集划分10%作为验证集) val_size: 0.1 # 评估时每个GPU的批次大小 per_device_eval_batch_size: 1 # 评估策略:按训练步数间隔评估 eval_strategy: steps # 每隔500训练步执行一次验证 eval_steps: 500启动训练:
llamafactory-cli train qwen2-7b-lora-sft.yaml这个就是 LoRA,大部分同学用这个就够了,如果你显卡更小,比如只有 16G 显存,那我们还有 QLoRA。
3.4.3 QLoRA 微调:单卡 10G 就能训!
QLoRA,就是量化的 LoRA,它先把模型量化成 4 位,然后再微调,这样显存直接降到 11G!你的 3060,16G 显存,都能训 7B 的模型!这也太猛了!
很多小白问,量化了再微调,效果会不会差?不会的,QLoRA 的效果,和全量微调几乎一样,但是显存省了 4 倍!
那 QLoRA 的原理是什么?我给大家通俗的讲一下:
首先,量化,就是把模型的参数,从 32 位的浮点数,改成 4 位的整数,这样就省了 8 倍的空间,但是量化会有误差,所以 QLoRA 用了几个技术来减少误差:
- 4 位 NormalFloat 量化:我们知道,模型的参数,都是符合正态分布的,0 附近的多,远的少,所以 NF4 量化,就不是均匀的量化,而是按正态分布的概率来量化,0 附近的分的更细,远的分的更粗,这样误差就小了很多
- 双重量化:第一次量化的那些常数,我们也量化一下,再省一点显存
- 分页优化:如果显存不够,就把不用的参数放到 CPU 内存里,要用的时候再拿过来,就像 ZeRO 的 offload 一样
这样,就做到了,4 位的量化,精度几乎没损失,显存省了 8 倍!
QLoRA 的配置文件:
模型配置 model: # 预训练模型的本地路径或HuggingFace模型ID(需确保路径正确) model_name_or_path: Qwen/Qwen2-7B # 必须开启以加载包含自定义代码的模型(如Qwen/ChatGLM等) trust_remote_code: true 训练方法 method: # 训练阶段:监督式微调(Supervised Fine-Tuning) stage: sft # 是否启用训练模式 do_train: true # 微调类型:QLoRA(量化低秩适配) finetuning_type: lora # QLoRA作用的目标层(all表示所有线性层) lora_target: all # 量化位数(4-bit量化) quantization_bit: 4 # 量化方法(使用bitsandbytes库实现) quantization_method: bitsandbytes # QLoRA的秩(矩阵分解维度) lora_rank: 16 # QLoRA的α值(缩放因子,通常等于rank) lora_alpha: 16 # QLoRA层的dropout率(防止过拟合) lora_dropout: 0.05 数据集配置 dataset: # 使用的数据集名称(对应data目录下的数据集文件夹) dataset: qwen_dataset # 使用的模板格式(需与模型架构匹配) template: qwen # 输入序列最大长度(单位:token) cutoff_len: 1024 # 是否覆盖已有的预处理缓存(数据集修改后需启用) overwrite_cache: true # 数据预处理的并行进程数(建议设置为CPU核心数的50-70%) preprocessing_num_workers: 16 输出配置 output: # 模型和日志的输出目录(QLoRA检查点保存路径) output_dir: saves/qwen2-7b/qlora/sft # 每隔100训练步记录一次日志 logging_steps: 100 # 每隔100训练步保存一次模型 save_steps: 100 # 是否生成训练损失曲线图(保存在output_dir/loss.png) plot_loss: true # 是否覆盖已有输出目录(新训练时建议开启) overwrite_output_dir: true 训练参数 train: # 每个GPU的批次大小(实际总batch_size = 此值 * gradient_accumulation_steps * GPU数) per_device_train_batch_size: 1 # 梯度累积步数(用于模拟更大batch_size,此处等效总batch_size=16*GPU数) gradient_accumulation_steps: 16 # 初始学习率(QLoRA典型学习率范围:1e-4 ~ 5e-4) learning_rate: 1.0e-4 # 训练总轮数 num_train_epochs: 1.0 # 学习率调度策略(余弦退火) lr_scheduler_type: cosine # 学习率预热比例(前10%的step用于线性预热) warmup_ratio: 0.1 # 启用BF16混合精度(需Ampere架构以上GPU,如A100/3090) bf16: true # 分布式训练超时时间(单位:毫秒,此处约50小时) ddp_timeout: 评估配置 eval: # 验证集划分比例(从训练集划分10%作为验证集) val_size: 0.1 # 评估时每个GPU的批次大小 per_device_eval_batch_size: 1 # 评估策略:按训练步数间隔评估 eval_strategy: steps # 每隔500训练步执行一次验证 eval_steps: 500启动训练:
llamafactory-cli train qwen2-7b-qlora-sft.yaml你看,这个就是 QLoRA,我们实测下来,7B 的模型,QLoRA 训练,显存只用了 10.97G!比 LoRA 的 20G 省了一半,比全量的 42G 省了太多了!普通的 3060,12G 显存,都能跑!
这就是为什么现在普通人也能微调大模型了,就是因为 QLoRA+Deepspeed,把门槛降的太低了!
3.5 合并权重:把 LoRA 的参数合到模型里
训练完 LoRA 或者 QLoRA,你会发现,保存的模型只有几十 M,因为它只保存了我们训练的那一小部分 LoRA 参数,原来的模型参数没保存,所以我们要把它合并到原来的模型里,才能用。
合并也很简单,用 LLaMA-Factory 的 export 命令:
llamafactory-cli export qwen2-7b-merge-lora.yaml配置文件就是指定原来的模型,还有 LoRA 的路径,导出的路径,就搞定了,合并完,就是一个完整的模型了,可以直接用来部署。
3.6 DPO 训练:让你的模型更符合人类偏好
很多同学,做完 SFT 微调,发现模型的回答,还是有点生硬,或者不符合人类的偏好,比如,它会说太多没用的套话,或者回答的逻辑不对,这时候,我们就需要做对齐训练,让模型的回答,更符合人类的偏好,更自然,更有用。
现在最常用的对齐方法,就是 DPO,Direct Preference Optimization,直接偏好优化,它比原来的 RLHF 简单太多了,不用你训练奖励模型,不用你做强化学习,只要你有偏好数据,也就是,同一个问题,一个好的回答,一个坏的回答,就能训练,而且效果特别好!
DPO 就不一样了,它直接用偏好数据,来训练模型,不用奖励模型,不用 PPO,直接把人类的偏好,融入到模型里,一步到位,而且效果和 RLHF 一样好,训练速度快,显存占用也低,所以现在大家都用 DPO 来做对齐。
LLaMA-Factory 也原生支持 DPO 训练,配置也很简单,只要改几个参数:
模型配置 model: model_name_or_path: Qwen/Qwen2-7B trust_remote_code: true 训练方法 method: stage: dpo # 改成DPO阶段 finetuning_type: lora lora_target: all lora_rank: 16 lora_alpha: 16 数据集配置 dataset: dataset: your_preference_dataset # 你的偏好数据集 template: qwen 训练参数 train: learning_rate: 1e-5 num_train_epochs: 1 bf16: true你的偏好数据,格式也很简单,就是同一个 prompt,两个回答,一个 chosen,一个 rejected,比如:
{ “prompt”: “北京今天天气怎么样?”, “chosen”: “北京今天18度,晴,适合出门”, “rejected”: “我不知道,你自己查” }就这么简单,模型训练的时候,就会学习,要生成 chosen 的回答,不要生成 rejected 的,训练完,模型的回答,就会更自然,更符合人类的偏好,是不是很简单?
而且 DPO 的显存占用也很低,7B 的模型,QLoRA 的 DPO,只要 12G 显存,你的 3060 就能跑,普通人也能做对齐,太方便了!
训练完模型,我们要部署了,但是原始的模型太大了,7B 的模型,FP16 要 14G,部署的时候,显存不够,速度也慢,所以我们要量化,把模型变小,变快,还不损失精度。
4.1 什么是量化?为什么要量化?
量化,简单说,就是把模型里的参数,从高精度的浮点数,改成低精度的整数,比如从 32 位改成 4 位,这样模型的体积就变小了 8 倍,显存占用也小了 8 倍,推理速度还能快 2-3 倍!
很多小白担心,量化了,模型会不会变笨?其实不会的,现在的量化技术,已经很成熟了,比如 GPTQ,4 位的量化,精度损失不到 1%,几乎感觉不到,但是模型小了 8 倍,速度快了 3 倍,太值了!
量化分两种:
- 训练中量化(QAT):训练的时候就做量化,这个效果好,但是麻烦,要重新训练
- 后量化(PTQ):训练完了再量化,不用重新训练,只要一点校准数据就行,这个就是我们用的 GPTQ,简单方便,效果还好
4.2 GPTQ 量化:现在最火的后量化方法
GPTQ,就是现在最常用的后量化方法,它的原理是什么?我给大家通俗的讲一下:
GPTQ 是逐层量化的,就是一层一层的处理模型,每一层,它用一小部分校准数据,来调整量化的参数,让量化后的输出,和原来的输出,误差最小,这样就能保证,量化完,模型的效果几乎不变。
简单说,就是,我量化这一层的时候,我拿几个样本跑一下,看看量化之后,输出差了多少,然后调整一下量化的参数,把误差补回来,这样就把误差降到最小了,所以效果特别好。
4.3 实操:用 GPTQ 量化我们的 Qwen 模型
好了,原理我们懂了,接下来实操,怎么用 GPTQ 量化我们的模型。
首先,安装依赖:
pip install auto-gptq optimum -i https://mirrors.aliyun.com/pypi/simple/
然后,我们写一个量化的脚本,很简单:
import argparse import json from typing import Dict import logging import torch import transformers from transformers import AutoTokenizer from transformers.trainer_pt_utils import LabelSmoother from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
# 定义一个常量 IGNORE_TOKEN_ID,用于在目标序列中表示忽略的标记 IGNORE_TOKEN_ID = LabelSmoother.ignore_index
def preprocess( sources, # 原始对话数据列表 tokenizer: transformers.PreTrainedTokenizer,# 使用的分词器 max_len: int,# 输入的最大长度 system_message: str = “You are a helpful assistant.” ) -> Dict:
""" 预处理对话数据,将其转换为模型可以理解的格式。 """ # 定义不同角色的前缀 roles = {"user": "<|im_start|>user", "assistant": "<|im_start|>assistant"} # 获取特殊标记的ID im_start = tokenizer.im_start_id im_end = tokenizer.im_end_id nl_tokens = tokenizer('
‘).input_ids # 获取换行符的输入ID
# 将系统、用户和助手的标记转换为输入ID _system = tokenizer('system').input_ids + nl_tokens _user = tokenizer('user').input_ids + nl_tokens _assistant = tokenizer('assistant').input_ids + nl_tokens # 初始化预处理后的数据列表 data = [] # 遍历每个对话 for i, source in enumerate(sources): source = source["conversations"] # 如果第一个元素不是用户,跳过它 if roles[source[0]["from"]] != roles["user"]: source = source[1:] # 初始化当前对话的输入ID和目标ID input_id, target = [], [] # 构建系统消息的输入ID和目标ID system = [im_start] + _system + tokenizer(system_message).input_ids + [im_end] + nl_tokens input_id += system target += [im_start] + [IGNORE_TOKEN_ID] * (len(system)-3) + [im_end] + nl_tokens # 确保输入ID和目标ID的长度相同 assert len(input_id) == len(target) # 遍历对话中的每个句子 for j, sentence in enumerate(source): role = roles[sentence["from"]] # 构建当前句子的输入ID _input_id = tokenizer(role).input_ids + nl_tokens + tokenizer(sentence["value"]).input_ids + [im_end] + nl_tokens input_id += _input_id # 根据角色构建目标ID if role == '<|im_start|>user': _target = [im_start] + [IGNORE_TOKEN_ID] * (len(_input_id)-3) + [im_end] + nl_tokens elif role == '<|im_start|>assistant': _target = [im_start] + [IGNORE_TOKEN_ID] * len(tokenizer(role).input_ids) + _input_id[len(tokenizer(role).input_ids)+1:-2] + [im_end] + nl_tokens else: raise NotImplementedError target += _target # 确保输入ID和目标ID的长度相同 assert len(input_id) == len(target) # 将输入ID转换为张量,并截断到最大长度 input_id = torch.tensor(input_id[:max_len], dtype=torch.int) # 计算注意力掩码,非填充符的位置为1 attention_mask = input_id.ne(tokenizer.pad_token_id) # 将预处理后的数据添加到列表中 data.append(dict(input_ids=input_id, attention_mask=attention_mask)) return data
if name == “main”:
# 创建命令行参数解析器 parser = argparse.ArgumentParser("Model Quantization using AutoGPTQ") # 添加命令行参数 parser.add_argument("--model_name_or_path", type=str, help="model path",default='/gemini/code/LLaMA-Factory/models/qwen2-7b-sft-lora-merged') parser.add_argument("--data_path", type=str, help="calibration data path",default='/gemini/data-1/Belle_sampled_qwen.json') parser.add_argument("--out_path", type=str, help="output path of the quantized model",default='/gpt_out') parser.add_argument("--max_len", type=int, default=8192, help="max length of calibration data") parser.add_argument("--bits", type=int, default=4, help="the bits of quantized model. 4 indicates int4 models.") parser.add_argument("--group-size", type=int, default=128, help="the group size of quantized model") # 解析命令行参数 args = parser.parse_args() # 创建量化配置 quantize_config = BaseQuantizeConfig( bits=args.bits, group_size=args.group_size,# 指定量化时使用的组大小。组量化是一种技术,它将模型中的多个权重组合在一起进行量化,以减少模型大小并提高计算效率 damp_percent=0.01, #用于在量化过程中控制权重的调整程度。较高的值可以减少量化带来的影响 desc_act=False, # 设置为 False 可以显著加快推理速度,但困惑度可能会略有下降 static_groups=False, # 是否在量化过程中使用静态量化。如果设置为 True,则在量化过程中组不会被动态调整 sym=True, # 对称性。控制量化是否是对称的,可以减少量化误差 true_sequential=True, # 控制量化过程中是否考虑权重的顺序。如果设置为 True,则量化过程会考虑权重的顺序,这可能会提高量化后的模型精度 model_name_or_path=None, model_file_base_name="model" ) # 从预训练模型路径加载分词器 tokenizer = AutoTokenizer.from_pretrained(args.model_name_or_path, trust_remote_code=True) # 设置分词器的填充符ID为结束符ID tokenizer.pad_token_id = tokenizer.eod_id # 使用预处理函数处理数据 data = preprocess(json.load(open(args.data_path)), tokenizer, args.max_len) # 从预训练模型路径加载模型,并应用量化配置 model = AutoGPTQForCausalLM.from_pretrained(args.model_name_or_path, quantize_config, device_map="auto", trust_remote_code=True) # 设置日志记录配置 logging.basicConfig( format="%(asctime)s %(levelname)s [%(name)s] %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S" ) # 使用处理后的数据对模型进行量化 model.quantize(data, cache_examples_on_gpu=False) # 保存量化后的模型到指定路径 model.save_quantized(args.out_path, use_safetensors=True) # 同时保存分词器文件到同一路径 tokenizer.save_pretrained(args.out_path)
这个脚本,做了什么?
- 预处理校准数据,把我们的校准数据,处理成模型能接受的格式
- 加载原来的模型,还有量化的配置
- 运行量化,一层一层的处理模型
- 保存量化后的模型
然后我们运行这个脚本,就能看到量化的过程了:
2025-05-09 22:59:17,329] [INFO] [real_accelerator.py:203:get_accelerator] Setting ds_accelerator to cuda (auto detect) Loading checkpoint shards: 100%|█████████████████████████████████████████████████████████████| 2⁄2 [00:13<00:00, 6.83s/it] INFO - Start quantizing layer 1⁄24 2025-05-09 22:59:49 INFO [auto_gptq.modeling._base] Start quantizing layer 1⁄24 INFO - Quantizing attn.c_attn in layer 1⁄24… 2025-05-09 22:59:50 INFO [auto_gptq.modeling._base] Quantizing attn.c_attn in layer 1⁄24… 2025-05-09 22:59:52 INFO [auto_gptq.quantization.gptq] duration: 1.91943 2025-05-09 22:59:52 INFO [auto_gptq.quantization.gptq] avg loss: 0. INFO - Quantizing attn.c_proj in layer 1⁄24… … 2025-05-09 23:08:37 INFO [auto_gptq.modeling._utils] transformer.h.23.mlp.c_proj 2025-05-09 23:08:37 INFO Model packed.
它会一层一层的量化,24 层的 7B 模型,大概 10 分钟就量化完了,量化完,模型就只有 3.5G 了,原来的 14G,直接变成 3.5G,小了 4 倍!
然后我们测试一下量化后的模型:
# 导入Hugging Face模型和分词器的自动加载类 from transformers import AutoModelForCausalLM, AutoTokenizer # 导入生成配置类(当前代码未直接使用,但建议保留以备扩展) from transformers.generation import GenerationConfig
# 加载分词器(从本地目录/gemini/code/gpt_out) # trust_remote_code=True 允许执行模型自定义代码(如特殊分词规则) tokenizer = AutoTokenizer.from_pretrained(“/gemini/code/gpt_out”, trust_remote_code=True) # 加载因果语言模型(从相同目录) # device_map=“auto” 自动分配模型到可用设备(GPU/CPU) # trust_remote_code=True 启用自定义模型架构加载 # .eval() 将模型设置为评估模式(禁用dropout等训练专用层) model = AutoModelForCausalLM.from_pretrained( “/gemini/code/gpt_out”, device_map=“auto”, trust_remote_code=True ).eval()
# 执行对话生成 # tokenizer: 加载的分词器实例 # “你好”: 输入的用户对话内容 # history=None: 清空对话历史(开始新对话) response, history = model.chat(tokenizer, “你好”, history=None) # 打印模型生成的响应 print(response) # 示例输出可能为:“你好!有什么我可以帮助您的吗?”
你看,是不是和原来的模型一样,回答正常,效果一点没差,但是模型小了 4 倍,显存占用只有 6G,普通的显卡就能跑了!
量化完模型,我们要部署了,把它做成一个 API 服务,让别人能调用,就像 OpenAI 的 API 一样,发个请求,就能得到结果。
5.1 什么是 vLLM?为什么用它?
部署大模型,原来我们用原生的 Transformers,但是速度太慢了,并发高了就卡,显存利用率也低,这时候 vLLM 就来了,它是现在最快的大模型推理引擎,没有之一!
vLLM 是加州大学伯克利分校开发的,它用了一个叫 PagedAttention 的技术,把 KV Cache 的管理做的特别好,显存利用率能到 90% 以上,吞吐量是原生 Transformers 的 5 倍!
什么是 KV Cache?我给大家通俗的讲一下,大模型推理的时候,分两个阶段:
- Prefill 阶段:把你的输入 prompt,整个跑一遍,算出 K 和 V,存起来
- Decode 阶段:一个 token 一个 token 的生成输出,每生成一个,就把 K 和 V 存起来,下次就不用重新算了
所以,这些 K 和 V,就是 KV Cache,它占了推理的时候大部分的显存,原来的框架,都是静态的给每个请求分配显存,要么不够,要么浪费,碎片很多,显存利用率很低。
vLLM 的 PagedAttention,就借鉴了操作系统的虚拟内存,把 KV Cache 分成小块,动态的分配,就像内存分页一样,这样就没有碎片了,显存利用率直接拉满,吞吐量就上去了!
而且 vLLM 支持 OpenAI 兼容的接口,你部署完,用 OpenAI 的 SDK 就能调用,和调用 GPT 一样,不用改代码,太方便了!
5.2 安装 vLLM
安装特别简单,直接 pip:
pip install vllm
就这么一行,搞定,它会自动帮你装所有的依赖。
5.3 先测试一下:用 vLLM 跑推理
安装完,我们先测试一下,用 vLLM 跑我们的模型,看看效果:
from vllm import LLM, SamplingParams
# 输入几个问题 prompts = [ “总结下面这段文本的摘要,随着科技的飞速发展,我们的生活方式也在悄然改变。智能手机、人工智能、物联网等科技产品的出现,为我们的日常生活带来了更多便利和舒适。比如,我们可以通过智能手机随时随地地获取信息,控制家庭设备,同时感受着人工智能为我们带来的智能化之便。但是,科技进步带来的便利也会对生活形成某种影响,比如,冲击传统行业和职业,改变人们的生产和消费模式。” ]
# 设置初始化采样参数 sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=100)
# 加载模型,确保路径正确 llm = LLM(model=“/gemini/pretrain2/Qwen1-8/”, trust_remote_code=True)
# 展示输出结果 outputs = llm.generate(prompts, sampling_params)
# 打印输出结果 for output in outputs: prompt = output.prompt generated_text = output.outputs[0].text print(f“Prompt: {prompt!r}, Generated text: {generated_text!r}”)
运行这个,你就能看到,速度特别快,输入 400 多 token,输出 100 个 token,只用了 0.5 秒,比原生的 Transformers 快了 10 倍都不止!
5.4 部署 API 服务:让别人能调用
测试完,我们就可以部署 API 服务了,vLLM 自带了 OpenAI 兼容的 API 服务,一行命令就启动:
python -m vllm.entrypoints.openai.api_server –model /gemini/pretrain2/Qwen1-8/ –host 0.0.0.0 –port 8000 –trust_remote_code True
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/269868.html