AI 写嵌入式代码,编译通过不等于做完——拆解 ChipCode 产品工厂的文件状态机与 evidence.jsonl 证据链
摘要:大模型写 C 代码早就不是问题了。真正卡住嵌入式 AI 开发的是另外三件事:需求没法验收、"已验证"是模型自己说的、项目状态只存在聊天记录里。本文以 ChipCode(原 TrieCode)的「产品工厂」为样本,拆解它如何用外置到磁盘的文件状态机、只追加的证据账本
evidence.jsonl、以编译器退出码为准的验证门禁,把「一句需求 → 需求条件 → 结构 → BOM → 固件 → 联调 → 产品包」串成一条可审计的链路。文末附可运行的证据覆盖率校验脚本、四条工具链的实际编译路径,以及这套方案当前的真实边界。
一、问题从来不是「AI 会不会写 C 代码」
用 AI 辅助做单片机项目,大部分人经历过同一个剧本:
在对话框里敲一句「做一个自动浇花器,土干了自动抽水,手机能看湿度」,几秒钟后拿到一份 main.c,看起来有模有样。然后呢?
| 断点 | 具体表现 | 后果 |
|---|---|---|
| 需求不可验收 | 「手机能看湿度」是自然语言,没有判定条件 | 无法回答「到底算不算做完了」,只能靠感觉 |
| 验证靠模型自述 | AI 回一句「已编译通过、已测试」 | 可能根本没跑编译器,或只跑了条无关命令 |
| 状态在会话里 | 选了哪颗传感器、引脚怎么分、验收条件是什么,全在聊天记录 | 换个会话 / 换台电脑 / 换个人接手,上下文归零 |
| 工具链割裂 | Arduino、ESP-IDF、STM32Cube、SDCC-51 四套命令、四种工程结构 | 生成的代码落不进一个能编译的工程 |
前两条是可信度问题,后两条是工程化问题。给 IDE 挂个大模型只能解决「写代码」这一段,解决不了这四条。
ChipCode 的做法有点反直觉:它没有继续在「让 AI 写得更准」上堆 prompt,而是把项目状态从对话里搬到了磁盘文件上,让 AI 只能写文件、不能自己宣布状态。下面拆开看。
说明:本文由 ChipCode 开发团队撰写,文中产品为团队自研。所引字段、命令与规则均来自公开官方文档与开源仓库,未做实机验证,已逐处标注来源与示意内容。
二、把状态搬到磁盘:五步文件映射
「产品工厂」把做一款嵌入式产品拆成五步,每一步的产出写进项目里固定的文件:
| 阶段 | 做什么 | 落盘产物 |
|---|---|---|
| 创意 | 把想法收敛成可验收的条件 | spec.json(条件与确认状态)、spec.md(需求正文) |
| 造型 | 外形尺寸、3D 模型、2D 视图 | design/shape.json(dims / model / mesh / views[])、design/、design/views/ |
| 硬件 | 选型,引脚 / 总线 / 电源约束 | bom.json、constraints.json |
| 软件 | 写固件 | firmware/ |
| 联调 | 跑通,并把结果记下来 | evidence.jsonl |
关键在阶段状态是从磁盘文件算出来的,不是 AI 说了算。官方文档给出的判定规则:
验收条件没确认 → 创意停在「进行中」
shape.json 里写的模型/网格/视图文件不存在 → 造型「阻塞」
有视图但没 3D 网格(或反过来) → 造型停在「进行中」
bom.json 里有一颗器件没有来源链接 → 硬件「阻塞」
任何一条有效证据的结果是 fail → 联调「阻塞」
文档里特别解释了一句:「阻塞的意思是有东西没落地,不是出错。」
整个流程是这样:
一句话需求
│
▼
[创意] spec.json / spec.md ← 条件未确认 → 进行中
│ ✅ 人工确认验收条件
▼
[造型] design/shape.json + model + mesh + views[] ← 缺 mesh → 阻塞
│ ✅ 人工确认结构
▼
[硬件] bom.json / constraints.json ← 器件缺来源链接 → 阻塞
│ ✅ 人工确认选型
▼
[软件] firmware/ ← 编译通过才计入证据
▼
[联调] evidence.jsonl ← 有效证据含 fail → 阻塞
│ ✅ 人工给物理结果
▼
hardware-package/ + .manifest.json
这本质是一个以文件为唯一真相源(single source of truth)的有限状态机:AI 的产出是文件,看板是文件的纯函数,人只在三处拍板——确认验收条件、确认结构与选型、给物理结果。文档原话:「AI 只提案,不替你确认。」
两个设计细节值得单独拎出来:
- 造型必须凑齐三件套:3D 源码(
model,.scad/.step)、3D 网格(mesh,.stl/.obj/.3mf)、2D 视图(views[])。只写.scad不导网格,看板只能画线框、造型会一直停在「进行中」。文档说这是有意的:「结构图应该是能转着看的,不该靠用户在对话里提醒。」 - 选型不许编型号:
bom.json每颗器件必须附来源链接(数据手册 / 官网 / 商城),查不到时 AI 会问人,不会自己造一个型号出来。对做过 BOM 的人,这条比什么花哨功能都实在。
三、evidence.jsonl:只追加的证据账本
这是整套设计里我最认可的一块。
evidence.jsonl 一行一条,只追加、不修改。官方文档给的例子:
{
"id":"ev-0007","at":"2026-11-02T13:20:00Z","stage":"integration","claim":"水泵启停 50 次无卡死","result":"pass","covers":["c1","c3"],"by":"ai"}
| 字段 | 含义 |
|---|---|
id / at |
证据编号、时间戳 |
stage |
所处阶段 |
claim |
这条证据声称验证了什么 |
result |
pass / fail / inconclusive,只有 pass 才算覆盖 |
covers |
覆盖了 spec.json 里的哪几条验收条件 |
does_not_cover |
写明没证明到什么,会进看板的「未证明」清单 |
by |
谁给出的证据 |
三条规则把「AI 自称通过」这条路堵死了:
result三态,而不是布尔值。「没能验证」(inconclusive)不等于通过,也不等于失败——这一条对硬件联调特别关键,示波器没接上和测出波形是两回事。covers把证据和需求条件显式关联。不是「我测了」,而是「我测的这件事,覆盖了 c1、c3,没覆盖 c2」。- 撤销靠追加,不靠删除:
{
"id":"ev-0008","invalidates":"ev-0007","reason":"换了传感器"}
为什么是 JSONL 而不是一个 JSON 数组或数据库?站在工程角度,好处很直接:git diff 能逐行看出哪条证据是新加的;多人 / 多智能体并发写只追加不冲突;jq、Python、CI 脚本一行就能消费;invalidates 保留了完整审计轨迹,谁在什么时候因为什么推翻了旧结论,一目了然。
一个能直接跑的覆盖率校验脚本
官方文档没有公开 spec.json 的完整字段表,所以下面这个脚本不解析 spec.json,直接把验收条件 id 作为参数传入。脚本是本文另写的示例,不是 ChipCode 内置功能:
#!/usr/bin/env python3
"""校验 evidence.jsonl 是否覆盖了全部验收条件(本文示例脚本,非产品内置)。
用法:python check_evidence.py evidence.jsonl c1 c2 c3
退出码:0 = 全覆盖,1 = 存在未证明的条件
"""
import json
import sys
from pathlib import Path
if len(sys.argv) < 3:
print(__doc__)
sys.exit(2)
path, required = Path(sys.argv[1]), set(sys.argv[2:])
records, invalidated = [], set()
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
try:
ev = json.loads(line)
except json.JSONDecodeError as e:
print(f"第 {lineno} 行不是合法 JSON:{e}")
sys.exit(1)
if "invalidates" in ev:
invalidated.add(ev["invalidates"])
else:
records.append(ev)
covered = set()
for ev in records:
if ev["id"] in invalidated:
continue
if ev.get("result") == "pass": # fail / inconclusive 都不算覆盖
covered.update(ev.get("covers", []))
missing = sorted(required - covered)
print(f"验收条件 {len(required)} 条,已覆盖 {len(required) - len(missing)} 条,"
f"未证明 {len(missing)} 条")
if missing:
print("未证明:" + ", ".join(missing))
sys.exit(1)
把它挂进 CI,或者提交前跑一遍 pre-commit,就得到了一个很朴素但有效的门禁:条件没被 pass 证据覆盖,就不许宣布联调完成。这套思路不依赖任何特定 IDE,你自己项目里拿 Git + JSONL 也能搭。
四、编译是硬门禁:验证发生在进程层,不是语言层
「AI 说通过」不可信,那什么可信?编译器的退出码和 stdout。
ChipCode 把四条嵌入式工具链都收进了同一个界面,编译 / 烧录 / 串口日志走同一处:
| 工具链 | 编译 / 烧录路径 | 前置条件 |
|---|---|---|
| Arduino | arduino-cli 编译上传(板卡库、端口一站式) | 需先选 FQBN 与端口 |
| ESP-IDF | idf.py build、idf.py -p {port} flash;图形化 Kconfig |
只需端口,目标芯片在设备选项卡选,选中即自动 set_target |
| STM32Cube | CMake 配置 + Ninja 构建,STM32_Programmer_CLI 经 SWD 烧录 |
SWD 烧录无需串口,串口只用于日志 |
| 51(8051) | sdcc -mmcs51 main.c → packihx main.ihx > main.hex,stcgal 串口 ISP |
内置 10 款常用 STC 芯片 |
官网给的 51 编译输出示例:
$ sdcc -mmcs51 main.c
main.c: 0 errors, 0 warnings
$ packihx main.ihx > main.hex
main.hex · 842 bytes · Flash 10.3% (1024/8192)
注意最后那行 Flash 占用——这类数字才是可以写进证据账本的东西。
配套的防谎报机制散落在几处:
- AI 改完代码会跑只读独立验证,结论是
PASS / FAIL / PARTIAL;工具调用失败会标红并注明原因(编译失败、插件报错、操作被拒绝如实显示),子代理与独立验证各有独立卡片,能看出执行者与结论。 - 桌面端
Ctrl+R编译、Ctrl+U上传(上传会先自动编译),输出实时进底部面板,编译错误带行号,点击可跳转。 - CLI 的
--format json输出 NDJSON(每行一个 JSON 事件),官方文档给出的片段:
{
"version":1,"seq":1,"type":"session.status","status":"start"}
{
"version":1,"seq":2,"type":"message.delta","content":"先看一下项目结构"}
{
"version":1,"seq":3,"type":"tool.step","name":"read_file","status":"completed","args":{
...},"result":"..."}
{
"version":1,"seq":9,"type":"message.done","content":"..."}
其中 tool.step 带 failed: true 才表示这一步确实失败——编译失败、插件返回失败、操作被拒绝都会如实标记,不会混在自然语言里被抹平。
- CLI 退出码把「真失败」和「没人确认」分开:
1= 出现工具失败或异常,2= 用法错误,4= 非交互环境下遇到需确认的操作,130= Ctrl+C。文档点明 CI 里最常见的是4,改成--yes或调整任务描述即可。
一句话总结这个思路:把验证从语言层降到进程层。模型可以用自然语言把任何事说成"已完成",但它改不了 sdcc 返回的 exit code,也改不了 0 errors, 0 warnings 这行输出。
五、实操:20 分钟走一遍自动浇花器
按官方文档的「试一次」流程:
- 新建项目,类型选「产品项目」——项目骨架自动创建上面那批文件。
- 打开左侧产品工厂,点「开始描述需求」,输入:
做一个自动浇花器:土干了自动抽水,手机能看湿度。 - 创意:AI 先问几个问题,把需求收敛成条件,输入框上方出现「待确认」卡,你确认后才继续。
示意:比如收敛成
c1湿度低于阈值启动水泵、c2湿度恢复后停泵、c3能读到当前湿度值。这是本文为说明流程写的示意条件,不是软件自动生成的原文。 - 造型:产出
design/shape.json的model/mesh/views[]三件套。装了 OpenSCAD 插件时,AI 用插件的render/preview出网格和三视图(真几何内核);没装就由 AI 写脚本导出 STL。插件不必提前启用——AI 发现缺建模工具会先查本地插件、弹一次确认请求启用。 - 硬件:
bom.json+constraints.json,每颗器件带来源链接。这里最容易卡住,缺链接就是「阻塞」。 - 软件:
firmware/写固件,编译通过才计入证据。 - 联调:结果写进
evidence.jsonl,covers指回 c1/c3 这类条件 id;没证明的写进does_not_cover。 - 导出产品包
hardware-package/:
hardware-package/
├── requirements.md 需求与验收条件
├── bom.csv 器件清单与来源
├── evidence.md 验证了什么、没证明什么
├── README.md
├── firmware/ 固件源码
├── design/ 结构模型与视图
└── .manifest.json 输入指纹,用来判断是否过期
.manifest.json 记录的是输入指纹:之后改了需求或选型,看板会把产品包标成「已过期」。这一笔很工程——交付物和需求版本绑定,避免拿着旧 BOM 去打样。
对要交「代码 + 文档 + 资料包」的场景(课程设计、毕设、方案交付),requirements.md / bom.csv / evidence.md 恰好就是常被要求、又最常被临时补的三份东西。
六、给工程化留的口子:插件契约与 CLI
如果只是个封闭 IDE,上面这些也就是个好用的功能。真正让它可扩展的是插件 manifest 契约。
插件只需一个 plugin.json,通过 transport 声明七类通用通道,全部是宿主通用驱动,无需宿主预注册:
transport |
用途 |
|---|---|
cli |
一次性命令行(spawn 数组参数,不经 shell,天然防注入);编译、烧录、流式输出 |
http |
REST API,宿主全局 fetch 并尊重代理设置 |
grpc |
插件自带 .proto,宿主动态加载,仅连接该插件自己声明的 backend 端口 |
mcp |
调用插件声明的 MCP 服务器工具 |
mqtt |
通用 MQTT 设备通道,publish / request / collect / read_state / status 五种 op |
internal |
复用宿主标准能力:串口、日志、状态、Arduino CLI、工具链检测 |
backend |
调插件自声明的 stdio 长驻后端(JSON-RPC over stdin/stdout),适合有状态工具如 idf.py confserver |
最小 manifest:
{
"id": "my-toolchain",
"name": "我的工具链",
"version": "1.0.0",
"developer": "你的名字",
"icon": "🔧",
"capabilities": {
"compile": {
"transport": "internal", "service": "arduinoCli", "method": "compile", "output": "avr-gcc" },
"board-manage": {
"transport": "internal", "service": "arduinoCli", "method": "board.search" }
}
}
capabilities 是类型化能力绑定:UI 主按钮(编译 / 上传 / 板卡 / 库管理)与 AI 工具走同一条能力路由,不会出现「界面上能点、AI 不会调」的割裂。安装时 validateManifest 会校验能力 key、transport、命令 placement、scaffold 路径安全(拒绝绝对路径 / .. 穿越)等,非法 manifest 直接拒绝安装。
另外几个对二次开发有意义的点:
detection声明式工具链检测:手动路径 →envVars→ 官方installerJson→pathGlobs→validateFile按序探测;derive.exportPathFrom从工具链自己的tools.json注册表合成 export PATH,复刻官方 export 脚本而不用跑慢命令。devicePanel数据驱动设备壳:宿主按 selectors 的key(fqbn/board/port)自动派生编译 / 上传前置门控needsBoard/needsPort——这解释了为什么 Arduino 要先选板和口,而 ESP-IDF 只需端口。- 9 个官方插件全部 Apache-2.0 开源:github.com/triecode-dev/triecode-plugins,含 arduino-cli、sdcc-51、esp-idf、stm32-cube、openscad、easyeda(原理图布局 + A* 绕障布线)、wokwi、browser、plugin-dev。可审计、可 Fork。
命令行侧还有一个与桌面端共用智能体引擎、模型层、权限内核与失败判据的 triecode CLI,支持 --format json 输出 NDJSON,适合脚本 / CI:
triecode run "把 main.c 编译并烧录到 uno" --yes --format json
triecode compile uno D:\proj --port COM3
triecode flash uno COM3 D:\proj
triecode monitor COM3 115200
triecode doctor
doctor 会打印 API Key、工具链、git、代理的自检结果,缺项给「引导」,任一失败退出码为 1。写 CI 时值得留意文档明说的差异:CLI 不读桌面配置(%APPDATA%\TrieCode\config.json 里的 Key / 代理 / 模型都不生效,只认参数和环境变量)、没有 --resume、工具集是独立的一份(桌面端的插件技能 / MCP / 记忆不经过 CLI),插件只用于 compile / flash 两步。
七、边界与踩坑:先说清楚,再决定用不用
这部分官方文档写得很实,我照搬,不替它遮:
| 项 | 现状 |
|---|---|
| 平台 | 仅 Windows 10 / 11 x64;macOS / Linux 开发中 |
| 体积 | 安装包 NSIS 约 144 MB,装后约 500 MB;首次编译 Arduino 另下 AVR 核心约 200 MB;建议内存 ≥ 4 GB |
| 代码签名 | 暂未做签名,Windows SmartScreen / 部分杀软可能报「未知发布者」,需手动「仍要运行」或加白名单 |
| 产品工厂边界 | 不做 PCB 设计;不提供代购、打样与发货;选型自由但型号必须附来源链接 |
| CLI | 尚未发布到 npm,需从仓库构建(corepack pnpm --filter @triecode/cli build),运行时要求 Node ≥ 24(用到 node:sqlite) |
| 计费 | 积分 = 系数 ×(输入 + 输出 token)÷ 2000,每模型系数不同;自填 Key 时请求直达厂商、不经服务端;也支持本地 Ollama |
| 模型 | 内置模型随版本更新、退役的会下架;切换模型不影响项目、记忆、技能与检查点 |
| 更名 | 原名 TrieCode,2026 年 10 月起更名 ChipCode,官网迁至 chipcode.cn;只改名称与域名,项目、插件、账号、积分、会话历史全部沿用,无需迁移 |
还有一条容易被忽略:造型阶段没装 OpenSCAD 插件也能走,但只写 .scad 不导网格,看板会一直停在「进行中」——这不是 bug,是设计上的门禁。
八、我的判断:适合谁,不适合谁
适合:
- 课程设计 / 毕设 / 方案交付这类本来就要交「需求 + BOM + 源码 + 验证记录」的场景——产品包直接把这四样生成出来,省的是最烦的那部分整理工作。
- 一个人或小团队从零做一款小硬件,需要 AI 覆盖选型、结构、固件,而不只是补全几行代码。
- 想借一套现成范式,把「可追溯」落到自己项目里的人——哪怕不用这个 IDE,
evidence.jsonl+covers+invalidates+ 退出码门禁这套组合也值得抄。
暂不建议:
- 已经有成熟 CI / PLM / 需求追溯体系的团队:
evidence.jsonl会和现有体系重叠,先想清楚谁是唯一真相源,别搞成两处维护。 - 只想要轻量代码补全:整套 IDE 太重,编辑器插件更合适。
- macOS / Linux 用户:等对应版本。
- 对供应链有严格签名校验的公司环境:当前安装包未签名,IT 大概率会拦。
最值得借鉴的其实不是某个功能,是三条原则:
- 状态外置到文件——让看板成为文件的纯函数,AI 无法用一句话改变项目状态;
- 验证以退出码为准——可信结论来自进程,不来自模型自述;
- 决策点显式留给人——验收条件、结构、选型由人确认,物理结果由人给,AI 只提案。
这三条不依赖任何具体工具,放在你现在的工程里也成立。
九、小结
嵌入式 AI 开发的瓶颈早已从「生成代码」移到了「生成可交付、可追溯的工程」。ChipCode 产品工厂给出的答案不算炫技,甚至有点笨:把每一步产出写成固定文件,把验证写成只追加的 JSONL,把阶段状态交给磁盘算,把关键决策留给人。
但恰恰是这种笨办法,解决了「AI 说做完了,我不敢信」这个最实际的问题。