news 2026/10/20 18:12:57

C#上位机实现空压机集群Modbus监控与故障预警实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#上位机实现空压机集群Modbus监控与故障预警实战

我们车间去年上了一套空压机集群,六台机器分布在三个不同的角落,有人值守也有无人站房。最头疼的不是设备本身出故障,而是故障发生前那几十分钟你根本察觉不到——排气温度慢慢爬升、压差悄悄变大、卸载时间越来越长,等报警灯亮了,往往已经对生产造成影响了。我花了两周时间用C#写了一个上位机监控系统,通过Modbus协议把这六台设备的控制器全部拉进一个界面里,实现了实时数据采集、多设备轮询通信和故障预警。这篇文章把整个实战过程中踩过的坑、关键设计和能直接搬运的代码逻辑整理出来,给正在做类似工控上位机项目的朋友一个完整参考。

1. 空压机集群的监控需求与Modbus通信的选型逻辑

1.1 空压机群控到底要采什么数据

先说清楚一个问题:空压机控制器本身有屏幕,现场也能看,为什么还要做上位机?因为设备分散、数据量多、故障是渐变式的。一台空压机控制器里的关键参数通常有几十个,真正影响运行判断的主要是这几类:

  • 运行参数:排气压力、排气温度、油温、油压、压缩机电机的电流和功率、风扇电机电流、运行频率
  • 状态信息:加载/卸载状态、运行/停机状态、故障代码、维护提醒、控制器内部计时
  • 能耗数据:累计运行时间、累计加载时间、当前功率、运行电流、电机启动次数

这些数据通过控制器上的通信接口暴露出来。不同品牌的空压机控制器支持不同协议,但绝大多数都支持Modbus,这也是我最终选择用Modbus实现集群采集的根本原因——兼容性好,几乎不会遇到"设备不支持"的尴尬情况。哪怕是十年前的旧控制器,只要有RS485口,基本都能通。

1.2 为什么最终选了Modbus RTU/TCP 而不是其他协议

做上位机通信方案之前我其实纠结过一阵子:OPC UA、MQTT、Modbus,到底选哪个?

MQTT走以太网,适合跨系统数据上云,但空压机控制器支持MQTT的很少,一般要加网关转换。OPC UA是目前工业互联的大趋势,但同样需要网关,而且对中小项目来说配置复杂、调试成本高。Modbus虽然老,但它有一个无法替代的优势:协议栈极简,任何支持串口通信的控制器基本都带Modbus寄存器,而且C#实现一套Modbus主站代码非常简单,调试工具也成熟。

现场控制器分布在一个车间和两个站房,距离都在两三百米以内,用RS485总线组网很合适。单独一台设备自带以太网接口的,我直接走Modbus TCP,省掉一层串口转换。其余几台走RS485总线,通过USB转485串口线接到上位机电脑上。所以最终的方案是:Modbus RTU为主、Modbus TCP为辅的混合采集架构。

1.3 设备寄存器映射表的整理方法

这是整个项目里最琐碎也最重要的一步。不同品牌的空压机控制器,寄存器地址定义完全不同。有的控制器用40001起始的保持寄存器地址,有的控制器把数据放在输入寄存器里,还有的同一个参数在两种寄存器里都有但精度不一样。我在拿到每一台设备的通信协议文档之后,先做了一张统一的寄存器映射表,后续所有代码都围绕这张表来做。

下面这张表是从我的项目里摘出来的简化版,以某品牌螺杆空压机控制器为例:

参数名称寄存器类型寄存器地址数据格式单位备注
排气压力保持寄存器(4x)40005uint16 / 100MPa实际值=原始值/100
排气温度保持寄存器(4x)40009uint16℃0~200对应0~200℃
运行电流保持寄存器(4x)40017uint16 / 10A实际值=原始值/10
运行状态保持寄存器(4x)40101位映射-Bit0=运行 Bit1=加载
故障代码保持寄存器(4x)40111uint16-1=高温 2=高压...
累计运行时间保持寄存器(4x)40201uint32小时高16位在前
保养剩余天数保持寄存器(4x)40211uint16天0表示已到期

整理这张表的过程看起来简单,实际上有个很关键的思维方式:字段清单要站在"监控系统"的角度定,而不是站在"某个控制器"的角度抄协议文档。不同厂家的控制器参数地址不一样,但监控系统只需要统一的参数名。我在C#代码里定义了一个设备模板类,每个设备实例只需要给出"寄存器地址对应哪个逻辑参数"的配置,采集层完全不关心具体是哪个牌子的控制器。

2. 上位机整体架构与C#数据采集层的搭建

2.1 采集层架构:一个主站调度器解决所有设备

C#上位机的整个体系我分成了三层:通信层、数据服务层、UI展示层。通信层是核心,它的职责只有一个:按照配置好的设备列表,通过Modbus协议把各台空压机的寄存器数据读回来,统一转换成标准格式的对象,交给上层使用。

通信层内部做了一个"主站调度器"。这个概念很关键——上位机本来就是Modbus主站,空压机控制器都是从站,一个主站轮流访问多个从站,这在Modbus协议设计里是天经地义的事。调度器维护一个设备队列,按照配置的轮询周期依次读每台设备的寄存器。读到的数据带时间戳,放进一个线程安全的缓存集合里,UI层和数据服务层从这个缓存取数,不直接触碰通信线程。

这样做的好处是:哪怕某一台设备通信中断,也只会影响它自己的轮询任务,不会把UI界面卡死。我一开始贪省事,直接在UI线程里串行读设备,结果设备一断线,整个界面就卡住不动了。后来改成调度器+缓存模型,通信异常和界面响应彻底解耦,这个问题才彻底解决。

2.2 C#实现Modbus主站的关键代码逻辑

Modbus RTU和Modbus TCP的数据帧结构不一样,但处理逻辑高度相似。RTU的报文结构是:从站地址、功能码、数据区、CRC校验。TCP的报文结构则是:事务标识符、协议标识符、长度字段、从站地址、功能码、数据区。核心区别就是TCP有MBAP头部且不需要CRC,RTU每包数据结尾有2字节CRC。

我用了开源的NModbus库来封装底层协议处理,避免自己写CRC校验和报文组包时出低级错误。库本身很稳定,但它默认的串口读写行为在工控场景下有些不符合实际需求,我做了两层包装:

  1. 串口初始化时设置DtrEnable=true和RtsEnable=true,很多USB转485模块需要这两个引脚置位才能进入发送状态
  2. 每次读操作加上超时控制,默认从站无响应会在500ms内返回错误,不让轮询线程无限阻塞

下面这段是从我的上位机里摘出来的核心读取逻辑,经过裁剪保留主干部分:

public class ModbusDeviceWorker { private readonly IModbusMaster _master; private readonly DeviceConfig _config; public async Task<DeviceSnapshot> PollAsync(CancellationToken ct) { var snapshot = new DeviceSnapshot { DeviceId = _config.DeviceId, Time = DateTime.Now }; foreach (var item in _config.RegisterItems) { try { ushort[] data; if (_config.UseTcp) { data = await _master.ReadHoldingRegistersAsync( _config.SlaveId, item.Address, item.Length, ct); } else { data = await _master.ReadHoldingRegistersAsync( _config.SlaveId, item.Address, item.Length); } snapshot.Values[item.LogicalName] = DataParser.Parse(item, data); } catch (Exception ex) { snapshot.ErrorCount++; // 记录日志,标记该设备本次轮询失败 } } return snapshot; } }

这段代码的核心思想:每个设备一个Worker对象,所有设备的Worker由调度器统一驱动。轮询失败不会中断整体流程,只是设备快照里多了一个错误计数。类似的代码在市面上很多文章里都能看到,我不太喜欢把每个细节都贴出来,因为工控项目的价值不在代码本身,而在代码和数据逻辑的配合。不过有一点可以确定:CancellationToken一定要传,没有这个机制,上位机关闭的时候后台线程很容易卡死在串口读取上,导致进程无法退出。

2.3 数据解析:字节序陷阱和数据类型转换

Modbus寄存器里的数据解析,是新手最容易翻车的重灾区。一个16位寄存器存的是uint16,这没什么争议。但当参数是32位整数、浮点数时,问题就来了:高16位在前还是低16位在前?有的控制器用大端序(低地址存高字节),有的用小端序(低地址存低字节),甚至同一个厂家的不同型号都可能不一样。

我调试某台设备的时候遇到一个很典型的情况:读取排气压力,协议文档上写的是"寄存器40005,高16位在前,除以100得到MPa",但我按照这个解析出来的数值是几十万。排查了半天,发现实际控制器返回的数据是小端序存储,和文档正好相反。从那之后我养成了一个习惯:所有设备的寄存器解析格式全部做成可配置项,不在代码里硬编码字节序。

public static float ParseValue(RegisterItem item, ushort[] rawData) { if (item.DataType == DataType.Float32) { // 根据配置的字节序决定组合方式 var raw = item.ByteOrder == ByteOrder.BigEndianFirst ? (rawData[0] << 16) | rawData[1] : (rawData[1] << 16) | rawData[0]; return BitConverter.ToSingle(BitConverter.GetBytes(raw), 0); } if (item.DataType == DataType.Uint32) { return item.ByteOrder == ByteOrder.BigEndianFirst ? (rawData[0] << 16) | rawData[1] : (rawData[1] << 16) | rawData[0]; } // uint16 类型直接转换 return item.Factor > 0 ? rawData[0] / item.Factor : rawData[0]; }

配置化的好处不仅仅是省事。当现场新增一台不同品牌的空压机时,不需要重新编译代码,只需要在XML配置文件里加一段设备描述,把寄存器地址、数据类型、字节序、缩放系数填进去,系统就能直接开始采集。这套机制在后面扩容到第八台设备时帮了大忙。

3. 多设备并发通信的调度算法与稳定性设计

3.1 轮询周期与超时参数怎么定

多设备采集有很多种策略。最简单的是一个for循环,逐个设备轮询,一个周期走完,再从头循环。但这种策略有个致命缺陷:如果其中一台设备响应慢或者断线,整个循环周期会被拖长,其它等待采集的设备数据新鲜度就无法保证。

我最终采用的方案是定时器驱动的独立轮询任务。每台设备一个独立计时器,各有各的轮询周期。空压机的排气压力、温度这类参数,变化不会特别剧烈,我设的轮询周期是2秒一次。设备断线重试策略单独设计:连续3次读取失败判定为设备离线,接下来每隔10秒才重试一次,避免对一台离线设备高频发送无效请求,浪费总线资源。

轮询超时时间默认500ms,但这是一个可以配置的参数。如果总线上挂的设备多,一条RS485总线串了多台设备,超时设置太短会让慢速设备永远读不到数据,太长又拖慢整个队列,要根据实际设备响应速度灵活调。

3.2 RS485总线的设备地址冲突与轮询队列管理

RS485总线上每个从站必须有唯一地址。听起来是老生常谈,但实际工程里真会遇到。我现场有一台空压机买回来时控制器默认地址是1,和另一台设备的地址撞了。结果上位机每次读到地址1返回的数据,有时候是这台设备的、有时候是另一台设备的,界面上的参数跳来跳去,排查了很久才发现是地址冲突。

解决方法是统一规划设备地址表,每台新设备接入前先通过控制器面板或专用软件把Modbus从站地址改成规划好的编号,并且贴好标签。同时在上位机侧增加一个"地址冲突检测"逻辑:连续两次轮询同一地址返回的数据特征变化异常(比如排气温度从40跳到90又跳回来),就弹出提示,让运维人员去检查设备地址设置。

3.3 串口并发安全:为什么不能多线程同时写同一个串口

RS485是半双工总线,同一时刻只允许一个主站发请求。所以我特别强调:虽然每台设备有独立轮询任务,但同一个串口上的所有设备必须共享一个读写锁。C#里用SemaphoreSlim来控制同一串口的读写并发,保证同一时刻只有一条Modbus请求在总线上。

如果忽略这一点,多个线程同时往同一个串口写数据,RS485总线上就会出现帧碰撞,所有设备返回的数据都是乱的,而且因为CRC校验失败还会让设备端的通信状态变得异常。很多现场问题排查到最后,问题根源不在设备,而在于上位机自己把总线搞乱了。

我封装了一个SerialPortGate类,内部维护一个信号量:

public class SerialPortGate { private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); public async Task<T> ExecuteAsync<T>(Func<Task<T>> action, int timeoutMs) { if (await _gate.WaitAsync(timeoutMs)) { try { return await action(); } finally { _gate.Release(); } } throw new TimeoutException("串口忙碌,获取控制权超时"); } }

实际运行中,串口层面的竞争远比想象中常见,尤其是程序里还有历史数据回读、参数下发等辅助功能时。这个门禁机制保障了所有操作都走同一个串口通道,不出现帧覆盖。

3.4 通信稳定性实测与断线恢复表现

整套系统上线初期我统计过一组数据:三台走RS485的设备加一台走TCP的设备,连续运行72小时,串口通信成功率99.6%左右,失败的次数大多出现在某一台设备控制器重启的时候。这个成绩不能说多优秀,但作为实时监控系统已经够用了。断线恢复方面,由于每台设备独立重试,一台设备重启的几分钟内,其余设备的数据流完全不受影响,界面不会卡顿,历史数据不会出现间歇空白。

4. 故障预警规则引擎:不仅仅是超限报警

4.1 固定阈值报警的局限

监控系统的第一层预警功能,是按固定阈值报警,比如排气温度超过95℃报"高温预警"。这种报警好实现,但问题也很明显:只能等故障发生或即将发生的那一刻才触发。空压机的高温很少是突然飙上去的,往往是油滤堵塞、散热器积灰导致温度一天比一天高,等到了95℃才报警,可能设备内部已经出现损伤了。

我更关心的是趋势性故障——参数虽然还在"正常范围"内,但变化速率已经说明问题了。这就是我要做的第二层预警:梯度预警。

4.2 梯度预警与变化率计算

以排气温度为例,我定义了两个报警参数:短时变化率和持续爬升趋势。短时变化率计算的是最近5分钟内排气温度的平均增长速度,如果连续两个5分钟内温度都在以每分钟超过0.8℃的速度上升,说明散热系统出问题了,触发"温度异常爬升"预警,提醒运维人员检查散热器和油路。相比固定阈值,这种预警能提前20到40分钟发现问题。

实现上不复杂:把每次采集到的排气温度放入一个环形缓冲区,保存最近30个采样点(对应一分钟数据),计算滑动窗口内的线性回归斜率。C#里实现线性回归也就几十行代码,核心公式是:

[ k = \frac{n\sum xy - \sum x \sum y}{n\sum x^2 - (\sum x)^2} ]

其中x是采样序号,y是温度值。斜率k就是每分钟温度变化率。算出来之后和历史趋势加权平均,得到平滑的趋势值,避免单次采样抖动引起的误报。

4.3 运行状态联动的报警去抖

预警系统最怕的其实是误报。空压机在加载和卸载两种状态下,同一参数的变化率是完全不同的。比如排气压力,加载时会稳定上升,卸载时基本不变。如果拿加载状态的变化率去卡卸载状态,分分钟误报。

我的做法是报警规则全部关联运行状态:压力变化率只在加载状态下计算,温度变化率在任何运行状态下都计算但门槛值分开设置。另外加了"持续时间"条件,一个触发条件要连续满足三次轮询周期(即6秒左右)才确认报警,过滤掉瞬时抖动。这套联动逻辑写完之后,连续跑了两个星期,误报数量从每天十几次降到了零。

4.4 分级预警与通知机制

报警分三级。黄色预警:参数异常但设备仍可运行,通知中控室大屏显示并在系统内记录;橙色预警:参数接近临界值,同时发送短信给设备维护人员;红色故障:设备已停机或存在严重安全风险,直接调用空压机控制器的远程停机指令(如果有权限),并弹窗提示处理。

现有的Modbus协议支持写单个寄存器,所以远程停机在底层技术上是可行的。但我强烈建议不在自动报警逻辑里直接给设备下发停机指令,因为误停机的代价远高于误报警。我的做法是红色预警时系统发出强烈提示,停机动作由现场人员确认后一键执行。

5. 数据入库与运行报表的辅助决策价值

5.1 历史数据存储设计

实时监控只能看到当前状态,真正能指导保养和维护决策的是历史数据。我用了本地的时序数据库思路存储:采集到的所有参数每5秒一条,写入一个SQLite数据库文件。考虑到数据量,这里做了个取舍:每台设备的全部原始参数存一份,但超过30天的数据自动归档成每小时平均值,减少单表体积,查询速度也不会因为数据累积而下降。

SQLite在工控上位机里是个不错的选择——免安装、零配置、单文件,直接通过C#的Microsoft.Data.Sqlite驱动访问。上位机重启之后,历史数据全部还在,报表查询也不依赖外部数据库服务。

5.2 报表与趋势曲线怎么服务运维决策

数据入库之后,我做了两个实用功能。第一个是日运行报告,自动统计每台空压机当天加载率、平均排气压力、最高排气温度、故障次数,在第二天凌晨生成汇总记录。第二个是趋势曲线,可以对任意参数画最近7天、30天、90天的变化曲线,并叠加设备保养记录时间线。

这个功能上线之后,有一个实际案例让我印象很深:四号机的排气温度曲线在三个月里呈现明显的缓慢爬坡趋势,从85℃一路升到92℃,曲线斜率一直在增加。根据这个趋势,我提前安排了油滤更换,换完之后温度马上回落到正常水平。如果没有趋势曲线,这个问题大概会拖到95℃报警才被发现。这就是监控系统的真正价值:不是等设备坏了再去修,而是通过数据变化趋势提前判断设备状态的变化方向。

6. 现场部署踩坑实录:RS485通信和干扰的处理经验

6.1 通信不稳定最常见的几个原因

系统部署初期,RS485通信经常出现"时而通、时而不通"的诡异问题。排查过程很折磨人,我把最终确认的几个原因和对应解决方案整理成一张表,你现场遇到类似情况可以按顺序排查:

现象可能原因解决方案
所有设备全都读不到A/B线接反对调A/B接线或软件里互换收发引脚
单台设备偶发无响应线路过长、分支过多缩短总线,加120Ω终端电阻
设备返回数据CRC错误多线缆屏蔽层未接地单端接地,避免形成地环路
特定设备总超时从站地址冲突用控制器面板改唯一地址
上位机重启后首轮全失败USB转串口驱动初始化慢启动时延时1~2秒再开串口

我踩得最深的坑是终端电阻。RS485总线规范要求在总线两端各接一个120Ω终端电阻,但很多人(包括第一次搞这个项目的我)都会忽略。我们的总线有三个站房,首尾距离大概200米,没接终端电阻时数据误码率不高,但每隔几分钟就会出现一次读超时。后来在总线的两端各并了一个120Ω电阻之后,通信稳定性的改善非常明显。抵抗干扰的能力有了质的提升。

6.2 上位机程序掉线重连与看门狗

工控上位机讲究的是长期稳定运行,不是跑几天就能下班。现场环境里电磁干扰、USB供电波动、控制器重启,各种意外都有可能打断通信链路。所以我加了两个健壮性机制:

  1. 设备自动重连:每台设备维护一个连接状态机,从正常到失败到离线再到重连成功,整个状态的切换逻辑都记录日志。尤其是USB转串口线偶尔会掉驱动,程序要做的是在检测到串口无响应时自动重开串口,而不是要求运维人员重启上位机。

  2. 上位机看门狗:这个看门狗不是硬件看门狗,我是用另一个定时任务每隔30秒检查一次核心采集线程是否还活着,如果发现采集心跳超时(说明通信线程卡死),就自动重启整个通信层重新初始化串口。这招看起来很粗暴,但在无人值守的站房场景里,自动恢复比什么都强。

6.3 上位机界面和信息呈现的取舍

最后简单说下界面。我没有做很炫酷的3D可视化,因为空压机集群监控的核心价值在数据和预警,不在视觉。主界面就是一个设备列表卡片,每台设备用状态色块标识运行/停机/告警,点击卡片弹出详细参数页和历史曲线。值班人员扫一眼主界面,就能知道当前六台设备的状态。

真正提高运维效率的是告警栏和事件流水。任何一条预警都会在界面上方滚动显示,并且带有"确认"按钮,谁处理的、什么时间处理的,全程留痕。这个功能一开始只是为了方便交接班,后来发现它对追查故障前因后果特别有帮助。

7. 这套系统的扩展空间与后续演进方向

项目做完之后运行了几个月,整体达到了预期效果,但我自己也清楚它还有很多可以完善的地方。比如现在预警规则是基于经验值设置的,下一步我打算引入控制器的输出频率和电机功率数据,结合加载率做更深层次的状态诊断——比如设备长期在低负载下频繁加载卸载,说明用气端有间歇性大流量需求,这时候可以考虑建议增加储气罐来优化运行效率。这个诊断方向目前只是初步设想,但方向是对的:从"监控设备状态"走向"分析设备性能"。

另外,Modbus TCP的几台设备目前是直连上位机,如果将来设备数量继续增加,我打算在前面加一层工业网关,把RS485转成TCP,统一走网络通信,减少串口依赖,也方便做远程监控。C#这边再用SignalR或者Web API把数据透传到Web端,值班人员甚至可以在办公室电脑上直接看实时数据,不需要专门再装客户端。

如果你正准备做类似的空压机集群监控项目,我给你三个最务实的建议:先把寄存器映射表和字节序确认清楚再写代码;轮询架构一定要把超时、重试、断线恢复这些"高级功能"在一开始就设计进去,别等跑起来再补;最后,所有报警规则都要经过现场实际运行数据的调参,纸上谈兵的阈值一定会被现场打脸。这套系统本身没什么高深的技术,但它稳定解决了我们车间的实际痛点,这才是它最大的价值。

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

大语言模型未解难题:从幻觉到推理的五大研究方向

ChatGPT 出来之后的这一年多&#xff0c;我身边几乎所有做 NLP 的朋友都在反复问同一个问题&#xff1a;大模型是不是把路走完了&#xff1f;答案显然不是&#xff0c;但到底还有什么没解决&#xff0c;大家又说不清。最近某高校的 NLP 团队在一次内部交流里给出了一个很提神的…

作者头像 李华
网站建设 2026/10/20 18:12:28

Jenkins前端卡顿?从主题插件到JVM调优的全流程排查实践

1. 先把症状说清楚&#xff1a;卡顿问题不能上来就“重启试试”发现自己负责的 Jenkins 前端打开越来越慢&#xff0c;是上周的事。先是同事在群里发了张截图&#xff0c;说页面转圈十几秒还出不来&#xff0c;我第一反应是网络问题。登录服务器看了一眼&#xff0c;CPU 被 Jen…

作者头像 李华
网站建设 2026/10/20 18:12:26

对话式Linux运维Agent:高危操作强制确认与断线自动守候复检实践

做运维的应该都经历过这种时刻&#xff1a;凌晨三点&#xff0c;业务告警&#xff0c;人还没完全清醒&#xff0c;手已经放在键盘上&#xff0c;对着屏幕敲命令。一边怕敲错&#xff0c;一边又必须快。我做的这个对话式 Linux 运维 Agent&#xff0c;就是冲着这个场景去的——用…

作者头像 李华
网站建设 2026/10/20 18:11:32

AI原生生成4K全景视频:原理、应用与实操复盘

最近AI视频生成赛道的屏霸&#xff0c;是一条关于“4K 360度全景视频原生生成”的消息。一支跨区域研究团队放出了技术细节&#xff0c;核心不是“生成了一部超高清全景片”&#xff0c;而是“模型直接输出球面视角的连续视频”。老玩家都知道&#xff0c;这俩区别是天壤之别。…

作者头像 李华
网站建设 2026/10/20 18:10:51

燃料电池电源MATLAB仿真:PEMFC与SOFC分布式发电模型拆解

简介&#xff1a;这份MATLAB仿真资源聚焦燃料电池分布式发电&#xff0c;面向电力电子、新能源方向的学生与研究人员&#xff0c;帮助理解PEMFC与SOFC两类燃料电池的建模思路与并网系统设计。压缩包共23个文件&#xff0c;约279KB&#xff0c;包含mdl与sim仿真模型、m脚本、txt…

作者头像 李华
网站建设 2026/10/20 18:10:13

AI效率决策地图:场景化工具选型与实操参数指南

1. 项目概述&#xff1a;这不是工具清单&#xff0c;而是一份“AI效率决策地图”“效率up&#xff01;热门AI工具盘点”——看到这个标题&#xff0c;很多人第一反应是点开扫一眼列表&#xff0c;记下几个名字&#xff0c;然后关掉页面。但作为连续三年深度参与多个跨行业AI落地…

作者头像 李华