Ralph Loop:嵌入式无人值守编码循环实战
嵌入式开发无人值守!用 Ralph 自主编码循环自动跑需求、改固件、SVN 提交、跑测试(公司环境实战)
一篇把"需求分析 → 任务执行 → 测试验证→ SVN/Git 提交"四件事在嵌入式项目里串成无人值守自动循环的硬核实战。源码级剖析
ralph.sh驱动器,配 STM32 + SVN 生产级prd.json,最后给一份避坑清单。(关于svn进行代码协作开发,我已经不想吐槽公司了,吐槽也没有用。)
引言:嵌入式那点"体力活",到底卡在哪?
你大概率经历过这样的下午:
- 产品丢来一句"给板子加个温湿度采集,顺便串口上报上去",你花了三天在寄存器手册、I2C 时序、CRC 校验、帧协议之间来回跳;
- 每改一行
.c,就要make一次、烧录一次、盯一次串口——编译-烧录-看 log 的循环重复几十遍; - 公司用 SVN,提交前还得
svn add一堆新文件、写提交信息、还要小心别把编译产物提交进去; - 测试?除了"灯亮没亮",剩下的全靠手感。
问题的本质不是技术难,而是"重复且无记忆"的体力活太多。 而 LLM 单次问答又记不住你的工程上下文——你刚让它改完 bsp_i2c.c,下一轮它连你用的是软件 I2C 还是硬件 I2C 都忘了。
Ralph 给出的答案很"硬核程序员":不要让大模型记住任何东西,而是把记忆全部落到文件里,然后用一个 bash 驱动器反复冷启动它,每轮只做一件小事、做完自动提交、自动跑检查,直到需求清单清空。

一、Ralph 协议本质:一个把"需求→代码→测试→提交"循环起来的文件驱动器
1.1 一句话本质
Ralph 不是被开发的目标软件,它是一台引擎。它的工作完全由 .ralph/prd.json(一张用户故事清单)定义,执行对象是某个目标项目的工作树。整套系统只活在 .ralph/ 目录里,仓库根目录其余部分就是被操作的目标工程。
你写需求(prd.json) -> ralph.sh -> 循环:冷启动LLM做1个故事->跑检查->SVN提交 -> 全done自动停
1.2 协议数据流:四要素如何在文件间流转
理解 Ralph 的关键,是看懂状态只在四个文件之间流动,没有任何内存变量跨迭代存活:
| 文件 | 角色 | 谁读 | 谁写 |
|---|---|---|---|
prd.json |
需求清单(故事 + passes 状态机) |
驱动器 + 智能体 | 智能体改 passes,驱动器读计数 |
progress.txt |
跨迭代记忆(顶部 Codebase Patterns + 故事日志) |
智能体(每轮开头) | 智能体只追加;驱动器周期性裁剪 |
CLAUDE.md |
智能体指令书 | 智能体(被当作 prompt 喂入) | 你维护,一般不改 |
run.log |
运行审计(每轮耗时/通过数/原因) | 你复盘用 | 驱动器 log_iteration 写 |
1.3 为什么这套协议天生适配嵌入式:冷启动 + 文件即记忆
嵌入式工程有两个特点是 Web 后端没有的:
- 编译环境极其脆弱——换一台机器、重新
svn checkout一份副本,工具链路径、宏定义、Keil 工程选项可能全变。Ralph 操作的就是你现有的工作副本,不拉新副本,不破坏编译环境。 - 知识高度"碎片化且强依赖"——哪个 GPIO 接了 SHT30、I2C 地址是
0x44还是0x45、CRC 多项式是什么,这些一旦记错就是玄学 Bug。Ralph 的Codebase Patterns段正好是沉淀这些"寄存器级事实"的容器,而且跨裁剪保留。
二、需求分析:把嵌入式需求拆成 prd.json(这是决定成败的 90%)
Ralph 跑得好不好,几乎完全取决于 prd.json 写得好不好。需求分析这一步,在 Ralph 里就是"把一个模糊的硬件需求,拆成一串小到一轮能做完、按依赖排序、验收标准可验证的故事"。
2.1 两种生成方式
2.1.1 方式 A:手写 prd.json(快速,适合熟手)
参考 .ralph/prd.json.example,结构是 project / branchName / description / userStories[],每个故事含 id / title / description / acceptanceCriteria[] / priority / passes / notes。
2.1.2 方式 B:/prd + /ralph 技能两步走(推荐复杂需求)
在 Claude Code 里:
/prd—— 输入功能描述,它会问你 3-5 个澄清问题(带字母选项,你回1A 2C 3B即可),然后在tasks/生成一份 PRD 文档;/ralph—— 把那份 PRD 转成规范的.ralph/prd.json,并强制执行"故事够小、依赖有序、验收可验证"三条铁律。
2.2 嵌入式故事的拆解铁律(核心认知增量)
通用版铁律是"Schema → 后端 → UI → 聚合"。映射到嵌入式,顺序应该改成:
底层驱动/寄存器抽象 → 器件驱动 → 协议解析/校验 → 业务状态机 → 集成与上报
为什么是这个顺序? 因为后面的故事要 #include 前面的头文件、调用前面的函数。Ralph 按.priority 顺序执行,如果 US-002(器件驱动)排在 US-001(I2C 抽象)前面,智能体冷启动时连 bsp_i2c_read() 都还没有,只能瞎编一个签名,下一轮又对不上——典型的"冷启动依赖地狱"。
2.3 生产级 prd.json:STM32 + SHT30 温湿度 + 串口上报
下面是一份可以直接拿去跑的嵌入式 prd.json,把"加温湿度采集并串口上报"拆成 5 个单轮可完成的故事,注意每个 acceptanceCriteria 末尾都挂了 make build 通过(替代 Web 项目的 “Typecheck passes”):
{
"project": "STM32_SHT30_UART_Report",
"branchName": "ralph/sht30-uart-report",
"description": "在 STM32F4 上新增 SHT30 温湿度采集(I2C) 并按帧协议从串口周期上报",
"userStories": [
{
"id": "US-001",
"title": "I2C 软件驱动抽象层",
"description": "作为开发者,我需要一个不依赖具体器件的 I2C 读写抽象,供后续器件驱动调用。",
"acceptanceCriteria": [
"新增 bsp_i2c.h / bsp_i2c.c,提供 bsp_i2c_init/bsp_i2c_write/bsp_i2c_read 三个函数",
"读函数支持 repeated-start 时序(SHT30 测量读取必需)",
"I2C 地址采用 7bit 左移约定,对外只暴露 0x44",
"make build 通过"
],
"priority": 1,
"passes": false,
"notes": "PB6=_SCL, PB7=SDA, 参考 CubeMX 默认配置"
},
{
"id": "US-002",
"title": "SHT30 器件驱动",
"description": "作为开发者,我需要封装 SHT30 的测量命令与温湿度换算。",
"acceptanceCriteria": [
"新增 sht30.h / sht30.c,提供 sht30_read_temp_humi(float*,float*)",
"使用单次测量命令 0x2C06,等待 15ms 后读取 6 字节",
"温湿度换算公式: T=-45+175*raw/65535, RH=100*raw/65535",
"make build 通过"
],
"priority": 2,
"passes": false,
"notes": "器件地址 0x44,依赖 US-001 的 bsp_i2c"
},
{
"id": "US-003",
"title": "SHT30 CRC-8 校验",
"description": "作为开发者,我要对温湿度原始数据做 CRC-8 校验,防止脏数据上报。",
"acceptanceCriteria": [
"新增 sht30_crc.c,实现 crc8(data, len),多项式 0x31,初值 0xFF",
"sht30_read_temp_humi 在 CRC 失败时返回非 0 错误码",
"新增 test_crc.c 覆盖手册给的样例向量",
"make build 通过 且 make test 通过"
],
"priority": 3,
"passes": false,
"notes": "手册样例: 0xBE 0xEF -> CRC=0x92"
},
{
"id": "US-004",
"title": "串口上报帧协议",
"description": "作为用户,我希望温湿度数据按固定帧从串口发出,上位机能解析。",
"acceptanceCriteria": [
"新增 frame.h/frame.c,封装 frame_pack(temp,humi)->buf",
"帧格式: 0xAA | len | temp_h | temp_l | humi_h | humi_l | crc8 | 0x55",
"make build 通过"
],
"priority": 4,
"passes": false,
"notes": "UART2 PA2/PA3, 115200, 依赖 US-003 的 crc8"
},
{
"id": "US-005",
"title": "周期采样上报任务",
"description": "作为用户,我希望板子上电后每 1s 采集并上报一次。",
"acceptanceCriteria": [
"main.c 在 while(1) 中调用 sht30 采集 + frame 打包 + uart 发送",
"用 HAL_GetTick() 实现 1000ms 节流,非阻塞",
"make build 通过",
"硬件在环验证:串口能看到稳定 0xAA...0x55 帧"
],
"priority": 5,
"passes": false,
"notes": "依赖 US-002 与 US-004"
}
]
}

2.4 验收标准:把"可验证"改造成嵌入式能跑的命令
/ralph 技能要求每条 acceptanceCriteria 必须可验证——"工作正常"是坏的,"make build 通过"是好的。嵌入式场景下,把通用的 Typecheck passes 替换成下面这组:
| 故事类型 | 验收命令 | 说明 |
|---|---|---|
| 纯逻辑/算法 | make test 通过 |
host 端 gcc 跑单元测试(CRC、换算、帧打包都能在 PC 上测) |
| 任意 C 改动 | make build 通过 |
交叉编译 arm-none-eabi-gcc,替代 typecheck |
| 外设/驱动 | make build 通过 + 硬件在环 |
涉及真实时序的只能上板验证 |
| 协议/通信 | 串口抓帧比对 | 上位机解析帧格式正确 |
关键洞察:把能在 PC 上测的逻辑(CRC、换算、帧打包)和必须在板上测的时序(I2C repeated-start)物理隔离——前者塞进 make test,后者写进"硬件在环验证"。这样 Ralph 能自动验证 70% 的故事,剩下的 30% 才需要你上手。
三、任务执行:每轮冷启动 spawn claude,单故事单轮闭环
3.1 单轮闭环六步(智能体视角)
每次迭代,ralph.sh 用 claude --dangerously-skip-permissions --print < .ralph/CLAUDE.md 拉起一个全新进程,喂进去的 CLAUDE.md 是它的指令书。智能体按下面六步走:
- 读
prd.json+progress.txt(先看顶部Codebase Patterns); - 选
passes: false里priority最高的那个故事; - 实现这一个故事;
- 跑质量检查(嵌入式的
make build/make test); - 通过则把该故事
passes改成true; - 往
progress.txt追加一条带"给后续迭代经验"的记录。
注意:智能体被明确禁止跑任何 VCS 命令(不 git/svn add、commit、checkout)——提交完全交给驱动器,下文第四节细讲。
3.2 冷启动如何"记住"项目知识:Codebase Patterns
因为每轮都是新进程,唯一能跨迭代传递知识的就是 progress.txt 顶部的 ## Codebase Patterns 段。下面是嵌入式项目里它应该长什么样:
## Codebase Patterns
- I2C: 软件模拟,PB6=SCL/PB7=SDA,地址 7bit 左移,SHT30=0x44
- 时序坑: SHT30 测量后必须 repeated-start 读取,不能发 STOP 再 START
- CRC: 多项式 0x31,初值 0xFF,与 SHT30 / 帧校验共用 sht30_crc.c
- 构建: make build=交叉编译, make test=PC 端 gcc 跑单测,二者分开
- 串口: UART2 PA2/PA3 115200,HAL_UART_Transmit 阻塞发送即可
- 不要碰: system_stm32f4xx.c / startup_*.s(改了会炸启动)
这一段是整个协议跨迭代记忆的锚点——驱动器裁剪 progress.txt 时会把它完整保留,所以你沉淀的寄存器事实不会丢。
3.3 嵌入式编译检查的接入:make build 替代 typecheck
智能体跑的"质量检查"完全由你工程里的命令决定。给嵌入式工程配一个 Makefile,让 make build 跑交叉编译、make test 跑 PC 端单测:
# Makefile —— 嵌入式工程的"质量检查"入口,供 Ralph 调用
CC = arm-none-eabi-gcc
CFLAGS = -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
-I./Drivers/CMSIS -I./Inc -Wall -O2
HOST_CC = gcc # PC 端跑单测用
SRC = $(wildcard Src/*.c)
TEST_SRC= $(wildcard test/test_*.c)
# Ralph 在每轮迭代里调用的"编译通过"检查
build:
$(CC) $(CFLAGS) $(SRC) -o build/firmware.elf 2>&1 | tee build.log
grep -qiE "error:" build.log && exit 1 || exit 0
# 纯逻辑单测(CRC/换算/帧打包),在 PC 上跑,速度快
test:
$(HOST_CC) -I./Inc $(TEST_SRC) Src/sht30_crc.c Src/frame.c -o build/test_bin
./build/test_bin
.PHONY: build test

3.4 源码级剖析:invoke_tool + run_agent_with_retry
下面是 ralph.sh 里把"拉起智能体"和"抗 API 繁忙"耦合在一起的核心两函数,带详尽注释(这是公司环境能无人值守的关键):
# ralph.sh —— 拉起选定的 agent 工具一次,combined output 打到 stdout。
# 把工具调用收拢在一个函数里,方便 run_agent_with_retry 调用。
invoke_tool() {
if [[ "$TOOL" == "amp" ]]; then
# amp 模式读 .ralph/prompt.md(本仓库不提供,需自建)
cat "$SCRIPT_DIR/prompt.md" | amp --dangerously-allow-all 2>&1
else
# claude 模式:把 .ralph/CLAUDE.md 当作 prompt 喂进去,--print 非交互
claude --dangerously-skip-permissions --print < "$SCRIPT_DIR/CLAUDE.md" 2>&1
fi
}
# 匹配"瞬时性 API 故障"的正则——公司模型在高并发下会返回"繁忙/满载"。
# 大小写不敏感。一次正常完成(无错误短语)的迭代不会命中,故不会重试成功轮。
API_ERROR_REGEX='overloaded|rate.?limit|too many requests|server_error|api.?error|timeout|throttl|50[0-9]|52[0-9]|繁忙|满载|超时|请稍后|服务器繁忙|限流|排队'
# 跑一轮智能体,遇到瞬时 API 故障则指数退避重试。
# 返回 0 = 工具跑完(exit 0 且无错误短语)
# 返回 1 = 重试用尽仍在失败(调用方跳到下一轮)
# 已重试次数写入全局 RETRIES_TAKEN。
run_agent_with_retry() {
local attempt=0 output ec delay
RETRIES_TAKEN=0
while :; do
output=$(invoke_tool) # 真正拉起 claude 进程
ec=$? # 捕获退出码
# 成功判定:退出码 0 且输出里没有繁忙/过载短语
if [ "$ec" -eq 0 ] && ! echo "$output" | grep -qiE "$API_ERROR_REGEX"; then
printf '%s' "$output"
return 0
fi
# 瞬时失败:重试预算用尽就放弃本轮
if [ "$attempt" -ge "$MAX_RETRIES" ]; then
echo " API still failing after $MAX_RETRIES retries (exit=$ec). Skipping to next iteration." >&2
printf '%s' "$output"
return 1
fi
# 指数退避,上限 RETRY_MAX_DELAY
delay=$(( RETRY_BASE_DELAY * (2 ** attempt) )) # 10, 20, 40, 80, 160... 秒
[ "$delay" -gt "$RETRY_MAX_DELAY" ] && delay="$RETRY_MAX_DELAY"
attempt=$((attempt + 1))
RETRIES_TAKEN=$attempt
echo " API busy/overloaded (exit=$ec). Retry $attempt/$MAX_RETRIES in ${delay}s..." >&2
sleep "$delay"
done
}
两个设计要点值得细品:
- 成功判定是"双条件"——
exit 0且输出不含繁忙短语。因为公司 API 有时返回 exit 0 但正文里是"服务器繁忙",单看退出码会误判成功。 - 退避公式
base * 2^attempt——起始 10s,翻倍到 300s 封顶。配合--max-retries 8 --retry-max-delay 600,能让 Ralph 在 API 持续满载时耐心等 10 分钟一轮,适合挂后台过夜跑。
四、SVN/Git 提交:驱动器接管版本控制,智能体绝不碰 VCS
4.1 为什么要解耦:冷启动智能体跑 VCS 命令是个坑
早期版本让智能体自己 commit,结果冷启动时它经常:在错误的分支上 commit、git add 漏掉新文件、甚至 svn revert 把别人的改动冲掉。改造后的协议把提交完全收归驱动器:
- 智能体只负责"改文件 + 改
passes+ 追加progress.txt"; - 驱动器在智能体返回后,只有当通过数真的涨了才提交;
- 换 SVN / 关提交(
--vcs none)智能体零感知,不用改指令书。
4.2 do_commit 全分支解析(git / svn / none)
这是你 IDE 里选中那一行 do_commit 所在的函数,也是嵌入式公司环境最关心的代码:
# ralph.sh —— 用配置的 VCS 提交当前工作树改动。
# 参数: story_id(用于构造提交信息)。VCS 工具自动探测改了哪些文件;
# 智能体从不自己跑 commit 命令。
do_commit() {
local id="$1" title msg
title=$(story_title_for_id "$id") # 从 prd.json 反查故事标题
msg="feat: ${id} - ${title}" # 统一提交信息规约
case "$VCS" in
none)
# 只改不提交:改动留在工作树,自己 review 后手动提交
echo " [vcs=none] skipping commit (changes left in working tree)"
return 0
;;
git)
# git add -A 自动包含新增/修改/删除,提交信息用规约格式
( git add -A && git commit -m "$msg" ) \
&& echo " [git] committed: $msg" \
|| echo " WARNING: git commit failed (no changes, or git error)."
;;
svn)
# --force: 对已版本化文件不报错,对新文件才执行 add;
# --auto-props --parents: 按 svn 配置自动属性、自动建父目录
svn add --force . --auto-props --parents -q 2>/dev/null || true
svn commit -m "$msg" \
&& echo " [svn] committed: $msg" \
|| echo " WARNING: svn commit failed (no changes, or svn error)."
;;
esac
}
触发时机(在主循环里)——只有"本轮通过数比上一轮多"才提交,避免空提交:
# 跑完一轮后,对比前后通过数;涨了才提交
if [ "$PASSING" -gt "$BEFORE_PASSING" ]; then
do_commit "$(latest_passing_story_id)"
fi
4.3 嵌入式 SVN 实战的三个致命坑
4.3.1 坑一:千万别为了跑 Ralph 去 svn checkout 新副本
公司环境下,你现有的工作副本里编译环境是调好的——Keil/IAR 的工具链路径、环境变量、工程相对路径都依赖当前副本位置。重新 checkout 一份新副本,这些全得重配。
正确做法:直接在已有的工作副本根目录里,把
.ralph/文件夹拷进去,然后原地运行脚本。Ralph 改的就是这份副本,提交的也是这份。
4.3.2 坑二:svn add --force 会把编译产物也加进去
--force 对新增文件无差别 add,build/、*.o、*.map 会被一锅端。必须在 svn:ignore 里排除:
# 一次性设置忽略(在工作副本根目录执行)
svn propset svn:ignore -F .svnignore .
# .svnignore 内容:
# build/
# *.o
# *.d
# *.map
# *.elf
# *.hex
4.3.3 坑三:SVN commit 失败不会中断循环
注意 do_commit 里 svn 分支用的是 || echo "WARNING..."——提交失败只告警、不 return 1。这是故意的:公司 SVN 偶发网络抖动、权限问题不该让整个无人值守循环挂掉。代价是你得事后看 run.log 补提交。

4.4 提交信息规约:feat: US-xxx - 标题
提交信息由 story_title_for_id 从 prd.json 反查标题拼出,格式固定 feat: ${id} - ${title}。好处是 svn log 里能一眼对应到故事:
svn log -l 5
------------------------------------------------------------------------
r1234 | dev | 2026-07-12 14:03 | feat: US-003 - SHT30 CRC-8 校验
------------------------------------------------------------------------
r1233 | dev | 2026-07-12 13:41 | feat: US-002 - SHT30 器件驱动
------------------------------------------------------------------------
story_title_for_id 的实现是个不依赖 jq 的 awk 窗口匹配——因为 prd.json 由 /ralph 技能生成时是"每字段一行",所以"找到 id 行,再找它后面第一个 title 行"就行:
story_title_for_id() {
local id="$1"
[ -f "$PRD_FILE" ] || return 0
awk -v id="$id" '
$0 ~ "\"id\"[[:space:]]*:[[:space:]]*\"" id "\"" { found=1 }
found && /"title"[[:space:]]*:/ {
sub(/.*"title"[[:space:]]*:[[:space:]]*"/, "")
sub(/".*/, "")
print; exit
}
' "$PRD_FILE"
}
五、测试验证:从 passes:true 到真实验收
5.1 双层验收:智能体自检 + 驱动器交叉校验
Ralph 的测试验证是两层的,理解这点能避免"为啥 Ralph 说做完了其实没做":
| 层 | 谁 | 做什么 | 信任度 |
|---|---|---|---|
| 第一层 | 智能体 | 自己跑 make build/make test,过了才把 passes 改 true |
中(智能体可能谎报) |
| 第二层 | 驱动器 | 直接读 prd.json 数 passes:true 个数,不信智能体口头说"做完了" |
高(客观文件状态) |
第二层的关键在下面这段——驱动器绕过智能体的自述,直接 grep prd.json:
5.2 count_passes:不依赖 jq 的客观计数
公司服务器上不一定有 jq,所以 Ralph 用纯 grep 计数(前提是 /ralph 生成的 prd.json 是每字段一行):
# ralph.sh —— 统计故事总数和 passes:true 数。缺 PRD 返回 "0 0"。
# prd.json 由 /ralph 技能生成,"passes": true|false 每字段一行。
count_passes() {
[ -f "$PRD_FILE" ] || { echo "0 0"; return 0; }
local total passing
# grep -c 在零匹配时 exit 1;set -e 下用 || true 吞掉(grep 仍输出 "0")
total=$(grep -c '"passes"[[:space:]]*:' "$PRD_FILE" || true)
passing=$(grep -c '"passes"[[:space:]]*:[[:space:]]*true' "$PRD_FILE" || true)
[ -n "$total" ] || total=0
[ -n "$passing" ] || passing=0
echo "$total $passing"
}
主循环里靠它判定"本轮有没有进展"“是不是全做完”:
read -r TOTAL PASSING <<< "$(count_passes)"
# 跑完一轮,通过数涨了 -> 提交
if [ "$PASSING" -gt "$BEFORE_PASSING" ]; then
do_commit "$(latest_passing_story_id)"
fi
# 全部通过 -> 停机
if [ "$TOTAL" -gt 0 ] && [ "$PASSING" -eq "$TOTAL" ]; then
echo "Ralph completed all tasks! ($PASSING/$TOTAL stories passing)"
exit 0
fi
5.3 嵌入式测试金字塔:编译 → 单测 → 硬件在环
把验收标准拆成三层,对应不同可信度:
┌─────────────────┐
│ 硬件在环验证 │ ← 30% 故事(外设/时序),人工或自动化上位机
├─────────────────┤
│ PC 端单元测试 │ ← 40% 故事(CRC/换算/帧打包),make test 自动
├─────────────────┤
│ 交叉编译通过 │ ← 100% 故事,make build 自动
└─────────────────┘
Ralph 能自动覆盖底部两层(编译 + 单测),顶层硬件在环需要在 acceptanceCriteria 里写明并在收尾时人工核对。这也是为什么 US-005 的验收里要写"硬件在环验证:串口能看到稳定 0xAA…0x55 帧"——它是 Ralph 跑完后你唯一的验收动作。
5.4 卡住检测:run.log 与 stuck 告警
如果连续 3 轮通过数没动,驱动器打 stuck 告警(只警告不中止,给你机会干预):
STUCK_THRESHOLD=3
if [ "$PASSING" -eq "$LAST_PASSING" ]; then
STUCK_COUNT=$((STUCK_COUNT + 1))
else
STUCK_COUNT=0
fi
LAST_PASSING=$PASSING
if [ "$STUCK_COUNT" -ge "$STUCK_THRESHOLD" ]; then
echo "WARNING: no progress in the last $STUCK_COUNT iterations (still $PASSING/$TOTAL passing). A story may be stuck."
fi
每轮还会用 log_iteration 往 run.log 写一条审计记录,tab 分隔、人机两读:
log_iteration() {
# 参数: 迭代号 耗时秒 通过数 总数 原因
printf '%s\titer=%s\tduration=%ss\tpassing=%s/%s\treason=%s\n' \
"$(date '+%Y-%m-%dT%H:%M:%S')" "$1" "$2" "$3" "$4" "$5" >> "$RUN_LOG"
}
run.log 长这样,复盘时一眼看出哪轮卡了:
2026-07-12T14:00:12 iter=1 duration=87s passing=1/5 reason=iteration
2026-07-12T14:01:45 iter=2 duration=76s passing=2/5 reason=iteration
2026-07-12T14:03:10 iter=3 duration=12s passing=2/5 reason=api-failed
2026-07-12T14:05:33 iter=4 duration=95s passing=2/5 reason=stuck
六、无人值守两道保险:抗 API 繁忙 + 上下文守护
6.1 抗繁忙:指数退避 + 跳轮
公司大模型 API 在工作日高峰经常"满载"。Ralph 的策略见 3.4 节的 run_agent_with_retry——重试用尽就 continue 跳到下一轮,绝不让一次 API 故障永久卡死无人值守运行。推荐配置:
# API 特别繁忙时:重试 8 次,单次最长等 10 分钟
.ralph/ralph.sh --tool claude --vcs svn --max-retries 8 --retry-max-delay 600 30
6.2 上下文守护:maybe_trim_progress 裁剪
progress.txt 是 append-only,跑久了会撑爆 200k 上下文窗口。驱动器每 --summarize-every(默认 3)轮裁剪一次,纯文本处理、零额外 API 调用:
# ralph.sh —— 裁剪 progress.txt,防上下文溢出。先归档完整副本,再重写为:
# 头部 + 整个 "## Codebase Patterns" 段 + 最后 KEEP_ENTRIES 条故事记录。
maybe_trim_progress() {
[ -f "$PROGRESS_FILE" ] || return 0
local iter="$1" date branch folder
# 1. 先归档完整副本(绝不丢数据)
date=$(date +%Y-%m-%d)
branch=$(get_branch_name)
folder="$ARCHIVE_DIR/${date}-$(echo "${branch:-run}" | sed 's|^ralph/||')"
mkdir -p "$folder"
cp "$PROGRESS_FILE" "$folder/progress-full-iter${iter}.txt"
# 2. awk 以 "\n## " 为记录分隔符:记录1是前导文本,其余每条是一个 "## ..." 段
local tmp
tmp="$(mktemp)"
awk -v keep="$KEEP_ENTRIES" -v iter="$iter" '
BEGIN { RS="\n## "; n=0 }
NR==1 { preamble=$0; next } # 第一条之前的前导文本
{
block="## " $0
if ($0 ~ /Codebase Patterns/) { patterns=block; next } # 整段保留
n++; blk[n]=block
}
END {
printf "%s", preamble
if (preamble !~ /\n$/) printf "\n"
if (patterns != "") { printf "%s", patterns; if (patterns !~ /\n$/) printf "\n" }
start = (n > keep) ? n - keep + 1 : 1 # 只留最后 keep 条
for (i = start; i <= n; i++) printf "%s", blk[i]
printf "> [trimmed at iter %s, older entries archived]\n", iter
}
' "$PROGRESS_FILE" > "$tmp" && mv "$tmp" "$PROGRESS_FILE"
}
两个细节体现工程师品味:
- 先归档再裁剪——
cp完整副本到archive/目录后才动原文,丢不了历史。 Codebase Patterns整段保留——if ($0 ~ /Codebase Patterns/) { patterns=block; next }把它单独存下来、不计入keep名额,确保寄存器级事实永远在上下文里。
激进裁剪配置(上下文吃紧时):
.ralph/ralph.sh --tool claude --vcs svn --summarize-every 2 --keep-entries 3 20
七、避坑指南(The Gotchas)
坑 1:故事太大 → 上下文爆出残次代码。
Ralph 每轮一个上下文窗口。故事太大,LLM 没做完就耗尽 token,留下半截 .c 还把 passes 标成 true。判断标准:用 2-3 句话描述不完的改动,必须拆。 "加个传感器驱动"要拆成 I2C 抽象 / 器件驱动 / CRC / 帧 / 周期任务五份。
坑 2:新拉 SVN 副本 → 编译环境崩。
见 4.3.1。永远在已有工作副本里跑 Ralph,别为它单独 checkout。
坑 3:只信 passes:true → 不跑真实编译。
智能体会谎报。passes:true 只代表"它说做完了",不代表"真能编译过"。收尾一定要自己 make build + 烧板上电看串口。run.log 里 reason=stuck 或 api-failed 多的故事重点查。
坑 4:没配 svn:ignore → svn add --force 把 *.o 提交了。
见 4.3.2。必须先 svn propset svn:ignore 排除 build/、*.o、*.elf、*.map。
坑 5:换功能没改 branchName → 旧进度混进新需求。branchName 变了,驱动器会自动把旧 prd.json + progress.txt 归档到 .ralph/archive/日期-功能名/ 并重置进度。新功能一定要改 branchName,否则旧故事的 passes:true 会让 Ralph 直接判定"全部完成"而秒退。
八、完整实战:一次 SVN 无人值守流程
把前面所有环节串起来,从零到跑完:
# 1. 进入【已有的】工作副本(不要新拉,避免编译环境变量被改)
cd /path/to/your-existing-stm32-working-copy
# 2. 把改造好的 .ralph 拷进来
cp -r /path/to/.ralph .
# 3. 配 svn:ignore 排除编译产物(坑2)
printf 'build/\n*.o\n*.d\n*.map\n*.elf\n*.hex\n' > .svnignore
svn propset svn:ignore -F .svnignore .
# 4. 写需求清单(手写或 /prd + /ralph),passes 全 false
vim .ralph/prd.json
# 5. 固定公司配置(可选,写一次)
echo 'export RALPH_VCS=svn' >> ~/.bashrc && source ~/.bashrc
# 6. 后台开跑:30 轮,抗繁忙 8 次重试,日志落盘
nohup .ralph/ralph.sh --tool claude --max-retries 8 --retry-max-delay 600 30 \
> ralph.out 2>&1 &
# 7. 实时观察
tail -f ralph.out
cat .ralph/run.log
# 8. 收尾验收(别只信 passes:true!)
svn log -l 20 # 看一串 feat: US-xxx
make build && make test # 自己跑一遍编译+单测
# 烧板上电,串口看 0xAA ... 0x55 帧稳定上报
跑起来你会看到这样的输出:
Starting Ralph - Tool: claude - Max iterations: 30 - VCS: svn
Retry: up to 8 (base 10s, cap 600s) | Trim: every 3 iters, keep 5 entries
===============================================================
Ralph Iteration 3 of 30 (claude)
===============================================================
API busy/overloaded (exit=1). Retry 1/8 in 10s... ← 繁忙自动退避
... (智能体实现 US-003 CRC 校验的过程) ...
Progress: 3/5 stories passing
[svn] committed: feat: US-003 - SHT30 CRC-8 校验 ← 自动 SVN 提交
Iteration 3 complete. Continuing...
Ralph completed all tasks! (5/5 stories passing)
Completed at iteration 7 of 30
九、总结与核心心法
Ralph 协议的心法就一句:别让大模型记东西,把记忆全部落到文件里,再用一个驱动器反复冷启动它。 对嵌入式而言,这意味着——
- 需求分析就是把硬件需求拆成依赖有序、单轮可做完、验收可验证的
prd.json(驱动→器件→协议→业务→集成); - 任务执行是每轮冷启动 spawn claude,读
Codebase Patterns接续项目知识,做完一个故事跑make build; - SVN/Git 提交由驱动器的
do_commit接管,智能体绝不碰 VCS,--vcs svn用svn add --force+svn commit -m "feat: US-xxx - 标题";(可以根据各自的提交规范修改) - 测试验证是双层结构——智能体自检 + 驱动器
count_passes客观交叉校验,收尾务必自己上板验证。
记住这四点,嵌入式那堆"重复无记忆"的体力活,就能挂后台过夜自动跑了。
写了这么多,如果对你有启发,点个赞👍让它被更多嵌入式同行看到。
你在嵌入式里最想自动化的"体力活"是哪一段?I2C 调试?寄存器配置?还是协议解析?欢迎在评论区告诉我——下一篇我可以针对投票最多的场景,手把手拆一个完整prd.json出来。
(CSDN 投票功能已开启,请在评论区为你最想看的实战场景投票👇)
十、相关参考及推荐
更多推荐
所有评论(0)