Ralph 架构解析:一个 bash 循环如何让失忆的 AI 无人值守交付功能
Ralph 是一个做法看上去很笨的项目,用一百来行的 bash 循环,反复启动全新的 AI 编码实例,每个实例只拿到干净的上下文,只做一个小任务,做完就退出,记忆清零,跨迭代的记忆完全外置在文件系统和 git 上。无状态的 agent,加有状态的文件,这就是它的全部架构。
Ralph Wiggum 模式本身是工程师 Geoffrey Huntley 提出的,名字来自《辛普森一家》里那个傻乎乎的小孩。把这个模式做成现成项目的是 Ryan Carson,仓库在 GitHub 的 snarktank/ralph,后文提到的 ralph.sh、skill 文件、README 都出自这个仓库。循环里启动的实例可以是 Claude Code,也可以是 Amp,两个都是跑在命令行里的 AI 编码 agent。
这篇一次讲全。循环怎么转、记忆怎么接力、质量怎么把住,上游怎么把一个模糊需求加工成循环消化得了的任务,两个 skill 文件和那份上岗指令书里到底写了什么,然后用仓库自带的四故事教学案例走一遍多迭代的接力,再用几组真实运行验证整个闭环,成功怎么收工、失败怎么停下,最后聊这套设计为什么成立、什么时候不该用,以及我自己的看法。
它要解决的问题
AI 编码工具有个绕不开的限制。模型的工作记忆,也就是上下文窗口,是有限的,而且聊得越久表现越差,上下文堆得越满,模型越容易忘记前面做过什么,还会被自己之前的错误带偏。你让它干一个大活,比如做一个完整功能,干到一半记忆满了,最后产出的就是烂尾代码。
矛盾就在这,活很大,AI 一次只能干好一小段。Ralph 反着来,干脆让每个实例都活得足够短。
循环本身,一百来行没有智能的 bash
整个编排器就是一个叫 ralph.sh 的脚本,数下来 113 行 bash,以 2026 年 2 月的 HEAD 为准,commit 6c53cb0。核心部分长下面这样,先看个形状就行,里面的门道下面逐个拆。
for i in $(seq 1 $MAX_ITERATIONS); do
OUTPUT=$(claude --dangerously-skip-permissions --print < CLAUDE.md ...)
if echo "$OUTPUT" | grep -q "<promise>COMPLETE</promise>"; then
exit 0
fi
doneclaude --print 是一次性的非交互调用,把 CLAUDE.md 这份 agent 指令书从标准输入喂进去当 prompt,跑完进程就死了。每次迭代因此都是全新进程,下一次迭代的实例对上一次毫无记忆。那个看着吓人的 --dangerously-skip-permissions,是让它跳过所有权限确认,毕竟没有人守在旁边点允许。
停机靠一个字符串协议。agent 被要求在所有任务完成时输出 <promise>COMPLETE</promise>,外层 shell 用 grep 检测到这个信号就退出,LLM 和 shell 之间,用的是最朴素的文本契约。
容错的策略只有重试,没有恢复。上面的片段里还省了一处细节,实际脚本的命令尾部挂着 || true,bash 里这个写法的意思是,前面的命令就算失败,整条命令也算成功,所以单次迭代崩溃不会杀死循环。坏掉的迭代不提交代码,下一个干净实例重新捡起同一个任务,兜底是最大迭代次数,默认 10 次。
记忆架构,状态全部外置
每个实例都失忆,记忆就必须存在实例之外,这一层是整个设计的灵魂。
这套机制像什么呢。像你要装修一套房子,雇的工人都有个怪病,每人只能干一小时,干完就彻底失忆走人,下一个工人进门时对之前发生的事一无所知。要让这群工人把房子装完,办法只有一个,别靠工人的脑子记事,靠留在房子里的东西记事。
Ralph 在项目里留了三样正式的记忆载体,外加一层不那么正式的。
prd.json 是任务清单,prd 是产品需求文档 PRD 的小写。功能被拆成一个个用户故事,用户故事是敏捷开发里的说法,指一小块能独立交付的功能。每个故事带一个 passes 字段,完成了是 true,没完成是 false。下个实例靠它知道活干到哪了,就像装修清单上的勾选框,贴厨房瓷砖打了勾,装客厅灯还空着。
progress.txt 是工作日志,append-only,只追加不修改。每个实例收工前要把这轮做了什么、学到什么写进去,文件顶部还有一块汇总出来的 Codebase Patterns 区,沉淀这个代码库的套路。像工人走之前在桌上留一句,「这房子的电线走的是吊顶,别凿墙找线」,下一个工人进门先看留言,少走弯路。
瓷砖贴没贴,看墙就知道,不用问任何人,这就是第三样,git history。已经提交的代码本身,就是最可靠的状态。
CLAUDE.md 里的指令把这三层串成闭环。每个新实例上岗的固定动作是,先读 prd.json 和 progress.txt,挑出优先级最高而且 passes 还是 false 的那一个故事,实现它,跑质量检查,提交,把 passes 改成 true,再把这轮学到的东西追加进 progress.txt。读状态,干活,写回状态,一个实例就是状态机上的一次转移。
把数据流单独画出来更清楚,实例和这些载体之间只有读和写两种关系,左边的实例随进程消失,右边的东西全部留在仓库里。
flowchart LR
subgraph EPHEMERAL["每轮换新的实例, 记忆随进程消失"]
I["Claude 实例 N"]
end
subgraph DURABLE["留在仓库里的记忆, 跨迭代存活"]
P["prd.json<br/>任务状态机"]
G["progress.txt<br/>经验日志"]
H["git history<br/>已提交的代码"]
A["AGENTS.md<br/>沉淀的套路"]
end
I -->|"上岗先读"| P
I -->|"上岗再读"| G
I -->|"过验收才 commit"| H
I -->|"收工前打勾"| P
I -->|"收工前留言"| G
I -.->|"发现可复用套路时回写"| A
P -->|"下一个实例接着读"| I
G -->|"下一个实例接着读"| I不那么正式的第四层,是让 agent 把可复用的发现回写进指令文件本身。两份模板在这里是对称的,Amp 的模板要求提交前把值得留给后人的套路补进就近的 AGENTS.md,Claude Code 的模板要求补进就近的 CLAUDE.md,各自都是对应工具会自动加载的那份约定文件。等于让 agent 给未来的自己留路标,知识随迭代次数累积复利。Ryan Carson 在推文里给过一句很直接的描述,「Learnings compound. By story 10, Ralph knew our patterns.」,学习会复利,跑到第 10 个故事时,Ralph 已经摸清了他们代码库的套路。
任务粒度,一个故事必须塞进一个窗口
skills/ralph/SKILL.md,项目上游的一个 skill 文件,后面有专门一节讲它,这里先借用它的第一条规则,它把这条称为 The Number One Rule,每个故事必须能在一次迭代内,从开工做到验收完,一次迭代就是一个上下文窗口。道理前面已经铺过了,任务太大,LLM 还没写完就把上下文耗尽,留下的是半拉子工程,而它又失忆,烂尾就是永久的烂尾。
什么算够小?给出的经验法则是,一个改动如果不能用两三句话描述清楚,就是太大了。加一个数据库列和迁移,够小。给列表加一个筛选下拉框,够小。做整个 dashboard,太大。加认证,太大,必须拆开。
故事之间还要按依赖排优先级,固定的顺序是 schema 和迁移最先,然后后端逻辑,然后 UI 组件,最后聚合视图,保证排在前面的故事永远不依赖后面的。失忆工人是按清单顺序干活的,清单顺序一错,轮到的工人挑起的就是一个依赖还没造出来的故事,当场卡死。
质量闸门,做没做完机器说了算
循环跑起来之后没有人盯着,做完了没有,就不能由 AI 自己感觉,必须由机器判定。项目 README 里有句原话,broken code compounds across iterations,坏代码会跨迭代复利式恶化。
每个故事的验收标准都被强制要求可验证。Typecheck passes 合格,这里的 typecheck 指静态类型检查,机器跑一遍就有结果。Works correctly 不合格,因为机器判不了什么叫 correctly。每个故事都强制带 typecheck 检查,有逻辑的加测试,UI 故事强制带浏览器验证,agent 要通过项目里的 dev-browser skill 真的打开浏览器,亲眼确认那个改动显示出来了。
检查不过,就不许 commit,也不许把 passes 改成 true。某次迭代干砸了,它就什么都留不下,下一个干净实例重新来过。能传给下一个实例的,只有过了验收的 commit、打上的勾和日志里的留言,坏结果传不下去。
到这里,循环、记忆、闸门三块都讲完了,一次迭代的完整生命周期串起来是下面这张图。
flowchart TD
A["ralph.sh 启动全新 Claude 实例"] --> B["读 prd.json 和 progress.txt"]
B --> C["挑优先级最高且 passes=false 的一个故事"]
C --> D["实现这一个故事"]
D --> E{"质量检查通过?<br/>typecheck / test / 浏览器验证"}
E -- "通过" --> F["git commit + passes 改 true<br/>+ 追加 progress.txt"]
E -- "不过" --> G["不提交不打勾<br/>这轮什么都不留下"]
F --> H{"所有故事都 passes=true?"}
H -- "是" --> I["输出 COMPLETE 暗号<br/>循环 exit 0"]
H -- "否" --> J["实例退出, 记忆清零"]
G --> J
J --> K{"还有剩余迭代次数?"}
K -- "有" --> A
K -- "没有" --> L["循环退出, 报告未完成"]上游,把活准备对
循环只负责可靠地重复。那份任务清单,来自项目的两个 skill,它们把一个模糊想法加工成循环吃得下的格式。skill 是给 AI 看的 Markdown 操作指南,没有一行可执行代码,内部构造下一节拆。流水线是,想法先变成 PRD 文档,再变成 prd.json。
skills/prd 面向人,解决需求的模糊性。你说一句想加什么功能,它不许直接开写,必须先抛出三到五个带字母选项的澄清问题,优先级分几档、默认值是什么、范围是最小可用还是完整版,你回一句 1B 2A 3A 就完成一轮需求澄清。产出是一份 PRD 文档,带编号的用户故事和验收标准,还有一节 Non-Goals,写明确不做什么,给后面所有失忆工人划红线,毕竟没人盯着的 AI 最容易范围蔓延。
这份 PRD 是整条流水线上唯一的人工检查点。你花三十秒过目,确认 AI 理解的需求就是你要的,再放它去自治跑。错误的需求理解要是直接流进循环,就是白跑十个迭代。
skills/ralph 面向机器,解决执行的可行性。它把 PRD 翻译成 prd.json,翻译过程强制执行的,正是前面任务粒度和质量闸门两节讲过的那几条硬约束。表面是格式转换,实际是把质量把关前置到进循环之前。
两步的分工对照放在一张表里看。
| skills/prd | skills/ralph | |
|---|---|---|
| 面向谁 | 人,和人对话澄清需求 | 机器,产出循环吃的 JSON |
| 解决什么 | 需求的模糊性 | 执行的可行性 |
| 核心动作 | 提问、定边界 | 拆分、排序、加验收标准 |
| 产出 | PRD 文档,人能读、能改、能评审 | prd.json,结构化任务状态机 |
flowchart LR
A["模糊的想法"] -->|"prd skill<br/>3-5 个澄清问题"| B["PRD 文档<br/>人工审查检查点"]
B -->|"ralph skill<br/>拆小 / 排序 / 注入验收标准"| C["prd.json<br/>任务状态机"]
C -->|"./ralph.sh"| D["自治循环"]
D --> E["一串过了质量闸门的 commit"]三层分工可以这样记,skill 负责把活准备对,ralph.sh 负责可靠地重复,CLAUDE.md 负责每次怎么干。prd skill 防做错东西,ralph skill 防一口吃太大,循环和验收防做坏了还往下传,对应的正是自治 AI 编程的三类典型失败。
skill 文件里面写了什么
上一节把两个 skill 当黑盒讲了分工,值得再往里看一层,因为整个项目的智能都在这两份文件里。
一个 skill 的全部构造,就是一个带 frontmatter 的 Markdown 文件。frontmatter 里的 description 写着触发短语,比如 convert this prd,你在对话里说出类似的话,AI 编码工具靠语义匹配决定加载它,把整个文件塞进上下文,然后照着里面的规则干活。description 同时就是路由规则,不需要注册表,也不需要路由代码。项目还把 skill 当可安装的包分发,可以拷进项目、装到全局,或者通过 Claude Code 的插件市场安装。
skills/prd 最值得看的是交互设计。它强制先问三到五个澄清问题,而且必须带字母选项,长这样。
1. What is the primary goal of this feature?
A. Improve user onboarding experience
B. Increase user retention
C. Reduce support burden
D. Other: [please specify]这是对症下药。AI 拿到模糊需求的默认行为是猜着写,这条规则强制它先消除歧义。文件里还有一条显式禁令,Do NOT start implementing. Just create the PRD. 防止它越权,这一步的交付物是文档,不是代码。PRD 本身的写作标准是写给初级开发者和 AI 看,显式、无歧义、需求带编号,因为最终读者是没有任何项目背景的全新实例,任何你懂的式表达都会变成事故。
skills/ralph 这份文件里,除了那三条硬约束的完整定义和正反例,末尾还有一份自检清单,要求转换完成后逐项确认,每个故事一次迭代能完成、按依赖排好序、每个故事都有 Typecheck passes、UI 故事都有浏览器验证、验收标准没有含糊表述。这是个经典的 prompt 工程手法,让 LLM 在交付前对照清单自查,把希望它做对,变成要求它逐项确认。
所以 Ralph 的智能不在 bash 里,在这些 Markdown 里。这两份文件做的事,是把资深工程师的判断力序列化成文本,怎么拆任务、什么算拆得太大、验收标准怎么写才不含糊,这些原本依赖人的经验,被固化成每个实例都会照做的规则。
把上岗须知也拆开看
上游两份 skill 拆完了,还剩运行时那份,ralph.sh 每轮喂给实例的 CLAUDE.md,104 行,同样没有可执行代码。全文反复说智能在 prompt 里,不把这份 prompt 翻开看,论断就是悬空的。
它的骨架是一张十步上岗清单,顺序本身就是设计。第 2 步读 progress.txt 时特意括注,先看 Codebase Patterns 区,把最值钱的沉淀放在每个实例必经之路的最前面。第 4 步限定只挑一个故事,第 8 步规定 commit message 的固定格式,feat 加故事编号加标题,git 历史因此天然可审计。
真正的巧思在几处防御性措辞里,每一处都在防一种具体的失败。进度报告那节的第一个词是大写的 APPEND,跟着一句 never replace, always append,防的是实例图省事重写整个日志,把前人的记忆抹掉。Codebase Patterns 的收录标准写明只收 general and reusable 的模式,story-specific 的细节不许进,防的是沉淀区膨胀成流水账,后面的实例读不完。回写 CLAUDE.md 那节甚至配了一张 Do NOT add 清单,故事细节、临时调试笔记、progress.txt 里已有的信息都不许写,防的是几层记忆互相污染。停机条件把边界写死,所有故事 passes 为 true 才许输出暗号,否则正常结束响应,把下一轮留给循环。
有意思的是它对失败几乎没写什么,质量要求一节只有四行,核心就一句 Do NOT commit broken code。后面失败实验里实例表现出的那整套阻塞协议,验证矛盾、拒绝作弊、请求人工介入,模板里一个字都没有,是实例从这一句原则里自己推出来的。
这就是判断力序列化的另一半。skill 序列化的是怎么把活拆对,这份序列化的是怎么把活干对。
四个故事怎么接力,仓库自带的教学案例
单故事看不出记忆接力,仓库自带的 prd.json.example 是个四故事的完整案例,给一个任务管理应用加优先级功能,顺着它把多迭代的运转推演一遍。
你说一句 create a prd for adding task priority,prd skill 被触发,先问你三个问题,优先级分几档、新任务默认哪档、范围是最小可用还是完整版。你回 1B 2A 3A,它生成 PRD,拆好四个用户故事,US-001 数据库加 priority 列,US-002 任务卡片显示优先级角标,US-003 编辑页加优先级选择器,US-004 按优先级筛选,外加一节 Non-Goals,不做优先级通知,不做按截止日期自动定级。
你过目确认,再让 ralph skill 把它翻译成 prd.json。上一节说的三件事都体现在成品里,四个故事都小到一口能吃下,priority 按数据库、展示、编辑、筛选的依赖顺序排好,每个故事都被塞进了 Typecheck passes,US-002 这种 UI 故事还多一条浏览器验证。
{
"id": "US-001",
"title": "Add priority field to database",
"acceptanceCriteria": [
"Add priority column to tasks table: 'high'|'medium'|'low' (default 'medium')",
"Generate and run migration successfully",
"Typecheck passes"
],
"priority": 1,
"passes": false
}开跑之前,把出场文件的读写关系列清楚,谁创建、谁读、谁改,一目了然。
| 文件 | 谁创建 | 谁读 | 谁修改 |
|---|---|---|---|
| PRD 文档 | prd skill | 你,评审用 | 你 |
| prd.json | ralph skill | 每个实例,开工第一件事 | 实例,完成故事后打勾 |
| CLAUDE.md | 你从仓库拷来 | ralph.sh 喂给每个实例 | 基本不动 |
| progress.txt | ralph.sh 初始化 | 每个实例,开工第二件事 | 实例,收工前追加 |
| ralph.sh | 你从仓库拷来 | 无 | 不动 |
然后跑循环。迭代 1 的实例读 prd.json,四个全是 false,挑 priority 为 1 的 US-001,改 schema、生成迁移、跑检查、提交、打勾,在 progress.txt 留言,这项目用 Drizzle ORM,typecheck 命令是 npm run typecheck,不是 tsc。它退出,记忆清零。
迭代 2 的实例对刚才的一切毫无记忆,但它一上岗就继承了三份遗产。prd.json 告诉它 US-001 已经过了,该干 US-002。progress.txt 告诉它这项目用什么 ORM、检查命令是什么,上个工人十分钟踩出来的坑,它一秒钟就知道了。代码本身告诉它 priority 字段已经在 schema 里,直接用。做完角标,打开浏览器亲眼确认,提交、打勾,再留一句,角标颜色定义在 theme.ts,以后改配色去那里。
教学案例还安排了一个失败剧本。假设迭代 3 写优先级下拉框时 typecheck 报错,改了两次还没过,上下文快用完了。这轮它什么都提交不了,最多在 progress.txt 留一句,US-003 尝试失败,PrioritySelect 的泛型推断有问题。然后迭代 4 的新实例读 prd.json,发现 US-003 还是 false,重新做这个故事,而且能看到前人的失败留言,绕开同一个坑。git 里没有烂代码,状态机里没有假勾。
等到某次迭代做完 US-004,按指令检查一遍,四个全是 true,输出暗号,循环收工。git log 里是四个干净的 commit,每个都过了质量闸门,review 之后合并即可。
五次迭代的接力画成时序,重点看记忆怎么跨实例传递,以及失败为什么传不下去。
sequenceDiagram
participant R as ralph.sh
participant A as 迭代1的实例
participant B as 迭代2的实例
participant F as prd.json + progress.txt + git
R->>A: 启动, 喂 CLAUDE.md
A->>F: 读状态, 全是 false, 挑 US-001
A->>F: 过验收, commit + 打勾 + 留言
A-->>R: 没有暗号, 实例死亡, 记忆清零
R->>B: 启动全新实例
B->>F: 读状态, US-001 已过, 挑 US-002, 继承前人留言
B->>F: 过验收, commit + 打勾 + 留言
B-->>R: 仍有 false, 继续下一轮
Note over R,F: 迭代3 验收不过, 不提交不打勾, 只留一句失败教训
Note over R,F: 迭代4 全新实例重做 US-003, 借前人教训绕坑, 成功
Note over R,F: 迭代5 做完 US-004, 全部 true, 输出暗号, 循环 exit 0还有个收尾细节。一个月后你做新功能,重新生成 prd.json,branchName 变了,ralph.sh 对比它记录的上一个分支名发现换活了,自动把上一轮的 prd.json 和 progress.txt 搬进 archive 目录,按日期加功能名命名归档,然后重置进度文件。历史经验不丢,新功能从干净状态出发。
拿一个最小项目真跑一遍
上面说的都来自读代码和文档,发布前把仓库拉下来核了一遍,顺手跑了一次真循环,基于 2026 年 2 月的 HEAD,commit 6c53cb0。
静态部分全对得上。|| true 容错、默认 10 次迭代、grep 检测暗号、归档机制,都在。唯一的出入是行数,ralph.sh 数下来 113 行,不是整整 100 行,量级没变。
动态验证搭了个最小项目,一个空 git 仓库,prd.json 里只放一个故事,创建 hello.js 输出固定问候语,验收标准三条,文件存在、输出精确匹配、退出码为 0。然后跑循环,给它最多 3 次迭代。
./scripts/ralph/ralph.sh --tool claude 3实际一次迭代就收工了。这个实例把前面讲过的上岗动作走了个全程,建分支、实现、跑 node 验收、提交、打勾、写日志,再检查一遍发现所有故事都是 true,当场输出暗号,外层循环 grep 到,退出。
US-001 已完成并提交。
...
<promise>COMPLETE</promise>
Ralph completed all tasks!
Completed at iteration 1 of 3最有意思的是 progress.txt 里留下的东西。没有任何人要求的情况下,这个实例在 Codebase Patterns 区自己写了两条,一条说这个仓库没有 lint 和 test 工具链,质量检查就是直接跑脚本比对输出,另一条说仓库没有 package.json,未来的故事如果要装依赖,先跑 npm init -y。
## Codebase Patterns
- Plain Node.js repo with no package.json, lint, or test tooling — quality
check is simply running the script with `node` and verifying output + exit code.另一条写在 US-001 的完工记录里。
- **Learnings for future iterations:**
- No package.json exists — if future stories need npm deps or scripts,
run `npm init -y` first.这正是前面说的给下一个失忆工人的留言,虽然这个 demo 里已经没有下一个工人了。
循环后来又复跑了两次。一次严格重复,干净状态,同样一次迭代闭环,产物一致。另一次是手滑造成的意外,拷 prd.json 时带上了已经打好的勾,仓库却是空的,结果那个实例没有照单全信,按验收标准重新验证,发现 hello.js 根本不存在,补齐实现后才喊收工,还在日志里给后人留了一条,不要盲信 passes 标志。这是单次运行观察到的行为,模板没有把它写成硬性要求,不过方向和整套设计对得上。
单故事看不出接力,又搭了个三故事的 demo,一个带依赖链的迷你任务库。US-001 写工厂函数,US-002 写过滤器,依赖前者,US-003 写 CLI,依赖前两者,验收标准全部是可以直接跑的 node 命令。结果是三次迭代、三个规范格式的 commit,严格按依赖顺序推进,第三轮做完最后一个故事,当轮输出暗号收工,CLI 的输出和验收标准逐字节一致。
接力的证据在 progress.txt 里,相当清楚。第一个实例收工时预告了下一棒,US-002 是下一个,它需要 require ./lib/tasks。第二个实例照着做完过滤器,又给第三个留了精确到输出内容的指令,还记下了一个自己做的设计决定,byPriority 不校验参数,因为验收标准没要求,未知优先级就返回空数组。第三个实例的留言印证它沿用了这个行为。
(US-001 的实例留下的)
- US-002 (lib/filter.js byPriority) is next; it depends on requiring './lib/tasks'
(US-002 的实例留下的)
- byPriority does NOT validate the priority argument (not required by AC) —
an unknown priority just returns []
- US-003 (cli.js) is next: must use makeTask from ./lib/tasks AND byPriority
from ./lib/filter; three fixed sample tasks; `node cli.js high` must print
exactly 'write report' then 'fix login bug'三个彼此失忆的实例,靠一个文本文件,把接口约定、验收方式、设计决定一棒一棒传了下去。耗时也有个可对照的数,progress.txt 的时间戳显示从启动到三个故事全过约六分钟,每轮两分钟上下,和 Ryan Carson 自述的每次迭代 2 到 5 分钟对得上。
成功路径试了三组,失败路径也故意造了一次。给一个故事写上自相矛盾的验收标准,hello.js 必须是恰好 0 字节的空文件,同时运行它必须精确输出 hello,然后放两轮迭代进去,看它怎么死。
第一轮实例的表现值得整段记下来。它先做实验证明矛盾是真的,空文件跑出来无输出、退出码 0,三条标准里两条互斥。然后它在结论里明确列出三种能骗过验收的手段,伪造 node shim、NODE_OPTIONS 预加载、符号链接,并逐一拒绝,理由是那属于欺骗验证而非满足需求。最后不实现、不提交、passes 保持 false,把阻塞原因写进 prd.json 的 notes 字段,在 Codebase Patterns 区置顶一条 BLOCKER,请求人工介入。
## Codebase Patterns
- BLOCKER: US-001 已被证明不可实现(验收标准自相矛盾),后续迭代不要重试、
不要用 hack 绕过验证,详见下方条目。等待人工修改 PRD。第二轮实例读到这条 BLOCKER,只核对了一遍 prd.json 有没有被人工改过,没改,直接结束,还给后人追加了一条,以后只需核对,不必重复实验。迭代次数用尽,循环打印 without completing all tasks,以退出码 1 收场。git 里零提交,状态机里零假勾,连每轮的成本都在递减,第一轮做实验,后面每轮只剩一次核对。
这次实验里最值得记的不是循环的兜底,是实例把整套失败协议自己推导了出来,验证矛盾、记录在案、保持 false、让循环自然耗尽去惊动人类。前面拆上岗须知时说过,模板对失败几乎没写什么,这套协议全是实例现场补齐的,那个观察在这里得到了实证。当然,这仍是单次观察,换个模型、换个更隐蔽的矛盾,守不守得住没有保证。
这套机制真的就这么转起来了,顺着设计转,也顺着设计停。
为什么这个设计成立
另一种做法是开一个长会话,让 agent 在一个会话里一直干下去。对比之下,Ralph 的取舍就清楚了。
长会话里,模型表现会随上下文堆积而退化,一次跑偏还会污染后续所有推理。Ralph 把这两个问题用同一招解决了。每次迭代都处在上下文最干净、最聪明的状态,退化无从积累。一次迭代跑偏,污染范围只有这一次,不提交就不留痕。
影响更深的一层在于,可靠性被从 LLM 身上挪到了结构上。循环控制、停机判定、状态持久化,全是确定性的 shell、JSON 和 git,LLM 只负责它真正擅长的那一小段,在一个干净窗口里实现一个小故事。整个系统的智能,都在 prompt 和任务拆分里。
失忆不是缺陷,是刻意为之。记忆放在文件里,比放在 AI 的脑子里可靠得多。
什么时候别用 Ralph
Ryan Carson 在同一条推文里明确划了不适用的边界,有四类场景。
- 探索性工作。你自己都不知道要什么,就写不出可验证的验收标准,循环没法自治
- 没有清晰验收标准的大型重构。重构完了没有,机器判不了
- 安全攸关的代码。出错代价太高,不能没人盯着
- 任何本来就需要人工 review 把关的工作
这条边界反过来印证了前面的原理。Ralph 的自治能力,完全建立在验收可以被机器判定这一件事上。哪天做没做对机器说了不算了,整个循环的根基就没了,这时候省不掉人,也就不该用 Ralph。
我怎么看
认同的部分前面写得够多了,只补一条自己的印证。我维护知识库用的 agent 流水线,和 Ralph 是同一套设计哲学,多轮内容核查,每一轮都换全新上下文的核查员,理由和 Ralph 换实例一模一样,同一个上下文会确认自己上一轮的意见,全新的眼睛才看得见新问题。跨轮的记忆同样全部外置,落在 log 文件和规则文件里。两个互不相干的场景收敛到同一个结构,这个原则大概率是对的。
保留的部分有三条。prd.json 的状态机太薄,一个 passes 布尔值表达不了做完了但做歪了的中间态,真实项目里这种状态恰恰最多,现在的兜底全压在验收标准写得够不够硬上。每轮全新实例,意味着每轮都要重新认识一遍代码库,token 成本实打实,学习复利成立的前提是 progress.txt 写得足够好,写得不好,每一轮都在重新交学费。还有,10 次迭代兜底用完之后没有升级路径,循环只会报告未完成,接下来只能人上,对无人值守这个卖点要有清醒的预期。
所以我的结论是,Ralph 最大的价值不在这个具体的仓库,在于它用 113 行 bash 把无状态的 agent 加有状态的文件这个原则讲得不能再清楚了。值得抄回家的是这个原则,脚本本身反而次要。
参考资料
- 项目仓库,https://github.com/snarktank/ralph (本文核对与实测基于 commit 6c53cb0)
- Geoffrey Huntley 的原始 Ralph Wiggum 模式,https://ghuntley.com/ralph/
- 项目主页的交互式讲解 How Ralph Works,https://snarktank.github.io/ralph/
- Ryan Carson 的实操指南推文(Step-by-step guide to get Ralph working and shipping code),https://x.com/ryancarson/status/2008548371712135632