引言

大语言推理模型常见的两个痛点:一是“答不对”,二是“答不规范”。前者意味着推理链条断裂或迁移失效,后者则让后续评估与系统对接步步惊心。

在这篇 How‑to 指南里,我们用 GSM8K 的标准数据与明确的评估规则,让 Qwen/Qwen2.5-Math-1.5B 通过 SFT 后,能力迁移到不同的数据集上, 学会“思考→作答”的结构化输出:不仅更常答对,也更懂规范。结果来自实测,正确率由 1.56% 提升到 62.9%,格式遵循由 18.9% 提升到 100%.


为什么零样本推理和“格式遵循”都很难?

  • 零样本推理难在“迁移”:模型虽见过大量文本,但缺少对“分步算术、单位处理、等式化简”的系统化经验,容易在多步推理或细节规范上出错。
  • 格式遵循难在“约束学习”:即使模型知道答案,若不按系统约定的输出协议(例如必须有 <think>...</think> <answer>...</answer>),评估与下游解析都会失败。
  • 两者耦合:不遵循格式会直接“判零分”,即使答案正确;而缺少分步推理(think)又会影响最终答案(answer)的稳定性。

一个好比喻:把模型想象成一个聪明但散漫的学生。零样本时,它能“蒙”对少数题;SFT 就像班主任的“规范化带教”,教它先写草稿()再交最终答案(),且必须按卷面格式来——这样既能提高质量,也能让阅卷更可靠。


核心概念与评估口径

  • 监督微调(SFT):用标注样本(这里是 GSM8K 的“问题 + 推理过程 + 最终答案”)对模型做下一 token 预测训练。通过标签掩码,只对“推理与答案”部分计算损失,指导模型输出完整的思维链与答案。
  • Prompt 架构:我们采用 R1 风格模板,强制输出 <think><answer> 标签,保证格式可解析:
  • <think> ... </think>:推理过程
  • <answer> ... </answer>:最终答案
  • 训练目标(Loss Target):标准自回归语言模型(next-token LM)损失,但只在“回应(think+answer)”区间计算,避免模型学习到重复的“系统提示与问题”的 token 序列。
  • 评估指标:
  • “推理准确率”:按每条样本的 答案是否正确(数学同值、LaTeX 等价、数值等价,详见 grader)计分 0/1,最后取平均。
  • “格式遵循率”:按每条样本 是否包含合法的 <think>...</think> <answer>...</answer> 标签计分 0/1,最后取平均。

我们在 alignment/drgrpo_grader.py 中实现了严格的格式检查与宽容但可靠的数学等价判断(符号化、数值化与 LaTeX 解析的组合),是本文准确率与格式遵循的核心评估逻辑。


方案与架构

本文的流水线架构如下:从 GSM8K JSONL 到 Prompt 构造,再到 SFT 训练与 vLLM 推理评估,最后汇总指标(准确率/格式遵循)。

flowchart LR
  A["GSM8K JSONL 数据集"] --> B["R1PromptTemplate 构造 Prompt"]
  B --> C["SFT 训练 (next-token LM)"]
  C --> D["保存 Checkpoint"]
  D --> E["vLLM 推理评估"]
  E --> F["r1_zero_reward_fn 计算格式与正确性"]
  F --> G["输出指标: 准确率 & 格式遵循"]

关键路径对应的源码均在 alignment 目录下:dataset.py(数据加载)、r1_prompt.py(模板)、sft.py(训练)、evaluate.py(评估)、drgrpo_grader.py(格式与答案打分)。


可复现配置与注意事项

  • 模型与精度:默认使用 Qwen/Qwen2.5-Math-1.5B,dtype 默认 bfloat16(见 alignment/args.py)。
  • 设备与显存:alignment/sft.py 通过 accelerate 的 infer_auto_device_map 自动切分模型到多卡(--sft_device),并设置 --max_sft_gpu_memory_use(默认 31GiB/卡)。评估使用 vLLM 独立进程(--eval_device)。
  • 随机性:--seed 默认 42。不同环境(驱动、库版本、显存压力)与随机种子可能导致轻微波动。
  • 批次与累积:--batch_size--gradient_accumulation_steps 控制有效批次大小,训练日志会打印累计后的损失。
  • 提示模板:alignment/prompts/r1_zero.prompt 强制 <think>/<answer> 标签,保证格式可检。

实践步骤:从数据到评估与训练

  1. 下载 GSM8K(train/test)到本地 data 目录(可根据你的项目目录调整):
cd dataset
wget https://raw.githubusercontent.com/openai/grade-school-math/master/grade_school_math/data/train.jsonl
wget https://raw.githubusercontent.com/openai/grade-school-math/master/grade_school_math/data/test.jsonl

建议将文件移动/复制为仓库约定路径:data/gsm8k/train.jsonldata/gsm8k/test.jsonl(alignment/args.py 默认如此)。

  1. 微调前的零样本评估(使用 vLLM + R1 模板 + 严格格式与答案打分):
uv run -m alignment.evaluate
  1. 执行 SFT 训练并在测试集上评估(训练中每个 epoch 结束都会评估并保存 checkpoint):
uv run -m alignment.sft

代码讲解:数据加载与样本构造(包含 / 标签)

首先用 R1 模板将问题转为 Prompt,将“推理过程 + 最终答案”打包为监督信号。alignment/dataset.py 与 r1_prompt.py 如下:

# alignment/dataset.py
from torch.utils.data import Dataset
import json
from .r1_prompt import R1PromptTemplate

class Gsm8kDataset(Dataset):
    def __init__(self, data_path: str, promt_template_path: str):
        template = R1PromptTemplate(promt_template_path)
        self.data = []
        self.label = []
        self.ground_truth = []
        with open(data_path, "r") as f:
            lines = f.readlines()
        for line in lines:
            qa = json.loads(line)
            question = qa["question"]
            answer_think = qa["answer"]
            think, answer = answer_think.split("####")
            think, answer = think.strip(), answer.strip()
            self.data.append(template.gen_prompt(question))
            self.label.append(template.gen_response(think, answer))
            self.ground_truth.append(answer)

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        return self.data[idx], self.label[idx], self.ground_truth[idx]
# alignment/r1_prompt.py(关键:强制 <think>/<answer> 标签)
class R1PromptTemplate:
    def __init__(self, template_path: os.PathLike):
        with open(template_path, "r") as f:
            self.template = f.read().strip()

    def gen_prompt(self, question: str) -> str:
        return self.template.replace(r"{question}", question)

    def gen_response(self, think: str, answer: str) -> str:
        return think + "</think>" + " <answer>" + answer + " </answer>"

模板文件 alignment/prompts/r1_zero.prompt:

A conversation between User and Assistant... <think> reasoning process here </think> <answer> answer here </answer>.
User: {question}
Assistant: <think>

这保证了训练时模型学习“先写 <think> 再写 <answer>”,评估时也能稳定抽取并验证答案。


代码讲解:SFT 训练循环与标签掩码(只对回应部分计算损失)

在 alignment/sft.py,我们将 prompt + completion 拼接,对 prompt 区间的 label 置为 -100,从而让交叉熵只在“回应(think + answer)”上回传梯度:

# alignment/sft.py(节选)
model.train()
for epoch in range(args.epochs):
    for i, batch in enumerate(train_data_loader):
        prompts, completions, _ = batch

        full_texts = [p + c + tokenizer.eos_token for p, c in zip(prompts, completions)]
        inputs = tokenizer(
            full_texts,
            return_tensors="pt",
            padding=True,
            truncation=True,
            max_length=args.max_seq_len,
        ).to(model.device)

        # 计算每个样本的 prompt token 长度
        prompt_tokens = tokenizer(list(prompts), add_special_tokens=False)
        prompt_lengths = [len(ids) for ids in prompt_tokens.input_ids]

        labels = inputs.input_ids.clone()
        for idx in range(len(prompts)):
            prompt_len = prompt_lengths[idx]
            labels[idx, :prompt_len] = -100  # 对 prompt 部分不计 loss

        # Mask padding tokens
        labels[labels == tokenizer.pad_token_id] = -100

        outputs = model(
            input_ids=inputs.input_ids,
            attention_mask=inputs.attention_mask,
            labels=labels,
        )
        loss = outputs.loss
        loss = loss / args.gradient_accumulation_steps
        loss.backward()

        if (i + 1) % args.gradient_accumulation_steps == 0:
            print(f"Epoch {epoch}, Iteration {i}, Loss: {loss.item() * args.gradient_accumulation_steps}")
            optimizer.step()
            optimizer.zero_grad()

    # 评估:将策略权重加载到 vLLM 并在测试集上跑评估
    load_policy_into_vllm_instance(model, eval_model)
    evaluate_math(
        eval_model,
        args.prompt_template_path,
        args.sft_test_data,
        args.batch_size,
        log_sample=True,
    )
    model.save_pretrained(args.checkpoint_path)

这段逻辑实现了标准的 SFT:用“回应”作为监督目标,配合累积梯度与分卡策略,保证 1.5B 规模模型在可控显存与吞吐下完成训练。


代码讲解:评估与指标计算(准确率与格式遵循)

评估由 alignment/evaluate.py 驱动,调用 r1_zero_reward_fn 计算每条样本的格式与答案得分:

# alignment/evaluate.py(核心)
from collections.abc import Callable
from vllm import LLM, SamplingParams
from .drgrpo_grader import r1_zero_reward_fn

def evaluate_vllm(vllm_model: LLM,
                  reward_fn: Callable[[str, str], dict[str, float]],
                  prompts: list[str],
                  ground_truths: list[str],
                  eval_sampling_params: SamplingParams,
                  log_sample: bool) -> dict:
    outputs = vllm_model.generate(prompts, eval_sampling_params)
    generated_texts = [output.outputs[0].text for output in outputs]
    rewards = [reward_fn(generated_text, ground_truth)
               for generated_text, ground_truth in zip(generated_texts, ground_truths)]

    avg_format_rewards = sum([r["format_reward"] for r in rewards]) / len(prompts)
    avg_answer_rewards = sum([r["answer_reward"] for r in rewards]) / len(prompts)
    avg_all_rewards = sum([r["reward"] for r in rewards]) / len(prompts)
    print(f"avg_format_rewards: {avg_format_rewards}")
    print(f"avg_answer_rewards: {avg_answer_rewards}")
    print(f"avg_all_rewards: {avg_all_rewards}")
    return {
        "avg_format_rewards": avg_format_rewards,
        "avg_answer_rewards": avg_answer_rewards,
        "avg_all_rewards": avg_all_rewards,
    }

奖励函数严格要求格式标签,并对答案进行多重等价校验:

# alignment/drgrpo_grader.py(节选)
def r1_zero_reward_fn(response, ground_truth, fast=True):
    # 格式严格:必须包含 </think> <answer> ... </answer>
    if "</think> <answer>" in response and "</answer>" in response:
        model_answer = response.split("<answer>")[-1].replace("</answer>", "")
        # 允许 \boxed{...} 的答案形式并抽取
        if "\\boxed" in model_answer:
            model_answer = extract_answer(model_answer)
            if model_answer is None:
                return {"format_reward": 1.0, "answer_reward": 0.0, "reward": 0.0}
        # 字符串、数值、LaTeX、SymPy 等价综合判断
        is_correct = grade(model_answer, str(ground_truth), fast)
        if is_correct:
            return {"format_reward": 1.0, "answer_reward": 1.0, "reward": 1.0}
        else:
            return {"format_reward": 1.0, "answer_reward": 0.0, "reward": 0.0}
    else:
        # 不符合格式:直接判 0 分(同时 answer_reward 也为 0)
        return {"format_reward": 0.0, "answer_reward": 0.0, "reward": 0.0}

因此:

  • 格式遵循率 = 所有样本的 format_reward 平均值。
  • 推理准确率 = 所有样本的 answer_reward 平均值(在格式正确的前提下计算答案是否等价)。

数据读取与 Ground Truth 解析(alignment/evaluate.py):

# alignment/evaluate.py(节选)
def get_gsm8k_test_data(test_data_path: os.PathLike) -> list[dict]:
    data = []
    with open(test_data_path, "r") as f:
        for line in f.readlines():
            obj = json.loads(line.strip())
            ts = obj["answer"].split("####")
            if len(ts) != 2:
                print(f"invalid answer: {obj['answer']}")
                continue
            data.append({"question": obj["question"],
                         "think": ts[0].strip(),
                         "answer": ts[1].strip()})
    return data

结果与讨论

在同一评估口径下,我们在 GSM8K 上得到如下对比(报告值):

指标微调前(零样本)SFT 微调后
推理准确率1.56%62.9%
格式遵循率18.9%100%

说明:

  • 上述结果是在 alignment.evaluate 与 alignment.sft 的评估框架下得到的报告值。不同硬件/软件环境与随机种子可能引起轻微差异。
  • “格式遵循率”达到 100% 的关键是训练时的模板约束与监督信号覆盖 <think>/<answer>,并在奖励函数中严格判定标签存在性。

常见问题与优化建议

  • 训练不收敛或损失震荡:适当降低学习率(如 1e‑5 → 5e‑6)、增大 gradient_accumulation_steps,或提高 max_seq_len 以覆盖完整推理链。
  • 显存不足:增加 --sft_device 的卡数或调大 --max_sft_gpu_memory_use,必要时裁剪序列长度或使用更小 batch。
  • 评估速度慢:调小 SamplingParamsmax_tokens;但过小的生成长度会影响完整 <think>/<answer> 输出与正确率。
  • 格式仍偶发不合规:检查模板与数据是否一致(是否始终以 <think> 开头),并确保 SFT 标签覆盖足量样本。

结论

通过对 Qwen/Qwen2.5-Math-1.5BGSM8K 上的监督微调,我们实现了两条主线的同步提升:

  • 推理链条更稳,零样本正确率从 1.56% 提升到 62.9%
  • 输出规范更强,格式遵循率从 18.9% 提升到 100%

这背后的关键是“结构化输出模板 + 只对回应部分计损失 + 严格但宽容的评估规则”。如果你也在做数学推理或其他需要“思考‑答案二段式”输出的任务,强烈建议复用本文的架构与代码。

行动号召:现在就下载 GSM8K,跑一遍 uv run -m alignment.evaluateuv run -m alignment.sft,观察你本地的改进幅度吧! 可以参考 llm-from-scratch 仓库中 alignment 模块,对照进行学习。

开放问题:在你的场景里,是否遇到过“答案正确但格式导致系统解析失败”的案例?你是如何设计模板与评估逻辑来避免它的?欢迎在评论区分享你的经验与挑战。

我推荐选 pretrain,理由如下:

pretrain 提高工程能力,sft 提高认知能力

作为校招新人,你的当务之急只有一条:**提升工程代码能力!**我们拆解一下两个团队所需要的技能,你判断下哪个提升代码能力更快。

在 pretrain 团队,你的必修课有:爬取互联网数据,大规模数据清洗与去重(hadoop、spark等),配置各种 torch 和 cuda 环境,搞懂、修改、优化 megatron 的代码,熟练 debug 多机通讯报错,成熟的炼丹技巧(数据配比,学习率,优化器,课程学习,分析 loss 曲线、scaling_law),跑通各 benchmark 验证模型能力;

在 sft 团队,训练框架和炼丹技巧只属于选修课,往往公司不给你时间学习,你的必修课大概只包含以下内容:手动标数据,用 gpt4 标数据,教会标注人员标数据,利用用户反馈标数据,根据实验结果优化数据,参考论文去合成数据。

pretrain 转 sft 的难度,小于 sft 转 pretrain 的难度

基于两个方向的必修课不一样,pretrain 团队的同学能轻松上手 sft 的工作,sft 团队的同学则不能快速接手 pretrain 的工作。毕竟,两个团队核心技术不同:

  • pretrain 的核心技术:训练代码
  • sft 的核心技术:训练数据

所以,当你从 pretrain 转去做 sft,花一天时间看一下训练数据,就可以开始干活了;但当你从 sft 转去做 pretrain,你可能得花两周时间学习 megatron,才能开始干活。

pretrain 的杂活比 sft 的杂活更能锻炼新人

前面列举了两个方向的工作内容,假设你作为新人只配去做数据工作这种杂活,那我们就再来仔细看下两个团队的杂活都是什么?

  • pretrain 的杂活是从海量较脏的互联网数据中清洗出有意义的、干净的训练数据。大概包括:训个小模型给数据打分,写大量规则去除网页文档里的垃圾语料,研究某个 domain 数据的数据特征并想办法过滤该 domain 数据;
  • sft 的杂活大概是:手动标数据,不断拿 GPT4 试出效果最好的 prompt 来标数据。

言尽于此,哪个杂活更能锻炼人我就不再分析了。

新人应该选最难的工作

pretrain 方向的最大痛点是什么?

短期内没有收益、长期训不过 llama、qwen,老板会选择拥抱开源。但是,你是校招新人,他不敢裁你。趁着你有“校招生”这个护身符,去做最难的工作、快速成长自己,丰满羽翼,有社会舆论和国家政策保护在,你只要别乱执行 rm -rf *,就都是安全的。

再过三两年,可能你就不能选有技术的活,而只能选有收益的活了。


我并没有说 sft 都是标数据的没意义的工作,我曾在另一个回答中力挺 sft 是有技术含量的。我只不过是认为,pretrain 能快速提升工程能力,这对新人非常可贵。成熟一点的程序员,写 hadoop、训 BERT 已经玩得滚瓜烂熟了,多跑一些 sft,找一找大模型的 feel 非常重要,但新人只需要去学习扎实的基本功。

很多人都说,新人只会被分配去打杂,与其去 pretrain 洗数据,不如去 sft 拿收益。但是,孟母三迁的故事并没有过时,人是有眼有腿的啊,可以主动去学习的,在 pretrain 团队,老板不让你做 scaling_law,但是总不能不让你看 scaling_law 的 wiki 吧,总不能不让你请教同事吧。(这段话同样说给选 sft 工作的新人,我们是可以主动去选择学习 pretrain 相关的知识的,不要被工作内容桎梏了眼界和学习范围


我并没有说 sft 都是标数据的没意义的工作,我曾在另一个回答中力挺 sft 是有技术含量的。我只不过是认为,pretrain 能快速提升工程能力,这对新人非常可贵。成熟一点的程序员,写 hadoop、训 BERT 已经玩得滚瓜烂熟了,多跑一些 sft,找一找大模型的 feel 非常重要,但新人只需要去学习扎实的基本功。

很多人都说,新人只会被分配去打杂,与其去 pretrain 洗数据,不如去 sft 拿收益。但是,孟母三迁的故事并没有过时,人是有眼有腿的啊,可以主动去学习的,在 pretrain 团队,老板不让你做 scaling_law,但是总不能不让你看 scaling_law 的 wiki 吧,总不能不让你请教同事吧。(这段话同样说给选 sft 工作的新人,我们是可以主动去选择学习 pretrain 相关的知识的,不要被工作内容桎梏了眼界和学习范围

不管选择哪个团队,记得抱紧大佬同事大腿,多学多问,“喊声爸爸,换一份 megatron 的训练代码与教程”,这买卖不亏,我就干过。

普通人如何抓住AI大模型的风口?

为什么要学习大模型?

在DeepSeek大模型热潮带动下,“人工智能+”赋能各产业升级提速。随着人工智能技术加速渗透产业,AI人才争夺战正进入白热化阶段。如今近**60%的高科技企业已将AI人才纳入核心招聘目标,**其创新驱动发展的特性决定了对AI人才的刚性需求,远超金融(40.1%)和专业服务业(26.7%)。餐饮/酒店/旅游业核心岗位以人工服务为主,多数企业更倾向于维持现有服务模式,对AI人才吸纳能力相对有限。

在这里插入图片描述

这些数字背后,是产业对AI能力的迫切渴求:互联网企业用大模型优化推荐算法,制造业靠AI提升生产效率,医疗行业借助大模型辅助诊断……而餐饮、酒店等以人工服务为核心的领域,因业务特性更依赖线下体验,对AI人才的吸纳能力相对有限。显然,AI技能已成为职场“加分项”乃至“必需品”,越早掌握,越能占据职业竞争的主动权

随着AI大模型技术的迅速发展,相关岗位的需求也日益增加。大模型产业链催生了一批高薪新职业:

img

人工智能大潮已来,不加入就可能被淘汰。如果你是技术人,尤其是互联网从业者,现在就开始学习AI大模型技术,真的是给你的人生一个重要建议!

如果你真的想学习大模型,请不要去网上找那些零零碎碎的教程,真的很难学懂!你可以根据我这个学习路线和系统资料,制定一套学习计划,只要你肯花时间沉下心去学习,它们一定能帮到你!

大模型全套学习资料领取

这里我整理了一份AI大模型入门到进阶全套学习包,包含学习路线+实战案例+视频+书籍PDF+面试题+DeepSeek部署包和技巧,需要的小伙伴文在下方免费领取哦,真诚无偿分享!!!

vx扫描下方二维码即可

在这里插入图片描述

部分资料展示

一、 AI大模型学习路线图

这份路线图以“阶段性目标+重点突破方向”为核心,从基础认知(AI大模型核心概念)到技能进阶(模型应用开发),再到实战落地(行业解决方案),每一步都标注了学习周期和核心资源,帮你清晰规划成长路径。

img

二、 全套AI大模型应用开发视频教程

从入门到进阶这里都有,跟着老师学习事半功倍。

在这里插入图片描述

三、 大模型学习书籍&文档

收录《从零做大模型》《动手做AI Agent》等经典著作,搭配阿里云、腾讯云官方技术白皮书,帮你夯实理论基础。

在这里插入图片描述

四、大模型大厂面试真题

整理了百度、阿里、字节等企业近三年的AI大模型岗位面试题,涵盖基础理论、技术实操、项目经验等维度,每道题都配有详细解析和答题思路,帮你针对性提升面试竞争力。

在这里插入图片描述

适用人群

在这里插入图片描述

第一阶段(10天):初阶应用

该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。

  • 大模型 AI 能干什么?
  • 大模型是怎样获得「智能」的?
  • 用好 AI 的核心心法
  • 大模型应用业务架构
  • 大模型应用技术架构
  • 代码示例:向 GPT-3.5 灌入新知识
  • 提示工程的意义和核心思想
  • Prompt 典型构成
  • 指令调优方法论
  • 思维链和思维树
  • Prompt 攻击和防范
第二阶段(30天):高阶应用

该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。

  • 为什么要做 RAG
  • 搭建一个简单的 ChatPDF
  • 检索的基础概念
  • 什么是向量表示(Embeddings)
  • 向量数据库与向量检索
  • 基于向量检索的 RAG
  • 搭建 RAG 系统的扩展知识
  • 混合检索与 RAG-Fusion 简介
  • 向量模型本地部署
第三阶段(30天):模型训练

恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。

到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?

  • 为什么要做 RAG
  • 什么是模型
  • 什么是模型训练
  • 求解器 & 损失函数简介
  • 小实验2:手写一个简单的神经网络并训练它
  • 什么是训练/预训练/微调/轻量化微调
  • Transformer结构简介
  • 轻量化微调
  • 实验数据集的构建
第四阶段(20天):商业闭环

对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。

  • 硬件选型
  • 带你了解全球大模型
  • 使用国产大模型服务
  • 搭建 OpenAI 代理
  • 热身:基于阿里云 PAI 部署 Stable Diffusion
  • 在本地计算机运行大模型
  • 大模型的私有化部署
  • 基于 vLLM 部署大模型
  • 案例:如何优雅地在阿里云私有部署开源大模型
  • 部署一套开源 LLM 项目
  • 内容安全
  • 互联网信息服务算法备案

学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。

如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

更多推荐