系统架构设计:顶层架构设计、模块划分原则、接口定义规范、数据流与控制流设计、架构文档撰写

好,咱们进入第二章。系统架构设计。

说实话,很多做算法的同学,一听到「架构」两个字就头大。觉得那是软件工程师的事。我当年也这么想。直到有一次,我花了三个月写好的算法模型,到了集成阶段发现根本跑不通——不是因为算法不对,而是因为数据流设计有缺陷,模块之间互相打架。

那次之后我学乖了。架构设计,其实是算法项目能落地的骨架。骨架歪了,肉长得再好也没用。

顶层架构设计:先画一张大图

顶层架构设计,说白了就是回答三个问题:

  • 输入从哪来? 传感器?数据库?网络流?
  • 中间怎么处理? 预处理、特征提取、推理、后处理。
  • 输出到哪去? 控制指令?可视化界面?存储系统?

我个人习惯,先用一张方框图画清楚。别急着画细节,先定边界。

顶层架构的核心原则:

  • 高内聚,低耦合 —— 每个模块只干一件事,模块之间少说话。
  • 单向依赖 —— 数据流尽量是单向的,别搞成循环依赖,否则调试起来你会想哭。
  • 可替换性 —— 任何一个模块,都应该能被另一个实现替换,而不影响全局。

举个例子。我之前做过一个视觉检测项目。顶层架构就三层:数据采集层、算法处理层、结果输出层。每一层只和相邻层通信。后来客户要求换摄像头,我只改了采集层,算法层一行代码没动。这就是顶层设计的好处。

模块划分原则:别把鸡蛋放在一个篮子里

模块怎么切?我总结了四个字:职责单一

你想想看,一个模块如果既要做图像预处理,又要做目标检测,还要做结果统计。那这个模块一旦出问题,你根本不知道是哪个环节挂了。

我建议按功能域来划分:

模块名称 职责 典型接口
数据采集模块 从硬件或文件读取原始数据 read_frame()
预处理模块 去噪、归一化、尺寸调整 preprocess(raw_data)
推理引擎模块 加载模型,执行前向推理 infer(features)
后处理模块 解析输出,生成业务结果 post_process(raw_output)
结果分发模块 将结果发送到下游系统 dispatch(result)

小技巧: 模块划分时,可以先用「动词+名词」的方式命名。比如「读取数据」「执行推理」「发送结果」。这样每个模块的职责一目了然。

接口定义规范:定好规矩,少吵架

接口定义,是架构设计里最容易扯皮的地方。我见过两个团队因为接口参数顺序吵了一下午。

所以,接口规范要提前定死。我个人习惯用以下几条:

  • 接口参数尽量少 —— 超过5个参数,就该考虑封装成结构体了。
  • 返回值统一 —— 要么全部返回错误码,要么全部抛异常。别混着来。
  • 版本号管理 —— 接口一旦发布,不要轻易改。非要改,就升版本。

举个例子,一个标准的推理接口,我通常会这样定义:

// 输入结构体
typedef struct {
    uint8_t* data;      // 图像数据
    int      width;     // 图像宽度
    int      height;    // 图像高度
    int      channel;   // 通道数
} InputFrame;

// 输出结构体
typedef struct {
    int   obj_count;    // 检测到的目标数量
    Rect* bboxes;       // 目标框列表
    float* scores;      // 置信度列表
} DetectionResult;

// 接口函数
int infer_object(InputFrame* input, DetectionResult* output);

你看,输入输出都定义成结构体。这样以后加字段,接口签名不用变。我在项目中吃过亏,一开始接口参数写了一大串,后来加了个「是否启用加速」的开关,所有调用方都得改。从那以后,我再也不用裸参数了。

数据流与控制流设计:谁在干活,谁在指挥

这两个概念容易混淆。我简单解释一下:

  • 数据流:数据怎么从一个模块流到下一个模块。比如图像从采集模块到预处理模块,再到推理模块。
  • 控制流:谁来决定什么时候做什么事。比如「当采集到一帧图像后,启动预处理」。

我建议把控制流和数据流分开设计。数据流用管道(Pipeline)模式,控制流用状态机或事件驱动。

数据流设计要点:

  • 每个模块只处理自己该处理的数据,不要越界。
  • 数据传递尽量用共享内存或消息队列,减少拷贝。
  • 关键路径上要加缓冲,防止数据抖动。

控制流设计,我习惯用状态机。比如一个典型的算法流水线,状态可能是:

IDLE -> LOADING -> PREPROCESSING -> INFERRING -> POST_PROCESSING -> OUTPUT -> IDLE

每个状态对应一个模块。状态切换由事件触发。这样设计的好处是,你一眼就能看出系统当前在干什么,出问题也好定位。

注意: 控制流里最容易出问题的,是异常处理。比如推理模块超时了怎么办?数据采集失败了怎么办?这些分支一定要在设计阶段就想清楚,别等到上线了再补。

架构文档撰写:写给人看的,不是写给机器看的

架构文档,很多人写成了流水账。我见过一份文档,把每个函数都列了一遍,但看完还是不知道系统怎么工作的。

我建议架构文档包含以下内容:

  1. 架构图 —— 一张顶层方框图,标注模块和接口。
  2. 模块说明 —— 每个模块的职责、输入、输出、依赖。
  3. 数据流描述 —— 用文字或流程图说明数据怎么走。
  4. 控制流描述 —— 状态机或事件驱动说明。
  5. 接口定义 —— 关键接口的签名和说明。
  6. 部署说明 —— 硬件要求、依赖库、启动方式。

我曾经带过一个新人,他看完我的架构文档后,三天就把整个系统搭起来了。这就是好文档的价值。

写文档的小习惯: 我每次写完代码,都会花半小时更新架构文档。别等到项目结束了再写,那时候你早忘了当初为什么这么设计。

嗯,架构设计这部分,内容其实挺多的。但核心就一句话:先想清楚再动手。你花在架构设计上的每一分钟,都会在后期开发中十倍地还给你。

下一章,咱们聊聊详细设计。到时候我会拿一个具体的算法模块,手把手带你走一遍设计流程。