news 2026/10/15 11:39:47

基于YOLOv5的钢材表面缺陷检测实战:从数据集到模型调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLOv5的钢材表面缺陷检测实战:从数据集到模型调优

简介:面向工业质检工程师与目标检测算法开发者,资源将YOLOv5工程代码与钢材表面缺陷数据集整合打包,清晰指向表面缺陷自动识别与定位这一落地场景。压缩包共2000个文件,总大小约501MB,核心数据为1971张真实钢材表面jpg图像、1954个TXT格式的YOLO标签和1800个XML格式的VOC标签,同时提供yaml数据集配置、训练/推理Python脚本、已训练pt权重以及团队工程配置,直接覆盖数据划分、模型训练、精度验证与部署转换等多环节。已有2503人学习下载。凭借完整标注和可运行骨架,研究者可快速跑通裂纹、凹坑、氧化皮、划痕等缺陷检测实验,也可在CSPNet、SPP、FPN/PAN基础上调整参数,比较改进效果,是深入YOLOv5工程应用的良好起点。

1. 需求与技术选型:钢材表面缺陷检测为什么从YOLOv5入手

产线上质检员盯着屏幕看钢材表面有没有裂纹、夹杂、划痕,一天八小时,到了下午漏检率肉眼可见地往上走,这是很多学生选这个题的现实背景。但如果你只是把YOLOv5官方代码拉下来,数据集往 train.py 里一丢开始训练,大概率翻车——不是标注格式不对导致 mAP 一直为 0,就是 loss 降了但检测结果全乱,最后答辩只能用“趋势正确”来搪塞。这套方案要解决的问题很具体:钢材表面缺陷检测怎么从数据集整理一路走到可验收的检测精度。适合正在做毕设、课设的学生,也适合要把质检方案快速落到原型验证的工程师。

2. 数据集构建与标注规范:六类缺陷的样本组织与校验流程

2.1 缺陷类别定义与样本分布:先搞清楚你要检测什么

钢材表面缺陷数据集通常覆盖六类常见缺陷:裂纹、夹杂、斑块、麻点、氧化铁皮压入、划痕。不同缺陷的形态差异非常大:裂纹是细长线状,宽度可能只有几个像素;斑块是成片的灰度异常;麻点和氧化铁皮压入都是点状或片状,视觉上容易混淆;划痕则是方向性很强的细线。这个差异直接决定了后面的模型配置——你不能用一套默认 anchor 打天下。

样本数量方面,公开数据集常见的规模是每类 300 张左右,总共约 1800 张,单图分辨率不高,很多是 200 × 200 左右的灰度图。这点数量对深度学习来说属于“小样本中等难度”,如果直接拿来训练,过拟合基本跑不掉。正确做法是先建立一套数据组织规范,把样本管理和训练流程分开。我一般会先在本地把数据集按类别统计一遍,看看每类样本数是否均衡、有没有空标注图,这步花不了十分钟,但能在后续训练里省下大量返工时间。

2.2 数据集目录组织:YOLOv5要求的图片与标注怎么放

YOLOv5 对数据集目录结构有固定要求,图片和标注要分开存放,训练集、验证集各自维护一份。目录组织如下:

steel_dataset/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 与 images/train 一一对应的标注文件 │ └── val/ ├── data.yaml # 数据配置文件 └── README.md # 标注规范说明

标注文件是纯文本,每行表示一个目标框,格式为:class x_center y_center width height,坐标全部归一化到 0~1。举个例子,一张 200 × 200 的图上有一个裂纹框,左上角在 (50, 40),宽 80 高 10,那么归一化后的中心坐标是 (90/200, 45/200) = (0.45, 0.225),宽高是 (80/200, 10/200) = (0.4, 0.05),对应的标注行就是:

0 0.45 0.225 0.4 0.05

如果你拿到的是 COCO 格式或 VOC 格式的标注,需要先做一次格式转换再进训练流程。这里最容易翻车的就是类别编号:YOLOv5 的类别索引从 0 开始,如果原数据从 1 开始编号,训练出来的模型所有预测框都会整体错位,且 mAP 恒为 0,很难排查。

2.3 标注校验脚本:把“坏标注”挡在训练之前

训练前一定要做标注校验。最省事的做法是写一个几十行的脚本,把标注文件全部扫一遍,检查四类问题:空标注、坐标越界、类别编号超范围、图片与标注文件不配对。通常我做标注校验时会用下面这个脚本:

import os from pathlib import Path def check_label(label_path, num_classes=6): errors = [] for txt in label_path.glob("*.txt"): name = txt.stem img = txt.parent.parent / "images" / (name + ".jpg") # 对应图片 if not img.exists(): errors.append(f"[缺失图片] {txt.name}") for i, line in enumerate(txt.read_text().strip().splitlines()): parts = line.split() if len(parts) != 5: errors.append(f"[格式错误] {txt.name} 第{i+1}行") continue cls, cx, cy, w, h = parts if not (0 <= int(cls) < num_classes): errors.append(f"[类别越界] {txt.name} cls={cls}") for val in (cx, cy, w, h): if not (0 < float(val) <= 1): errors.append(f"[坐标越界] {txt.name} {val}") return errors if __name__ == "__main__": label_dir = Path("steel_dataset/labels/train") errs = check_label(label_dir) print(f"校验完成,共 {len(errs)} 个问题") for e in errs[:20]: print(e)

这个脚本做了四件事:检查图片是否存在、标注行是否为 5 个字段、类别是否在 0~5 之间、坐标是否在 (0,1] 范围内。任何一项报错都值得停下来修数据,不要带着坏标注硬训。训练前用这个脚本扫一遍,能拦住大部分数据问题。另一个常用手段是把标注框画回到图上,生成一张可视化预览图,人工抽查几十张,重点看框是否贴合缺陷边缘、是否出现一个缺陷被拆成两个框的情况。

2.4 数据增强策略:先保真实样本,再谈数量

1800 张图不足以支撑一个稳定的检测模型,数据增强不是可选项,是必需品。YOLOv5 默认开启 Mosaic 增强:把四张图随机裁剪后拼成一张新图,这能显著提升小目标检测能力,因为拼接过程天然制造了大量小尺寸目标。但 Mosaic 有个副作用,它在训练后期会让模型接触到的目标布局和真实分布出现偏差,所以通常的做法是前 90 个 epoch 开启 Mosaic,最后 10 个 epoch 把它关掉,让模型回到真实样本分布上做微调。

除了 Mosaic,我还会针对钢材缺陷做两个定制增强。一是对裂纹、划痕这类细长目标,做随机旋转和细长方向上的拉伸,模拟不同角度轧制产生的缺陷;二是把提取出来的小缺陷直接复制粘贴到其他无缺陷的背景区域,相当于手工制造更多难样本。增强策略的度要控制好,过量增强会把模型逼疯,典型表现是训练集 loss 正常但验证集 mAP 波动极大,这时候优先减小增强强度,而不是加更多花样。

3. 模型架构与训练配置:YOLOv5关键参数的落地抉择

3.1 网络结构怎么看:三个尺度输出解决什么问题

YOLOv5 能被工业场景选中,核心在于它在单阶段检测器里做到了速度和精度的平衡。网络分成三块:Backbone 负责提取特征,Neck 用 PANet 把不同层级的特征做融合,Head 在三个不同尺度上输出预测。三个尺度的意义在于匹配不同大小的目标:大尺度特征图负责小目标,小尺度特征图负责大目标。钢材缺陷正好是小目标密集的场景,细裂纹可能只有十几个像素,所以输入分辨率不能太小,多尺度输出的有效性也直接取决于输入尺寸和数据中目标的真实尺寸分布。

模型规模上,我建议直接选 YOLOv5s,而不是 m 或 l。理由很简单:数据集只有 1800 张,模型越大过拟合越快;检测对象类别只有 6 类,不是那种几千类的开放场景,s 级别的模型容量完全够用,而且训练时间和显存占用更友好,学生机 8G 显存就能跑。如果你发现 s 怎么调 mAP 都上不去,那大概率是数据或训练配置的问题,不是模型太小,不要急着换大模型。

3.2 数据配置文件:data/steel.yaml 这样写

数据配置文件是训练入口的第一步,内容不多,但写错一个路径或类别名就会白跑一轮。下面是完整的 data.yaml 写法:

# 钢材表面缺陷数据集配置 path: ../steel_dataset # 数据集根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 nc: 6 # 类别数量 names: # 类别名称,顺序必须与标注中的编号一致 0: crack # 裂纹 1: inclusion # 夹杂 2: patch # 斑块 3: pitting # 麻点 4: rolled_in # 氧化铁皮压入 5: scratch # 划痕

这里有一个容易忽略的细节:path字段如果填了,那么 train 和 val 的值就是相对于 path 的路径,不要写成绝对路径,不然后面迁移到别的机器上跑训练又要改一遍。类别名称用什么语言都可以,但必须保证 names 列表的下标和标注文件的类别编号一一对应。

模型配置文件 yolov5s.yaml 里最需要改的只有一处:把nc从默认的 80 改成 6。anchor 参数我建议第一轮训练时先不动,用默认值跑基线,后面根据标注框的实际尺寸分布再决定要不要调整。不要一上来就改 archor,那等于把两个变量同时引入实验,出了问题你根本分不清是数据问题还是 anchor 问题。

3.3 训练超参:epochs、batch、lr 与 Mosaic 的搭配

训练配置没有固定的最优解,但有稳定的起点。我一般会这样设:epochs 设 120,batch-size 设 16,初始学习率 0.01,开启余弦退火,Mosaic 在最后 10 个 epoch 关闭。这些参数对 YOLOv5 + 小规模工业数据集基本是安全的默认值。

batch-size 的选择要结合显存看:如果 16 跑不动,就降到 8,同时学习率建议同步从 0.01 降到 0.008,否则 loss 曲线会出现明显震荡。学习率和 batch 是联动的,换了 batch 不换 lr,训练前期会非常不稳定。还可以开启预热 warmup,让模型在前几个 epoch 用很小的学习率热身,避免一开始就大步长更新导致损失发散。

3.4 预训练权重的正确姿势:冻结训练再解冻

很多人直接拿 YOLOv5 在 COCO 上的预训练权重来训自己的数据集,一跑到底,这是最常见的翻车点之一。预训练权重在自然图像上学到的特征对钢材这种纹理单一、灰度为主的图像帮助有限,而且数据规模差距悬殊,很容易在微调时把学到的通用特征冲刷掉。通常的做法是两阶段训练:第一阶段冻结 Backbone,只更新 Neck 和 Head,跑 20 个 epoch 左右;第二阶段解冻全部层,用较小的学习率全量微调。冻结 Backbone 相当于保留预训练学到的基础特征,只让检测头和特征融合层去适应钢材数据。

实现冻结训练的方法是在训练脚本里指定冻结层数,YOLOv5 支持在 train.py 中传入冻结前 n 层的参数。如果你用的代码版本不支持这个参数,也可以手动修改模型定义,但工程量大一些。我一般先冻结跑一轮,再解冻跑完整个训练,对比两轮的 mAP 曲线,多数情况下两阶段方案比直接全量训练高出 3~5 个点的 mAP@0.5。

4. 训练实战与指标调优:从基线到可交付的检测精度

4.1 训练启动与日志解读:训练日志怎么读才不慌

训练启动命令看起来很简单,但每个参数背后都有讲究:

python train.py \ --data steel.yaml \ --weights yolov5s.pt \ --epochs 120 \ --batch-size 16 \ --imgsz 640 \ --patience 20 \ --cache ram \ --seed 0 \ --device 0

几个关键参数要理解:--imgsz 640表示输入尺寸,虽然原始图只有 200 × 200,模型会自动做 letterbox 缩放补边,我建议训练阶段统一用 640,等部署时再根据推理速度要求调低;--patience 20是早停参数,连续 20 个 epoch 验证集 mAP 没有提升就提前停止,防止无效训练浪费时间;--cache ram把图片缓存到内存,1800 张图全部加载也就几百 MB,能显著缩短每轮 epoch 的时间。

训练日志是黑匣子的窗口。你要盯的不是 loss 清零,而是三条曲线:box_loss、obj_loss、cls_loss 是否都在下降,验证集 mAP@0.5 是否还在涨。如果 loss 一直降但 mAP 不动,说明模型在过拟合训练集;如果 loss 快速下降后反弹,说明学习率太大。日志里的 P(Precision)和 R(Recall)也要分开看,钢材缺陷场景宁可 Recall 高一点,因为漏检一个缺陷上产线的代价远大于多标一个候选框。

4.2 一套调参顺序:先解决欠拟合,再解决过拟合

调参要有顺序,否则就是在碰运气。我的血泪经验是:先用 20 张图跑一个过拟合小实验,验证代码链路没问题,再上全量数据。小实验能过滤掉 90% 的代码级问题。

调参顺序通常是固定的四步。第一步确认数据链路没问题,看模型在 20 张图上能不能把 mAP 训到 0.9 以上,训不到说明代码或标注有问题。第二步检查 anchor 是否匹配你数据里的目标尺寸,不匹配时训练初期的 obj_loss 会有个奇怪的平台期。第三步调学习率,先看 loss 曲线是否平滑下降,震荡就降 lr。第四步才是拉长 epochs 或者换更大的模型,因为数据量和模型容量匹配才是精度瓶颈。

4.3 类别不平衡:把“氧化铁皮压入”的漏检降下来

训练完第一轮,你要从 results.csv 里统计每个类别的 AP,通常是裂纹和麻点这类小目标类偏低。处理类别不平衡不要靠简单复制图片,那只是让模型多看了几遍一样的东西,泛化收益有限。更好的手段是针对性增强:把低 AP 类别的目标从原图裁剪出来,随机旋转、缩放后粘贴到无缺陷图上。

另一个手段是调整单类置信度阈值。YOLOv5 的检测头里每个类别共享置信度分支,但你可以通过修改损失权重让模型更关注某个类别,不过这个操作偏底层。对毕设来说,我建议优先做数据层面的重采样和 copy-paste 增强,效果直观且不容易引入新的训练问题。

4.4 推理验证:导出 ONNX 并跑一个检测脚本

训练完成后,验证和导出流程要打通。先用 val.py 看验证集指标,再导出 ONNX 格式做推理验证:

python val.py --data steel.yaml \ --weights runs/train/exp/weights/best.pt \ --imgsz 640 \ --conf-thres 0.25 \ --iou-thres 0.45 python export.py --weights runs/train/exp/weights/best.pt \ --include onnx \ --imgsz 640

推理脚本可以用 PyTorch 或 ONNX Runtime 加载模型,核心是不要忘记 letterbox 预处理。用一段简单的推理代码验证模型能正确输出框:

import cv2 import torch import numpy as np # 从本地代码目录加载训练好的权重 model = torch.hub.load('./yolov5', 'custom', path='runs/train/exp/weights/best.pt', source='local') img = cv2.imread('steel_dataset/images/val/0001.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results = model(img, size=640, conf_thres=0.25) # 输出结果中 boxes: [x1, y1, x2, y2, conf, cls] for *box, conf, cls in results.xyxy[0].cpu().numpy(): x1, y1, x2, y2 = [int(v) for v in box] print(f"类别 {int(cls)} 置信度 {conf:.2f} 框 ({x1},{y1})-({x2},{y2})")

这里最容易踩的坑是坐标映射:模型内部会做 letterbox,推理结果的坐标已经映射回原图,但如果你手动做预处理却没有按同比例反向映射,画出来的框就会整体偏移。用 torch.hub 加载会自动处理这些细节,适合快速验证;如果要写进正式部署服务,建议用 ONNX Runtime 并自己实现完整的预处理和后处理逻辑。

5. 常见问题与排查:小目标漏检、类别失衡与过拟合的坑

5.1 训练 loss 在降但 mAP 一直是 0

现象:训练日志里 loss 平滑下降,但验证集 mAP 始终是 0,预测框全部为空或乱飘。

原因:最常见的是标注文件为空,或者标注里的类别编号从 1 开始而配置里从 0 开始,导致模型学到的映射整体错位。另一个原因是图片和标注没有严格一一对应,训练时候选了没有标注或标注错误的图片。

解决:回到第 2 章的校验脚本,确认每个标注文件都有有效内容,类别编号在 0~5 范围内。再打印任意一个 batch 的增强后图像和标注框,用可视化确认框真的画在了目标上。这一步能一次性过滤掉数据层面的绝大多数问题。

5.2 细裂纹全漏:小目标问题怎么处理

现象:验证集上大目标缺陷如斑块、夹杂检得很好,但裂纹、划痕这类细长目标几乎全漏,偶尔检出几个置信度也在 0.3 以下。漏检的裂纹往往只有几个像素宽。

原因:三个层面的原因叠加。输入 640 尺寸时裂纹经下采样后只剩 2~3 个像素,特征不明显;默认 anchor 是为 COCO 数据集设计的,对细长形目标不友好;NMS 后处理时低置信度框容易被抑制。

解决:按优先级操作。先跑一遍数据集中所有标注框的宽高统计,看长宽比分布;然后修改模型配置里的 anchors,加入一组细长形状的 anchor(比如 3 × 15 这种比例);最后在增强阶段加入小目标随机裁剪放大后再贴回背景的操作,人为制造更多小目标样本。前两步能解决大部分问题,第三步是锦上添花。

5.3 训练集 AP 高、验证集 AP 塌:过拟合怎么收

现象:训练集上 mAP@0.5 能到 0.95,验证集只有 0.6 甚至更低,且随着 epoch 增加差距越拉越大。

原因:数据量只有 1800 张,模型容量相对过剩,加上训练轮次拉满,模型开始背训练集样本而不是学泛化特征。

解决:先把早停参数调小,验证集 mAP 连续 15 个 epoch 不涨就停,不要硬训满 120 轮。然后检查增强强度,把 Mosaic 概率、旋转角度范围降下来。最后再考虑换更小的模型或者增加 Dropout。毕设场景下,一个稳定的 0.7 比一个波动的 0.8 更有说服力,因为答辩时你讲不清为什么训练集和验证集差这么多。

5.4 类别之间互相混淆:标注边界要统一

现象:氧化铁皮压入和麻点的预测框经常互相覆盖,同一张图上两个类别的置信度交替领先,混淆矩阵里这两类的交叉项特别高。

原因:这两类缺陷在视觉上本来就相近,标注时不同标注者对边界的理解不一致,导致模型学到的类间边界是模糊的。数据集的标注本身存在主观性,这不是模型能完全解决的。

解决:先做标注复核,把混淆严重的类别样本拉出来人工再看一遍,统一标注口径;如果仍然分不清,就考虑在答辩里诚实说明这个限制,并展示你在验证阶段对这两类做了额外的人为评估。工业检测里有些类别合并是合理的,你可以在实验里做一个“合并相似类”的消融对比,反而能体现你对问题场景的理解深度。

5.5 换了电脑结果对不上:随机种子与确定性

现象:同一份代码、同样的参数,在实验室的机器上 mAP 0.82,回家在自己电脑上变成 0.78,两次训练结果对不上。

原因:训练中的随机性来自数据增强的随机采样、随机权重初始化、甚至 GPU 上的浮点运算顺序。不固定随机种子,每次训练结果都会有波动。

解决:训练命令里加上--seed 0,保证数据增强和采样序列可复现;固定 CUDNN 的确定性模式,代价是训练速度略微下降。这个操作尤其重要,因为如果你做了多组对比实验,结果不可复现就等于实验白做。

6. 验收标准与答辩准备:用实验记录和对比数据交差

验收不能只给一张训练曲线截图,要从指标、稳定性、可解释性三个维度组织证据。我建议做一张验收表,把指标目标值和实测值列清楚:

验收项目标值实测值说明
mAP@0.5≥ 0.85填入你最好一轮的结果主要验收指标
mAP@0.5:0.95≥ 0.55填入对应值更严格的综合指标
单张 GPU 推理耗时≤ 30ms用 batch=1 实测体现部署可行性
单张 CPU 推理耗时≤ 500ms用 ONNX Runtime 实测无 GPU 场景的参考
训练集与验证集 mAP 差≤ 0.15两者相减衡量过拟合程度
六类 AP 最低类别≥ 0.70对 classes 单独统计防止靠某一类拉总分

答辩准备上,第一优先级是把思路讲成“选型有依据、实现有验证、问题有对策”。开场从产线质检的痛点切入,说明为什么需要自动检测;选型环节强调比较过两阶段检测器和单阶段检测器的差异,并说明选择 YOLOv5 是因为它在同等精度下推理更快,更适合产线节拍;实现环节重点讲你做了什么额外工作,比如标注校验脚本、细长 anchor 调整、两阶段迁移训练,这些才是区分你是不是只抄代码的关键点。

最后准备几个高频追问的答案。问“为什么用 YOLOv5 不用 YOLOv8 或更新的模型”,回答思路是:核心诉求是稳定复现和快速部署,YOLOv5 的工程生态、文档和部署工具链更成熟,我也做了对比实验,新模型不是不能用,但在同样数据规模下收益有限,反而增加调试成本。问“公共数据集上的结果能说明什么问题”,回答思路是:公共数据集保证可复现,我在其上完成了完整的方法验证,如果接入现场数据,流程可以直接复用。问“你的改进点在哪里”,不要在架构层面硬编,把改进集中在数据校验、增强策略、anchor 适配和训练流程这四件事上。

答辩展示环节有一个小技巧:准备几张“困难样本”的推理图,包括一张标注正确但模型漏检的、一张误检的、一张模型在低置信度下正确检出的,每张图配一句解释。这比展示十张完美检测图更有说服力,因为这说明你真的理解模型的边界在哪里。我做完这套项目之后,每次做检测任务都强制自己先走一遍数据校验和过拟合小实验,再上全量训练,这套顺序帮我少走了很多弯路。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:39:42

Angular 18 到 V21 跨版本升级实践:信号、构建优化与踩坑记录

最近刚把一个维护了三年的中后台管理平台从 Angular 18 一路升到了 V21&#xff0c;整个过程花了两周多一点。说实话&#xff0c;一开始我也觉得跨了三个大版本会很痛苦&#xff0c;但真正跑下来之后发现&#xff0c;只要把升级思路理清楚&#xff0c;绝大多数坑都是可以提前避…

作者头像 李华
网站建设 2026/10/15 11:39:26

IWOA算法:改进鲸鱼优化算法原理与实践

1. 算法优化领域的现状与挑战在智能优化算法领域&#xff0c;研究人员一直在寻找更高效、更稳定的解决方案。传统优化算法在面对高维、非线性、多峰等复杂问题时常常表现出收敛速度慢、易陷入局部最优等局限性。这就像是在茫茫大海中寻找宝藏&#xff0c;如果没有好的导航方法&…

作者头像 李华
网站建设 2026/10/15 11:39:25

微电网调度优化:MPC算法设计与Python实现

1. 微电网调度优化与MPC的黄金组合 微电网作为分布式能源系统的核心单元&#xff0c;其调度优化直接关系到能源利用效率和运行经济性。传统调度方法往往采用静态规则或离线优化&#xff0c;难以应对可再生能源的波动性和负荷突变。三年前我在参与某工业园区微电网项目时&#x…

作者头像 李华
网站建设 2026/10/15 11:39:15

数据库索引是什么?为什么添加索引后查询会更快?

数据库索引是什么&#xff1f;为什么添加索引后查询会更快&#xff1f; 网站访问速度不仅取决于服务器CPU、内存和带宽&#xff0c;也与数据库查询效率密切相关。当数据库中的数据越来越多时&#xff0c;如果每次查询都需要逐条检查记录&#xff0c;响应时间就可能明显增加。 数…

作者头像 李华
网站建设 2026/10/15 11:38:58

C语言变量与常量:内存管理与优化实战

1. 为什么变量与常量是C语言的基石 十年前我刚接触C语言时&#xff0c;曾经天真地认为变量就是"会变的量"&#xff0c;常量就是"不会变的量"。直到在调试一个嵌入式系统时&#xff0c;因为误用了const修饰符导致整个内存区数据被意外改写&#xff0c;我才真…

作者头像 李华
网站建设 2026/10/15 11:38:50

解决n8n与飞书日历API的OAuth2授权问题

1. 问题现象与初步诊断最近在将n8n工作流与飞书日历对接时&#xff0c;遇到了一个棘手的错误&#xff1a;当工作流尝试访问飞书日历API时&#xff0c;服务器返回了400状态码&#xff0c;并附带错误信息{"code":99991679,"msg":"Unauthorized"}。…

作者头像 李华