AI编程(三) - 用 Agent 做嵌入式代码架构审查

AI编程(三) - 用 Agent 做嵌入式代码架构审查

背景

在嵌入式固件开发中,代码审查(Code Review)往往依赖资深工程师的经验积累——他们需要在脑中同时追踪状态机流转、I2C 分片逻辑、寄存器写入顺序,以及 uint8_t 移位这类细节 bug。这类工作耗时且高度依赖个人经验。

本文记录了使用 Claude Code 的 自定义 Agent 对电量计驱动进行架构审查的完整过程,包括:Agent 的调用方式、输出报告的关键发现,以及如何在一个主任务中调用多个 Agent 并行工作。


一、arch-review-reporter Agent 是什么

arch-review-reporter 是我自定义的 Agent,通过 Claude Code 的 Agent 框架创建,存放在 ~/.claude/agents/arch-review-reporter.md。它不是 IDE 内置功能,而是由工程师根据自己的审查需求定制的专用角色。

与直接问 Claude “帮我看看这段代码有没有问题”的区别:

方式 上下文 输出格式 记忆
直接问 Claude 共用主对话 context 随意,依赖提问质量
自定义 Agent 独立 context window 按 Agent 定义的结构输出 有持久记忆(见第二节)

二、如何创建自定义 Agent

使用claude的 /agent命令创建自定义agent

如图引导页面会要求输入提示词说明自定义agent要做什么 ( 我的提示词是做代码架构分析,审查风险,考虑可优化方向 )

然后选择模型和访问权限等能力,最终输出agent的markdown配置
image-20260602153247275
image-20260602153329991

2.1 文件位置

自定义 Agent 是一个 Markdown 文件,放在:

1
2
~/.claude/agents/<agent-name>.md        # 全局 Agent,所有项目可用
.claude/agents/<agent-name>.md # 项目级 Agent,仅当前项目可用

arch-review-reporter 创建为全局 Agent,路径是:

1
C:\Users\cursorhu\.claude\agents\arch-review-reporter.md

2.2 文件结构

Agent 定义文件由两部分组成:YAML frontmatter(元数据)和正文(系统提示)。

YAML frontmatter

1
2
3
4
5
6
---
name: "arch-review-reporter"
description: "Use this agent when the user wants a comprehensive code architecture review..."
model: sonnet
memory: user
---

关键字段说明:

  • name:Agent 标识符,用于 @agent-<name> 触发
  • description:触发条件描述,Claude 根据这段文字判断何时自动调用此 Agent。写好 description 是关键——它决定 Agent 是否被正确路由
  • model:指定运行此 Agent 的模型(sonnet/opus/haiku),不填则继承主对话模型
  • memory:设为 user 表示开启用户级持久记忆(跨项目、跨对话有效)

正文:即这个 Agent 的系统提示(system prompt),定义它的角色、工作流程和输出格式。

2.3 arch-review-reporter 的系统提示设计

正文定义了一个四阶段工作流程:

1
2
3
4
5
6
7
8
9
10
11
第一阶段:代码理解
→ 阅读代码,识别架构层次,梳理模块依赖和数据流

第二阶段:架构分析
→ 职责划分 / 依赖关系 / 扩展性 / 分层合理性 / 接口设计 / 数据流

第三阶段:风险识别
→ 并发安全 / 资源管理 / 异常处理 / 安全风险 / 性能瓶颈 / 可观测性

第四阶段:生成报告
→ 按固定 Markdown 结构输出:架构概览 / 优化方向 / 风险分析 / 行动建议

输出规范要求每个问题必须定位到具体代码行号,不允许泛泛而谈,代码示例只能从实际源码中提取,不编造示例。

2.4 触发 Agent 的两种方式

方式一:显式调用(@前缀)

在对话框输入:

1
@agent-arch-review-reporter 分析当前项目

Claude 直接路由到该 Agent,无需判断。

方式二:隐式触发(自然语言)

1
2
帮我做代码架构审查
上线前帮我看看这个模块有没有问题

Claude 根据 description 字段判断匹配度,自动调用 arch-review-reporterdescription 中包含了三个具体示例(<example> 标签),帮助 Claude 精准识别触发场景。


三、Agent 的持久记忆机制(Memory)

3.1 为什么需要记忆

没有记忆的 Agent 每次都从零开始,无法积累对项目的认知。开启记忆后,Agent 可以记住:

  • 项目采用的架构模式和设计约定
  • 重复出现的代码风险类型(如这个项目里 const 数组被写入的问题)
  • 已确认的技术债务和待改进项
  • 用户的审查偏好和反馈

3.2 记忆的存储位置

memory: user 表示用户级记忆,存储路径:

1
2
3
4
5
~/.claude/agent-memory/arch-review-reporter/
MEMORY.md # 记忆索引(每条一行,自动加载到 context)
user_role.md # 用户角色和偏好
feedback_xxx.md # 用户对审查方式的反馈
project_xxx.md # 项目级发现(跨项目通用的模式)

每个记忆文件使用标准 frontmatter 格式:

1
2
3
4
5
6
7
8
9
10
11
12
---
name: embedded-const-write-risk
description: 嵌入式项目中 const 数组被 memcpy 写入的常见风险
metadata:
type: project
---

MCU 项目中,static const 数组通常链接到 Flash 段,运行时写入会触发 HardFault
或静默失败。审查嵌入式 C 代码时,优先检查所有 memcpy/memset 的目标地址是否为 const。

**Why:** SD77428 项目中发现 g_rc_tab 声明为 const 但被 fn_memcpy 写入,在部分平台上导致参数更新失效。
**How to apply:** 嵌入式 C 项目审查时,第三阶段风险识别增加 "const 写入" 专项检查。

MEMORY.md 是索引文件,始终加载到 Agent 的 context 中(超过 200 行会截断),因此只存指针,不存内容:

1
2
- [嵌入式 const 写入风险](embedded-const-write-risk.md) — MCU 项目中 static const 被 memcpy 写入的 HardFault 风险
- [状态机死区模式](statemachine-deadlock.md) — 分段式状态机中 step 条件互斥导致死锁

3.3 记忆的生命周期

首次审查时记忆目录为空,Agent 完成审查后会将本次发现的模式和约定写入记忆文件。下次审查同一类项目时,Agent 会先读取 MEMORY.md,带着历史认知开始审查,输出的建议会更精准。

用户级记忆(memory: user)跨项目有效。如果切换到另一个嵌入式项目,Agent 仍然记得”嵌入式 C 项目要重点检查 const 写入”这条经验。


四、调用方式总结

场景 操作
快速触发(知道 Agent 名) @agent-arch-review-reporter 分析当前项目
自然语言触发 “帮我做代码架构审查” / “上线前审查一下”
需要与审查结果继续对话 Agent 完成后回到主对话追问
单函数级 bug 直接问 Claude,无需 Agent
多模块跨文件分析 必须用 Agent(保护主 context)

五、审查对象:SD77428 电量计驱动

5.1 模块定位

fuel_gauge_sd77428.c 是 SD77428 电量计芯片的 MCU 侧驱动,核心职责:

  • 通过 I2C 向芯片下发电池模型参数(level0~level8 分级参数体系)
  • 读取 SOC、电压、电流、温度等实时信息
  • 管理充电状态和省电模式(suspend/resume)
  • 通过统一接口 sFuelGaugeInterface 对上层 BMS 暴露功能

5.2 架构核心:分段式异步状态机

这是驱动最重要的设计特征。由于 MCU 主循环不能阻塞等待 I2C,所有 I2C 操作被拆分为多个 step:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 典型分段式函数骨架
int8_t sd77428_read_bytes(uint8_t reg, void *data, uint16_t len, uint8_t pec_en)
{
switch (g_sd77428.read_step) {
case 0:
// 发起 I2C 读请求
g_sd77428.read_step = 1;
return STAY_PUT; // 本次调用未完成,下次继续
case 1:
// 检查结果,打包数据
g_sd77428.read_step = 0;
return SUCCESS;
}
}

sd77428_loop() 以 10ms 为节拍驱动 g_loop_list[],初始化流程由 g_init_list[] 中 8 个步骤按序完成。参数下发通过 g_download_list[] 描述符表批量执行,具有良好的可扩展性。


六、架构审查报告关键发现

Agent 将问题分为三级,共识别 4 个高风险(🔴)、6 个中风险(🟡)、4 个低风险(🟢)

6.1 🔴 高风险:向 const 数组执行 memcpy 写入

位置fuel_gauge_sd77428.c:1101,函数 sd77428_get_model_params

1
2
3
4
5
6
7
// g_rc_tab 声明为 static const,但此处写入
static const uint8_t g_rc_tab[][XAxis] = { ... }; // line 549
static const uint8_t g_rc_tab_a3[][XAxis_A3] = { ... }; // line 572

// line 1101: 向 const 数组写入 —— 未定义行为
fn_memcpy(g_rc_tab_a3, g_model_sample.rc_tab, sizeof(g_rc_tab_a3));
fn_memcpy(g_rc_tab, g_model_sample.rc_tab, sizeof(g_rc_tab));

风险const 数据在 MCU 上通常链接到 Flash(ROM)段。运行时写入会触发 HardFault,或在某些平台上静默失败——模型参数实际未更新,但代码认为已更新,电量计算使用错误的 RC 表,SOC 显示严重偏差。

修复:去掉 const 修饰。

1
2
static uint8_t g_rc_tab[][XAxis] = { ... };
static uint8_t g_rc_tab_a3[][XAxis_A3] = { ... };

6.2 🔴 高风险:状态机死区,lut_step 永远无法推进

位置fuel_gauge_sd77428.c:1436-1448,函数 sd77428_check_lut_ocvsoc

1
2
3
4
5
6
7
8
9
10
} else if (1 == g_sd77428.lut_step) {
if (batt_ocv_volt < (BATT_DESIGN_EOD - 150) && batt_rsoc > 800) {
g_sd77428.lut_step = 2;
if (0 == batt_rsoc) { // ← 与外层 batt_rsoc > 800 互斥,永远不执行
...
}
}
// ← 条件不满足时没有 else,lut_step 保持 1,函数返回 SUCCESS
// 但状态机实际未推进,下次调用继续卡在 step 1
}

风险:初始化流程卡在 E_STEP_CHECK_LUT_SOC 阶段,电量计永远无法进入正常 loop。

修复:补充 else 分支,正常情况下直接推进到终态:

1
2
3
4
5
6
7
} else if (1 == g_sd77428.lut_step) {
if (batt_ocv_volt < (BATT_DESIGN_EOD - 150) && batt_rsoc > 800) {
g_sd77428.lut_step = 2;
} else {
g_sd77428.lut_step = 4; // OCV 正常,直接推进到出口
}
}

6.3 🔴 高风险:uint8_t 移位到符号位,未定义行为

位置fuel_gauge_sd77428.c:835-841,函数 sd77428_pack_rd_data

1
2
3
4
// per_memb_size == sizeof(uint32_t) 时,slave_idx 最大为 3
// uint8_t 经整型提升为 int,左移 24 位侵入符号位 —— UB
p_rd_u32[rd_idx] |=
(p_rx_u8[rd_idx + slave_idx] << (slave_idx * PER_BYTE_BITS));

C 标准规定:对有符号整数执行左移到符号位是未定义行为(UB)。在高优化级别下编译器可能产生不可预测的结果,导致 32 位寄存器读回值的最高字节解码错误,电压/电流等参数静默偏差。

修复:显式转型为 uint32_t

1
2
p_rd_u32[rd_idx] |=
((uint32_t)p_rx_u8[rd_idx + slave_idx] << (slave_idx * PER_BYTE_BITS));

6.4 🔴 高风险:芯片版本检测精确匹配,固件升级后驱动失效

位置fuel_gauge_sd77428.c:1152,函数 sd77428_detect_chip_id

1
2
// FW_VER_A3_MIN 命名暗示"最低版本",但逻辑是精确匹配
if (fw_ver == FW_VER_A3_MIN) { ... } // 应为 >=

修复

1
2
3
4
if (chip_id == chip_list[idx].chip_id) {
matched = (idx == E_A3_IDX) ? (fw_ver >= FW_VER_A3_MIN)
: (fw_ver == chip_list[idx].fw_ver);
}

6.5 🟡 中风险:design_fcc - 600 无符号下溢

位置fuel_gauge_sd77428.c:1110-1112

1
2
uint32_t reg_val2 = g_model_sample.design_fcc - 600;  // 小容量电池时下溢
g_level1_tab[9] = g_level1_tab[11] = ((reg_val1 << 16) | reg_val2);

小容量电池(design_fcc < 600)时,reg_val2 下溢为 0xFDxx,写入芯片的 mindfcc 为异常大值,DFCC 算法行为失控。

修复:加下界保护:

1
uint32_t reg_val2 = (design_fcc > 600) ? (uint32_t)(design_fcc - 600) : 0u;

6.6 🟡 中风险:Flash 频繁写入(单位不一致导致每次都触发)

位置fuel_gauge_sd77428.c:1598

batt_rsoc 经过 sd77428_batt_convert 映射后是显示值(0100 的百分比),但 Flash 中存储的历史值是原始寄存器值(01000 的千分比)。两者单位不一致,每次比较都不相等,每次都触发 Flash 写入,加速 Flash 磨损。


6.7 设计层面:全局状态结构体职责过重

sSd77428Rung_sd77428)同时承载 20+ 个字段,其中 read_idx/write_idx/verify_idx 等实质是各子函数私有的局部状态。这导致:

  • 任意函数都能意外修改其他函数的状态字段
  • 无法在同一帧内连续调用两次 sd77428_read_bytes
  • 无法支持多实例(多颗电量计芯片)场景

建议将子状态提取为函数内 static 局部结构体,仅在 g_sd77428 中保留跨函数共享的字段(chip_typeaddrreinit_meter 等)。


七、如何在对话中触发多个 Agent 并行工作

单个 Agent 一次只能专注一件事。当任务需要同时分析多个维度时,可以用一句话让 Claude 并行启动多个 Agent。

7.1 用户侧:一句话触发并行

不需要编辑 CLAUDE.md,不需要写任何配置。直接在对话框描述多个独立任务,Claude 会自行决定并行执行:

1
2
3
请同时做两件事:
1. 用 arch-review-reporter 审查 fuel_gauge_sd77428.c 的架构风险
2. 搜索项目中所有调用 i2c_read_bytes 的位置和行号

或者明确要求并行:

1
2
3
4
请并行启动多个 agent 分别完成:
- 架构审查(arch-review-reporter)
- 搜索所有 sd77428_read_bytes 调用点
- 对比 A2/A3 两个芯片版本的参数差异

Claude 收到后,会在同一次响应中并发调用多个 Agent,你能看到多个任务同时推进,总耗时约等于最慢的单个 Agent。

7.2 幕后机制:Claude 是编排者

多 Agent 并行的本质是:Claude 作为”主任务编排者”,在一次响应中同时发出多个子 Agent 请求。下面是 Claude 内部执行的逻辑(不是用户需要写的代码,仅作原理说明):

1
2
3
4
5
6
# Claude 内部编排逻辑(原理示意,非用户操作)
同一响应中并发调用:
Agent(subagent_type="arch-review-reporter", prompt="审查架构风险...")
Agent(subagent_type="Explore", prompt="搜索 i2c_read_bytes 调用点...")
Agent(subagent_type="general-purpose", prompt="对比 A2/A3 参数差异...")
→ 三个子 Agent 并行运行,Claude 等待全部完成后汇总

用户只需用自然语言描述任务,Claude 自动完成调度。

7.3 并行 Agent 的典型场景

1
2
3
4
5
6
7
8
9
10
11
12
13
用户一句话:对电量计固件做全面评估

Claude 自动拆解并并行启动

┌─────────────────┼─────────────────┐
▼ ▼ ▼
Agent A Agent B Agent C
arch-reviewer Explore general
架构审查 搜索调用点 参数版本对比
│ │ │
└─────────────────┴─────────────────┘

Claude 汇总结果

7.4 串行 vs 并行的选择

Claude 会根据任务依赖关系自动判断,也可以在提示词中明确指定:

模式 适用场景 提示词示例
并行 任务之间无依赖 “同时/并行做以下几件事”
串行 B 依赖 A 的结果 “先…,完成后再…”
混合 第一阶段并行收集,第二阶段综合 “先并行读取各模块,然后汇总生成报告”

7.5 后台运行

对于耗时长、不需要立即结果的任务,可以要求后台执行:

1
请在后台搜索整个项目中所有使用 STAY_PUT 返回值的函数,完成后通知我

Claude 会启动后台 Agent,主对话继续响应,任务完成后收到通知。


八、工程实践建议

何时启动架构审查 Agent

  • 合并大型特性分支前(代替 PR review 的第一道过滤)
  • 重要版本发布前(替代 checklist 式人工走查)
  • 新人接手陌生模块时(快速建立风险地图)
  • 移植到新平台前(识别平台相关的潜在问题,如 Flash 写 const)

Agent 输出的局限性

Agent 基于静态代码分析,无法替代以下工作:

  • 运行时行为验证(实际挂载电池的 SOC 曲线测试)
  • 边界条件的硬件级验证(真实 Flash 写保护触发)
  • 需要跨 repo 上下文的分析(依赖外部 HAL 层的具体实现)

最佳实践是:Agent 负责提速和覆盖面,工程师负责判断优先级和验证修复效果


总结

本文展示了用 arch-review-reporter Agent 审查约 1800 行嵌入式 C 代码的完整流程。Agent 在单次运行中识别出:

  • 1 个运行时崩溃风险(向 Flash 段写入 const 数据)
  • 1 个状态机死锁(初始化永远卡住)
  • 2 个 C 语言 UB(符号位左移、无符号下溢)
  • 多个设计层面的技术债(全局状态集中、Flash 频繁写入等)

这些问题中,const 写入和状态机死区两项在量产环境下会直接导致设备不可用,而单靠人工 review 在 1800 行代码中发现它们需要相当的经验积累。

多 Agent 并行模式进一步将这类分析从”串行耗时”变为”并行提速”,是复杂嵌入式项目代码质量管理的实用工具。用户侧无需任何配置,用自然语言描述多个独立任务,Claude 自动完成并行调度和结果汇总。