真实踩坑实录:做 GaryCLI 后,我发现嵌入式 AI Agent 最难的根本不是写代
最近我一直在开发 GaryCLI。
它想做的事情,是让 AI 不只是“帮你写几行嵌入式代码”,而是真正进入嵌入式研发现场:
一开始我觉得,这件事应该没有那么复杂。
毕竟现在的 Coding Agent 已经可以:
读代码
改文件
执行 Shell
跑测试
看 Diff
那给它再接几个工具:
esptool
idf.py
串口
BLE
RS485
Modbus
不就行了吗?
后来发现:
完全不是这么回事。
真正开始让 AI Agent 接触 ESP32、串口、BLE、RS485 和 Modbus 之后,我越来越确定一件事:
嵌入式 AI Agent 最难的,可能根本不是写代码。
而是:
它到底操作的是哪台设备?
命令到底执行到了哪一层?
日志到底是不是这一次运行的?
Timeout 后到底能不能重试?
Build 成功以后,功能真的正常了吗?
这些问题如果处理不好,AI 会出现一种非常危险的状态:
事情只做成功了一半,但它会非常自信地告诉你:“已经修好了。”
这篇文章就聊聊,我在做 GaryCLI 过程中越来越重视的 10 个坑。
有些坑看起来很小。
但真正到了嵌入式现场,可能足够让 AI Agent 查半天,甚至把问题越修越复杂。
第一个让我印象很深的坑:AI 对着错误的串口认真分析了半天
有一次,电脑上同时插着几个设备。
GaryCLI 扫描到了:
COM3
COM5
COM8
Agent 选中了:
COM5
然后开始烧录:
esptool.py --port COM5 ...
失败。
接下来 AI 开始非常认真地分析。
可能 ESP32-S3 没有进入 Download Mode。
建议:
1. 按住 BOOT
2. 点击 RESET
3. 重新烧录
看起来非常合理。
我低头看了一眼设备管理器。
ESP32-S3 在 COM8。
COM5 是旁边那个 USB 转 RS485。
也就是说:
这种情况特别符合我后来对 Embedded Agent 的一个认识:
如果最底层的上下文错了,后面推理得越认真,反而离答案越远。
对人来说是设备,对 AI 来说只是字符串
工程师看到:
COM8
脑子里可能自动关联:
桌子右边那块 ESP32-S3
但 AI 看到:
COM3
COM5
COM8
它没有这种物理环境上下文。
更麻烦的是,串口还会变化。
今天:
ESP32-S3 -> COM8
重新插拔以后:
ESP32-S3 -> COM11
Linux 下也一样:
/dev/ttyUSB0
/dev/ttyUSB1
/dev/ttyACM0
如果 Agent 只是记:
上次设备在 COM8
那这个记忆其实没有多大价值。
这个坑之后,我开始重视 Device Identity
GaryCLI 真正应该识别的不是:
COM8
而是:
这是哪一块设备?
例如:
device:
name: esp32s3-dev-01
chip: ESP32-S3
vid: 0x303A
pid: 0x1001
serial_number: ABCD1234
last_port: COM8
选择目标设备的时候,不应该:
ports = scan_ports()
return ports[0]
而应该:
VID / PID
↓
USB Serial Number
↓
芯片握手
↓
设备历史
↓
最后才参考 COM 口
必要时甚至可以执行:
esptool.py chip_id
确认:
Chip is ESP32-S3
之后再继续烧录。
第一个结论
Port 不等于 Device。
代码文件可以通过路径定位。
但真实硬件必须先回答一个更基础的问题:
我现在到底在操作谁?
第二个坑:Build Success,然后 AI 就宣布“问题已经修复”
这个我觉得是 Coding Agent 进入嵌入式以后最容易出现的问题。
比如 GaryCLI 修改了 BLE 代码。
执行:
idf.py build
返回:
Project build complete.
AI 很容易给出:
问题已经修复。
但工程师重新拿手机一扫:
还是搜不到 BLE。
这时候就会出现一个非常经典的场景:
AI:编译已经成功。
我:然后呢?
AI:代码没有编译错误。
我:但是 BLE 还是连不上。
AI:……
这里真正的问题不是 AI 不会写代码。
而是它把:
Build Success
错误地理解成:
Problem Solved
嵌入式里的“成功”其实分很多层
对于一个 BLE Bug 来说:
所以真正的验证链路应该是:
到了这里,才能比较有底气地说:
BLE 问题已经修复并验证。
GaryCLI 不应该只有一个 success
如果工具返回:
{
"success": true
}
这个信息对 Agent 来说太模糊了。
到底什么成功?
更合理的是:
{
"build": "passed",
"flash": "passed",
"boot": "passed",
"runtime_check": "passed",
"functional_test": "passed"
}
如果现在只有:
build = passed
那 Agent 最多只能说:
当前代码已经通过编译。
而不能说:
问题已经修复。
这个坑让我越来越确定一件事
嵌入式 AI Agent 的验证能力,可能比代码生成能力更重要。
现在模型写代码已经越来越强。
但真正进入硬件场景以后,区别会逐渐变成:
你能不能证明自己真的改对了?
第三个坑:BLE 明明显示 Write Success,设备其实根本没执行成功
这个坑也非常有代表性。
GaryCLI 向设备发送:
{
"cmd": "set_config",
"slave": 1,
"poll_ms": 1000
}
BLE API 返回:
Write Success
Agent:
配置成功。
但设备串口里面可能正在输出:
JSON Parse Error
或者:
Invalid Parameter
甚至:
Command Queue Full
这个时候你就会发现:
AI 说的“成功”和你理解的“成功”,根本不是一回事。
Write Success 到底代表什么?
BLE 里的:
Write Success
很多时候只能证明:
数据已经成功写到了 Characteristic。
但完整的业务流程可能是:
所以:
Transport Success
和:
Business Success
完全不是一个概念。
更合理的方式是 request_id
例如 GaryCLI 发:
{
"request_id": "req_1024",
"cmd": "set_config",
"slave": 1,
"poll_ms": 1000
}
设备真正执行完成以后再返回:
{
"request_id": "req_1024",
"status": "ok"
}
只有这里 GaryCLI 才能告诉用户:
配置已经成功执行。
否则应该明确说:
BLE 数据写入成功,但尚未确认设备业务执行结果。
这个问题其实不只是 BLE
同样的坑还存在于:
串口 write success
HTTP 200
MQTT publish success
WebSocket send success
AI 必须知道:
数据发出去了
和:
事情做成了
之间还有多少层。
第三个结论
Transport ACK 不等于 Business ACK。
这是 GaryCLI 这种硬件 Agent 必须明确区分的一层。
第四个坑:Modbus 地址只差 1,AI 能陪你查半天硬件
做过 Modbus 的应该很容易有感觉。
例如寄存器文档里面写:
30101
Agent 直接发:
FC04
Address = 101
然后:
Timeout
接下来 AI 的排查过程可能是:
AI:建议检查 RS485 A/B 是否接反。
我:刚才别的寄存器还能读。
AI:建议确认波特率。
我:9600,没改。
AI:建议确认从机地址。
我:1。
AI:建议检查终端电阻。
我:……
最后发现:
Address 应该是 100。
只差一个。
为什么 Modbus 特别容易让 Agent 翻车?
因为一个地址背后可能同时存在:
文档参考地址
寄存器编号
功能码
PDU Address
比如:
30101
可能表达的是:
3xxxx
→ Input Register
→ FC04
101
→ 第 101 个寄存器
Modbus PDU
→ 从 0 开始
最终 Address
→ 100
同样:
10012
可能对应:
FC02
Address = 11
如果每一次都让 LLM 自己算:
30101 应该怎么转换?
实际上是在把一个确定性问题交给概率模型。
GaryCLI 里我更倾向于这样做
Agent 只表达:
我要读取 30101。
然后交给确定性工具:
resolve_modbus_reference(30101)
返回:
{
"reference": 30101,
"function_code": 4,
"address": 100
}
再由底层 Driver 负责:
CRC
Frame
Length
Address
Timeout
也就是说:
AI
负责理解“我要干什么”
程序
负责保证“协议怎么算”
这个坑给我的启发非常大
凡是可以通过确定性代码解决的问题,就尽量不要让 LLM 自由发挥。
例如:
CRC
寄存器地址换算
长度
字节序
类型转换
帧编码
这些东西并不需要“智能”。
它们需要的是:
永远算对。
第五个坑:Timeout 自动重试,结果继电器被切回去了
这个是我觉得最值得警惕的一类问题。
软件系统里我们非常习惯:
Timeout
↓
Retry
网络请求失败了?
重试一下。
接口超时了?
重试一下。
但硬件不是这么玩的。
举一个最简单的例子
Agent 发:
toggle relay
设备第一次其实已经执行成功:
OFF
→
ON
但是 ACK 丢了。
GaryCLI 看到:
Timeout
于是非常自然地:
Retry 1
设备又执行:
ON
→
OFF
然后第二次 ACK 成功回来。
GaryCLI:
执行成功。
实际继电器:
OFF
用户:
?
这背后其实是幂等性问题
例如:
set_relay(ON)
执行:
1 次
10 次
100 次
最终状态都是:
ON
这种命令比较容易重试。
但下面这些:
toggle()
increment()
pulse()
move_relative(+10)
reset_counter()
就不能简单重试。
因为第二次执行可能真的会再次影响物理世界。
所以 Tool 应该描述副作用
例如:
{
"tool": "set_relay",
"idempotent": true,
"retry_policy": "safe"
}
而:
{
"tool": "toggle_relay",
"idempotent": false,
"retry_policy": "verify_before_retry"
}
Timeout 后应该:
读取真实状态
↓
判断第一次是否已经执行
↓
决定是否需要再次执行
而不是:
Timeout
↓
无脑 Retry
这个坑让我非常警惕“自动化”
AI Agent 自动化程度越高,并不天然代表系统越可靠。
因为软件世界里的:
Retry
到了物理世界,可能变成:
再执行一次真实动作。
前面 5 个,是我觉得最有“现场感”的坑
下面 5 个,我快速讲。
它们没有那么戏剧性,但实际上同样重要。
第六个坑:00 EB 到底是 235,还是 23.5℃?
Modbus 已经读成功:
00 EB
转换成十进制:
235
AI:
当前温度 235℃
但寄存器定义:
type: int16
scale: 0.1
unit: C
正确值:
23.5℃
再复杂一点:
12 34 56 78
可能还有:
ABCD
BADC
CDAB
DCBA
不同字节序。
所以 GaryCLI 更适合让协议层直接返回:
{
"name": "ambient_temp",
"raw": 235,
"value": 23.5,
"unit": "C"
}
而不是把:
00 EB
扔给 LLM 猜。
结论:
协议层负责字节变语义,AI 负责理解语义。
第七个坑:AI 一边 Poll,一边 Write,RS485 被自己搞乱
后台正在:
每 1000ms Poll 一次
用户又说:
把 40010 写成 1。
如果两个任务直接同时操作 UART:
TX Poll
TX Write
RX Poll
RX Write
最后:
CRC Error
Timeout
Wrong Slave Response
AI 可能分析:
疑似现场电磁干扰。
但实际:
干扰来自自己。
所以 Modbus:
Polling
Read
Write
Diagnostic
Agent Test
都应该进入统一:
Modbus Queue
↓
Bus Worker
↓
RS485
结论:
AI 可以并发思考,但硬件总线不能并发乱发。
第八个坑:AI “优化”了接口,然后把 Raw Transport 优化没了
例如 RS485 原始帧:
01 04 00 64 00 02 ...
某个接口的目的只是:
收到什么
就转发什么
这是:
Raw Transport
但 Agent 觉得结构化更漂亮。
于是改成:
{
"slave": 1,
"fc": 4,
"address": 100
}
结果原来依赖 Raw 数据的调试工具全部挂了。
所以工具名称应该明确:
raw_rs485_send()
modbus_read()
modbus_write()
modbus_diagnostic()
而不是一个:
send_rs485()
让 AI 自己猜。
结论:
Tool 越模糊,Agent 越容易聪明地做错事。
第九个坑:AI 分析了 5000 行日志,结果分析的是上一次启动
比如日志文件一直 append:
10:01 第一次启动
10:05 第二次启动
10:08 第三次启动
AI 看到:
E BLE: init failed
然后继续修改 BLE。
再次烧录以后,它又看到同一句:
E BLE: init failed
于是判断:
修复无效。
最后发现:
两次分析的都是第一次启动的旧日志。
所以 GaryCLI 需要:
Run ID
Boot ID
Firmware Hash
Device ID
例如:
runs/
└── 20260831-102500-A71F/
├── plan.json
├── build.log
├── flash.log
├── serial.log
├── test.json
└── result.md
这样 Agent 才知道:
这是谁的日志?
哪一版固件?
哪一次烧录?
哪一次启动?
结论:
没有上下文的日志,只是一堆字符串。
第十个坑:我让 AI 修 BLE,它最后改了 17 个文件
用户:
BLE 偶尔断线,帮我看看。
Agent:
修改 ble_manager.c
然后发现:
event handler 可以一起整理
继续。
然后:
queue 也可以优化
继续。
然后:
日志风格也不统一
继续。
最终:
17 files changed
+1264
-738
我只是让它:
修一下 BLE 断线。
结果差点变成:
BLE Architecture V2
这也是 Coding Agent 很典型的问题:
Scope Creep
所以 GaryCLI 可以有:
problem:
BLE reconnect occasionally fails
expected_files:
- main/ble_manager.c
max_files_changed: 2
max_lines_changed: 80
如果超过:
Patch Budget Exceeded
就重新评估。
最终再:
git diff
↓
build
↓
flash
↓
test
失败就:
rollback
结论:
AI 能改代码,不代表应该无限制地改代码。
做 GaryCLI 以后,我对 AI Agent 的理解变了
最开始,我觉得:
LLM + Tools = Agent
现在我觉得,至少对于嵌入式来说,真正完整的公式更像:
Embedded AI Agent
=
LLM
+
Tool System
+
Device Identity
+
Device State
+
Protocol Semantics
+
Resource Scheduler
+
Execution Context
+
Safety Boundary
+
Verification Loop
这里面任何一个环节缺失,都可能出现一种状态:
AI 看起来完成了很多操作
↓
但其实没有真正解决问题
我现在越来越觉得,GaryCLI 真正应该解决的是这 5 件事
1. AI 到底在操作谁?
不是:
COM8
而是:
这是一块 ESP32-S3
这是 dev-board-01
这是我刚才烧录的那台设备
这叫:
Device Identity
2. 设备现在到底是什么状态?
例如:
Disconnected
Connected
Bootloader
Running
BLE Ready
Modbus Polling
Fault
这叫:
Device State
3. Agent 调用的这个 Tool 到底是什么意思?
不是模糊的:
run()
send()
write()
而应该是:
flash_firmware()
collect_boot_log()
ble_scan()
ble_connect()
modbus_read()
modbus_write()
每个 Tool 都应该明确:
输入
输出
副作用
权限
是否可重试
4. 现在到底成功到了哪一层?
需要明确区分:
Tool Success
Transport Success
Protocol Success
Business Success
Functional Success
例如:
idf.py build success
只能证明:
Tool Success
而:
BLE Write Success
可能只证明:
Transport Success
它们都不能直接等于:
问题已经解决。
5. AI 怎么证明自己真的修好了?
这才是我现在最在意的部分。
例如:
修改 BLE
↓
重新 Build
↓
Flash
↓
读取 Boot Log
↓
Scan
↓
Connect
↓
Read / Write
↓
验证结果
Modbus 也是:
修改 Modbus
↓
重新运行
↓
读取真实寄存器
↓
校验返回值
最终所有问题都应该回到:
真实硬件验证
这也是 GaryCLI 和普通 Coding Agent 最想拉开的地方
普通 Coding Agent 的闭环通常是:
代码
↓
运行
↓
测试
而 GaryCLI 想尝试继续往下延伸:
需求
↓
代码
↓
Build
↓
Firmware
↓
Flash
↓
MCU
↓
Serial
↓
BLE / RS485 / Modbus / Ethernet
↓
真实设备
↓
真实结果
↓
AI 再分析
也就是:
从代码闭环,走向硬件闭环。
我理想中的 GaryCLI 工作方式
比如用户只输入一句:
为什么我的 ESP32-S3 BLE 连不上?
理想情况下,GaryCLI 不应该只回复一篇排查建议。
而应该真正执行:
这里面我觉得最重要的是两个词:
Observe
和:
Verify
没有 Observe:
AI 不知道真实世界发生了什么。
没有 Verify:
AI 不知道自己的修改有没有真的生效。
如果这两个环节缺失,中间的大模型再强,也很容易变成:
根据有限信息做一个看起来合理的猜测。
10 个坑,最后再快速总结一下
| 编号 | 现场问题 | 本质 |
|---|---|---|
| 01 | AI 选错串口 | Port 不等于 Device Identity |
| 02 | Build 成功就宣布修复 | 编译成功不等于功能成功 |
| 03 | BLE Write Success 被认为配置成功 | Transport ACK 不等于 Business ACK |
| 04 | Modbus 地址差 1 | Reference 不等于 PDU Address |
| 05 | Timeout 后无脑 Retry | 非幂等硬件操作风险 |
| 06 | 23.5℃ 被解析成 235℃ | Scale / Type / Byte Order |
| 07 | Poll 和 Write 同时访问 RS485 | AI 并发不等于硬件可以并发 |
| 08 | Raw 接口被“优化”掉 | Tool 和协议边界不清 |
| 09 | AI 分析旧日志 | 缺少 Run / Device / Firmware Context |
| 10 | 修一个 Bug 改 17 个文件 | 缺少 Patch Budget 和 Rollback |
写在最后
做 GaryCLI 以后,我越来越觉得:
嵌入式 AI Agent 最危险的,不是它告诉你“我不知道”。
真正危险的是:
它连接成功了
↓
命令发出去了
↓
代码也编译了
↓
然后告诉你:
“问题已经解决。”
但真实设备可能根本没有按预期工作。
所以我现在对 GaryCLI 的目标也越来越明确。
不是单纯做一个:
AI + CLI
也不是再做一个:
帮你生成 C/C++ 代码的 AI 助手
我更希望它最终变成:
AI
+
Embedded Toolchain
+
Device Context
+
Hardware Verification Loop
让 AI 真正进入:
整个嵌入式研发流程。
换句话说:
GaryCLI 想做的,不是让 AI “会写嵌入式代码”,而是让 AI 真正有能力参与嵌入式研发。
从:
Coding Assistant
走向:
Embedded AI Engineer
这条路肯定还有很多坑。
但现在看来,真正有价值的东西,可能恰恰就藏在这些坑里面。
关于 GaryCLI
GaryCLI 是我目前正在开发的一个面向嵌入式研发场景的 AI CLI Agent。
希望逐步把这些原本比较割裂的动作连接起来:
代码分析
+
代码修改
+
Build
+
Flash
+
Serial
+
BLE
+
RS485
+
Modbus
+
设备诊断
+
真实硬件验证
形成:
需求
→
分析
→
修改
→
执行
→
验证
→
再分析
的完整闭环。
后面我也准备继续整理 GaryCLI 开发过程中的一些真实问题,比如:
AI 怎么自动识别嵌入式设备?
Agent Tool 到底应该怎么设计?
AI 怎么安全操作串口和 RS485?
怎么让 AI 自动分析 ESP32 Crash / Backtrace?
AI 怎么理解 Modbus 寄存器表?
怎么构建真正的 Hardware Verification Loop?
嵌入式 Agent 怎么避免“自信地判断错误”?
如果你也在做:
Embedded AI
AI Agent
ESP32
BLE
RS485
Modbus
嵌入式自动化测试
AI Coding Tool
这些方向,欢迎一起交流。
代码写出来只是第一步,真实硬件验证通过,才算真正完成。
更多推荐

所有评论(0)