前天晚上改完一行 CMakeLists,我盯着它看了几秒。就一行,平平无奇,但我知道它值多少钱。
REQUIRES nvs_flash
形状是举例,不是原文里那个组件名。
这行字让我想起前阵子刷到的一篇 dev.to 复盘。作者 EffessDev,9 月中发的,标题很冲:I just did something my AI agents couldn't。标签一串 ai、devjournal、programming、automation,我本来只想扫两眼。
结果从头读到尾。读完坐了一会儿。
他干的事特别普通:把 ESP-IDF 项目里的代码按通行做法搬进 components 目录,一个功能一个组件。纯体力活。
第一次他拿免费网页版的 Gemini 3.6 Flash 试。用自己写的 ReptClip 把项目结构和文件内容整包喂过去,让模型决定哪些文件搬到哪、新代码怎么写。模型给了方案,他照做。
然后编辑器红了。不是慢慢红,是唰地铺满整屏。
接下来就是熟悉的那套——模型给下一步指令,他照做,还是红;换个说法再问,还是红。反复几个小时,最后气得把工作目录整个重置,从头来。
第二次换 Gemini 3.1 Pro,同样流程走一遍。报错一模一样。
我完全理解那种"换个更贵的应该就好了"的心理,因为我自己也干过。那不是理性决策,是赌气。
后面他又试了 Flash Extended、Pro Extended、DeepSeek v4 的两个版本,全没过,前后两天没了。再上 GitHub Copilot,Copilot 大概花了三十分钟,回了个带对勾的完成提示——画外音:这个对勾现在的信息量约等于"我已阅读并同意用户协议"——项目照样 build 不过,代码里还是一片波浪线。他把项目搁了几天。
这周期的实习生,Copilot 是干活最快的那个,也是最会回"已完成"的那个。这话不只说 Copilot 一家,是所有实习生的通病:它们对"完成"的定义,跟我们对"通过"的定义,从来不是一回事。
几天后他重新打开 gemini.google.com 上的 Pro,改了个做法:不再整份粘贴模型输出,自己动手改 #include、自己写 CMakeLists.txt。
症结这才露头。报错集中在 #include 上。
模型告诉他得在 CMakeLists.txt 的 REQUIRES 或 PRIV_REQUIRES 里声明依赖组件。这招修好了一部分。还剩几个头文件死活找不到。他最后靠 Google 查——查哪些组件定义了那几个头文件,发现模型给的名字跟实际的对不上,改一行,过了。
- REQUIRES <模型给的那个名字>
+ REQUIRES <真正导出这个头文件的组件>
关键的一句在后面。他在评论区自己补了一刀:那些模型生成的代码其实是对的,只是适用于 ESP-IDF v5.2 及更早的版本。要是最开始就把自己用的版本作为上下文给出去,能省掉一大半麻烦。
我看到这句的时候"哦"了一声。
这不是模型蠢。它在给一个已经不存在的世界写代码。
我想把这里说得再重一点:REQUIRES 里那个组件名,根本不是一个能推出来的东西。它是一条事实,住在你硬盘上某个版本的 IDF 目录里。
$ find $IDF_PATH/components -maxdepth 1 -type d | grep -i 那个词
$IDF_PATH/components/真名
秒回。模型能给你的,是它训练数据里出现频率最高的那个名字。高频不等于正确,尤其当这个字段在版本之间改过名的时候。
它没有"去你磁盘上看一眼"的冲动,因为它看不到。它眼前只有一个文本窗口——你贴进去的那堆文件内容,加上它那几十亿 token 的先验。你贴的 include 写着某个头文件名,从这行路径跳到"该在 REQUIRES 里填哪个组件",中间需要的是"哪个组件导出哪个公共头文件"这张表。这张表在你的 IDF 安装目录里,不在它的上下文里。
而且这个字段是弱类型的。名字填错,配置阶段不一定拦得住你,报错会一路推到编译期,变成一句"某个头文件找不到"——一个跟病因隔了两层的症状。他那屏红,红的是这个。查错的人容易顺着"头文件找不到"去搜路径、去搜 include 写法,其实根子在一行谁都没注意的配置上。
顺带说,这不是嵌入式专属的病。
EffessDev 在评论区还提了一嘴他最近搭网站的事:用最新版 Next.js 和 Better Auth,模型一直在生成过时的会话获取代码。他把新语法写进 AGENTS.md,模型还是退回旧写法。
## Auth
- 本项目用的是新签名,示例见下
- 不要再用 getServerSession
这个我信,因为我自己刚撞过同一面墙。手上一个小项目 Next.js 跳了个大版本,模型给我的路由缓存配置还是老一套。你把它掰过来,下一轮对话它又回去了。
(本来想顺手吐槽某个 IDE 的 agent 模式,算了,另开一篇。)
不是你指令没写清楚,是你在跟先验拔河。旧写法在训练数据里的样本量是新写法的几百倍,你往 context 里塞一份 AGENTS.md,那几行字是在一屋子人说旧话的时候小声说一句新话。指望加一条约束就翻盘,想多了。
他那篇下面有条评论被选成了 gem。一个叫 Reid Marlow 的人说,ESP-IDF 的构建问题基本都能归到 CMake 怎么解析组件依赖上:当头文件住在独立组件里,编译路径就得在 REQUIRES 或 PRIV_REQUIRES 里声明那个组件名,而目标名称恰恰是工具会忽略的地方。
idf_component_register(
SRCS "link.c"
INCLUDE_DIRS "include"
REQUIRES "a" # 我的公共头文件里 include 了它
PRIV_REQUIRES "b" # 只在 .c 里用,头文件不外露
)
最有意思的是作者的回复。他说自己对 ESP-IDF 和 CMake 都还是新手,因为根目录和 main 的 CMakeLists.txt 根本不用填 REQUIRES,所以一直没意识到单个组件需要。
看到这句,我替他把那两天的账都算清了。
还有一条我更喜欢。Jo Do 说,代理连跑几天挂掉、人一次会话解决,差别不在智能——在于人手里有一套"这个系统是怎么运作的"的理论,代理手里只有一个文本窗口。他还提了个成本曲线:每次代理尝试都要人工审查,一旦审查的结论不再收敛,人工介入反而更便宜。
这个说法我认。代理跑两天的成本从来不是那点订阅费,是你两百次收工看结果、两百次"还是红的"。曲线不收敛的时候,你花掉的那两天不叫投入,叫税。
写到这里我本来想收一句"所以这类活就该自己干,别喂给模型"。
想了一下,这个结论太糙,而且不对。
回头看那两天,模型的产出不是零。至少"去 REQUIRES 里声明依赖组件"这个方向是它给的——方向对,名字错。这其实是很典型的一种分工:模型出假设,你负责证伪。
他真正踩的坑不在于问了模型,而在于把证伪这一步也一起外包了。模型给个名字,他填进去,编译,红,再问。循环两天。
证伪没法外包,因为证伪需要的是"这个头文件到底谁提供"这个事实。事实不在模型的参数里,它只在你的硬盘上。
绕一圈,还是那句:不是智能问题,是它没有关于这个系统的理论。
我现在的做法很土,就两条。
一,凡是"A 叫什么名字"的问题,问机器。组件名、包名、导出的符号名,一律 find 和 grep,不从模型嘴里要。它可以给我候选,机器给的是事实。名字这类东西,猜错一次的代价远高于查一次的麻烦。
二,凡是"这几样东西该怎么摆"的问题,才去问模型。结构、边界、取舍,这些它比我有耐心。
再加一条,是上次那次数据库迁移翻车之后一直守着的:版本号当一等上下文,原样贴。
# 贴给模型之前先跑
$ idf.py --version
$ node -v
$ npm ls next better-auth --depth=0
不是"我在用 ESP-IDF",是版本号的输出;不是"我在用最新的 Next.js",是 package.json 里那几行依赖的实际版本。看着笨,省下来的时间对得起它 😅。
至于我自己的 components 目录——到今天还是平的。不是什么值得学的样板,我就是暂时没空缴这笔税。
