news 2026/9/7 11:51:24

MicroPython日志模块uLogLite实战:级别、轮转与过滤

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MicroPython日志模块uLogLite实战:级别、轮转与过滤

做嵌入式开发这些年,我一直坚持一个观点:凡是打算在设备上跑超过一个月的 MicroPython 项目,日志模块从来就不是“锦上添花”,而是“保命工具”。很多朋友图省事,从开发到量产全程用print打日志,结果现场出问题后对着串口海量输出翻半天,那种体验我经历过太多次了。后来我专门写了一个轻量日志模块 uLogLite,支持日志级别控制、文件轮转、按标签过滤,代码量不大,但项目里能顶大用。这篇文章我就把整个模块的设计思路和核心代码掰开揉碎讲一遍,你照着敲一遍,就能直接搬到自己的项目里用。

1. 日志模块到底在解决什么问题

1.1 从 print 到日志:嵌入式排障的痛点

先说说我为什么对print意见这么大。原型阶段用print调试确实爽,插上串口线什么都看得见,但项目一旦复杂起来,问题全冒出来了。

第一个痛点是日志没有级别区分。调试信息、运行状态、错误信息全混在一起,串口一刷屏,你真正想找的关键错误早被淹没。我记得有个网关项目,WiFi 模块每 5 秒打印一条 RSSI,传感器模块每 100ms 打印一条采集值,整个串口输出跟瀑布一样,后来排查一个偶发的 MQTT 断连问题,只能靠人眼去刷屏里找线索,效率极低。

第二个痛点是缺少时间戳。设备在客户那儿跑了一天,回头问“昨天下午这台设备崩没崩过”,你拿不出任何时间维度的线索。没有时间戳的日志,只能证明某件事发生过,但完全没办法还原时序,排查问题等于少了一条腿。

第三个痛点是无法按模块屏蔽。嵌入式项目里模块太多了,传感器、网络、显示、存储、协议栈,每个模块都有自己的输出,但排障时往往只关心其中一个。没有过滤机制,你只能把所有日志拉下来慢慢筛。

第四个点比较隐蔽——输出目标单一。串口是调试阶段的好帮手,但到了现场部署,串口线不可能永远插着。真正要回溯历史问题,必须把日志落盘,存到文件系统里。print默认输出到 stdout,想重定向到一个文件,在 MicroPython 里做起来很别扭。

所以一个像样的日志模块,至少应该具备这些能力:有日志级别,能控制什么等级的消息可以输出;有时间戳,记录每条日志的发生时刻;有来源标签,标明日志是哪个模块打的;支持输出到文件和串口;能按级别和标签做过滤;当文件过大时支持轮转。这就是 uLogLite 设计时的需求清单。

1.2 级别、轮转、过滤:日志三大件的设计逻辑

先聊日志级别。级别本质上是一组从小到大排列的数字,代表消息的“严重程度”。uLogLite 沿用了 Python 官方 logging 的划分方式:

级别数值适用场景
DEBUG10调试细节,比如每次读取的原始值
INFO20关键状态变更,比如开机、连接成功
WARNING30异常但还能恢复,比如重试
ERROR40功能失效,但系统还能跑
CRITICAL50系统级故障,无法继续工作

用数字而不是字符串的好处是判断起来非常高效。比如 logger 的当前级别是 INFO(20),那么低于 20 的 DEBUG 日志直接丢弃,一句话if level < self.level: return就完事了。

再聊过滤。级别过滤只能控制严重程度,但解决不了“我只想看网络模块日志”这种需求,所以还需要标签过滤。我给 uLogLite 设计的过滤方式很简单:每条日志在写入时可以带一个标签(比如networksensor),然后通过set_filter(include_tags=['network'])指定只输出哪些标签,或者用exclude_tags排除某些标签。实现上就是两次集合成员判断,代码不复杂,但实战价值极高。

最后是轮转。嵌入式设备的 flash 空间有限,日志文件不能无限增长。轮转的思路是:当当前日志文件超过设定大小(比如 64KB)时,把当前文件改名为app.log.1,然后新开一个app.log继续写;下一次再超限,就把app.log.1改名为app.log.2,原来的app.log变成app.log.1,新的app.log重新开始。这样磁盘上最多保留 N 份历史日志,空间可控。

2. uLogLite 接口设计与现实约束

2.1 API 设计:一个类还是散装函数

在动手写代码之前,我想先聊一下接口设计的取舍。有人喜欢用全局函数,比如log_info("xxx"),觉得调用方便。但我建议用类封装。原因是嵌入式项目里往往有多个独立的业务模块,如果只用一组全局函数,就只能共享一个全局配置,没法做到“这个模块写文件、那个模块只写串口”的精细控制。

uLogLite 的 API 设计非常简单,核心就一个类,几个方法。初始化时传入名称、级别、日志文件路径、轮转大小、备份份数等参数,之后调用debuginfowarningerrorcritical就能写日志。每个方法还可以额外传一个tag参数,用来标记这条日志的来源模块。

有人可能会问,既然每个方法都能传标签,那name参数还有什么用?我的设计思路是:name是这个 logger 实例的默认标签,如果调用时不传tag,就用name代替。这样大多数日志只需要写log.info("sensor init ok"),只有需要细分的场景才显式传tag,代码不会显得啰嗦。

2.2 嵌入式约束:内存、文件系统与 flash 寿命

写 MicroPython 日志模块,绕不开三个现实约束。

第一个是内存。以 ESP32 为例,跑完 MicroPython 和 WiFi 协议栈之后,可用 RAM 非常紧张。所以模块实现必须克制:不要在每条日志里创建大量临时对象,不要在构造函数里贪心地做太多初始化,更要避免把整个日志文件读进内存来判断大小。

第二个是文件系统行为差异。MicroPython 的open不会自动创建父目录,如果/sd/log/这个目录不存在,直接open('/sd/log/app.log', 'a')必然抛 OSError。还有一点很关键:MicroPython 的os.stat()返回的是一个元组,文件大小是第 7 个元素,索引是 6,很多人第一次用都会拿错索引。

第三个是 flash 寿命。几乎所有 MicroPython 开发板的文件系统都跑在 SPI flash 上,而 flash 的擦写次数是有限的。如果设备每秒钟往日志文件里写几十条数据,日志分区几个月就可能磨损报废。所以我在实际项目中,量产阶段通常会把日志级别从 DEBUG 调到 WARNING,高频落盘改成仅串口输出,只有当需要排障时才临时开 INFO 级文件日志。这个习惯能让设备的寿命长很多。

2.3 为什么不直接移植 PC 端 logging 库

有人会问:MicroPython 官方不是提供了一个logging模块吗,直接import logging不就行了?确实,micropython-lib 里有一个 logging 库,如果你用的固件恰好预装了它,直接用也是一种选择。我有段时间也是这么干的,但后来遇到几个问题。

一是不是所有固件都预装了这个模块。换一块开发板、换一个固件版本,可能import logging直接就 ModuleNotFoundError,你还得想办法把库文件塞进去。二是官方 logging 模块的设计目标是兼容 CPython 的 logging API,功能多、层级多、Handler 概念也多,在资源受限环境下显得有点臃肿。三是它默认不提供文件轮转能力,想要轮转还得自己扩展 FileHandler,本质上还是要写代码。

所以我最后决定自己写一个 150 行左右的实现,只保留三样核心能力——级别、轮转、过滤。这样整个模块的行为完全可控,出问题也能快速定位。如果你只是临时用用,官方 logging 没问题;但如果你想要一个长期维护、行为稳定、完全看得懂的日志基础组件,我推荐自己写一个 uLogLite 这样的轻量实现。

3. 核心代码实现:uLogLite 逐段拆解

3.1 完整代码先给你

先上完整代码。这个文件我是按照 MicroPython 的语法和标准库写的,不依赖任何第三方库,在 ESP32、RP2040 等主流开发板上都能跑。你新建一个uloglite.py,把下面这段复制进去即可。

""" uLogLite - 轻量级 MicroPython 日志模块 支持日志级别、文件轮转、标签过滤 """ import os import time class LogLevel: DEBUG = 10 INFO = 20 WARNING = 30 ERROR = 40 CRITICAL = 50 class uLogLite: _LEVEL_NAMES = { LogLevel.DEBUG: 'DEBUG', LogLevel.INFO: 'INFO', LogLevel.WARNING: 'WARNING', LogLevel.ERROR: 'ERROR', LogLevel.CRITICAL: 'CRITICAL', } def __init__(self, name='app', level=LogLevel.INFO, log_file=None, max_size=64 * 1024, backup_count=2, fmt='[{time}] [{level}] [{name}] {msg}'): self.name = name self.level = level self.log_file = log_file self.max_size = max_size self.backup_count = max(2, backup_count) self.fmt = fmt self.include_tags = None self.exclude_tags = None self._fh = None if self.log_file: try: self._fh = open(self.log_file, 'a') self._check_rotation() except OSError as e: print('[uLogLite] open log file failed:', e) self._fh = None def _format_time(self): now = time.localtime() return '%04d-%02d-%02d %02d:%02d:%02d' % ( now[0], now[1], now[2], now[3], now[4], now[5]) def _write(self, level, tag, msg): if level < self.level: return if self.include_tags and tag not in self.include_tags: return if self.exclude_tags and tag in self.exclude_tags: return line = self.fmt.format( time=self._format_time(), level=self._LEVEL_NAMES.get(level, str(level)), name=tag, msg=msg, ) + '\n' if self._fh: self._check_rotation() self._fh.write(line) self._fh.flush() else: print(line, end='') def _check_rotation(self): if not self._fh or not self.log_file: return try: size = os.stat(self.log_file)[6] except OSError: return if size < self.max_size: return self._fh.close() self._fh = None for i in range(self.backup_count - 1, 0, -1): src = '%s.%d' % (self.log_file, i) dst = '%s.%d' % (self.log_file, i + 1) try: os.remove(dst) except OSError: pass try: os.rename(src, dst) except OSError: pass dst = self.log_file + '.1' try: os.remove(dst) except OSError: pass try: os.rename(self.log_file, dst) except OSError: pass self._fh = open(self.log_file, 'w') def debug(self, msg, tag=None): self._write(LogLevel.DEBUG, tag or self.name, msg) def info(self, msg, tag=None): self._write(LogLevel.INFO, tag or self.name, msg) def warning(self, msg, tag=None): self._write(LogLevel.WARNING, tag or self.name, msg) def error(self, msg, tag=None): self._write(LogLevel.ERROR, tag or self.name, msg) def critical(self, msg, tag=None): self._write(LogLevel.CRITICAL, tag or self.name, msg) def set_level(self, level): self.level = level def set_filter(self, include_tags=None, exclude_tags=None): self.include_tags = include_tags self.exclude_tags = exclude_tags def close(self): if self._fh: self._fh.close() self._fh = None

3.2 日志级别与过滤判定:一行 if 里的大学问

整个模块最核心的逻辑集中在_write方法里,它做了三件事:级别判断、标签过滤、组装输出。

级别判断是性能第一道关卡。if level < self.level: return这行代码看似简单,但它保证了当日志级别不够时,后续的时间格式化、字符串拼接、文件写入全部不会执行。这在高频日志场景下很重要。比如调试阶段开了 DEBUG 级,但现场运行级别只开 INFO,那些每 100ms 打一条的 DEBUG 日志会被直接丢弃,不会产生任何 CPU 和时间开销。

标签过滤是第二道关卡。include_tagsexclude_tags的语义要理清楚:include_tags是白名单,设置了之后只有标签在列表里的日志才会输出;exclude_tags是黑名单,设置在列表里的标签一律不输出。如果两个都设置了,先判白名单再判黑名单。这个顺序我是有意安排的,因为白名单筛掉的比例通常更高,先过滤可以少做一些无效判断。

组装输出阶段有一个细节值得注意:我用的是_LEVEL_NAMES.get(level, str(level))而不是if-elif判断。原因很简单,用字典查表比一长串 if-else 更清晰,而且将来要扩展自定义级别,只需要往字典里加一项就行。时间格式化方面,MicroPython 的time.localtime()返回一个 8 元组,前六个元素分别是年、月、日、时、分、秒,所以索引 0 到 5 直接取出来格式化。我建议用百分号格式化,因为 MicroPython 对str.format的一些高级格式语法支持不完整,而%04d这种百分号格式化是兼容性最稳的。

3.3 日志轮转:先关文件再重命名

轮转是整个模块里最容易写错的地方。我来说说_check_rotation里的几个关键细节。

首先是获取文件大小。os.stat(self.log_file)[6]这个写法,很多人会疑惑为什么是索引 6。MicroPython 的os.stat()返回一个元组,模仿的是 CPython 的 stat_result,其中第 7 个元素是st_size,也就是索引 6。如果不确定,可以先print(os.stat(self.log_file))看一遍再索引。还有一点要注意,如果文件被删了或者路径有问题,os.stat会抛 OSError,所以这里必须用 try-except 包住。

其次是轮转的顺序。为什么先关闭文件句柄再重命名?因为文件打开状态下,不同文件系统对 rename 的处理不一样,有些 VFS 实现会直接拒绝重命名一个正在打开的文件,或者导致数据没刷入。保险起见,一律先close(),再操作文件。

然后是重命名链的逻辑。假设backup_count = 3,轮转时要保留app.logapp.log.1app.log.2三份文件。具体操作是:先删除最老的app.log.3,然后把app.log.2改名为app.log.3,把app.log.1改名为app.log.2,最后把app.log改名为app.log.1,再新开一个app.log。代码里for i in range(self.backup_count - 1, 0, -1)正好是从 2 到 1 的逆序遍历,完成的是“从旧到新”的搬运。

这里有一个 MicroPython 特有的坑:CPython 在 Unix 系统上,os.rename在目标文件存在时是可以覆盖的,但 MicroPython 的底层 VFS 实现五花八门,很多移植版遇到目标文件已存在会直接抛 OSError。所以正确姿势是:先os.remove(dst),再os.rename(src, dst)。不要嫌这两步麻烦,这是我在实际项目里踩过坑之后总结出来的稳妥方案。

最后一个细节:轮转完成后,重新打开文件用的是'w'模式而不是'a'模式。因为新文件本来就是空的,用哪个都行,但'w'有“从零开始”的语义,更符合直觉。每次写日志之后调用flush()也非常重要,MicroPython 的缓冲区策略在不同平台表现不一致,如果不主动 flush,掉电的时候很可能丢日志。

3.4 对外 API 与动态配置:让日志模块“可调”

五个对外方法debuginfowarningerrorcritical本质上都是_write的薄封装,区别只在于传入的级别常量不同。我选择一口气暴露五个而不是用一个log(level, msg),纯粹是为了调用时的可读性。你写log.info("boot ok")和写log.log(20, "boot ok"),一眼看过去前者清晰得多。

set_levelset_filter这两个动态配置方法,是我在实际项目中用得很频繁的。比如设备出厂前把级别定在 WARNING,到了现场出问题后,我远程发一条指令把级别动态降到 INFO,日志就开始记录更详细的信息了。这个能力在 PC 端 logging 里都有,但在嵌入式环境里,因为资源紧张,很多人会忘记加。我的建议是:日志模块必须支持运行期动态调整级别,否则每次调日志参数都要重新烧录固件,现场排障的效率会低到让你怀疑人生。

close方法虽然简单,但它在两个场景下是必须的:一是 OTA 升级前要关闭文件句柄,确保日志数据完整落盘;二是某些文件系统在卸载 SD 卡之前,必须把所有打开的句柄都关掉,否则会报错。所以即使是一个看起来“跑完就不用管”的日志模块,也一定要留一个清理出口。

4. 项目接入实操:从抄代码到会调参

4.1 初始化与基础使用

uloglite.py放进项目的根目录或者在main.py里 import,然后按下面的方式初始化:

from uloglite import uLogLite, LogLevel log = uLogLite( name='gateway', level=LogLevel.DEBUG, log_file='/sd/log/app.log', max_size=32 * 1024, backup_count=3, ) log.info('system boot ok') log.debug('free memory: %d bytes' % gc.mem_free()) log.warning('rssi is low: %d' % rssi) log.error('mqtt connect failed, retry later')

如果你的设备暂时不需要落盘,只想往串口输出,那log_file就传None。此时 uLogLite 会自动退化成print模式,不管有没有文件句柄都能正常工作。这个特性在调试阶段非常有价值:先在串口确认逻辑没问题,再切换成文件模式做长稳测试,代码一行都不用改。

初始化之后,log.info(...)会生成类似这样的一行日志:

[2025-03-18 10:24:31] [INFO] [gateway] system boot ok

方括号里的四个字段分别对应模板里的timelevelnamemsg。如果你对格式不满意,可以通过fmt参数自定义,比如想加上线程名、去掉时间、或者调整字段顺序,都在初始化时传一个fmt就行。注意fmt里的花括号占位符必须使用{time}{level}{name}{msg}这四个名字,因为_write里就是用str.format把这四个变量替换进去的。

4.2 轮转参数怎么定:容量估算与场景选择

轮转参数要选择,核心是理解每份日志能装多少信息。max_size决定单份文件的最大体积,backup_count决定保留几份历史文件,整体占用的磁盘空间就是max_size * backup_count

我以一个典型的电池供电采集终端为例:每分钟写一条 INFO 日志,每条日志约 120 字节,一小时 7200 字节,一天约 173KB。如果max_size设为 64KB,那么大约 9 个小时轮转一次;如果设成 128KB,就是 18 个小时轮转一次。backup_count = 3时,总日志空间为 384KB,大概能覆盖 2-3 天的完整日志记录。对大多数 4MB flash 的开发板来说,这个占用比例可以接受。

我个人的选择习惯是:调试阶段用 32KB 加backup_count=2,保证日志不占太多空间;量产阶段用 128KB 加backup_count=3,因为现场出问题时需要更长的历史记录来回放。如果你的设备有 SD 卡,空间比较宽裕,可以把max_size调大一点,减少轮转频率,降低 flash 擦写次数。

有一点必须提醒:max_size的单位是字节,不是 KB。有些朋友写max_size=1024以为代表 1MB,结果日志文件写到 1KB 就被轮转,排查半天才发现是单位搞错了。

4.3 多模块下的过滤实战

假设你做了一个多功能传感器网关,代码分成三块:传感器采集、网络通信、蓝牙广播。每个模块在打日志时传不同的标签,就可以在排障时非常精准地过滤。

log = uLogLite(name='main', level=LogLevel.INFO, log_file='/sd/log/app.log') # 传感器模块 log.info('temp read ok: %.2f' % temp, tag='sensor') # 网络模块 log.error('wifi reconnect fail', tag='network') # 蓝牙模块 log.debug('adv packet sent', tag='ble')

正常运行时,你只想知道整体的健康状态,级别设为 INFO 就够了。某天用户反馈网络频繁断开,你想看网络模块到底发生了什么,直接执行:

log.set_level(LogLevel.DEBUG) log.set_filter(include_tags=['network'])

此时只有当标签是network的日志才会输出,而且 DEBUG 级也放开了,网络模块的每个细节都会落盘。其他模块的日志全部静音,不会干扰你的排查。这个操作甚至可以通过 MQTT 远程下发指令来触发,我之前的网关就是这么干的——用户报障后,我在后台发一条消息,远程把日志过滤打开,过半小时再拉日志文件分析,问题基本都能定位。

5. 现场排障:uLogLite 常见问题实录

5.1 日志文件写不了

很多人第一次跑起来就发现日志文件是空的,或者文件根本没创建。最常见的原因有三个:路径目录不存在、文件系统只读、存储空间已满。

MicroPython 的open不会自动创建父目录,所以如果路径是/sd/log/app.log,你得先确认/sd/log/这个目录存在,不存在就用os.mkdir('/sd/log')创建。第二个常见问题是 SD 卡或 flash 分区挂载成了只读,这种情况在异常断电后比较常见,检查方式是os.getcwd()或直接尝试手动创建文件。第三个是空间满,日志写进去返回成功,但实际没落盘,这时os.statvfs('/')看一下剩余空间就知道了。

还有一个容易忽略的点:如果你改了代码之后重新运行,但日志的内容还是旧的,大概率是文件缓冲没刷。uLogLite 里面每次写完都会flush(),可以避免这个问题。如果你的代码里自己调用了open写文件,也记得要 flush。

5.2 轮转不生效或备份丢

轮转不生效,先说最经典的一个原因:os.stat(self.log_file)[6]取错索引。MicroPython 在部分版本里os.stat()返回的元素个数和顺序可能让你意外,但st_size基本都在索引 6。如果实在不确定,先用print(os.stat(self.log_file))打印出来看看。

第二个原因是打开了文件但没写入。uLogLite 只在写入前检查轮转,如果你初始化之后一条日志都没打过,文件大小一直为 0,当然永远不会轮转。这不算 bug,但确实容易让人误以为轮转坏了。

第三个原因比较隐蔽:轮转时os.rename的目标文件已经存在。我前文提过,MicroPython 的 VFS 实现里,rename 遇到目标已存在有时会抛异常。uLogLite 的代码里已经用try: os.remove(dst)做了容错,但如果你的代码是参考别人的实现、没有这一步,那轮转到你设置的备份份数上限后就会不断失败,备份文件永远只有最新的一份,老文件都被覆盖了。排查的时候看一眼文件列表,如果只有app.logapp.log.1,没有app.log.2,多半就是这个问题。

5.3 时间不对,日志全变 2000 年

MicroPython 开发板上电之后,time.localtime()返回的时间默认是固件编译时间或者某个固定的初始时间,不会是真实时间。如果设备没有 RTC 模块也没联网同步时间,日志里的时间戳会显示成 2000-01-01,非常扎眼。

解决方式分三种:带 WiFi 的模块可以在联网后通过网络时间协议同步一次系统时间;带外部 RTC 模块的可以在启动时读取 RTC 值并machine.RTC().datetime(...)写入系统 RTC;如果设备纯粹离线运行,那就得在代码里配置一个编译时基准时间,再自己维护一个运行时长累加器。不管用哪种方式,只要最终系统时间是对的,uLogLite 的时间戳就没问题了。

还有个小技巧:调试阶段如果不想开文件日志,又想确认时间同步成功了,可以先用print(log._format_time())看一眼当前时间,确认没问题再开文件日志,省得一打开文件就是一堆错误时间戳。

5.4 内存暴涨与文件句柄泄漏

有些用户反映日志模块跑一段时间之后设备变卡,甚至死机。这往往不是日志模块本身的问题,而是使用方式不当。

一种常见情况是每次写日志都拼接大字符串。比如在日志里打印整个传感器数据缓冲区,几百个字节的字符串反复拼接、传递,MicroPython 的垃圾回收跟不上,内存碎片就会越来越严重。我的建议是:日志内容尽量精简,临时的格式化字符串用完即弃,不要保存在长生命周期变量里。

另一种情况是打开了日志文件却不关闭。如果你的代码在循环里创建新的uLogLite实例,却不调用close(),文件句柄就会一直累积。MicroPython 的文件描述符数量有限,超过阈值后所有文件操作都会失败。正确做法是:整个应用只创建一个 logger 实例,全局复用;如果确实要重建,先调用旧实例的close()释放句柄。

最后提醒一个很多人忽略的问题:日志模块占用文件句柄期间,如果程序要做 OTA 升级或文件系统操作,最好先关闭日志,升级完成后再重新打开。否则在某些文件系统实现上,按住句柄会导致升级失败或者文件系统损坏。

我在实际项目里长期用下来,uLogLite 这套设计最让我省心的就是两点:一是轮转逻辑足够稳,日志文件永远不会超限;二是标签过滤太好用了,排障时只开自己关心的模块,其他噪音全部关掉。如果你在项目里也遇到过 print 日志刷屏、排查困难的情况,不妨花一两个小时把 uLogLite 移植过去,后面省下的时间绝对不止这一两个小时。至于还能怎么扩展,后面你可以按需加远程日志上报、崩溃栈自动抓取,都是在这个骨架上长出来的功能。

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

PID控制器原理与工程整定实战:从核心算法到工业应用

1. PID 控制器的核心原理拆解&#xff1a;为什么三位一体能通吃全场1.1 三个字母的通俗解读&#xff1a;比例、积分、微分到底各管什么事聊 PID 控制器之前&#xff0c;先把这三个字母代表的算法逻辑说清楚。P 是比例&#xff0c;I 是积分&#xff0c;D 是微分&#xff0c;这三…

作者头像 李华
网站建设 2026/9/7 11:49:10

AI Agent安全警钟:思维链泄露与多轮上下文边界防御

最近圈子里有个话题很热闹&#xff1a;有人通过多轮对话把 Claude 的思维链“套”出来了&#xff0c;甚至用上了“加密漏洞被爆破”这种说法。乍一看很像一次针对大模型的顶级攻击秀&#xff0c;但如果你正在做 AI Agent 开发&#xff0c;这件事其实是一记警钟&#xff0c;而且…

作者头像 李华
网站建设 2026/9/7 11:48:39

十年深耕法界,实战赋能合规|资深法律讲师、律所执行主任 管萍

十年深耕法界&#xff0c;实战赋能合规&#xff5c;资深法律讲师、律所执行主任 管萍一、个人基础履历广东卡夫律师事务所执行主任管萍&#xff0c;法学本科毕业&#xff0c;拥有系统完备的法学理论体系&#xff0c;法律专业功底扎实、法理素养深厚。自2016年正式入行&#xff…

作者头像 李华
网站建设 2026/9/7 11:48:13

2022美赛F题制胜攻略:森林碳封存建模与多目标优化实战解析

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

作者头像 李华
网站建设 2026/9/7 11:45:37

中配电脑免费AI视频生成实战:通义千问+Seedance 2.5+Grok组合方案

这次我们来看一套不依赖本地旗舰显卡的免费 AI 视频生成路线&#xff1a;通义千问负责脚本与提示词&#xff0c;Seedance 2.5 负责视频画面生成&#xff0c;Grok 作为备选的对话与生成通道。标题里的“[中配]”指的就是普通办公电脑也能直接用&#xff0c;不需要 4080、4090 这…

作者头像 李华
网站建设 2026/9/7 11:44:30

嵌入式Linux设备安全加固实战:裁剪、权限、日志与防火墙

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

作者头像 李华