自主 Agent 与 Ralph 自主开发循环
30 秒导读: 前面几章的 Agent 每次「你问一句、它答一句」。本章讲 Upsonic 最有辨识度的那类 agent:它自己会动手——在一个受限的工作区(workspace)里读写文件、执行 shell 命令;更进一步,
RalphLoop让它自己迭代——你 只给一句「做一个 FastAPI 待办应用」,它就分解需求、列 TODO、一轮实现一个任务,直到测试全部通过。核心两件套:能动手的AutonomousAgent/DeepAgent,和能自迭代的RalphLoop。
本章聚焦「沙箱化 + 自迭代」这一层。一次运行怎么跑完 24 步管线见 02-execution-pipeline.md;通用工具系统(自定义工具 / MCP / HITL) 见 04-tools-system.md;这里只讲把工具箱预装进 agent、并把「运行」包成一个能容错的循环。
1. 这是什么(零基础也能懂)
一句话定义: 自主 Agent 是预装了「手脚」的 agent——出厂即带文件工具和 shell 工具,并把所有操作关进一个工作区目录里;Ralph 循环则是把这样的 agent 反复运行、每轮换一个全新实例、跑完一个任务的自动开发引擎。
解决什么问题 / 给谁用:
- 想让 AI 帮你改一个真实项目的代码(读文件、改文件、跑测试)——但你不想让它误删你主目录的东西。
- 想给 AI 一个目标(而不是一步步指令),让它整晚自己把项目写出来,早上回来看结果。
它能做什么:
| 能力 | 由谁提供 |
|---|---|
| 读/写/编辑/搜索/移动/删除文件 | AutonomousFilesystemToolKit |
| 跑 shell 命令、跑 Python 片段、查命令是否存在 | AutonomousShellToolKit |
| 所有操作只限工作区,危险命令拦截 | 两个 toolkit 的路径校验 + 命令黑名单 |
| 会话记忆(默认开) | 出厂注入 InMemoryStorage + Memory |
| 可插拔虚拟文件系统(内存 / 持久化 / 混合) | DeepAgent + backends |
| 给一个 goal 自动写完项目 | RalphLoop |
用起来什么样: 一个最小真实示例(来自 RalphLoop 类文档串,ralph/loop.py:36-45):
from upsonic import RalphLoop
loop = RalphLoop(
goal="Build a FastAPI TODO app", # 只给目标
model="openai/gpt-4o",
test_command="pytest", # 用它当「验收门」
)
result = loop.run() # 自己跑,直到 TODO 清空
print(result.summary())
一句话直觉/类比: 把它想成**「土拨鼠之日」(Groundhog Day)里的程序员**:每天早上失忆醒来(全新 agent、干净上下文),但桌上留着三份纸——需求书、待办清单、给自己的便签(learnings)。他每天只做一件待办、做完打勾、给自己写张便签,然后睡去;明天的「他」接着干。日复一日,项目就长出来了。Ralph 论文管这叫「eventually consistent(最终一致)」的开发。
本节不出现底层代码。记住两个词:沙箱(sandbox) 和 自迭代(loop)。
2. 顶层全景(它大概怎么转)
Upsonic 在通用 Agent(见 01-agent-and-task.md)之上,派生出三个「会动手」的类,再用一个循环把它们编排起来:
Agent (通用基类,第 1、2 章)
│ 继承
┌──────────────┼───────────────────┐
▼ ▼ ▼
AutonomousAgent DeepAgent (Ralph 内部临时 Agent)
├ 文件工具 ├ 可插拔虚拟FS 每轮 new 一个、用完 del
├ shell 工具 ├ 规划工具 write_todos
├ 工作区沙箱 └ 子 agent 委派 task
└ 默认内存+记忆
RalphLoop ──把「运行一个 agent」包成能容错的循环──
│
▼
需求阶段 → TODO 阶段 → 增量阶段(死循环,一轮一个任务)
│
每轮:全新 Agent + 背压门(build/test)
怎么读这张图: 上半是三种能动手的 agent(都继承通用 Agent);下半是把 agent 反复跑的 RalphLoop。两半的连接点:Ralph 的增量阶段每轮临时 new 一个普通 Agent(不是 AutonomousAgent),给它装上 Ralph 自己的一套工具。
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
AutonomousAgent | 预装文件+shell 工具、工作区沙箱、默认记忆 | agent/autonomous_agent/autonomous_agent.py:30 |
AutonomousFilesystemToolKit | 读写编辑搜索文件,含读前置校验 | agent/autonomous_agent/filesystem_toolkit.py:19 |
AutonomousShellToolKit | 跑命令/Python,含超时和黑名单 | agent/autonomous_agent/shell_toolkit.py:22 |
DeepAgent | 规划 + 可插拔虚拟文件系统 + 子 agent 委派 | agent/deepagent/deepagent.py:29 |
| backends | 虚拟 FS 的三种后端:内存/持久化/混合路由 | agent/deepagent/backends/ |
RalphLoop | 三阶段编排 + 死循环 + 信号处理 | ralph/loop.py:24 |
BackpressureGate | 跑 build/test/lint,当「验收门」 | ralph/backpressure/gate.py:85 |
StateManager | 读写工作区里的状态文件(specs/fix_plan/AGENT.md) | ralph/state/manager.py:16 |
SubagentSpawnerToolKit | 主 agent 派生一次性 subagent 干重活 | ralph/tools/subagent_spawner.py:25 |
主线走一遍(高层):
输入一个 goal → RalphLoop.run() 先跑需求阶段(把目标写成 specs/*.md)→ TODO 阶段(把 specs 拆成 fix_plan.md 勾选表)→ 进入增量阶段死循环:每轮读状态、new 一个全新 agent、让它挑一条未勾选任务做完、跑背压门、打勾、记 learnings → 当 fix_plan.md 里没有未勾选项时,循环判定 completed 退出。
3. 核心原理之一:AutonomousAgent —— 把工具箱和沙箱预装进 agent
它要解决的小问题: 通用 Agent 你得自己配工具、自己配记忆、自己防止它乱碰文件系统。写一个「编码助手」时这些是每次都要重来的样板。AutonomousAgent 把这套打包成开箱即用。
思路/直觉: 它就是一个带默认值的 Agent 子类——__init__ 里把「默认存储 + 默认记忆 + 文件工具 + shell 工具 + 一段讲清工具用法的系统提示」都准备好,再调 super().__init__()。
三个默认值的装配(源码走读):
- 工作区兜底——没给
workspace就用当前目录,并确保它存在:
# agent/autonomous_agent/autonomous_agent.py:216
if workspace is not None:
self.autonomous_workspace = Path(workspace).resolve()
else:
self.autonomous_workspace = Path.cwd().resolve()
-
默认存储/记忆——没传 storage/memory/db 时,自动 new 一个
InMemoryStorage,并据此建Memory(autonomous_agent.py:224-259)。所以full_session_memory默认为True,对话历史默认就有。 -
默认工具——按开关把两个 toolkit 塞进
default_tools,再和用户自带工具拼一起:
# agent/autonomous_agent/autonomous_agent.py:271
if enable_filesystem:
self.filesystem_toolkit = AutonomousFilesystemToolKit(workspace=self.autonomous_workspace)
default_tools.append(self.filesystem_toolkit)
if enable_shell:
self.shell_toolkit = AutonomousShellToolKit(workspace=self.autonomous_workspace, ...)
default_tools.append(self.shell_toolkit)
all_tools = default_tools + (tools or [])
关键细节:动态系统提示。 _build_autonomous_system_prompt(autonomous_agent.py:358)根据开了哪些工具拼出提示词:列出每个工具、给使用守则(「edit 前必须先 read」)、最后附一段安全声明——
# agent/autonomous_agent/autonomous_agent.py:470
security_notes = ["\n## Security Restrictions"]
if enable_filesystem:
security_notes.append("- All file operations are sandboxed to the workspace directory.")
security_notes.append("- Path traversal (../) outside the workspace is blocked.")
if enable_shell:
security_notes.append("- Dangerous shell commands are blocked for security.")
注意:这段提示只是告诉模型有沙箱;真正的强制在下面 §4 的 toolkit 代码里。用户若自带 system_prompt,则原样包进 <AutonomousAgent>...</AutonomousAgent>(autonomous_agent.py:379),不再拼默认那套。
附带的心跳(heartbeat)能力: 传 heartbeat=True 后,接口层可周期性给 agent 发 heartbeat_message,由 aexecute_heartbeat(autonomous_agent.py:508)以静默方式(临时把 print 关掉)跑一次 do_async 并取回文本。这是「让 agent 定时自己动一下」的钩子。
4. 核心原理之二:两个工具箱与沙箱边界
这是自主 agent 的「手脚 」。两个 toolkit 都继承通用 ToolKit(工具系统见 04-tools-system.md),差别在于每个方法都先过一道工作区校验。
4.1 文件工具:路径穿越拦截 + 读前置校验
沙箱靠 _validate_path 一个函数守住——把任意路径 resolve 成绝对路径后,用 relative_to 试探是否仍在工作区内,不在就抛错:
# agent/autonomous_agent/filesystem_toolkit.py:64
def _validate_path(self, path: str) -> Path:
if path.startswith("/"):
resolved = Path(path).resolve()
else:
resolved = (self.workspace / path).resolve()
try:
resolved.relative_to(self.workspace) # 不在工作区内 → ValueError
except ValueError:
raise ValueError(f"Path '{path}' is outside workspace '{self.workspace}'")
return resolved
因为先 resolve() 再判断,../../etc/passwd 这种穿越会在解析后落到工作区外,被拦下。所有读写工具的第一行都是它。
巧妙处:read-before-edit 强制。 edit_file 不允许「盲改」——toolkit 用一个 self._read_files 集合记录读过哪些文件;read_file/write_file 成功后把路径加进去(filesystem_toolkit.py:134),而 edit_file 开头就检查:
# agent/autonomous_agent/filesystem_toolkit.py:309
if resolved_str not in self._read_files:
return (
f"❌ Error: You must call read_file('{file_path}') before editing.\n\n"
...
)
没读过就返回一条给模型看的错误(而非抛异常),引导它先 read_file 再重试。此外 edit_file 还处理「old_string 找不到」「出现多次但没开 replace_all」等情况,都以文字反馈回给模型自愈。
工具清单(都在 filesystem_toolkit.py):read_file(带行号分页)、write_file、edit_file、list_files、search_files(glob)、grep_files(正则)、move_file、copy_file、delete_file、file_info、create_directory。每个都有 a* 异步版。
4.2 shell 工具:超时 + 危险命令黑名单
默认黑名单在构造函数里写死了几条「毁灭性」命令:
# agent/autonomous_agent/shell_toolkit.py:74
self.blocked_commands = blocked_commands or [
"rm -rf /", "rm -rf /*", ":(){:|:&};:", "mkfs", "dd if=/dev/zero",
]
_validate_command(shell_toolkit.py:82)做子串匹配——命令小写后只要包含黑名单里任一串就拒。还可选传 allowed_commands 当白名单(只允许列出的基础命令)。
一个值得注意的工程决定:关掉工具层的超时和重试。 run_command 的装饰器写着 @tool(timeout=None, max_retries=0),注释解释了原因——subprocess 自己已经带 timeout,外层再包一层 wait_for 会重复、且取消时会泄漏子进程:
# agent/autonomous_agent/shell_toolkit.py:104
# Subprocess owns its own timeout; outer wait_for duplicates it and
# leaks processes on cancel. Disable the generic tool-layer retry.
@tool(timeout=None, max_retries=0)
def run_command(self, command, timeout=None, env=None, shell=True) -> str:
...
result = subprocess.run(command, shell=shell, cwd=str(self.workspace),
capture_output=True, text=True, timeout=effective_timeout, env=environment)
命令始终在工作区目录里跑(cwd=self.workspace),输出超过 max_output_length(默认 10000)会截断。三个工具:run_command、run_python(把代码转义后用 python3 -c 跑)、check_command_exists。
一句提醒: 这层沙箱是软约束——文件工具限死在工作区,但 shell 黑名单只是子串匹配,
run_command仍能跑任意未被列黑的命令(且能访问真实文件系统,只是默认 cwd 在工作区)。它防的是「模型手滑」,不是「对抗性逃逸」。真正的策略护栏见 05-safety-engine.md。
5. 核心原理之三:DeepAgent —— 规划 + 可插拔虚拟文件系统
它要解决的小问题: 复杂多步任务里,你希望 agent(1)先显式拆解再动手,(2)把中间文件写进一个可换存储的虚拟盘(纯内存?落库持久化?两者混合?),(3)能把子任务委派给专职 subagent。
DeepAgent(agent/deepagent/deepagent.py:29)在通用 Agent 上加了三样,各由开关控制:
5.1 规划工具 write_todos —— 一个「认知强制函数」
PlanningToolKit(agent/deepagent/tools/planning_toolkit.py:73)提供 write_todos。它几乎不做实际计算——价值在于逼模型把任务拆成离散步骤。首调强制至少 2 条待办(TodoList 的 min_length=2,planning_toolkit.py:52);后续调用按 id 合并更新(planning_toolkit.py:192)。文件注释直言这是「cognitive forcing function」。
5.2 可插拔后端 —— 同一套文件工具,底下换存储
DeepAgent 的文件工具不直接碰真实磁盘,而是走一个 backend 协议(backends/protocol.py:12,一个 runtime_checkable 的 Protocol,定义 read/write/delete/exists/list_dir/glob 六个异步方法)。三种实现:
| 后端 | 存哪儿 | 特性 | 文件 |
|---|---|---|---|
StateBackend(默认) | 进程内一个 dict | 纯内存、快、不跨会话 | backends/state_backend.py:6 |
MemoryBackend | Upsonic Storage(可 SQLite 等) | 持久化、跨进程、可多 agent 共享 | backends/memory_backend.py:41 |
CompositeBackend | 按路径前缀路由到不同后端 | 混合:/memories/ 持久、/tmp/ 临时 | backends/composite_backend.py:16 |
DeepAgent 默认用 StateBackend(deepagent.py:114)。同一套 ls/read_file/write_file/edit_file/glob/grep 工具(tools/filesystem_toolkit.py)透过协议操作它们,底层换存储对模型完全透明。CompositeBackend 的路由是「首个匹配前缀者胜」(composite_backend.py:116),让你把「长期记忆」和「临时草稿」放进不同后端。
统一的路径纪律: 三个后端都要求绝对路径、拒绝 .. 穿越、拒绝空字节、限长 4096(见 state_backend.py:45 的 _validate_path)。和 §4 的真实磁盘沙箱思路一致,只是这里守的是虚拟盘。
5.3 子 agent 委派 —— task 工具
若 enable_subagents,DeepAgent 会:先确保有一个 general-purpose 子 agent(用父 agent 的工具、但 memory=None 完全隔离,deepagent.py:197),再挂上 SubagentToolKit(deepagent.py:212)。它提供 task(task_description, subagent_type) 工具:按名字查子 agent、跑 do_async、把结果返回(tools/subagent_toolkit.py:81)。这套「主 agent 派活给子 agent」的模式,正是下面 Ralph 循环的核心武器。