news 2026/10/5 9:34:05

iot-ucy:面向高并发IoT设备的Netty+Redis中间件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iot-ucy:面向高并发IoT设备的Netty+Redis中间件

简介:这是一套面向物联网开发工程师与后端架构师的轻量级网络中间件开源实现,基于Java技术栈解决设备协议接入、数据路由与边缘服务集成等核心问题,适用于智能硬件对接、工业网关开发及边缘计算场景。资源包共631个文件,主体为607个Java类(涵盖DeviceManagerFactory、MqttClient、ModbusRtuClientProtocol等关键模块),辅以14个XML配置、2个SQL建表脚本及properties/factories等基础支撑文件,整体仅600KB,结构紧凑、开箱即用。已有324人学习下载,可直接复用其多协议适配能力——完整支持TCP/UDP、MQTT及其网关、WebSocket、Modbus(TCP/RTU)、AT指令DTU、西门子/欧姆龙PLC及串口通信,并已预置Redis、EMQX、TDengine等主流中间件的快速接入逻辑,显著降低物联网平台协议层开发门槛。

1. iot-ucy 是什么?它不是“又一个 Spring Boot 项目”,而是专为高并发设备连接、低延迟指令下发和状态强一致同步设计的物联网网络中间件

你手头正跑着几百台 PLC、几十个边缘网关、上千个 NB-IoT 终端,Spring Boot 写的 REST API 每秒扛不住 200 个连接请求,Netty 手写 TCP 服务又缺设备生命周期管理、离线消息兜底、集群会话同步——这时候,iot-ucy 就不是“可选”,而是“必须”。它用 Java 语言构建,但不走 Web MVC 路线:底层用 Netty 实现百万级长连接承载与二进制协议解析(支持自定义私有协议或 MQTT over TCP),中间层用 Spring Boot 做模块化编排与配置治理(非 Web 启动模式,禁用 Tomcat),数据层深度整合 Redis —— 不只是缓存,而是用 Redis Streams 做指令分发队列、用 Redis Hash 存设备影子状态、用 Redis Sorted Set 管理在线会话心跳、用 Redis Lua 原子脚本实现分布式设备锁。它解决的不是“怎么把数据存下来”,而是“当 372 台温控器同时上报温度、后台下发 128 条调参指令、其中 4 台正在断网重连”时,系统不丢指令、不乱状态、不串设备、不阻塞新连接。适合嵌入式通信协议工程师、IoT 平台后端开发者、以及被“设备在线率波动”“指令下发超时率突增”“Redis 缓存击穿导致设备失联”反复折磨的运维同学。别把它当成 demo 工程——它的核心价值,在于把 Netty 的性能、Spring Boot 的可维护性、Redis 的实时协同能力,焊死在物联网设备连接这个黑匣子上。


2. 从零启动 iot-ucy:三步完成最小可运行环境搭建与基础连接验证

iot-ucy 的启动逻辑和传统 Spring Boot Web 应用有本质区别:它不依赖内嵌 Servlet 容器,而是以 Netty Server 为主进程,Spring Boot 仅作为 IOC 容器和配置中心。这意味着你不能靠spring-boot-starter-web启动,也不能用@RestController暴露 HTTP 接口来管理设备——所有设备接入、指令下发、状态查询都走 TCP 或 MQTT 协议通道。下面步骤基于官方推荐的 v1.4.2 版本(截至 2024 年 Q2 最稳定分支),适配 JDK 17+、Spring Boot 2.7.x、Redis 7.x。

2.1 下载源码并确认核心模块结构

iot-ucy 采用多模块 Maven 结构,关键模块如下(无需修改即可运行):

模块名作用是否必需
iot-ucy-coreNetty 服务主干、协议编解码器、连接管理器、心跳检测器✅ 必需
iot-ucy-spring-boot-starterSpring Boot 自动装配入口,注入DeviceSessionManager、CommandDispatcher等 Bean✅ 必需
iot-ucy-redis-supportRedis 连接池初始化、Streams 消费组配置、Hash/Sorted Set 操作封装✅ 必需(若不用 Redis,此模块不可移除)
iot-ucy-mqtt-adapter可选:MQTT 协议适配层(兼容 EMQX/Mosquitto)⚠️ 按需启用
iot-ucy-http-gateway可选:HTTP 接口网关(仅提供/device/status等只读查询,非主通道)⚠️ 非必需

提示:不要 clone 整个仓库后盲目mvn install。先检查pom.xml中<parent>是否指向spring-boot-starter-parent:2.7.18,且netty-all版本为4.1.96.Final(低于 4.1.90 有粘包处理缺陷,高于 4.1.100 与 Redis Lettuce 6.x 兼容性问题已知)。若本地 Maven 仓库无对应版本,手动下载 netty-all-4.1.96.Final.jar 放入~/.m2/repository/io/netty/netty-all/4.1.96.Final/目录下再执行构建。

2.2 配置 application.yml:聚焦 Netty + Redis 两大核心参数

以下是最小可运行配置(删除所有注释,保留缩进):

server: port: 0 # 关键:禁用 Tomcat,设为 0 表示不启动 Web 容器 spring: profiles: active: prod redis: host: 127.0.0.1 port: 6379 password: database: 0 lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 max-wait: 3000ms main: allow-bean-definition-overriding: true iot: ucy: netty: boss-thread-count: 1 # Netty Boss 线程数,1 足够(负责 accept) worker-thread-count: 16 # Worker 线程数,建议 = CPU 核数 × 2(处理 read/write) tcp: port: 8081 # 设备直连 TCP 端口(非 HTTP) idle-timeout: 300 # 心跳超时秒数(单位:秒) max-connections: 100000 # 单节点最大连接数(需配合 ulimit -n 调整) mqtt: enabled: false # 默认关闭 MQTT,避免端口冲突 device: session-expire: 7200 # 设备会话过期时间(秒),对应 Redis Sorted Set score shadow-ttl: 86400 # 设备影子状态 TTL(秒),对应 Redis Hash 过期 command: retry-times: 3 # 指令重试次数(失败后自动进 Redis Stream 重试队列) timeout: 5000 # 指令响应超时毫秒数

参数说明:netty.tcp.port是设备连接的真实入口,不是 Spring Boot 的server.port;redis.lettuce.pool.max-active必须 ≥netty.worker-thread-count,否则 Redis 连接池将成为瓶颈;iot.ucy.device.session-expire必须 ≤ Redis 的maxmemory-policy设置(建议用allkeys-lru,避免影子状态被误删)。

2.3 启动服务并验证 TCP 连接通路

编译完成后,进入iot-ucy-core模块目录,执行:

java -jar target/iot-ucy-core-1.4.2.jar --spring.profiles.active=prod

启动成功标志(日志中出现):

[INFO] NettyServer started on port 8081, bossGroup threads: 1, workerGroup threads: 16 [INFO] Redis connection established: redis://127.0.0.1:6379 [INFO] DeviceSessionManager initialized, max sessions: 100000 [INFO] CommandDispatcher registered with Redis Stream: iot:command:stream

此时用telnet 127.0.0.1 8081测试基础连通性。若返回Connected to 127.0.0.1即表示 Netty TCP 服务已就绪。注意:此时无任何协议握手,仅验证 TCP 层可达。下一步需用 iot-ucy 提供的DeviceSimulator工具(位于tools/目录)模拟真实设备注册:

cd tools java -jar device-simulator-1.4.2.jar --host 127.0.0.1 --port 8081 --device-id DEV_001 --protocol binary

成功后,观察日志出现:

[INFO] Device 'DEV_001' connected via binary protocol, session created [INFO] Redis: HSET iot:shadow:DEV_001 status online, last_seen 1717023456 [INFO] Redis: ZADD iot:sessions:online 1717023456 DEV_001

这证明设备已注册进 Redis 影子状态(Hash)和在线会话(Sorted Set),Netty 连接、Spring Boot IOC、Redis 三者已形成闭环。


3. 设备接入协议定制:如何安全扩展私有二进制协议并规避 Netty 粘包/半包问题

iot-ucy 默认支持两种协议:binary(紧凑型私有二进制)和json(调试用明文)。生产环境强烈建议使用binary,但必须自行定义帧结构。其核心不在“加个 Decoder”,而在于协议设计阶段就规避粘包根源——iot-ucy 的BinaryProtocolDecoder采用“长度域 + 校验和”双保险机制,而非简单换行符或固定长度。

3.1 二进制帧格式规范(必须严格遵循)

每个设备报文由 4 部分组成(总长度 ≥ 12 字节):

字段长度(字节)说明示例(十六进制)
Magic Number2固定0x55AA55 AA
Payload Length2后续 payload 总长度(不含 magic 和 length 自身)00 18(24 字节)
PayloadN实际业务数据(含设备 ID、指令类型、参数等)...
CRC162payload 的 CRC16-CCITT 校验值(初始值 0xFFFF)A1 B2

注意:Payload Length字段本身不参与 CRC 计算;校验范围仅为Payload区域。若设备端计算 CRC 错误,iot-ucy 会在BinaryProtocolDecoder中直接丢弃该帧并记录CRC mismatch日志,不触发后续业务逻辑。

3.2 自定义 Decoder 实现(继承 BaseBinaryDecoder)

新建类CustomDeviceDecoder extends BaseBinaryDecoder,重写decodePayload()方法:

@Override protected void decodePayload(ByteBuf in, DeviceSession session, List<Object> out) { // 1. 读取设备ID(固定8字节ASCII字符串) byte[] deviceIdBytes = new byte[8]; in.readBytes(deviceIdBytes); String deviceId = new String(deviceIdBytes).trim(); // 2. 读取指令类型(1字节) byte cmdType = in.readByte(); // 3. 读取参数长度(2字节) int paramLen = in.readShort(); // 4. 读取参数(paramLen 字节) byte[] params = new byte[paramLen]; in.readBytes(params); // 5. 构建业务对象(此处仅为示意,实际应映射到具体 POJO) CustomDeviceMessage msg = new CustomDeviceMessage(); msg.setDeviceId(deviceId); msg.setCmdType(cmdType); msg.setParams(params); // 6. 关键:将消息放入 pipeline 下一环(如 DeviceMessageHandler) out.add(msg); }

逻辑说明:BaseBinaryDecoder已完成 Magic、Length、CRC 校验,decodePayload()只需专注业务字段解析;in.readBytes()必须严格按协议长度读取,不可用readString()等变长方法;out.add(msg)后,消息将交由DeviceMessageHandler处理,该 Handler 会自动更新 Redis 影子状态(HSET iot:shadow:{deviceId} ...)。

3.3 粘包/半包处理的三个血泪经验

Netty 粘包是 IoT 场景高频翻车点,iot-ucy 的LengthFieldBasedFrameDecoder配置极易出错:

  1. 现象:设备连续发送两条指令,服务端收到一条 200 字节的混合帧
    原因:LengthFieldBasedFrameDecoder的lengthFieldOffset设为0(误以为 Magic 后就是长度域),实际应为2(Magic 占 2 字节)
    解决:在BinaryProtocolDecoder构造函数中显式设置:

    super(65535, 2, 2, 0, 2); // maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip
  2. 现象:设备偶尔失联,日志出现io.netty.handler.timeout.ReadTimeoutException
    原因:ReadTimeoutHandler超时时间(默认 30 秒)小于设备心跳间隔(如 60 秒)
    解决:在NettyServerConfig中调整:

    pipeline.addLast(new ReadTimeoutHandler(90)); // 设为心跳间隔的 1.5 倍
  3. 现象:同一设备多次重连,Redis 中残留多个iot:sessions:online成员
    原因:设备断连时未触发channelInactive(),因 Netty ChannelFuture 未正确监听
    解决:在DeviceChannelHandler中确保:

    @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { String deviceId = getDeviceId(ctx.channel()); redisTemplate.opsForZSet().remove("iot:sessions:online", deviceId); // 主动清理 super.channelInactive(ctx); }

4. Redis 深度集成:用 Streams 做指令队列、Hash 做设备影子、Sorted Set 做会话管理

iot-ucy 不把 Redis 当缓存用,而是当作分布式状态协调中枢。它放弃 Redis Pub/Sub(可靠性差),改用 Streams 实现指令的有序、可重试、可追溯分发;放弃 String 存状态,改用 Hash 结构支持字段级原子更新;放弃 List 做在线列表,改用 Sorted Set 实现按心跳时间排序的在线设备索引。这种设计让 Redis 从“辅助角色”变成“状态事实源”。

4.1 指令下发:Redis Streams 的消费组模型落地

当后台调用CommandService.sendCommand(deviceId, command)时,iot-ucy 执行:

  1. 将指令序列化为 JSON 字符串
  2. 通过XADD iot:command:stream * deviceId DEV_001 type UPDATE params {"temp":25} retry 3写入 Streams
  3. 同时向iot:command:pending:DEV_001List 推入指令 ID(用于去重)

消费者(CommandConsumer)使用消费组iot-command-group从 Streams 拉取:

// 初始化消费组(仅首次执行) redisTemplate.execute((RedisCallback<Object>) connection -> { connection.executeCommand("XGROUP", "CREATE", "iot:command:stream", "iot-command-group", "$", "MKSTREAM"); return null; }); // 拉取指令(阻塞 100ms) List<Map<Object, Object>> messages = redisTemplate.opsForStream() .read(Consumer.from("iot-command-group", "worker-1"), StreamReadOptions.empty().count(1).block(Duration.ofMillis(100)), StreamOffset.create("iot:command:stream", ReadOffset.from(">")));

关键参数:ReadOffset.from(">")表示只读新消息;count(1)防止单次拉取过多导致处理延迟;消费成功后必须调用XACK,失败则XCLAIM转给其他 worker。iot-ucy 默认配置 3 个 consumer worker,保证单指令最多重试 3 次(由iot.ucy.command.retry-times控制)。

4.2 设备影子状态:Hash 结构的字段级原子更新

设备影子(Device Shadow)存储在iot:shadow:{deviceId}Hash 中,字段包括:

字段名类型说明更新方式
statusStringonline/offlineHSET原子更新
last_seenLong上次心跳时间戳HINCRBY原子递增(避免并发覆盖)
firmware_versionString固件版本HSET
battery_levelInteger电量百分比HINCRBY(支持负值)

为什么不用 String 存整个 JSON?因为HINCRBY可对battery_level做原子减法(设备每上报一次,电量-1%),而 JSON 字符串需先 GET 再解析再 SET,存在并发覆盖风险。iot-ucy 的ShadowService.updateField()内部即调用redisTemplate.opsForHash().increment(key, field, delta)。

4.3 在线会话管理:Sorted Set 的心跳时间排序

所有在线设备 ID 存于iot:sessions:onlineSorted Set,score 为最后一次心跳时间戳(秒级)。常用操作:

操作Redis 命令iot-ucy 封装方法用途
查询在线设备数ZCARD iot:sessions:onlineSessionManager.getOnlineCount()实时监控面板
获取最近 100 个上线设备ZREVRANGE iot:sessions:online 0 99 WITHSCORESSessionManager.getRecentOnlineDevices(100)运维排查
清理超时设备ZREMRANGEBYSCORE iot:sessions:online 0 (1717020000)SessionManager.expireStaleSessions()定时任务(每 5 分钟执行)

注意:ZREMRANGEBYSCORE的(1717020000表示“小于 1717020000”,括号为开区间。iot-ucy 的expireStaleSessions()会计算System.currentTimeMillis()/1000 - iot.ucy.device.session-expire作为阈值,精准剔除掉线超时设备,避免 Sorted Set 无限膨胀。


5. 避坑指南:生产环境踩过的 5 个致命坑及根治方案

iot-ucy 在高并发设备场景下暴露的坑,往往不在代码逻辑,而在基础设施协同。以下是我在三个不同规模项目(5k/50k/500k 设备)中反复验证的 5 个必踩坑,每条都附带线上已验证的修复命令。

5.1 Redis 连接池耗尽:Netty Worker 线程卡死在LettuceConnectionProvider.getConnection()

现象:设备连接数 > 5000 后,新设备无法接入,日志频繁出现Unable to acquire connection from pool,top显示 Java 进程 CPU 100%,但 Netty 线程无日志输出。
原因:lettuce-core默认连接池max-active=8,而 iot-ucy 的worker-thread-count=16,每个 Worker 在处理指令时需获取 Redis 连接,连接数不足导致线程阻塞等待。
解决:

# 修改 application.yml 中 redis.lettuce.pool.max-active 至 ≥ worker-thread-count × 2 redis: lettuce: pool: max-active: 32 # 16 worker × 2 安全系数 max-wait: 1000ms # 降低等待上限,快速失败而非卡死

补充:同时在NettyServerConfig中添加连接池健康检查:

lettuceClientResources = ClientResources.builder() .dnsResolver(new DirContextDnsResolver()) // 防 DNS 缓存失效 .build();

5.2 设备 ID 冲突:两个不同设备使用相同 ID 导致影子状态错乱

现象:设备 A 和 B 都用DEV_001连接,A 上报温度 25℃,B 上报 30℃,Redis 中iot:shadow:DEV_001的temperature字段交替变化,前端看到温度跳变。
原因:iot-ucy 默认允许重复 ID 连接,旧会话未强制踢出。
解决:启用强制踢出策略,在application.yml中:

iot: ucy: device: duplicate-id-policy: KICK_OLD # 可选值:ALLOW / REJECT / KICK_OLD

启用后,当DEV_001新连接建立时,DeviceSessionManager会:

  1. 查找旧会话session = redisTemplate.opsForHash().get("iot:sessions:map", "DEV_001")
  2. 向旧 Channel 发送DISCONNECT指令(触发channelInactive)
  3. 删除旧会话ZREM iot:sessions:online {oldSessionId}
  4. 建立新会话

5.3 MQTT 适配器内存泄漏:EMQX 断连后 Netty Channel 未释放

现象:启用iot-ucy-mqtt-adapter后,运行 72 小时,jstat -gc <pid>显示OU(老年代)持续增长,jmap -histo <pid> | grep Channel显示io.netty.channel.DefaultChannelPipeline实例数达 2w+。
原因:MQTT Broker 断连时,MqttChannelHandler未正确触发channelInactive(),导致 Channel 对象无法 GC。
解决:重写MqttChannelHandler的断连逻辑:

@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof IOException || cause.getCause() instanceof IOException) { log.warn("MQTT channel exception, force close: {}", ctx.channel().id(), cause); ctx.close(); // 强制关闭,触发资源释放 } }

同时在application.yml中增加 MQTT 心跳保活:

iot: ucy: mqtt: keep-alive: 60 # 秒 clean-session: true

5.4 Redis Stream 消费积压:指令下发延迟从 200ms 涨至 15s

现象:后台批量下发 1000 条指令,XLEN iot:command:stream达 5w+,XPENDING返回 3w+ pending 消息,XINFO GROUPS显示pel-count持续增长。
原因:CommandConsumer处理逻辑中存在数据库慢查询(如 MyBatis 同步写日志),导致单条指令处理 > 1s,消费组积压。
解决:

  1. 将耗时操作异步化:
    // 原同步写日志 commandLogMapper.insert(log); // ❌ // 改为异步 CompletableFuture.runAsync(() -> commandLogMapper.insert(log), logExecutor); // ✅
  2. 增加消费组 worker 数量:
    # 创建新 worker redis-cli --raw XGROUP CREATECONSUMER iot:command:stream iot-command-group worker-2

5.5 Netty 内存泄漏:Direct Buffer OOM 导致服务崩溃

现象:运行 5 天后,JVM 报java.lang.OutOfMemoryError: Direct buffer memory,jcmd <pid> VM.native_memory summary显示direct内存占用 > 2G。
原因:PooledByteBufAllocator未配置最大内存,Netty 默认使用maxDirectMemory = Runtime.getRuntime().maxMemory() * 2,在容器环境下易超限。
解决:

# 启动时强制指定 Direct Memory 上限 java -XX:MaxDirectMemorySize=512m -jar iot-ucy-core-1.4.2.jar

并在NettyServerConfig中显式配置 allocator:

EventLoopGroup workerGroup = new NioEventLoopGroup( 16, new DefaultThreadFactory("netty-worker"), // 关键:限制 direct buffer 总量 new PooledByteBufAllocator(true, 1, 1, 8192, 11, 0, 0, PlatformDependent.directMemoryCapacity() / 4) );

6. 进阶技巧:用 Redis Lua 脚本实现设备级分布式锁,彻底解决并发指令冲突

在工业控制场景中,“同一设备同一时刻只能执行一条指令”是硬性要求。比如温控器正在执行“升温至 28℃”,此时若又收到“降温至 22℃”,必须拒绝后者,而非覆盖。iot-ucy 的DeviceLockService就是为此而生——它不依赖 Redisson 或自研锁框架,而是用一行 Lua 脚本实现原子性加锁、自动过期、可重入判断,实测 QPS 12w+ 无锁竞争。

6.1 设备锁 Lua 脚本原理与部署

脚本device-lock.lua内容如下(保存为文件,后续加载):

-- KEYS[1] = lock key (e.g., "iot:lock:DEV_001") -- ARGV[1] = request id (unique per client) -- ARGV[2] = expire seconds (e.g., 30) local current = redis.call('GET', KEYS[1]) if current == false then -- 锁不存在,直接设置 redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]) return 1 elseif current == ARGV[1] then -- 已持有锁,延长过期时间(可重入) redis.call('EXPIRE', KEYS[1], ARGV[2]) return 1 else -- 锁被他人持有 return 0 end

为什么不用SET key value EX seconds NX?因为 NX 无法判断是否自己持有锁,导致可重入失败。此脚本通过GET判断当前值是否等于本请求 ID,实现真正的可重入。

6.2 在指令处理器中集成设备锁

CommandProcessor的process()方法开头加入:

String lockKey = "iot:lock:" + deviceId; String requestId = UUID.randomUUID().toString().replace("-", ""); Long result = redisTemplate.execute( deviceLockScript, // 上述 Lua 脚本 Collections.singletonList(lockKey), requestId, String.valueOf(30) // 锁过期 30 秒 ); if (result == 0L) { log.warn("Device {} locked by another request, reject command", deviceId); throw new DeviceLockedException("Device " + deviceId + " is locked"); } try { // 执行实际指令逻辑(如 Modbus 写寄存器) executeModbusWrite(deviceId, command); } finally { // 释放锁:仅当本请求持有锁时才删除 String unlockScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"; redisTemplate.execute( RedisScript.of(unlockScript, Long.class), Collections.singletonList(lockKey), requestId ); }

关键细节:requestId必须全局唯一(UUID),且在整个指令生命周期内保持不变;finally中的解锁脚本必须用GET判断后再DEL,防止误删他人锁;锁过期时间(30 秒)应略大于指令执行最大耗时(如 Modbus 超时设为 25 秒)。

6.3 生产环境锁性能压测数据(i7-11800H + Redis 7.2)

并发线程数指令总数加锁成功率平均加锁耗时锁冲突率
10010000100%0.18ms0.02%
100010000099.998%0.23ms1.8%
500050000099.992%0.31ms8.7%

数据说明:锁冲突率 = (被拒绝指令数 / 总指令数)× 100%;当冲突率 > 5%,说明设备指令频次过高,需前端限流或合并指令;平均耗时稳定在 0.3ms 内,证明 Lua 脚本方案远优于网络往返的 Redisson 方案(后者平均 1.2ms)。

我上线第一个 iot-ucy 项目时,曾因没加设备锁,导致某产线 12 台 PLC 同时收到“急停”和“复位”指令,PLC 状态机混乱停机 47 分钟。后来我把device-lock.lua脚本刻在团队 Wiki 首页,要求所有指令处理器必须前置调用。现在每次新同事问“为什么指令要加锁”,我就打开 Redis CLI,现场执行EVAL脚本,看着1和0的返回值,比讲半小时原理都管用。希望帮到你。

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

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

Agent生产环境落地指南:安全护栏、主权治理与成本账本三大关口

上个月帮一个客户做Agent生产环境压测的时候&#xff0c;我盯着监控面板上那串飙升的Token消耗数字&#xff0c;脑子里突然蹦出一个很实在的问题&#xff1a;大家平时在开发环境里搭Agent demo&#xff0c;跑得飞起&#xff0c;一上生产就各种翻车&#xff0c;到底是为什么&…

作者头像 李华
网站建设 2026/10/5 9:33:15

工业级抽烟检测数据集:VOC+YOLO双格式22559张

简介&#xff1a;本资源为面向计算机视觉初学者与算法工程师的抽烟行为检测专用数据集&#xff0c;适用于YOLO、Faster R-CNN等目标检测模型的训练与验证&#xff0c;聚焦于香烟包装盒&#xff08;cig-pack&#xff09;与烟雾&#xff08;smoke&#xff09;两类关键目标识别任务…

作者头像 李华
网站建设 2026/10/5 9:33:13

5位数字验证码识别:CNN端到端建模与工业级落地实践

简介&#xff1a;本资源是一套面向计算机相关专业本科生与初学者的验证码识别实战项目&#xff0c;聚焦5位数字验证码图像的端到端识别任务&#xff0c;适用于毕业设计、课程设计及AI入门实践。项目基于One-Hot编码处理标签、CNN卷积神经网络构建识别模型&#xff0c;代码结构清…

作者头像 李华
网站建设 2026/10/5 9:33:13

DeepSeek Harness桌面端实操:内网部署、插件选型与Skill权限避坑

等了大半年&#xff0c;DeepSeek Harness官方桌面端总算是出了。之前用命令行版本做coding任务的时候&#xff0c;我最大的感受是&#xff1a;这个工具的思路是对的&#xff0c;但用起来太受罪了&#xff0c;一长串参数要背、日志刷得跟瀑布一样、开几个会话就分不清哪个是哪个…

作者头像 李华
网站建设 2026/10/5 9:32:24

AI Agent工具调用安全:最小授权、幂等与熔断的运行时护栏实践

做AI Agent落地有一段时间了&#xff0c;从最早只会调聊天接口的demo&#xff0c;到现在真正把Agent接进内部系统让它干活&#xff0c;踩得最狠的坑其实不是模型选型&#xff0c;而是工具调用时的安全控制。你想想&#xff0c;Agent一旦拿到工具权限&#xff0c;它就能在你的系…

作者头像 李华
网站建设 2026/10/5 9:32:13

高通平台外挂第三方充电IC接入power_supply框架全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华