当前位置: 首页 > news >正文

Day10 | SFT 训练实操——用 QLoRA 微调 Qwen3-8B

苦猿的大模型日记 · Day10 · SFT 训练实操——用 QLoRA 微调 Qwen3-8B-帮普通人把AI学进简历系列

前言:那个 lr=2e-5 的凌晨

我给你讲个事。

上个月某个周三,凌晨两点半,我对着屏幕上跳动的 loss 数字发呆。

那时候我在调一个 QLoRA 微调任务,base 模型是 Qwen3-8B。我按照"教程默认值"配了r=64alpha=128lr=2e-5,自以为很稳——毕竟参数都是网上抄来的。

结果跑了 200 步,train loss 从 1.2 缓慢爬到 1.3,越训越烂。

我当时第一反应是——数据有问题

换了一份数据集,还是烂。

第二反应——r 不够大。我把r调到 128,显存当场爆掉。

折腾到凌晨四点,我才反应过来一件事——

不是 r 的问题,是 lr 太小。

QLoRA 只训那 0.5% 的参数,你给它配一个为全量微调设计的2e-5,它当然纹丝不动。我把它提到1e-4,loss 立马开始下降。

那一刻我突然想明白——

Day09 给的是地图,Day10 要给的是铲子。

地图告诉你 SFT 是什么、LoRA 怎么省显存。但真到铲子下去那一刻,决定成败的不是你懂不懂原理,而是你会不会看 loss、会不会调那个该死的 lr

今天这篇,就带你真刀真枪跑通一次 Qwen3-8B 的 QLoRA 微调——从数据准备到合并权重,从超参默认值到 loss 曲线诊断手册,Day09 埋的 5 个坑,我一个个带你填上


PART 01:目标 & 显存账本

先把话说前面——

今天的目标:让 Qwen3-8B 学会按 Alpaca-zh 的指令风格回答问题。

不追求 SOTA,不追求商用,只追求一件事:跑通,且能看出来微调生效了

为什么选 Qwen3-8B

我对比过几个常见尺寸:

  • 7B(Qwen2.5/Llama):经典,但有点过气,中文表现被 Qwen3 压一头
  • 8B(Qwen3):当前中文甜点尺寸——比 7B 新一代,比 14B 省一半显存
  • 14B:效果好,但 QLoRA 都要 16G+ 起步,普通人玩不起

8B 是普通人在 12G 显存上能舒服跑起来的最大尺寸。这就是它的价值。

显存账本(必须重算)

Day09 我说过"6G 显存微调 7B",那是 Qwen2.5 时代的乐观估算。到 Qwen3-8B 这里,账要重算

项目占用
8B 权重(4bit 量化)~5-6 GB
激活值(batch=1, seq=512)~1-2 GB
LoRA 参数 + 优化器状态~0.5-1 GB
梯度 + 临时缓冲~0.5 GB
合计8-10 GB

所以硬件建议是——

  • 12G(3060 12G / 4070 12G):舒服,batch 能开到 2
  • 8G(4060 / 4060Ti):极限可跑,batch=1 + grad_accum=8 凑等效 batch=8
  • 6G 以下:建议换 7B 或者直接用云算力

别信那些"4G 显存微调大模型"的标题党——能跑起来和能训出东西是两回事。

工具链选择

今天主讲底层transformers + peft + trl

为什么不用 LLaMA-Factory 一把梭?因为出 bug 的时候你看不懂。面试官问你"QLoRA 的 target_modules 挂了哪些",你不能回答"我点了一下 yaml 就跑起来了"。

PART 07 我会附一份 LLaMA-Factory 的等价配置给懒人,但主篇幅必须走底层


PART 02:数据准备(坑①②高发区)

数据准备是 SFT 里最容易翻车的环节——80% 的"训练不收敛"都是数据问题,不是模型问题

Alpaca-zh 字段说明

我们用的是 Alpaca-zh,经典中文指令数据集,约 4.8 万条。每条三个字段:

{ "instruction": "请把这句话翻译成英文", "input": "今天天气真好", "output": "The weather is nice today." }
  • instruction:指令本身
  • input:指令的输入(可为空)
  • output:期望的回答

看起来很简单对吧?坑就在"看起来简单"上

Qwen3 的 ChatML template

你不能直接把这三段拼成一坨丢给模型。Qwen3 用的是ChatML 格式

<|im_start|>user {instruction}{input}<|im_end|> <|im_start|>assistant {output}<|im_end|>

那两个<|im_start|><|im_end|>特殊 token,模型在预训练阶段就认识它们。你少了任何一个,模型就听不懂你在说啥。

loss mask:只算"答案"那段

这是 SFT 最关键的一个细节——

前文(user 那段)不算 loss,只算 assistant 那段。

为什么?因为 SFT 的目标是教模型"怎么回答",不是教它"怎么提问"。如果你把 user 段也算进 loss,模型会学着模仿你的提问方式,反而稀释了回答能力。

代码上,这通过labels实现:user 段的位置填-100(PyTorch 的 ignore_index),assistant 段填真实 token id。

坑①:chat template 拼错

最常见的错误是手拼 template——

# ❌ 错误做法:手拼 text = f"<|im_start|>user\n{instruction}{input}<|im_end|>\n<|im_start|>assistant\n{output}<|im_end|>"

为什么错?因为你不知道<|im_end|>后面到底有没有换行特殊 token 有没有被正确 tokenize。手拼出来的字符串,到 tokenizer 里很可能把<|im_start|>拆成 5 个普通字符而不是 1 个特殊 token。

正确做法——

# ✅ 正确做法:用 apply_chat_template messages = [ {"role": "user", "content": instruction + input}, {"role": "assistant", "content": output}, ] text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)

让 tokenizer 自己拼,它知道每个特殊 token 怎么处理。

坑②:target_modules 漏挂

LoRA 不是随便挂哪都行——挂错位置,等于没挂

很多人直接抄网上配置target_modules=["q_proj", "v_proj"],这是早期 LoRA 论文的配置,只挂 attention 的 Q 和 V

但 Qwen3-8B 是个 36 层的大家伙,光挂 Q/V 太保守。正确做法是先 print 模型架构看清楚有哪些线性层——

model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-8B", torch_dtype="auto") print(model.model.layers[0].self_attn) # 看 attention print(model.model.layers[0].mlp) # 看 MLP

输出里会告诉你有q_proj/k_proj/v_proj/o_proj(attention 四件套)和gate_proj/up_proj/down_proj(MLP 三件套)。

我建议的配置是——attention 全挂 + MLP 全挂

target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]

PART 03 会详细讲"只挂 attention vs 加 MLP"的效果差异。


PART 03:QLoRA 配置 & 超参到底怎么填

这是本文最干货的部分。每个超参我都给你三段式:默认值 / 为什么 / 调错后果

LoRAr:能力上限旋钮

r是 LoRA 的秩,决定你能学多少新东西。

  • r=8:默认值。够应付"改语气、改格式"这种轻量任务
  • r=16:我推荐的新手起步值。能学新知识,显存涨得不多
  • r=32:适合数据集大、任务重(比如代码生成、长文改写)
  • r=64+:除非你知道为什么需要,否则别碰

调错后果

  • r 太小 →欠拟合,loss 降不下去,模型学不会新行为
  • r 太大 → 显存暴涨 +过拟合,模型只会背训练集,换个问法就废

我的经验:先从 r=16 起步,看 loss 曲线再决定调不调。r 不是越大越好,是"够用就好"

alpha:缩放系数

alpha控制 LoRA 那部分更新的"音量"。

默认公式alpha = 2 × r

  • r=8 → alpha=16
  • r=16 → alpha=32

什么时候偏离这个公式?

  • 训练不收敛但 r 已经够大→ 试着把 alpha 调小(比如 alpha=r),让更新温和一点
  • loss 降得太慢→ 试着把 alpha 调大(比如 alpha=4r),让更新激进一点

新手别动 alpha,老老实实2r就行。它是高级玩家的微调旋钮,不是入门工具。

dropout:防过拟合开关

LoRA 默认dropout=0.05

什么时候调到 0.1?

  • 训练集 loss 远低于验证集 loss(比如 train=0.3,val=1.5)→ 典型过拟合,加 dropout
  • 数据集很小(<1000 条)→ 容易背书,加 dropout
  • 训练步数很多(>3 epoch)→ 同上

调错后果:

  • dropout 太大(>0.2)→ 模型学不进去,欠拟合
  • dropout=0 → 大多数时候没事,但小数据集上必过拟合

target_modules:挂哪些层

前面说过,attention 全挂 + MLP 全挂是我的推荐。但你要知道差异——

配置可训参数效果显存
只挂 q_proj, v_proj~0.3%轻量任务够用最省
attention 四件套~0.5%大多数 SFT 任务够用省一点
attention + MLP 全挂~0.8%重任务、学新知识稍涨

我的建议:8B 模型上直接全挂。多出来的显存占用对 12G 卡不算啥,但效果差异是肉眼可见的。

4bit 量化:NF4 还是 8bit

QLoRA 的核心是 4bit 量化,把权重压到 4bit 省显存。

  • NF4(NormalFloat 4):默认推荐,正态分布优化的 4bit 格式
  • double quant:再量化一次量化常数,再省 0.5GB
  • 8bit:质量更好但显存翻倍

什么时候退回 8bit?

  • 微调后效果明显变差(生成内容质量下降、出现乱码)→ 4bit 损失太大,换 8bit
  • 显存充裕(16G+)→ 直接 8bit,质量优先

99% 的情况 NF4 + double quant 就够了。

lr / batch / grad_accum:调参三件套

这是新手最容易抄错的三个值——

参数全量微调值QLoRA 推荐值为什么差这么多
learning_rate2e-51e-4 ~ 2e-4QLoRA 只训 0.5% 参数,步子要迈大
per_device_batch8-321-2显存受限
gradient_accumulation18-16凑等效大 batch

这就是我开头那个故事的根源——我把全量微调的 lr=2e-5 抄到 QLoRA 上,结果模型纹丝不动。

其他几个——

  • warmup_ratio=0.03:前 3% 步数线性升温,防止初期梯度爆炸
  • weight_decay=0.01:正则化,防过拟合
  • num_train_epochs=3:Alpaca-zh 这种数据量,3 epoch 起步


PART 04:跑起来 & 训练监控怎么读

配好超参,该跑了。

SFTTrainer 启动代码

直接上代码——

from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training from trl import SFTTrainer, SFTConfig from datasets import load_dataset # 1. 加载 tokenizer + 模型(4bit) tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B") model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen3-8B", load_in_4bit=True, device_map="auto", ) # 2. 准备 4bit 训练 model = prepare_model_for_kbit_training(model) # 3. LoRA 配置 lora_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], task_type="CAUSAL_LM", ) model = get_peft_model(model, lora_config) # 4. 加载数据(假设已转成 ChatML 格式的 jsonl) dataset = load_dataset("json", data_files="alpaca_zh_chatml.jsonl", split="train") # 5. 训练配置 sft_config = SFTConfig( output_dir="./qwen3-8b-qlora", num_train_epochs=3, per_device_train_batch_size=1, gradient_accumulation_steps=8, # 等效 batch=8 learning_rate=1e-4, # ⚠️ 不是 2e-5 warmup_ratio=0.03, weight_decay=0.01, logging_steps=10, save_strategy="epoch", bf16=True, # 40系卡用 bf16,30系换 fp16 ) # 6. 开训 trainer = SFTTrainer( model=model, args=sft_config, train_dataset=dataset, processing_class=tokenizer, ) trainer.train()

跑起来你会看到每 10 步打印一次 loss——

{'loss': 1.85, 'learning_rate': 9.5e-05, 'epoch': 0.01} {'loss': 1.62, ...} {'loss': 1.43, ...} ...

这串数字怎么读?这是本文最值钱的部分。

loss 曲线诊断手册

我总结过四种典型 loss 形态,每种对应一种病——

形态 1:正常下降

1.85 → 1.62 → 1.43 → 1.28 → 1.15 → ...

前 50 步快速下降,之后缓慢收敛。这是健康的样子

形态 2:train 降,val 不降(过拟合)

train loss 一路走低,但验证集 loss 开始回升——

train: 1.85 → 1.20 → 0.80 → 0.50 → 0.30 val: 1.90 → 1.40 → 1.20 → 1.35 → 1.60 ← 这里开始回升

信号:模型在背训练集,没学到泛化能力。

对策:加 dropout / 减 epoch / 加数据。

形态 3:前 20% 步不降(lr 问题)

1.85 → 1.86 → 1.84 → 1.85 → 1.83 → ...(20 步后才开始动)

信号:lr 太小,模型在原地踏步。

对策:lr × 5(比如 1e-4 → 5e-4)。这就是我开头那个坑

形态 4:loss 突然飙 NaN(梯度爆炸)

1.20 → 1.15 → 1.10 → NaN → NaN → NaN

信号:梯度炸了,权重被污染。

对策

  1. 立刻停(别等)
  2. lr 减半重启
  3. 检查数据里有没有超长样本(>2048 token)
  4. bf16 不稳的话换 fp16,反之亦然

什么时候该早停

别迷信"训满 3 epoch"

判断信号——

  • train loss 连续 100 步不创新低 → 早停
  • 生成质量开始下降(模型开始重复、胡说)→ 早停
  • val loss 触底回升 → 立刻停

SFT 不是训得越久越好。很多模型在 1.5-2 epoch 就到顶了,第 3 epoch 反而训废

如何判断微调真的生效

别只看 loss 数字。loss 从 1.8 降到 0.5,听起来很美好,但不代表模型变聪明了——可能只是它学会了"短回答"(短回答天然 loss 低)。

真正判断微调生效的方法是肉眼看生成质量——

# 训练完后立刻测一条 inputs = tokenizer.apply_chat_template( [{"role": "user", "content": "用三句话介绍一下中国春节"}], return_tensors="pt", add_generation_prompt=True ).to(model.device) output = model.generate(inputs, max_new_tokens=200) print(tokenizer.decode(output[0], skip_special_tokens=True))

对比微调前后的输出——

  • 微调前:base 模型可能续写成"用三句话介绍一下中国春节\n用三句话介绍一下美国圣诞节\n..."(接龙机器)
  • 微调后:应该规规矩矩给你三句话

这才是"微调生效"的硬证据


PART 05:调参经验法则小结

我把前面散落的经验整理成一张表,这是本文最该截图保存的部分——

症状大概率原因该动哪个旋钮
loss 降不下去lr 太小 / r 太小先 lr ×5,不行再 r ×2
loss 突然 NaN梯度爆炸lr 减半,检查数据长度
train 降 val 升过拟合dropout↑ / epoch↓ / 加数据
生成内容重复lr 太大或训太久lr 减半 / 早停
模型不响应指令template 拼错用 apply_chat_template
输出乱码merge / tokenizer 问题见 PART 06

还有几条心法——

心法 1:先调 lr,再调 r

lr 是步长,r 是脑子大小。步长不对,脑子再大也走不动。

心法 2:数据不够先加数据,不是调参

100 条数据调出花来也是过拟合。1 万条平庸数据 > 100 条精调数据

心法 3:r 和 lr 要联动

r 调大,参数变多,lr 要适当调小(否则容易炸)。这是一个联动旋钮,不是独立旋钮。

心法 4:永远先看 loss,再调参

不要凭感觉动超参。loss 是模型在跟你说话,你得先听懂它在说什么


PART 06:合并权重 & 推理验证(坑⑤)

训完之后,你拿到的是一个 LoRA adapter(几十 MB 的小文件),不是一个完整的模型。要部署的话,得合并

merge LoRA 回 base

from peft import PeftModel from transformers import AutoModelForCausalLM # 1. 加载 base 模型(这次不量化,用 fp16) base = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen3-8B", torch_dtype="float16", device_map="auto" ) # 2. 挂上 LoRA adapter model = PeftModel.from_pretrained(base, "./qwen3-8b-qlora/checkpoint-xxx") # 3. 合并 model = model.merge_and_unload() # 4. 保存 model.save_pretrained("./qwen3-8b-merged") tokenizer.save_pretrained("./qwen3-8b-merged")

坑⑤:合并后输出乱码(三段式排查)

症状:合并完一推理,模型输出胡言乱语 / 重复 / 不响应指令。

排查三步

  1. template 对齐了吗?

合并后的模型推理时,必须用和训练时完全一致的 chat template。训练用 ChatML,推理也得用 ChatML。这是最常见的翻车点。

  1. tokenizer 配置丢了吗?

save_pretrained默认只保存模型权重,tokenizer 的特殊 token 配置可能没保存全。检查合并目录下有没有tokenizer_config.jsonspecial_tokens_map.json。没有的话手动复制一份过去。

  1. merge 真的成功了吗?

有时候 LoRA adapter 加载路径错了,合并出来其实是个"裸 base"。验证方法——

# 合并前后各生成一条,对比输出 # 如果完全一样,说明 merge 失败(LoRA 没生效)

修复后,跑一遍对话验证——

# 简单对话验证 def chat(question): inputs = tokenizer.apply_chat_template( [{"role": "user", "content": question}], return_tensors="pt", add_generation_prompt=True ).to(model.device) output = model.generate(inputs, max_new_tokens=200, do_sample=True, temperature=0.7) return tokenizer.decode(output[0][inputs.shape[1]:], skip_special_tokens=True) print(chat("用三句话介绍中国春节"))

输出规规矩矩给你三句话,微调闭环完成


PART 07:懒人附赠——LLaMA-Factory 等价 yaml

前面讲了一堆底层,我知道有人会想——

"我不想知道原理,我只想跑起来。"

行,给你一份等价的 LLaMA-Factory 配置——

# qwen3-8b-qlora.yaml model_name_or_path: Qwen/Qwen3-8B stage: sft do_train: true finetuning_type: lora lora_target: q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj lora_rank: 16 lora_alpha: 32 lora_dropout: 0.05 quantization_bit: 4 quantization_method: nf4 dataset: alpaca_zh template: qwen cutoff_len: 1024 output_dir: ./qwen3-8b-qlora per_device_train_batch_size: 1 gradient_accumulation_steps: 8 learning_rate: 1e-4 num_train_epochs: 3 warmup_ratio: 0.03 bf16: true

一行命令跑起来——

llamafactory-cli train qwen3-8b-qlora.yaml

什么时候用懒人方案

适合用 LLaMA-Factory

  • 快速验证想法("这个数据集值不值得训")
  • 不想懂底层、只要结果
  • 跑标准流程、不折腾

必须回到底层

  • 训练出 bug 需要排查(yaml 报错你看不懂)
  • 要定制非标准流程(比如自定义 loss)
  • 面试被问"QLoRA 的 target_modules 挂了哪些"

我的建议——先用 LLaMA-Factory 跑通一遍找信心,再用底层重跑一遍找理解。两条腿走路,最稳。


PART 08:效果对比与避坑总结

不同 r / 不同 lr 下的效果差异

我用同一份 Alpaca-zh 子集(5000 条)做过对比——

配置训练 loss生成质量评价
r=8, lr=2e-51.5(不降)没变化抄默认值的悲剧
r=8, lr=1e-40.9能响应指令,回答偏短轻量够用
r=16, lr=1e-40.7响应好,回答流畅推荐配置
r=32, lr=1e-40.6接近 r=16,略过拟合边际递减
r=64, lr=2e-51.3(不降)没变化r 大 lr 小,走不动
r=64, lr=2e-40.5过拟合,开始胡说太激进

看出规律没?

r 和 lr 是联动旋钮。光调 r 不调 lr,等于换了个更大的脑子但没给它吃饭。

Day09 留的 5 个坑,一次盘点

回到 Day09 结尾我埋的 5 个坑——

  1. chat template 拼错→ 用apply_chat_template,别手拼(PART 02)
  2. target_modules 漏挂→ print 架构,attention + MLP 全挂(PART 02)
  3. lr 抄全量微调的值→ QLoRA 用 1e-4,不是 2e-5(PART 03,本文最大的坑)
  4. 4bit 量化质量损失→ 默认 NF4 + double quant,效果差再退 8bit(PART 03)
  5. 合并后输出乱码→ template 对齐 + tokenizer 配置 + merge 验证(PART 06)

5 个坑,今天全填完了

什么时候该上 DPO/RLHF

SFT 解决的是"会不会回答"。

但有些问题 SFT 解决不了——

  • 模型回答太啰嗦("作为 AI 语言模型,我认为...")
  • 模型回答有害(教人做坏事)
  • 模型回答不一致(同一个问题不同时候答得不一样)

这些是对齐问题,需要RLHF 或 DPO——基于人类偏好再训一轮。

这就是 Day10 之后的下一站。先把 SFT 跑通,再谈对齐


结尾:跑通是入场券,调对才是本事

最后说句掏心窝子的话——

跑通一次 QLoRA 微调,真的不难。网上教程一抓一大把,复制粘贴半小时就能跑起来。

但跑通和调对,中间隔着一万个 loss 曲线

我见过太多人,照着教程跑通了就以为"我会 SFT 了"。一问"为什么 lr 用 1e-4"、"train 降 val 不降怎么办",瞬间哑火。

模型不会因为你跑通了代码就更聪明,只会因为你读懂了 loss 而更听话。

这才是 SFT 实操真正的门槛——不是代码,是听懂模型在说什么

跑通是入场券,调对才是本事。

下一篇我们往哪走?RLHF 还是 DPO,你说了算


互动时间:你第一次跑 SFT 时栽在哪个坑里?评论区聊聊,我把高赞的坑整理成"读者踩坑合集"。


下一篇预告:Day11——把"会回答"的模型,调成"答得好"的助手(RLHF/DPO 二选一)

— END —

http://www.jsqmd.com/news/1099713/

相关文章:

  • BetterJoy完整指南:让Switch手柄在PC游戏上完美运行
  • 智谱大模型LLM一面,人麻了!!!
  • 【JAVA毕设源码分享】基于springboot的小区公共收益管理系统 的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 光电经纬仪测量中的坐标系体系及其应用
  • CPT Markets:把外汇用户支持体系做到位——维度复盘与提示整理
  • 抖音内容批量采集与智能管理工具:从零到精通的完整指南
  • OpenAI / Claude API 报错 401、403、429 怎么解决?一文讲清 API Key 失效排查思路
  • 量子虚时演化算法原理与sine-Gordon模型模拟实践
  • FreeCAD源码分析: Property View
  • 我一个人 11 天交付了两个模块——不是会分身,是让两个 AI 打了配合
  • 1115.交替打印FooBar
  • 【课程设计/毕业设计】基于 SpringBoot 的农业设备销售订单管理系统的设计与实现 基于 SpringBoot 的智慧农机综合服务管理系统【附源码、数据库、万字文档】
  • 修改很简单,但网上讲这点的文档不多,因此多记一笔。另外基于out_ptr会临时转移所有权这点来看,共享所有权模型的std::shared_ptr其实并不适合使用out_ptr,虽然标准没有禁止甚至还要
  • playwright-拖拽验证码
  • LeWorldModel:基于JEPA的轻量化世界模型实践指南
  • 为什么要将 RTF 转换为 PDF?
  • 告别泰拉瑞亚原版限制:tModLoader模组开发实战手册
  • Opencv延迟优化
  • 项目包含项目源码、项目文档、数据库脚本、软件工具等资料;
  • 欧姆龙NJ系列EtherCAT总线通信常用系统状态字
  • Agibot第15000台人形机器人下线,具身AI量产加速
  • 【课程设计/毕业设计】基于 SpringBoot 的电子化招投标数据统计分析系统的设计与实现 基于 SpringBoot 的中小型企业线上招标管理平台【附源码、数据库、万字文档】
  • 【GitHub】 fastText:当“快“成为核心竞争力——从源码拆解 Facebook 的 10 亿词级 NLP 利器
  • 新版通达信多空主力拉升1主图2副1选股指标套装工具
  • 破局生物医药研发:实验数据标准化管理平台如何重塑科研新范式
  • web9使用RESTful完整项目的用户增删改查的项目代码
  • 从厨房秤到智能称重:用STM32F103和HX711打造你的第一个物联网传感器节点
  • Jmeter性能测试与SQL优化——电影收藏清单小程序获取收藏列表
  • 从零构建企业级多智能体教育辅助系统
  • 别把RAG当架构:Ontology(本体)才是Agent的业务世界