本文梳理了 Ralph 范式的核心机制与标准实施 SOP,并以 Matt Skills Issues 本地文件体系为例展示基于依赖拓扑的 Issue 驱动 AFK 实践,同时探讨如何将 Ralph 的核心思想扩展至多仓库全局处理等场景。
在传统的 AI 辅助编程中,开发者通常采用“单次对话”模式:向 AI 描述需求,获取代码,复制粘贴,手动测试。这种模式在面对复杂功能重构或跨文件修改时,易因上下文超载、AI 幻觉或遗漏细节而导致逻辑错乱。
Ralph 范式的核心在于:将 AI 交互从“单次问答”转变为由外部持久化状态驱动的、可重复执行的 Agent Loop。
每一轮 Agent 在相对独立的上下文中读取当前状态,识别并执行可验证的任务,完成测试与状态更新后退出;外部循环随后重新启动 Agent,继续推进剩余任务,直至满足预设的完成条件。
在一种典型的 Coding Agent 实现中,可以将 Ralph 的工程实践归纳为以下四项:
ONE ATOMIC TASK PER ITERATION):建议强约束 Agent 每次循环只处理一个可验证的原子任务,防止 Agent 在单次循环中过度扩展修改范围。PRD.md / progress.txt):PRD.md 负责描述目标状态和任务清单,作为任务规划层面的单一事实来源 (Single Source of Truth);progress.txt 跨循环记录增量履约细节,确保后续循环平滑衔接。[注] 关于运行沙箱 (Sandbox):
在无人值守模式下,由于 Agent 可能连续自主执行 Shell 命令,建议通过 Docker、Dev Container、虚拟机或其他 Sandbox 机制限制文件系统、网络和凭据访问范围。需要注意的是,容器的实际隔离能力取决于权限、挂载、网络和凭据配置,需结合具体场景评估。
落地 Ralph 范式需要构建从“人机协同 (Human-in-the-Loop)” 到 “无人值守模式 (AFK Loop)” 的标准化流程。
[注] 本章的 SOP 实施脚本参考了 Matt Pocock 的教程 Getting Started With Ralph。
可以使用 Plan 模式(快捷键 Shift+Tab)交互生成计划并保存至 PRD.md,同时创建一个空文件 progress.txt:
PRD.md:定义终态需求与 Checklists。progress.txt:记录 Agent 跨循环已完成的工作细节。每次循环 Agent 都会读取两者,提取首个未勾选的任务并履约。ralph-once.sh在开启全自动循环前,先通过单次触发脚本观察 Agent 行为,校准 Prompt 效力:
#!/bin/bash
# ralph-once.sh - 单步人工监护脚本
claude --permission-mode acceptEdits "@PRD.md @progress.txt \
1. 读取 PRD 和 progress 文件。 \
2. 找到下一个未完成的任务并实现它。 \
3. 提交你的修改。 \
4. 将你完成的操作更新至 progress.txt。 \
单次只能处理一个任务。"
设置可执行权限并运行:
chmod +x ralph-once.sh
./ralph-once.sh
观察终端输出,确认代码改动与 Git Commit 正确后,再次手动运行。
afk-ralph.sh单步验证稳定后,即可使用 Bash 循环实现离席自动运行。脚本通过引入完成标志 (Completion Sigil,如 <promise>COMPLETE</promise>),当 PRD 所有 Task 履约完成后自动退出循环。
#!/bin/bash
# afk-ralph.sh - 离席全自动循环脚本
set -e
if [ -z "$1" ]; then
echo "Usage: $0 <iterations>"
exit 1
fi
for ((i=1; i<=$1; i++)); do
result=$(claude --permission-mode acceptEdits -p "@PRD.md @progress.txt \
1. 查找优先级最高的任务并实现它。 \
2. 运行测试与类型检查。 \
3. 勾选 PRD 中已完成的任务。 \
4. 将进度追加至 progress.txt。 \
5. 提交你的修改。 \
单次仅处理单个任务。 \
如果 PRD 已全部完成,输出 <promise>COMPLETE</promise>。")
echo "$result"
if [[ "$result" == *"<promise>COMPLETE</promise>"* ]]; then
echo "PRD complete after $i iterations."
exit 0
fi
done
运行脚本并指定最大迭代次数(例如 20 次):
./afk-ralph.sh 20
参数说明:
set -e:遇到命令错误时立即退出。-p(Print Mode):以非交互模式运行 Claude Code 并将输出捕获到变量中,用于检索 Completion Sigil。<promise>COMPLETE</promise>:当需求完全履约时由 Agent 在末尾打印的收敛信号。
标准 SOP 使用扁平 Checklist(PRD.md)管理任务,Agent 自主选择下一个未勾选项执行。当任务间存在复杂的前后依赖关系时,这种扁平结构难以表达"任务 B 必须在任务 A 完成后才能开始"的约束——这引出了基于 Issue 文件体系的结构化方案。
Matt Skills 的 /to-tickets 将规格书拆分为独立的 Issue Markdown 文件,每个文件自带 Status、Blocked by、验收 Checklist 等结构化元数据,天然构成了一套可被程序解析的任务状态存储。下面以一个实际项目的 afk.py 脚本为例,解析如何将这套 Issues 文件体系接入 Ralph Loop,实现基于依赖拓扑的有序 AFK 执行。
在 Matt Skills 的标准流程中,/to-spec 将追问共识固化为一份 spec.md 规格书,/to-tickets 再将规格书拆分为按编号排列的独立 Issue 文件,存放于 Feature 目录下的 issues/ 子目录:
.scratch/customer-service-backend/
├── spec.md # 规格书(由 /to-spec 生成)
└── issues/ # Issue 文件目录(由 /to-tickets 生成)
├── 01-project-skeleton-and-chat-roundtrip.md
├── 02-flow-configuration-and-loader.md
├── 03-fixed-task-vertical-slice.md
├── 04-llm-planning-validation-clarification.md
├── 05-multi-task-orchestration.md # Blocked by: 03
├── 06-object-message-handling.md
├── ...
└── 10-end-to-end-acceptance.md
每个 Issue 文件遵循统一的结构化格式:
# 05 — 多任务编排(打断/挂起/取消/恢复)
**What to build:** 用户在任务中途发起新任务时,当前任务被挂起...
**Blocked by:** 03
**Status:** ready-for-agent
- [ ] 退款进行中发起物流查询 → 退款快照进入挂起列表
- [ ] "继续刚才的"按 LIFO 恢复最近挂起任务
- [ ] ...
这套文件体系具备以下特点:
spec.md 即可工作。Blocked by 字段声明任务间的前置依赖关系,外壳脚本可据此进行拓扑排序与依赖解除检查。Status 字段承载任务状态(如 ready-for-agent → resolved),可被程序直接解析和更新。afk.py 是驱动这套 Issues 文件体系的 Ralph AFK 控制脚本。它的核心逻辑可以分为三层:
脚本通过正则表达式从每个 Issue 文件中提取 Status 和 Blocked by 两个关键元数据:
_STATUS_RE = re.compile(r"^\*{0,2}Status:\*{0,2}\s*(.+)$", re.M | re.I)
_BLOCKED_RE = re.compile(r"^\*{0,2}Blocked by:\*{0,2}\s*(.+)$", re.M | re.I)
_DONE = {"resolved", "completed"}
def parse_issues(issues_dir: str) -> dict[str, dict]:
"""返回 {issue_id: {filepath, status, blocked_by}} 映射。"""
result = {}
for fp in sorted(glob.glob(os.path.join(issues_dir, "*.md"))):
m = re.match(r"^(\d+)", os.path.basename(fp))
iid = m.group(1).zfill(2) if m else os.path.basename(fp)
text = Path(fp).read_text(encoding="utf-8")
# 提取 Status 与 Blocked by
sm = _STATUS_RE.search(text)
status = sm.group(1).strip().lower() if sm else "unknown"
bm = _BLOCKED_RE.search(text)
raw = bm.group(1).strip() if bm else "none"
blocked = (
[x.zfill(2) for x in re.findall(r"\d+", raw)]
if raw.lower() not in {"none", "n/a", "-"}
else []
)
result[iid] = dict(filepath=os.path.abspath(fp), status=status, blocked_by=blocked)
return result
解析结果是一张 {issue_id: {filepath, status, blocked_by}} 的映射表,程序据此构建任务的依赖拓扑。
pick_next 函数实现了依赖拓扑感知的任务挑选逻辑:
def pick_next(issues: dict[str, dict]) -> str | None:
"""返回下一个可执行 Issue 路径,全部完成返回 'ALL_DONE',无可用返回 None。"""
done_ids = {k for k, v in issues.items() if v["status"] in _DONE}
pending = {k: v for k, v in issues.items() if v["status"] not in _DONE}
if not pending:
return "ALL_DONE"
for iid in sorted(pending):
v = pending[iid]
if v["status"] == "ready-for-agent" and all(b in done_ids for b in v["blocked_by"]):
return v["filepath"]
return None
调度规则可以归纳为:
status 为 resolved 或 completed 的 Issue ID 加入 done_ids。blocked_by 依赖都已出现在 done_ids 中时,该 Issue 才可被调度。"ALL_DONE" 表示全部 Issue 已完成;返回文件路径表示找到下一个可执行 Issue;None 表示存在未完成的 Issue 但当前无可调度项(依赖未解除或状态不满足)。执行顺序:
01, 02, 04, 06可并行调度 →03等待01, 04完成 →05等待03完成 →10等待05完成。
主循环的结构遵循标准的 Ralph 模式,但用 Issue 文件替代了 PRD + progress(以下为核心片段节选):
def main() -> None:
# ...
for i in range(1, args.iterations + 1):
issues = parse_issues(issues_dir) # 每轮重新解析文件状态
target = pick_next(issues) # 拓扑调度
if target == "ALL_DONE":
sys.exit("所有 Issues 均已完成!")
if target is None:
sys.exit("没有满足条件的 Issue,自动停止。")
# 构建 Prompt:引用 spec.md + 当前 Issue 文件
spec = os.path.join(feat, "spec.md")
prefix = f"/implement 依据 {spec} 文件," if os.path.isfile(spec) else "/implement "
prompt = f"{prefix}实现 {target} 文件,完成后修改 Issue 文件的 Status 为 resolved 并勾选验收列表复选框"
# 调用 Agent CLI(支持 agy 和 claude 两种工具)
cmd = build_agent_cmd(args.agent, prompt, log_path)
ret = subprocess.run(cmd, shell=True)
# ...
脚本还包含一个 fallback_resolve 保底机制:当 Agent 完成了实现工作但未将 Issue 文件的 Status 更新为 resolved 时,外壳脚本会检查 Agent 日志中是否包含完成信号,若匹配则由脚本代为更新状态:
def fallback_resolve(issue_file: str, log: str) -> bool:
"""Agent 未更新状态时的保底处理。"""
content = Path(issue_file).read_text(encoding="utf-8")
if not re.search(r"Status:\*{0,2}\s*ready-for-agent", content, re.I):
return False
if re.search(r"resolved|<promise>NO MORE TASKS</promise>|完成", log, re.I):
new = re.sub(r"(\*{0,2}Status:\*{0,2}\s*)ready-for-agent", r"\1resolved", content, flags=re.I)
Path(issue_file).write_text(new, encoding="utf-8")
return True
raise RuntimeError("Agent 执行完毕,但未将 Issue 标记为 resolved")
这种设计体现了防御性工程思维:Agent 是概率性系统,不能假设它每次都会遵循指令更新状态文件,外壳脚本需要为这种失败模式设置兜底。
这个基于 Issue 文件体系的方案,同样遵循 Ralph 的核心不变量:外部状态持久化 → 原子任务执行 → 确定性验证 → 状态更新。Issue 文件体系方案可以理解为标准 SOP 在"任务状态管理"维度上的一种结构化演进——将扁平 Checklist 升级为带依赖拓扑的结构化任务图,将 Agent 自主选择任务改为程序化调度。
前两节分别展示了单项目内的标准 Ralph Loop 与基于 Issue 文件体系的依赖调度。当场景从单项目扩展到跨数十个仓库/微前端/微服务的全局数据盘点与治理时,Ralph 的核心思想同样适用,只需在外层引入一个程序化的目录控制链:
直接在大模型中打开包含数十个子项目的根目录并指令 AI “提取所有子目录中的路由并写入 Excel”,这种做法在工程上容易失效,对比分析如下:
| 评估维度 | 最外层直接投喂 (Outer Single Prompt) | Ralph 目录控制链 (Python Master-Worker) |
|---|---|---|
| 搜索空间与定位 | 搜索空间过大。跨多个项目搜索,信息匹配与定位成本高。 | 空间聚焦。每个 Worker 将当前子项目作为主要代码上下文,避免多个项目的代码状态混杂。 |
| 状态复杂度与覆盖率 | 容易漏检。全局任务状态集中在单 Agent,易遗漏子目录节点。 | 程序化保证。Master 程序化遍历目录,保障覆盖率。 |
| Token 成本 | 随规模快速增长。单次携带所有仓库信息,难于控制。 | 单项目可控。避免 Token 成本随整体工作区规模盲目增长,使单轮消耗主要取决于当前项目复杂度。 |
| 失败恢复与断点 | 难以恢复。中途中断后需重新全量扫描。 | 支持断点重试。可按项目独立重试与跳过已完成项。 |
| 结果可靠性 | 格式易漂移。Agent 直接拼接并追加文本结果。 | 格式收敛。Structured Artifact + Schema Validation。 |
借鉴 Ralph 的“独立上下文与原子循环”机制,引入 Python Master 控制脚本:
cwd)让 Worker 以当前子项目作为工作上下文,实现项目级的 Context Isolation。./routes.json。采用 “产物驱动 (Artifact-driven)” 模式,无需在目标仓库写入临时 PRD.md 文件。routes.json,由程序统一清洗、校验 Schema 结构并导出为标准 all_routes.csv。若项目已存在合法 routes.json 则直接跳过,支持基本的断点续传。[架构延伸] 任务收敛的信号层级 (Signal Hierarchy):
在 Agent 工程实践中,确认任务是否完成存在不同层级的校验强度:
- 弱信号:Agent 口头文本输出“处理完成”。
- 中信号:输出特定的收敛 Tag(如
<promise>COMPLETE</promise>)。- 强信号:结构化文件产物 (Structured Artifact) 成功生成。
- 更强信号:产物通过 Schema Validation 结构校验。
- 最强信号:通过与任务目标对应的独立验证机制(如代码修改对应单元测试与构建,数据提取对应 Schema 格式与字段完整性校验)。
import csv
import json
import subprocess
from pathlib import Path
# 1. 定义工作区目录、全局路由标准字段元数据与统一 Prompt
WORKSPACE = Path("./projects")
if not WORKSPACE.exists():
WORKSPACE = Path(".")
OUTPUT_CSV = Path("./all_routes.csv")
# 统一规范路由导出的核心字段列表(仅包含 3 个目标字段)
FIELDNAMES = ["project", "path", "title"]
PROMPT = """你现在正在处理子项目。
1. 只分析提取当前项目的前端页面路由(无需提取后端 API 接口)。
2. 将提取到的页面路由定义输出为结构化 JSON 文件 ./routes.json。
3. routes.json 为 JSON 数组,每个对象包含以下字段:
- path: 页面路由路径(字符串,必填,例如 "/diary/list")
- title: 页面标题/说明(字符串,可选,无标题则填 "")"""
def is_valid_routes_json(path: Path) -> bool:
"""校验 routes.json 是否存在且符合结构契约"""
if not path.exists():
return False
try:
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, list):
return False
return all(
isinstance(item, dict)
and isinstance(item.get("path"), str)
and bool(item.get("path").strip())
for item in data
)
except (json.JSONDecodeError, UnicodeDecodeError):
return False
# 2. 遍历子项目仓库运行微型 Ralph 循环
failed_projects = []
for project in WORKSPACE.iterdir():
if not project.is_dir() or project.name.startswith(".") or not (project / "package.json").exists():
continue
print(f"\n[日志] 开始处理项目: {project.name}")
route_json = project / "routes.json"
# 产物与 Schema 双重校验:若存在合法 routes.json 则直接跳过(支持断点续传)
if is_valid_routes_json(route_json):
print(f"[日志] 项目 {project.name} 已存在合法 routes.json,自动跳过。")
continue
completed = False
# 运行微型 Ralph 循环(限制单项目最多尝试 3 次)
for iteration in range(1, 4):
print(f" └─ 正在执行微循环 Iteration {iteration}")
try:
result = subprocess.run(
["claude", "--dangerously-skip-permissions", "-p", PROMPT],
cwd=str(project),
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
timeout=90
)
# 校验 Exit Code 与产物 Schema,符合契约即算收敛
if result.returncode == 0 and is_valid_routes_json(route_json):
completed = True
print(f"[日志] 项目 {project.name} 处理收敛!")
break
except subprocess.TimeoutExpired:
print(f" └─ [警告] Iteration {iteration} 执行超时 (90秒),准备下一次重试...")
if not completed:
failed_projects.append(project.name)
print(f"[错误] 项目 {project.name} 处理失败,已达到最大重试次数。")
# 3. Master 统一汇总各项目的 routes.json 并导出为标准 CSV
all_routes = []
for project in WORKSPACE.iterdir():
if not project.is_dir() or project.name.startswith(".") or not (project / "package.json").exists():
continue
route_json = project / "routes.json"
if is_valid_routes_json(route_json):
routes = json.loads(route_json.read_text(encoding="utf-8"))
for r in routes:
record = {"project": project.name}
for field in FIELDNAMES:
if field != "project":
record[field] = r.get(field, "")
all_routes.append(record)
# 写入 CSV(由 Python 显式控制 Header 与字段顺序,防止数据漂移)
with OUTPUT_CSV.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
writer.writeheader()
writer.writerows(all_routes)
print(f"\n[日志] 全局路由导出完成!共汇总 {len(all_routes)} 条路由记录 -> {OUTPUT_CSV}")
# 4. 输出失败项目报告,保障批处理可观测性
if failed_projects:
print(f"\n[警告] 以下 {len(failed_projects)} 个项目处理失败,请排查:")
for p in failed_projects:
print(f" - {p}")
[注] 关于 Schema Validation 扩展:
示例代码实现了最小结构契约校验;在生产实践中,可进一步结合 JSON Schema、Pydantic 或 Zod 等定义强类型数据契约,进行自动化验证。
[注] 关于 Worker 中的 Git Commit:
在本例的数据提取场景中,生成routes.json属于只读分析任务而非业务代码重构,因此示例省略了子仓库 Commit。若产物属于仓库日常维护资产,可在 Prompt 中引导 Agent 提交 Commit;若属于临时分析产物,建议将结果写入独立的结果目录,避免污染业务仓库历史。
在掌握 Ralph 的基础 Loop 与 Master-Worker 控制链后,可根据质量治理要求构建不同类型的专职 Ralph 循环:
Vitest / Jest 覆盖率报告,引导 Agent 识别未覆盖代码行,每次循环补全特定模块的单元测试,逐步提升覆盖率。ESLint 或 Biome 报错日志作为任务输入,每次循环修复单个文件或单一类型的报错并单独 Commit,避免大范围变更引入破坏性修改。jscpd 等代码重复率检测工具,引导 Agent 提取重复逻辑为公共组件或工具函数。pnpm tsc 或 npm test),由测试结果指导后续变更。MAX_ITERATIONS 上限,配合 <promise>COMPLETE</promise> 终止标识,保证运行可控。