一、起点

代码库主体为 C++,约 2000 行核心业务逻辑,运行在 ARM 嵌入式和 x86 双平台上,通过 MQTT 与无人机通信,串口与多个外设交互。我在这个项目上工作了一年,近期整理出一份架构重构计划(Strand + Offload 模式),但缺少外部视角的校验。

我将代码库和重构计划一并提交给 AI,要求其以独立视角进行全面审查。AI 的初始动作是并行扫描目录结构、核心文件、依赖关系和 CMake 构建系统,在五分钟内建立起对代码库的整体理解。


二、第一轮:信息差暴露

AI 输出了涵盖十个维度的架构评估报告——线程模型、瓶颈识别、改进方案、组件交互优化、可扩展性增强、第三方库集成策略、UAV/UAVNode 关注点分离、SystemFactory 设计模式改进、实施路线图、风险矩阵。

覆盖全面,但有三处与实际情况不符:

  1. 计算卸载方案中 ComputePool 的设计是冗余的。项目已引入 ZLMediaKit,其内置的 WorkThreadPool 可直接复用。
  2. 第三方库仅有 x86 预编译版本,而目标平台为 ARM,跨架构编译的管理策略未被考虑。
  3. 设备配置中 enable 字段的语义需要明确——实际含义为「是否通过串口通信」,而 link 才表示「是否启用设备」。此外,设备通过 MQTT 通信时 topic 命名已有固定约定,不需要在配置中重复声明。

这三个偏差指向同一个根因:AI 在通用性假设下给出方案,没有利用已有基础设施的信息。ZLMediaKit 的线程池已在项目中完成初始化,方案中却重新设计了一个——这是典型的「未审查已有代码即开始设计」的问题。但造成这一偏差的直接原因是我在第一轮输入中未提供 ZLMediaKit 的相关上下文。

结论:向 AI 提交已有项目的审查任务时,必须将项目的基础设施信息——已引入的库、已有的约定、目标平台的约束——作为上下文一并输入。信息差越大,AI 的输出偏离实际情况的概率越高。


三、第二轮:校准方向

我向 AI 确认了三个前提:

  • 计算卸载直接复用 WorkThreadPool,计算线程仅读取数据副本,结果通过 Strand 回帖到主业务线程。
  • Build 脚本不加后缀对应本地编译,加 _x86_arm 后缀对应交叉编译。
  • 设备 topic 约定为 {设备名}_pub{设备名}_sub,这是团队已有共识,无需在配置中重复声明。

在上述信息被消化后,AI 提出了两个新的审视点:StateMachine 全局单例是否应该调整?EmergencyFactory 如何融入新的 Strand 线程模型?

这两个问题触及了架构的核心——状态管理和组件生命周期——在我原先的设计中恰好是被遗漏的维度。AI 的优势在此体现:它能将局部方案放入更大的设计模式图谱中对照,识别出人容易忽略的边界。


四、第三轮:抽象层级的跃迁

当 AI 建议「设备传输层应显式声明 transport: serialtransport: mqtt,而非使用一个语义含混的布尔值」时,这条建议触发了一个更关键的抽象:

是否考虑统一的 ITransport 接口?不同传输方式仅在注册和发送路径上不同,解析逻辑是一致的。

这个方向改变了整个设计。重新审视代码后,发现 SerialDevice 基类内部混杂了三层逻辑:串口读写(传输层)、帧边界检测(帧提取层)、NMEA 协议解析(业务层)。其中帧提取和解析与传输方式无关,却被耦合在 SerialDevice 中。

结论:AI 的具体建议未必是最优解——它的更大价值在于触发思考。一条看似细节的建议,结合人对代码的深度理解,可能引出比原建议更根本的设计改进。人机协作中,AI 提供催化,人完成合成。


五、第四轮:用数据终结设计争议

本轮涉及整个审查中最关键的线程模型决策——消息解析的位置。

两种候选方案:

  • 解析在 I/O 线程:延迟低,各设备天然并行,但 MQTT 回调中解析会阻塞网络线程。
  • 解析在 Strand 线程:网络线程永不阻塞,但所有设备的解析变为串行。

AI 倾向于 Strand 方案(职责更清晰),我最初倾向于 I/O 方案(延迟更低)。争论的突破口来自性能数据:AIS 高频场景下(每秒上百条消息),单条解析耗时约 200μs。若在 MQTT 回调中解析,累计阻塞 mosquitto_loop 约 10ms,将导致 broker 端消息积压,反而增加端到端延迟。

数据使得决策方向清晰化:

I/O 线程的唯一职责为读字节、push、返回。解析统一在 Strand 执行。

最终形成明确的分层原则:I/O 线程仅做 I/O,Strand 线程处理全部业务逻辑。读/解分离。

结论:技术决策不应基于「哪个更优雅」,而应基于瓶颈数据。AI 擅长枚举选项和分析利弊,但性能数据只能来自人对系统的测量。数据 + AI 的模式匹配能力 = 最快路径收敛到正确方案。


六、第五轮:业务语义决定数据结构

我提出了消息积压场景的处理需求:积压时应丢弃最早的消息,保证最新消息入队。

AI 的初始设计是 RingBuffer 满时返回 false(丢弃最新)。但传感器数据流的业务语义与通用队列不同——AIS 船只位置、GPS 坐标、IMU 读数的最新值永远比 100ms 前的旧值具有更高的决策价值。

在明确业务语义后,AI 对比了 RingBuffer(lock-free SPSC)和 mutex + deque 两种实现,最终锁定 RingBuffer 配合 drop-oldest 语义,并补充了 _dropped 原子计数器用于生产环境的可观测性监控。

结论:数据结构的选型不只取决于算法复杂度,更取决于业务语义。AI 提供默认实现方案,人根据业务需求判断默认方案是否适用——若不适用,反馈原因,AI 即可针对性调整。业务语义由人定义,实现方案由 AI 匹配。


七、第六轮:帧格式统一的边界

AI 提问:「是否考虑为所有数据加上统一的帧头帧尾,然后统一通过 NMEA 工厂解析?」

统一帧格式是一个表面吸引力很强的方案——解析层可以极度简化。但检查实际设备后,约束条件否决了这一方案:

设备 帧格式 格式来源
定位设备I !,... NMEA 行业标准,硬件固件固定
定位设备II $CC... 硬件厂商固定格式
CoolingSystem 0x4C 0x51 ... 固定 4 字节帧,含 CRC8

每种设备已有固定的帧格式,且均来自硬件固件,不可修改。在此约束下,再叠加一层统一帧头是冗余设计。

在获得上述约束信息后,AI 将方向调整为:不统一帧格式,而统一帧提取接口FrameExtractor 支持多种帧格式(NMEA / Delimited / FixedLength / PassThrough),对上暴露一致的回调接口。

结论:面对「格式不统一」的问题,直觉解法是「强制统一」。但工程约束(硬件固件不可改)往往使这一路径不可行。更务实的策略是承认多样性,在软件层通过统一接口做适配。将约束条件完整提供给 AI,它能在约束内找到最优的统一层级。


八、第七轮:分层的粒度判断

我向 AI 确认:CAN 通信是否纳入 FrameExtractor 体系统一处理?

结论是不纳入。CAN 与串口的根本区别在于:CAN 控制器在硬件层面已完成了帧定界(CAN ID + DLC + data),socket.read() 返回的即为完整帧。FrameExtractor 的核心职责是「从字节流中定位帧边界」——而 CAN 不存在此问题。

但 CAN 设备和串口设备在「获得完整帧之后」的处理路径完全一致:解析 payload → 写入状态。统一的层级不在帧提取,而在解析到状态写入这一段。CAN 通过 PassThrough 模式接入,绕过帧查找,在下一层汇合。

结论:分层统一需要有明确的边界判断。每向上推进一层,都需要独立评估:该层在所有传输方式下是否有统一的必要和可能?AI 负责梳理出清晰的分层结构,人负责逐层判断统一的适用性。


九、第八轮:从运行时派发到编译期路由

当前 NmeaFactory 的实现为字符串匹配调度器:提取 NMEA sentence 前缀的后三个字符,在注册表中查找,创建对应 Processor 实例,调用虚函数。每一帧都要触发一次完整的字符串解析和哈希表查找。

核心问题在于:运行时字符串匹配的错误只能在运行时暴露。Ais 设备必然输出 VDM 语句,BeiDou 必然输出 RMC/GGA——这些对应关系在编译期已确定,不应通过运行时字符串解析来决策。

AI 的分析更进一步:不仅 NmeaFactory 应移除,Processor 基类和虚函数继承体系也应一并去除——编译期确定的类型关系不需要运行时的多态开销。

随之产生的问题是:解析函数的调度方式——编译期直接分支还是路由表?

  • 直接分支:每个设备的 ParseMessage 中使用 if/else,实现简单但存在重复。
  • 设备级路由表:更具结构性,但路由逻辑仍分散在各设备中。
  • Strand 集中路由:将路由决策提升至与 FrameExtractor 同一层,Device 完全不需要 ParseMessage,仅需声明 RouteTable。

最终选择了集中路由方案。Device 类的规模从 50–200 行缩减为 5–10 行——getRouteTable() 返回一个编译期常量表。

结论:AI 在识别设计异味(运行时字符串派发、不必要的虚函数继承)上表现出高灵敏度。但「替代方案选哪个」——路由表还是直接调用、分散还是集中——依赖于人对系统全局的理解。AI 枚举选项并分析利弊,人基于系统约束做出选择。


十、第九轮:部署模型决定设计模式

在方案接近收敛时,AI 提出了一个更根本的问题:是否放弃全局单例,改用 Context 指针显式传递依赖?

两种模式的对比:

维度 Singleton Context
调用复杂度 低,全局访问 需传递指针
依赖可见性 隐式 显式
可测试性
多实例支持 不支持 支持
改动范围 27 个调用点 + 7 个构造函数

在本项目的部署模型下——单 UAV 部署、每种设备唯一实例、无并行测试需求——引入 Context 的收益为零,成本却涉及 27 个调用点签名变更和 7 个类的构造函数重构。选择 Singleton 是务实的。

AI 在确认部署模型后未继续坚持原建议,而是补充了一个风险提示:如果未来需要支持多 UAV 部署,全局单例将是最先需要拆除的部分。

结论:教科书将「全局单例」标记为反模式,但这一判断有其适用前提。部署模型是评估设计模式适用性的关键变量——在单实例、单部署的场景下,Singleton 的成本收益比优于 Context。对 AI 提供的任何「最佳实践」建议,都应放入项目的具体约束中重新评估。


十一、最终方案

经过十轮对话,方案收敛为十一项设计决策:

  1. 线程模型:Strand 单业务线程 + N×I/O 线程(仅负责阻塞 read + push)
  2. 消息积压:RingBuffer drop-oldest 语义 + _dropped 原子计数器用于可观测性
  3. 计算卸载:复用 ZLToolKit WorkThreadPool
  4. Transport 抽象:ITransport 接口统一 Serial / MQTT / CAN 三种实现
  5. 帧格式:FrameExtractor 多格式适配,不强制统一帧头
  6. CAN 处理:PassThrough 模式接入,硬件已定帧
  7. 解析线程:I/O 线程只 push,Strand 线程集中解析
  8. 路由方式:RouteTable 集中路由,Device 仅声明路由表
  9. 工厂移除:NmeaFactory 删除,改为命名空间直达解析
  10. 设备配置:yaml 驱动,声明 transport + framing + routes
  11. 构建脚本:build.sh(本地)/ build_x86.sh / build_arm.sh

十二、回顾

角色互补:人掌握业务上下文——设备行为、部署环境、已有基础设施、硬件约束。AI 提供模式知识——Strand、SPSC、ITransport、集中路由——但不假设这些模式一定适用,最终适用性判断由人完成。

迭代收敛:十一项决策无一项在第一轮即确定。每项都经历了「提议 → 质疑 → 调整 → 收敛」的循环。ComputePool 方案因忽略已有基础设施被推翻,统一帧头方案因硬件约束被推翻,解析线程的定位经历两次调整才收敛。

AI 提问的价值:「是否考虑统一的 ITransport」「路由还是直接编译」「Singleton 还是 Context」——这些提问是推动方案深化的关键节点。AI 在正确的抽象层级提出问题,促使人的模糊直觉被转化为清晰的技术决策。

数据终结争论:设计争议不应靠观点对决来解决。在「解析在哪做」的讨论中,AIS 高频场景下 MQTT 回调阻塞 10ms 的具体数据使选择显而易见。数据 + AI 的模式知识 = 高效收敛。

人的判断不可替代:AI 在整个过程中未做出任何最终决策。它提供选项、分析利弊、提出质疑——但每个选择均由人根据系统约束做出。技术决策的上下文——明天要在 ARM 板子上部署、后续可能新增设备类型、未来可能交接给其他维护者——这些信息不在代码中,但决定了每个决策的方向。


代码重构不是人写代码、AI 检查结果的过程。它是一场对话——每轮都可能推翻上一轮的假设,每一步都在逼近更准确的设计。

AI 的角色不是替代人做决策,而是作为对话者,持续追问假设、补充知识盲区、枚举候选方案。最终的选择权和责任始终在人。