EvoAgent

工程日志

项目里一共记录了 13 个真实事故,这里挑出 6 个成体系地写。 每个都按同一个结构:现象 → 证据 → 排除过程 → 根因 → 修复 → 方法论。

为什么写这一页:功能清单谁都能列,怎么找到根因才是工程能力的证据。 而且我把走过弯路的部分也留着——过程真实比结论漂亮更有用。


CASE 1 · 无日志复位:硬件问题伪装成软件崩溃

语音板硬件排障最难的一个

现象

语音板在播报时无规律复位,且没有任何日志——只有开机痕迹,崩溃点不可复现、不可定位。 软件层的第一反应是"某个任务死循环或者栈溢出"。

排除过程(我走的弯路)

转折点:换了一个电源做对照实验 用充电器供电时完全不复位,用电脑 USB 口供电必触发。 串口日志随即出现 E BOD: Brownout detector was triggered。

根因

功放(MAX98357A)从 3.3 V 轨取电,播放瞬间的瞬态大电流把 3.3 V 轨拉垮, 电压跌破 BOD 阈值(实测跌到 2.1 V 以下)→ 触发硬件欠压复位。

这就解释了"为什么没有日志":BOD 复位是硬件级的,比软件写日志快得多, 软件根本来不及留下任何痕迹。不是软件 bug,是供电设计问题。

修复

方法论


CASE 2 · 音频链路的四个坑:每个根因都不一样

语音板I2S 驱动四连坑

这块语音板是裸 I2S 方案(数字麦克风 + 数字功放,中间没有 codec 芯片)。 好处是省掉 codec 和它的 I2C 控制线;代价是采样率、槽宽、声道全得自己在软件里配对—— 四个坑就是这么来的。

#现象根因修复
1 麦克风完全没声音 I2S 通道没有显式使能,读写直接返回 ESP_ERR_INVALID_STATE,外设根本没启动,既没时钟也没数据 显式调用 i2s_channel_enable()
2 采到的数据是错的 槽位宽用错:配 32-bit 槽位时 BCLK 翻倍到 1.024 MHz,与麦克风时序假设不匹配 → 输出 8192 步长的乱码 换 16-bit 槽位(BCLK 512 kHz)即正常
3 唤醒词触发不了 输入幅度只有满幅的 ~1.5%,电平太低 读取后 ×16 增益 + 限幅
4 播放沙哑、语速像 2 倍速 发送端是立体声槽,单声道数据直接写进去被按双声道解释 → 有效采样率减半、播放速度翻倍 mono → stereo 复制(L = R)

两个值钱的判据

方法论

音频链路上游的默认配置最坑:克隆来的代码带着一堆隐式假设(通道已使能、槽宽 16、增益够大、mono 会自动展开), 在你的器件组合上可能一条都不成立。质疑默认配置,比读懂自己的代码更重要。


CASE 3 · 雷达丢一个字节,整帧就乱:用状态机做自愈

跌倒板协议解析设计取舍

问题

毫米波雷达以固定格式持续发帧:SOF(0x01) + ID(2B) + LEN(2B) + TYPE(2B) + HCK(1B) + DATA(N) + DCK(1B), 两个校验字段都是 XOR 取反。

bring-up 阶段发现雷达 UART 会偶发丢字节。如果按"收到完整一帧再解析"的写法, 丢一个字节会让后续所有帧整体错位,而且错位后就再也对不齐了。

方案:逐字节状态机

我实现的是逐字节状态机而不是整帧解析——因为 UART 字节流没有任何成帧保证。 每收一个字节走一个状态:找 SOF → 读 ID/LEN/TYPE → 校验 HCK → 缓存 DATA → 校验 DCK → 帧合法才上抛。

关键收益:丢包自愈是自然发生的——任一校验失败就退回"找 SOF"状态, 不需要缓冲区重同步,也不需要上层干预。上层最终拿到 x/y/z/speed 四个浮点值,直接进判定逻辑。

可观测性

解析失败不是静默吞掉:失败帧计入丢帧计数,并且能报告 "前导噪声长度 + SOF 首次出现的偏移"——排障时这比"解析失败"四个字有用得多。

方法论

流式数据要给"重新同步"留一条低成本路径。 状态机让"恢复"成为默认行为,而不是异常处理分支。


CASE 4 · 引脚选错:GPIO 和 PSRAM 总线打架

语音板板级数据手册陷阱

现象

第一版引脚方案把麦克风放在 GPIO33/35。刷进去直接崩溃: Guru Meditation Error: Core 0 panic'ed (Interrupt wdt timeout)。

根因

GPIO33–37 是这块板 PSRAM 的总线引脚(OCT 模式)。 I2S 占用它们 = 和外设总线抢引脚,I2S 初始化直接挂起。另一段 GPIO26–32 是 flash 总线,同样不可用。

第二个坑:引脚"可用"不等于"干净"

换到 GPIO15/16 后程序能跑,但数据仍然不对。我没有示波器, 就用 PCNT 硬件计数器飞线测时钟频率——读到 1.126 MHz / 120 kHz 这种"不该存在"的频率, 判断这两个脚被板载信号干扰。

换成 GPIO12/13 后数据立刻正常。所以最终接线表里这两个脚是明确避开的。

方法论


CASE 5 · 审计缺口:「校验逻辑存在」不等于「校验有效」

EvoAgent数据依赖可证伪验证

现象

系统里需求进入时有两条把关:工具名冲突检测、防重复投递去重。 文档上写得好好的,看起来互为补充。

根因:同源依赖

我实测发现两条检查都从来没有生效过——因为它们的"已装插件"维度 读的是同一个从未被写入的文件(保存函数有实现,但全代码库零调用点)。 于是两条检查的那一维度同时恒为空集:

这里的关键动作不是"修报错的那条" 发现第一条失效后如果只修它,第二条依然坏着。正确做法是顺着数据依赖往上找,找出还有谁读同一个坏数据源。
"同源"是排查盲区——两条看起来独立的检查,可能共享同一个从未被生成的事实来源。

修复

怎么证明修好了:造一个「本应被拦下」的场景

不是"看返回值对了",而是投一条重复能力需求,看它是否真的被拦下:

检查结果
重复需求是否被拦5 秒内被拒,错误信息点名是「已装插件工具」冲突
反证:配置文件有没有被改动没有(mtime 未变)→ 说明构建进程根本没被启动,零浪费构建
真实进化一次(新能力)安装审计事件自动落盘,插件注册表自动更新

第二行才是关键:它证明的不是"返回值正确",而是"系统没有做多余的动作"。 这就是"可证伪"和"看了一眼日志"的区别。

方法论

验证机制时不信它存在,信它拦得住。 另外:把没修的部分也如实写进公开文档(安全扫描器的一处误报、一条被遮蔽的分支), 暴露边界比假装完美更能赢得信任。


CASE 6 · 安全扫描器的误报:真正的风险是「告警疲劳」

EvoAgent安全判断力

现象

系统对自动生成的新插件做装后危险模式扫描,报出: 检测到危险模式 ['exec('],请人工复核。

根因:裸子串匹配

复查后确认是误报:命中点是插件里正则的 RegExp.prototype.exec() 调用 (解析日志文本的常规写法),与命令执行毫无关系。 根因是危险模式表里的 "exec(" 走的是裸子串匹配,因此会命中任何 .exec(。

为什么我没改它

这是安全扫描器的灵敏度,属于取舍,不该我擅自决定 影响是:任何用正则解析文本的插件都会被判"危险模式"——长期代价是运维对该告警脱敏。
但另一方面,安全扫描器"宁可误报"本身是合理设计,且消息写的就是"请人工复核"。 所以我只记录、不擅自降低灵敏度,把修法(改成"前面不是点号的 exec(",需把扫描从子串改为正则)写进文档交给人决定。

方法论

误报本身不危险,它导致的"告警疲劳"才危险——当一条告警经常是假的, 真出问题时它也会被忽略。所以误报值得进文档、值得被修,但修法要由承担风险的人定。


四条 90 秒 STAR(练过、能顺口讲出来)

① 雷达丢包自愈

UART 字节流没有成帧保证 → 用逐字节状态机代替整帧解析 → 校验失败退回找 SOF, 重新同步成为默认行为而不是异常分支;丢帧可观测。

② 无日志复位 → 欠压

软件层全部排除后做换电源对照实验 → 定位到功放瞬态电流拉垮 3.3 V 轨触发 BOD。 硬件问题伪装成软件崩溃,先查电。

③ I2S 四连坑

四个症状、四个不同根因:通道未使能 / 槽宽错 / 增益低 / mono 写进立体声槽。 判据:"固定步长乱码 = 位错位"。

④ 审计缺口闭环

顺着数据依赖找出两条检查同源失效 → 修复 → 用"本应被拦下"的场景做可证伪验证 (含"没发生多余动作"的反证)。

三条我坚持的工程判断

  1. 该用规则的地方不用模型 —— 跌倒判定的初筛是雷达规则状态机,不是神经网络。 用最简单的机制解决能解决的问题,把模型留给真正需要它的地方。
  2. 无日志复位先怀疑硬件 —— 软件排查走完就换对照实验,别在错误的方向上深挖。
  3. 没做完的不写成做完了 —— 未测量的指标主动列出,未验证的版本明确标注, 未修的问题写进公开文档。