ARES 源码解析
withmartian/ares —— 把 LLM Agent 当成 RL 问题的"考场+监考系统"。 用 asyncio.Queue 拦截 Agent 的 LLM 调用,让线性 Agent 代码无感地被 RL 环境托管。 Python 8.3K 行 + Go HTTP 代理,双栈容器(Daytona / Docker),双栈 Agent(mini-swe-agent / terminus2)。
§1TL;DR
一页纸看懂 ARES 是什么 / 不是什么 / 值得抄什么。
🎯 一句话
ARES 不是 Agent 产品,也不是训练算法库——它是 Agent RL 的基础设施层: 把每个 LLM 请求变成 observation,把 LLM 响应变成 action,让训练框架(trl / verl / openpipe)能按 RL 循环驱动 Agent。
核心抽象
dm_envasync
LLMRequest = observation,LLMResponse = action,reward 从容器 /reward.txt 或 /reward.json 读出。
最关键模式
50 行全框架杠杆点
QueueMediatedLLMClient 用 asyncio.Queue + Future 拦截 Agent 调用。Agent 线性代码无感被 RL 环境托管。
双栈容器
云端默认本地备用
Daytona(10 次 retry + auto-stop)+ Docker(本地 build + tar 传输)。Janitor 用 atexit 兜底清理,防云资源泄露。
双栈 Agent
轻量生产级
MiniSWECodeAgent(~260 行,SWE-bench 跑分)vs Terminus2Agent(1,110 行,tmux 持续会话 + 主动概括)。
双栈拦截
同进程跨进程
in-process Python Queue(0 延迟)+ ares-proxy Go HTTP(跨容器/跨机器,RTT 10-100ms)。
附加彩蛋
mech_interpTUI
transformer-lens 集成、激活抓取、线性探针、CAA 干预;Textual TUI 评估看板。
§2生态位 · 它不是什么
理解 ARES 的第一步是搞清楚它和相邻层的边界。
| 常见误解 | 实际 |
|---|---|
| 成品 Agent 产品(类 Manus) | ❌ 不是终端产品,不直接服务用户 |
| 训练算法库(类 trl / verl) | ❌ 不做 PPO / GRPO 权重更新 |
| LLM 路由器(withmartian 主业) | ❌ 那是另一条产品线 |
它在哪一层
谁会用它
- 做 Agent 后训练(post-training / fine-tuning)的研究员
- SWE-bench Verified 类基准的大规模并行评估者
- 想把自家 Agent 接入"统一评估 + 统一沙箱"的工程团队
- withmartian 自家的 Router / Agent 产品线
§3核心抽象:Agent 当成 RL
ARES 实现了 dm_env 的 async 版本。一次 episode 就是 Agent 从接到任务到 reward 出现的全过程。
Environment 协议
class Environment(Protocol[ActionType, ObservationType, RewardType, DiscountType]):
async def reset(self) -> TimeStep[ObservationType, RewardType, DiscountType]: ...
async def step(self, action: ActionType) -> TimeStep[...]: ...
async def close(self) -> None: ...
来源:src/ares/environments/base.py:71-138
TimeStep 的三种状态
| step_type | 语义 | reward | observation |
|---|---|---|---|
FIRST | episode 开始 | None(强制) | 首个 LLMRequest |
MID | episode 进行中 | 0.0(稀疏奖励) | 下一个 LLMRequest |
LAST | episode 结束 | 从 /reward.* 读出 | None |
来源:src/ares/environments/base.py:31-69
§4架构全图
从公开 API 到 Go 代理的五层栈。
§5Environment 层
RL 主循环的编排者。管容器生命周期、驱动 Agent Task、收集 reward。
CodeEnvironment 的签名
class CodeEnvironment(base.Environment[
response.LLMResponse, # ActionType
request.LLMRequest | None, # ObservationType
float, # RewardType
float, # DiscountType
])
reset() 流程 code_env.py:95-127
- 清空步数,停旧容器(:104-108)
- 随机选任务
_reset_task()(:110) - 启动容器
_start_container()(:111) - 启动 agent 作为独立 asyncio Task(:112)——Agent 代码线性,环境在后台跑它
- 等 agent 发出第一个 LLM 请求
_get_time_step()(:114) - 包装成
FIRSTTimeStep 返回
step(action) 流程 code_env.py:129-161
- 步数 +1(:138)
- 把
action喂回 agent:_llm_req_future.set_result(action)(:142)—— 唤醒 agent 的 await - 等 agent 下一个 LLM 请求或 agent 任务完成(:146)
- 步数超限(默认 250)强制
LAST,取消 agent task(:148-153) - 否则返回
MID(reward=0.0),episode 终止才算分
Reward 双格式读取 code_env.py:302-315
/reward.txt
直接 float(content)。最简单场景用。
/reward.json
解析 JSON,取唯一 key 的 value。Harbor 数据集约定。
Episode 终止三条路
| 触发 | 位置 | 终局 |
|---|---|---|
| Agent Task 完成 | :174-193 | LAST(reward 从 /reward.* 读) |
| 步数超限 | :148-153 | LAST(reward=上一 reward) |
| 已 LAST 再 step | :135-136 | 抛异常,要求 reset |
§6Container 层
隔离 Agent 执行环境。双实现覆盖云 / 本地,Janitor 兜底防泄露。
Container Protocol containers/containers.py:24-131
async def start(env: dict[str, str] | None) -> None
async def exec_run(command, workdir, env, timeout_s) -> ExecResult
async def upload_files/download_files/upload_dir/download_dir
def stop_and_remove() -> None # 唯一同步方法,给 atexit 用
双实现对比
| 特性 | Daytona(云,默认) | Docker(本地) |
|---|---|---|
| 启动介质 | 云 API | docker-py,本地 build |
| 重试 | 10 次指数退避 daytona.py:35-46 | 无 |
| 超时处理 | 抛 TimeoutError,不重试 | asyncio.wait_for |
| 文件传输 | 原生 SDK sbx.fs.upload_files() | tar 打包 put_archive() |
| 资源配置 | CPU / Memory / Disk / GPU | ❌ TODO 未支持 |
| 清理 | auto_stop(30min) + auto_delete(0) | force remove |
| 挂起方式 | Sandbox 自管 | tail -f /dev/nulldocker.py:83 |
tail -f /dev/null,容器 CMD 执行完就会退出。这是常见的"容器秒退"问题的标准解法。
Janitor atexit 兜底 code_env.py:348-389
关键设计约束:atexit 不能跑 async,所以 Container 协议强制提供同步的 stop_and_remove()。
§7Code Agent 层 · 双栈 Agent 对比
ARES 内置两种 Agent:轻量跑分的 MiniSWECodeAgent 和生产级复杂度的 Terminus2Agent。
协议
class CodeAgent(Protocol):
async def run(self, task: str) -> None
MiniSWECodeAgent
轻量级,封装 mini-swe-agent 库
- 步数上限:250
- 会话:每步无状态
- 循环:step → query → execute_action
- bash 解析:markdown 代码块
- 错误:异常分层(
_NonTerminatingError/_TerminatingError) - 代码量:约 260 行
- 适用:SWE-bench 快速跑分
mini_swe_agent.py:156-258
Terminus2Agent
生产级,Terminal-Bench 的 tmux 会话
- 步数上限:1,000,000
- 会话:tmux 持续会话,160×40 分辨率,50k 历史
- 循环:tmux check → query → parse → execute → 两步完成确认
- 上下文:主动概括(200k token)+ 被动救援(context_length_exceeded)
- 输出追踪:增量定位(rfind 锚点)
- Parser:JSON / XML 可切换,三级降级
- 代码量:1,110 行
- 适用:长期交互、超长轨迹
terminus2/terminus2_agent.py:482-849
Parser 三级降级 terminus2/json_parser.py:75-99
容错 Parser 模板(可直接抄)
try:
data = json.loads(json_str)
except JSONDecodeError:
json_str = self._auto_fix_json(json_str) # Level 2: 补括号 / 引号
try:
data = json.loads(json_str)
except JSONDecodeError:
fallback = self._parse_with_regex(original) # Level 3: regex 降级
XML 侧还有 salvage_truncated_response,从被截断的响应中抢救合法标签。
2 字符 = 1 token 估算都是硬编码。对英文 / Unicode 混杂的 tmux 输出不完全准确。生产需按模型实际 tokenizer 调。
§8Queue-Mediated ⭐ 全框架杠杆点
整个 ARES 最精妙的 50 行代码。它让 线性 Agent 代码(随手写一堆 await llm(...))和 RL 环境协议(强制 reset/step/close 循环)无感接合。
核心 50 行 queue_mediated_client.py:47-50
class QueueMediatedLLMClient:
q: asyncio.Queue[ValueAndFuture[LLMRequest, LLMResponse]]
async def __call__(self, req: LLMRequest) -> LLMResponse:
future = asyncio.Future[LLMResponse]()
await self.q.put(ValueAndFuture(value=req, future=future))
return await future # ← Agent 挂在这里
它是怎么工作的
结果:
- Agent 作者:正常写
response = await llm(req),完全不知道自己被托管 - 训练者:拿到符合 dm_env 规范的
reset/step,可以喂给任何训练框架 - 零妥协:不需要 Agent 实现"RL-aware 接口",也不需要训练器懂 Agent 内部
支撑抽象:ValueAndFuture async_utils.py
@dataclasses.dataclass(frozen=True)
class ValueAndFuture[ValType, FutureType]:
value: ValType
future: asyncio.Future[FutureType]
8 行泛型 dataclass。任何"把值和响应承诺打包传递"的场景都能抄:模拟器、游戏引擎、多租户推理队列、RPC 中间件。
💡 值得抄
这是那种"看一眼就该记住"的模式。比 RxJS 的 Subject 简单得多,但在 async Python 场景里解决了"外部事件驱动 + 内部线性代码"的终极矛盾。
§9LLM Client 层
多个实现共享 LLMClient 协议。拦截版、API 版、本地版、可解释性版各司其职。
协议
class LLMClient(Protocol):
async def __call__(self, request: LLMRequest) -> LLMResponse
QueueMediatedLLMClient ⭐
核心
RL 拦截版,把请求塞进 queue 等 future。见 §8。
ChatCompletionCompatibleLLMClient
API 默认
OpenAI 兼容 HTTP 客户端。Martian API 默认后端。线程局部 httpx + tenacity 3 次重试 + 成本追踪。
LlamaCppLLMClient
本地 GGUF
对接本地 GGUF 模型。asyncio.to_thread() 包装阻塞推理。
HookedTransformerLLMClient
mech_interp
底层 transformer-lens,支持抓中间激活 / 钩子干预。
ChatCompletionCompatibleLLMClient 的四个亮点
1. 线程局部 httpx 客户端 chat_completions_compatible.py:22-41
_thread_local = threading.local()
def _get_llm_client(base_url, api_key):
key = (base_url, api_key)
clients = getattr(_thread_local, "clients", {})
if key not in clients:
clients[key] = openai.AsyncClient(...)
_thread_local.clients = clients
return clients[key]
为什么要这样写:httpx.AsyncClient 绑创建线程的 event loop。跨线程用同一个实例会死锁。线程局部是最优雅的解。
2. Tenacity 装饰器 :44-53
@tenacity.retry(
stop=tenacity.stop_after_attempt(3),
wait=tenacity.wait_exponential(min=1, max=60) + tenacity.wait_random(min=0, max=1),
before_sleep=tenacity.before_sleep_log(_LOGGER, logging.INFO),
)
3 次尝试 + 指数退避 + 随机抖动。异步 API 客户端通用模板。
3. GPT-5 特判 :66-67
GPT-5 不支持 temperature,动态移除参数。提醒你:模型家族碎片化在 API 适配层是常态。
4. 成本内置 :72 + accounting.py:70-97
每个 LLMResponse 带 cost 字段。价目表从 Martian API 拉取(LRU 缓存),按 prompt/completion token 累加。
cached_tokens 和 reasoning_tokens(Martian 当前未区分)。对 GPT-o3 / Claude 3.7 thinking 场景要另行处理。
转换层:两份而非一份
OpenAI 的 Chat Completions API 和 Responses API 的消息/工具结构完全不同,单一转换器写起来会很乱,所以 ARES 拆了两份各司其职。
| openai_chat_converter.py (395 行) | openai_responses_converter.py (435 行) | |
|---|---|---|
| 目标 API | Chat Completions | Responses |
| system prompt | messages[0] = system 角色 | instructions 参数 |
| 工具调用 | 展平成 AssistantMessage.tool_calls | 多态 input 数组 |
| 损失检测 | top_k、stop_sequences>4 | stop_sequences 完全不支持 |
§10ares-proxy · Go HTTP 版 Queue-Mediated
in-process Queue 只在同一 Python 进程内有效。Agent 在隔离容器跑时,需要把队列搬到 HTTP。 ares-proxy 就是这个跨进程版本,用 Go 实现。
为什么要 ares-proxy
in-process Queue
Agent 和环境同一个 Python 进程。
- 0 网络延迟
- asyncio 原生
- 单机单进程才能用
ares-proxy (Go HTTP)
Agent 跑在 Docker / Daytona 容器,通过 HTTP 跨进程通信。
- RTT 10-100ms
- goroutine + channel
- 跨进程 / 跨容器 / 跨机器都能用
三端点数据流
端点实现对照
| 端点 | 文件:行 | 行为 |
|---|---|---|
POST /v1/chat/completions |
main.go:34-59 broker.go:36-73 |
生成 UUID,创建 responseChan,加入 map + 队列,阻塞等(默认 15min timeout) |
GET /poll |
main.go:64-80 broker.go:90-102 |
原子读整个 requestQueue,立即清空(:99),返回 JSON 数组 |
POST /respond |
main.go:85-109 broker.go:106-122 |
查 ID,responseChan <- response,关闭通道 |
Broker 数据结构 broker.go:14-22
type Broker struct {
mutex sync.Mutex
pendingRequests map[string]chan json.RawMessage // ID → 响应通道
requestQueue []PendingRequest // 待轮询队列
}
为什么选 Go
- goroutine + channel 天然适合队列代理
- 纯 stdlib,无外部依赖
- 单二进制部署,扔进任何容器都能跑
- Python 做这个反而要装 httpx / aiohttp / uvicorn 一堆
responseChan 缓冲大小 = 1(broker.go:41)。如果 Agent 不及时读取响应,会堵塞后续处理。高并发场景建议调大。
§11Registry + Presets + 任务切片
用字符串魔法 "sbv-mswea:0:10" 精准定位 "SWE-bench Verified 上 mini-swe-agent 的前 10 个任务"。
三种 Selector registry.py:31-217
| Selector | 构造 | 行为 |
|---|---|---|
IndexSelector(5) | :47-58 | tasks[5] |
SliceSelector(0, 10) | :62-75 | tasks[0:10] |
ShardSelector(2, 8) | :79-109 | 均匀分 8 片取第 2 片 |
语法糖 parse_selector:112-217
"sbv-mswea" # 全选 → SliceSelector(None, None)
"sbv-mswea:5" # 单任务 → IndexSelector(5)
"sbv-mswea:0:10" # 切片 → SliceSelector(0, 10)
"sbv-mswea:5:" # 从 5 到末尾 → SliceSelector(5, None)
"sbv-mswea@2/8" # 第 2/8 片(分布式评估) → ShardSelector(2, 8)
已注册预设 presets.py
HarborSpec 系列 :39-82
从 code_env.list_harbor_datasets() 动态枚举所有 Harbor 数据集,× {mini_swe_agent, terminus2_agent} 笛卡尔积。
命名:{dataset_id}-{agent_id}
例如:sbv-mswea、sbv-terminus2
TwentyQuestionsSpec :85-119
20 Questions 猜谜游戏(无容器,纯文本)。
125 个内置对象。
展示 ARES 非 SWE-bench 的能力边界。
§12Examples · 渐进式学习梯度
四个示例精心设计,每一步只换一个组件,体现 ARES 的模块化。
01_sequential_eval_with_local_llm.py
最小
最小循环:async with ares.make("sbv-mswea:0")。用 llama_cpp 加载本地 Qwen2-0.5B。默认 Docker 容器。
02_sequential_eval_with_api.py
API 切换
唯一差别:agent 换成 ChatCompletionCompatibleLLMClient(model="openai/gpt-5-mini")。环境/容器代码一模一样。
03_parallel_eval_with_api.py
并行核心
Semaphore(20) + gather + TUI 看板。几百任务同时跑。见下方剖析。
20q_case_study/
可解释性
5 阶段:采集激活 → 训探针 → 方向识别 → CAA 干预 → 因果验证。展示非 SWE-bench 应用。
并行机制拆解 examples/03_parallel_eval_with_api.py
# Semaphore 流控
sem = asyncio.Semaphore(args.num_parallel_workers) # 默认 20
# 装饰器包装:每个任务抢信号量
async def _await_with_semaphore(coro):
async with sem:
return await coro
# gather 批量启动
tasks = [_await_with_semaphore(run_one(task_id)) for task_id in task_ids]
results = await asyncio.gather(*tasks, return_exceptions=True)
并行瓶颈
- num_parallel_workers:Semaphore 上限(默认 20)
- 容器工厂配额:Daytona API 并发创建配额
- 单点 CPU / 内存:TUI Dashboard 渲染 + asyncio 调度
§13测试 + Mock 体系
单元测试用 mock,集成测试用真容器。
单元测试 · Mock
MockContainer testing/mock_container.py:10-130
记录所有 exec_commands / uploaded_files / downloaded_files。支持 exec_handler 回调动态生成响应。
MockLLMClient testing/mock_llm.py:10-72
循环预设响应列表 / 自定义 response_handler。记录全部请求,get_last_request() 断言入口。
集成测试 · 真 Daytona
test_default_workdir.py integration_tests/:L10-48
验证 SWE-bench /testbed vs TerminalBench /app 工作目录。
流程:ares.make(preset) → reset() → exec_run("pwd") → 断言
用 Daytona(避开本地 Docker 兼容问题)。
§14StatTracker · 三实现一协议
时序指标和标量的非侵入式追踪。
协议 stat_tracker.py:16-21
class StatTracker(Protocol):
@contextlib.contextmanager
def timeit(self, name: str) -> Generator: ...
def scalar(self, name: str, value: float) -> None: ...
三种实现
| 实现 | 位置 | 机制 |
|---|---|---|
NullStatTracker | stat_tracker.py:23-30 | 无操作,生产低开销路径 |
LoggingStatTracker | :33-62 | 后台任务每 60s 打分位数(p0/p25/p50/p75/p100),np.percentile() |
TensorboardStatTracker | tensorboard.py:14-42 | 60s 周期 SummaryWriter.add_histogram() |
· 无 MLflow / wandb 集成(仅 tensorboard)
· 60s 周期硬编码
· 无接口配置
· 想接 wandb 只能自己实现 Protocol
§15mech_interp · 机制可解释性附加
ARES 不仅能跑分,还支持可解释性研究的完整闭环。这是 withmartian 的亮点附加。
三个核心组件 contrib/mech_interp/
| 文件 | 行 | 作用 |
|---|---|---|
hooked_transformer_client.py | 13-140 | 实现 LLMClient,底层 transformer-lens.HookedTransformer.generate() |
activation_capture.py | 13-89 | TrajectoryActivations:列表存每步 ActivationCache,torch.save/load 持久化 |
hook_utils.py | 20-100 | 零融合钩子(ablate 位置/头)+ 路径补丁钩子(clean → corrupted 替换做因果分析) |
和训练什么关系?
不是直接训练反馈,而是 离线可解释性研究:
§16关键设计模式
从 ARES 源码提炼的七个高杠杆率模式。
1. Queue-Mediated Communication
⭐ 最重要
asyncio.Queue + Future 让线性代码与外部控制器无感接合。50 行代码,但抽象力巨大。
2. Protocol-Oriented Design
结构子类型
几乎所有核心类型都是 typing.Protocol。无继承树,duck typing + 类型检查两全。
3. Factory Pattern
依赖注入
环境收"工厂"而非"实例"。container_factory / code_agent_factory。便于 A/B 切换。
4. Async Context Manager
生命周期
所有资源都 async with。保证 __aexit__ 清理。
5. Frozen Dataclass
并发安全
大部分 dataclass frozen=True。async 并发下避免状态污染。
6. Atexit Janitor
兜底清理
异常退出时清理外部资源(容器、临时文件)。atexit.register + 同步版 stop_and_remove。
7. YAGNI
哲学
CLAUDE.md 明说:不做过度抽象。CodeEnvironment 直接实现 Environment,不搞继承塔。
§17亮点 · 坑点 · 可抄
最值得拿的三类清单。
✨ 亮点 8 条
Queue-Mediated 50 行
让 Agent 线性代码无感被 RL 托管。全框架最关键杠杆点。
Parser 三级降级
JSON → auto-fix → regex。XML 还有 salvage_truncated_response。容错极强。
增量输出追踪
rfind 锚点定位新增内容。适配超长 tmux 会话不爆上下文。
双保险概括
主动 200k 阈值 + 被动 context_length_exceeded 捕获。
双栈拦截
in-process Python Queue + out-of-process Go HTTP。覆盖所有部署形态。
成本内置
每个 LLMResponse 带 cost。精细计费无痛。
线程局部 httpx
规避 async 事件循环跨线程死锁的最佳实践。
Janitor atexit
云资源兜底清理,防泄露。任何管外部资源的系统都该抄。
⚠️ 坑点 8 条
- Terminus2Agent tmux 初始化复杂(:196-319):动态 apt-get 装 tmux,建议生产镜像预装
- 200k token 阈值硬编码(:666):
2 字符 = 1 token估算粗糙,对 Unicode 不准 - ares-proxy 响应通道大小 = 1(broker.go:41):agent 不及时取会延迟后续处理
- Chat 与 Responses 转换器有重复(tool_choice 部分)
- 增量输出定位失败兜底(:453-456):rfind=-1 时输出整屏,可能重复
- Docker 不支持资源配置(CPU/Memory TODO 未完成)
- StatTracker 周期 60s 硬编码,无配置接口
- 无 wandb / mlflow,仅 tensorboard
💎 可抄片段 6 条(直接能用)
| 片段 | 位置 | 适用场景 |
|---|---|---|
| Queue-Mediated 50 行 | queue_mediated_client.py:47-50 | 任何"线性代码 + 外部控制"场景:模拟器、多租户推理、游戏 AI |
| 三级降级 Parser | json_parser.py:75-99 | LLM 输出解析的最佳实践模板 |
| Tenacity 重试装饰器 | chat_completions_compatible.py:44-53 | 异步 API 客户端通用重试 |
| 线程局部 httpx | chat_completions_compatible.py:22-41 | 多线程 async 场景规避事件循环冲突 |
| Janitor atexit | code_env.py:348-389 | 任何管外部资源(容器、临时文件、远程 session)的系统 |
| ValueAndFuture | async_utils.py | 8 行泛型 dataclass,"值 + 响应 future"原子单位 |
§18对标 / 启发
放到 Agent + RL 生态里看,ARES 占哪块地,能给其他项目什么。
vs 其他 Agent RL 框架
| ARES | Verl | OpenPipe Mini-ART | Gymnasium | |
|---|---|---|---|---|
| 定位 | 环境层 | 训练器 + 环境 | Fine-tune + eval 一体 | 通用 RL 标准 |
| Agent 支持 | SWE + terminal | SWE | 多场景 | 非 LLM |
| 沙箱 | Daytona + Docker | 自研 | 自研 | 无 |
| 拦截机制 | asyncio.Queue + Go proxy | RPC | 直接调用 | N/A |
| 可解释性 | mech_interp 附加 | 无 | 无 | 无 |
对标 Manus
边界与重叠
Manus = 成品 Agent(应用层);ARES = 训练/评估基础设施(基础设施层)。角色不同。
但 ARES 里的 Agent 运行内核(terminus2_agent + ares-proxy + Daytona 沙箱)
≈ 一个 Manus-like 的 Agent 运行器。
把 ARES 的 RL 训练钩子(reward 读取、并发 rollout、gather 聚合)拆掉,剩下的部分可以当独立 Agent 运行时复用。 这是最值得拿的"后半"。
对个人项目的启发
| 项目 | 可借鉴 |
|---|---|
| HiClaw / OpenClaw 魔改 | QueueMediatedLLMClient + ares-proxy 的双栈拦截可直接借鉴,给多 Agent 编排做统一观察接口 |
| 手机 GUI Agent | Terminus2Agent 的 tmux 增量输出追踪 + 主动概括策略可迁移到 GUI 长轨迹 |
| Hermes Personal | ChatCompletionCompatibleLLMClient 的线程局部客户端 + tenacity 重试模板直接抄 |
| Manus 逆向 | 参考 ARES 的 Agent runtime 设计,对比 Manus 公开行为里哪些已经实现、哪些还差 |
| 通用 | Parser 三级降级 + Janitor atexit 属于"看过一次就该用"的基础模式 |
§19延伸思考 · 从 ARES 沙箱到私人 Cloud Agent Runtime
拆完 ARES 之后冒出来的一个念头:"我在 VPS 上装 Linux + Claude Code + 做个面板,是不是就等于 ARES 那种沙箱?能干任何事 + 有长久记忆?" 答案是"基本对,但有几个概念要拆清楚"。下面是把这个思路推完整的全过程。
🎯 本节一句话结论
"VPS + Linux + Claude Code + Web 面板" ≈ 一个 24×7 在线、跨设备、有累积记忆、隔离可控的私人 Agent 工作台。 这和 ARES 的"训练沙箱"不是一回事(那是 ephemeral 的跑分考场),但共享同一个底层隔离技术栈。"能干任何事"的真正门槛不在架构,在 沙箱权限 + 工具桥。
§19.1沙箱 ⊂ Agent Runtime
"沙箱"这个词在 Agent 语境里经常被跟"整套 Agent 工作环境"混用。拆清楚。
关键区分:
- Sandbox(沙箱) = 隔离盒子,防 agent 把宿主搞坏。LXC / Docker / VM 都是实现。
- Container(容器) = 沙箱的一种实现。
- Agent Runtime = 沙箱 + UI + 鉴权 + 持久化 + 工具桥。
ARES 的沙箱 vs 你说的"VPS + Claude Code"
| 维度 | ARES 沙箱(训练用) | VPS + Claude Code(你想做的) |
|---|---|---|
| 生命周期 | ephemeral,每任务一个,跑完就删 | 长寿命,跨会话保留 |
| 目标 | 产 reward 给训练器算梯度 | 给真人干活 |
| 谁是用户 | 训练框架(trl / verl) | 你自己 |
| 状态 | 无状态(reset 重来) | 有状态(项目 / 历史 / 凭证) |
| 隔离粒度 | 每任务独立容器 | 一个 runtime 跑一段时间 |
| UI | 无,CLI 跑分 | 必须有 |
| 类比 | 健身房器械 | 出租公寓 |
§19.2"能干任何事"的 4 个边界
能干多少事,跟你用几个 Claude Code 无关,取决于 4 个维度:
| 维度 | 决定 | 可调 |
|---|---|---|
| 🔓 沙箱权限 | 能摸什么(挂了哪些目录、有没有 sudo) | 挂 /workspace 只能祸害工作区;挂 / 能改系统 |
| 🛠 工具桥 | 能做什么(MCP / CLI 工具 / API) | 装 Playwright → 操作浏览器;装 adb → 操作手机;加 HA MCP → 控家电 |
| 🌐 外网出口 | 能碰谁(VPS 网络 / 防火墙 / VPN) | 公网全开 = 爬网页 / 调 API 都行;不通内网 = 摸不到 NAS |
| 💳 API 配额 | 能跑多久(Anthropic / OpenAI 账号限流) | 订阅等级决定长任务能不能跑通 |
默认就能干
- ✅ 写代码 / 改文件 / 跑脚本
- ✅ 部署服务 / 管 git 仓库
- ✅ 爬公网 / 调任意 API
- ✅ 长任务(几小时)
- ✅ 跨设备接续(手机 / iPad / 别的电脑)
加工具桥才能干
- 🛠 浏览器操作 → Playwright / browser-use
- 🛠 手机控制 → adb / scrcpy
- 🛠 家电控制 → Home Assistant MCP
- 🛠 邮件 / 日历 → Gmail MCP / Google Calendar MCP
- 🛠 设计 → Figma MCP / Pencil MCP
§19.3"长久记忆"的 4 层
关键事实:Claude(LLM 本身)每次 API 调用都是从 0 开始,没有任何内生记忆。 "长久记忆"完全靠文件系统 + prompt 注入。
💡 活例子:你正在读的这份报告
我(Claude Code)在这个会话里之所以"记得"你做过法考、AutoScan、LobeChat、Hermes、HiClaw、ARES 这几十个项目,知道你用 Coolify + Traefik,知道 Gitea 用户名是 kangwan 不是 kangwang ——
全部从 ~/.claude/projects/-Users-kangwan/memory/ 读出来的。这就是"长久记忆"的实体。
VPS 场景下的结论:只要沙箱内的 ~/.claude/ 目录持久化,记忆就一直在。用得越久,Claude 对你的工作习惯、项目、凭证、偏好、决策风格的了解越深。
⚠️ 三个实际的坑
- 登录凭证也在沙箱里:
~/.claude/.credentials.json—— 沙箱销毁 = 登录失效。沙箱必须私有 + 持久化,别放 ephemeral 卷。 - 多窗口并发抢 API 配额:VPS 一套 + 手机 + Mac 三个会话共享一个 Anthropic 账号的限流。
- 跟本机 Claude Code 相比:VPS 合盖不断 / 跨设备好,但 CPU 慢 / 网络 IO 慢 / VPS 供应商理论能看数据。
§19.4Claude Code vs Claude Desktop / Web
两个工具共享同一个 Claude 模型(API 层),但上层设计目标完全不同,决定了擅长什么、不擅长什么。
两种设计取向的对比
| Claude Desktop / Web | Claude Code | |
|---|---|---|
| 设计目标 | 聊天 + 多模态展示 | 工程任务 + 工具调用 |
| 前端 | ✨ 漂亮 UI / Artifacts / 图表 | 📟 终端(文字为主) |
| 上下文 | 几百 K(看订阅) | 🔥 1M token(本报告生成会话就是) |
| 长任务 | ❌ 容易丢早期内容 | ✅ 自动压缩 + 断点续传 |
| 文件系统 | ❌ 只能上传片段 | ✅ 随便读写 |
| 跑命令 | ❌ 不能 | ✅ bash / git / apt 随便 |
| 并行子任务 | ❌ 一条线 | ✅ Subagent 并发(本次报告用了 3 个) |
| MCP 生态 | ✅ 同样支持 | ✅ 同样支持 |
| 视觉资产 | ✅ Artifacts 实时渲染 | ⚠️ 要开浏览器看 |
| 连续 8 小时工作 | ❌ 会断 / 会丢 | ✅ 稳 |
为什么 Claude Code 擅长长任务(不是偶然,是设计)
1M context
1 小时对话里积累的文件 / 日志 / 截图都记得住。
自动压缩
逼近 context 极限自动摘要历史,不打断任务。
Subagent 并行
大任务拆小任务同时跑,省 token 省时间。
Task / Todo 持久化
任务列表跨轮次不丢。
Auto-memory
跨会话的偏好 / 约定记得。
File-first
长产出写文件不堆 context,下次直接读。
Hooks / Skills
自动化流程 / 钩子 / 专家模块可配置。
Plan mode
复杂任务先规划后执行,不盲目动手。
§19.5合成路径 · Desktop 的体验 + Claude Code 的能力
这是自然会冒出来的念头: 能不能把 Claude Desktop 的漂亮 UI 和 Claude Code 的长任务 / 工具 / 文件 / 稳定合在一起? 可以,有 4 条路径,工程量不等。
| 路径 | 做法 | 成本 | 优 / 劣 |
|---|---|---|---|
| 🟢 本机并用 | Claude Desktop 聊天 + 开终端跑 Claude Code。手动搬任务。 | 0(现在就能用) | 优:零成本;劣:两 session 不联动 |
| 🟡 Desktop 前台 + MCP 后台 | Desktop 配 MCP server,MCP 背后是 Claude Code / 自写 agent。Desktop 判断"需要长任务"时丢过去。 | 中(要写 MCP server) | 优:看起来像"Desktop 内嵌 Claude Code";劣:当前没有成熟现成的方案 |
| 🟠 自建 Web UI 套 Claude Code | 做 Web 聊天界面(像 LobeChat / Chatbot UI),后端跑 Claude Code。 | 中(几天 UI 开发) | 优:完全自控 / 能跑云端 / 跨设备;劣:UI 工程 |
| 🔴 魔改 Claude Code 做 Agent 平台 | Fork Claude Code,把 TUI 换成 Web UI,嵌进自己的壳。 | 高(大工程) | 优:能力最完整;劣:跟不上 Claude Code 上游迭代 |
你的最经济路径
如果已有下面这些积累(大部分开发者都有某种变体):
- 一个 Web UI 聊天壳(LobeChat / Chatbot UI / 自写的)
- 一个 VPS + 容器平台(Coolify / Dokploy / K8s)
- 熟悉 LXC / Docker / 鉴权 / 反代
那么合成路径其实就是路径 3 的简化版:
改动只在后端加一个"长任务分流到 Claude Code"的路由。UI 不动。
🧭 这就是 Manus / Devin / OpenHands / Cursor 在做的事
这些商业产品的核心逻辑都是:把 Claude Code 那种工程能力包装成 Chat UI。 你自建的好处 = 私有 + 数据留自己手里 + 不用每月付费 + 能接你特有的工具桥(公司内网 / 私有 API / 私有 MCP)。
§20阅读路径推荐
想理解 ARES,按以下顺序读最省脑。
CLAUDE.md—— 13,139 字节,仓库自带,密度比 README 高 3 倍src/ares/__init__.py—— 公开 API 清单src/ares/environments/base.py—— Environment 协议 + TimeStepsrc/ares/llms/queue_mediated_client.py—— 50 行核心,看一眼就懂src/ares/async_utils.py—— ValueAndFuture 抽象src/ares/environments/code_env.py—— 250 行 RL 主循环src/ares/code_agents/mini_swe_agent.py—— 简单 Agentsrc/ares/containers/docker.py—— 熟悉容器抽象examples/03_parallel_eval_with_api.py—— 端到端用法ares-proxy/*.go—— 跨进程版队列中介src/ares/code_agents/terminus2/terminus2_agent.py—— 1,110 行生产级 Agentsrc/ares/contrib/mech_interp/*—— 可解释性加成