RL-first · Agentic Research Suite

ARES 源码解析

withmartian/ares —— 把 LLM Agent 当成 RL 问题的"考场+监考系统"。 用 asyncio.Queue 拦截 Agent 的 LLM 调用,让线性 Agent 代码无感地被 RL 环境托管。 Python 8.3K 行 + Go HTTP 代理,双栈容器(Daytona / Docker),双栈 Agent(mini-swe-agent / terminus2)。

8,339
Python LOC(非测试)
3,561
测试 LOC
72
Python 文件
5
Go 文件(ares-proxy)
250→1M
步数上限范围
~50
核心模式代码行数
版本:commit c804aa2 上游withmartian/ares License:MIT Python:≥ 3.12

§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 行全框架杠杆点

QueueMediatedLLMClientasyncio.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 主业)❌ 那是另一条产品线

它在哪一层

应用层 Manus · HiClaw · OpenClaw · 手机 GUI Agent ↑ 用 LLM 干活 编排层 LangGraph · CrewAI · AutoGen ↑ 多 Agent 协作 基础设施 ★ ARES ★ ← 你在这里 ↑ RL 环境 / 沙箱 / 观察-动作适配 训练器 trl · verl · OpenPipe · unsloth ↑ PPO / GRPO / DPO 权重更新 模型 Qwen · Llama · GLM · Mistral

谁会用它

  • 做 Agent 后训练(post-training / fine-tuning)的研究员
  • SWE-bench Verified 类基准的大规模并行评估者
  • 想把自家 Agent 接入"统一评估 + 统一沙箱"的工程团队
  • withmartian 自家的 Router / Agent 产品线

§3核心抽象:Agent 当成 RL

ARES 实现了 dm_env 的 async 版本。一次 episode 就是 Agent 从接到任务到 reward 出现的全过程。

┌─────────────────────────────────────────────────────┐ │ RL Loop │ │ │ │ Env.reset() ─────────→ TimeStep(FIRST) │ │ │ ↓ │ │ │ observation = LLMRequest │ │ │ ↓ │ │ ↓ Agent LLM │ │ Env.step(action) ←───────── action = LLMResponse│ │ │ ↑ │ │ ↓ agent 继续 │ │ TimeStep(MID, reward=0) ─→ 下一个 LLMRequest │ │ │ │ │ ↓ │ │ TimeStep(LAST, reward) ← 终止(250步/完成/错误) │ └─────────────────────────────────────────────────────┘

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语义rewardobservation
FIRSTepisode 开始None(强制)首个 LLMRequest
MIDepisode 进行中0.0(稀疏奖励)下一个 LLMRequest
LASTepisode 结束/reward.* 读出None

来源:src/ares/environments/base.py:31-69

§4架构全图

从公开 API 到 Go 代理的五层栈。

┌──────────────────────────────────────────────────────────────┐ │ Public API (__init__.py) │ │ ares.make() · ares.info() · @register_env · TimeStep │ └──────────────────────────────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ Registry (registry.py) + Presets (presets.py) │ │ · HarborSpec × {mini_swe_agent, terminus2_agent} │ │ · TwentyQuestionsSpec │ │ · Selector 语法: sbv-mswea:0:10, sbv-mswea@2/8 │ └──────────────────────────────────────────────────────────────┘ │ ↓ ┌────────────────────────┐ ┌─────────────────────────────┐ │ CodeEnvironment │────→│ Container │ │ (code_env.py) │ │ ├── DaytonaContainer (云) │ │ · reset / step 主循环 │ │ └── DockerContainer (本地) │ │ · 250 步上限 │ │ Janitor (atexit 兜底) │ │ · reward 读取 │ └─────────────────────────────┘ └────────────────────────┘ │ ↓ 启动 agent 作为独立 asyncio Task ┌────────────────────────┐ ┌─────────────────────────────┐ │ CodeAgent (protocol) │────→│ LLMClient (protocol) │ │ ├── MiniSWECodeAgent │ │ ├── QueueMediatedLLMClient │ │ └── Terminus2Agent │ │ │ ← 拦截到环境 │ │ (tmux + 概括) │ │ ├── ChatCompletionClient │ └────────────────────────┘ │ ├── LlamaCppClient (本地) │ │ │ └── HookedTransformerClient │ ↓ └─────────────────────────────┘ ┌──────────────────────────────┐ │ ares-proxy (Go) │ │ HTTP 版 queue-mediated │ │ 用于跨进程/跨容器拦截 │ └──────────────────────────────┘

§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

  1. 清空步数,停旧容器(:104-108
  2. 随机选任务 _reset_task():110
  3. 启动容器 _start_container():111
  4. 启动 agent 作为独立 asyncio Task(:112)——Agent 代码线性,环境在后台跑它
  5. 等 agent 发出第一个 LLM 请求 _get_time_step():114
  6. 包装成 FIRST TimeStep 返回

step(action) 流程 code_env.py:129-161

  1. 步数 +1(:138
  2. action 喂回 agent:_llm_req_future.set_result(action):142)—— 唤醒 agent 的 await
  3. 等 agent 下一个 LLM 请求或 agent 任务完成(:146
  4. 步数超限(默认 250)强制 LAST,取消 agent task(:148-153
  5. 否则返回 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-193LAST(reward 从 /reward.* 读)
步数超限:148-153LAST(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(本地)
启动介质云 APIdocker-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/null
docker.py:83
Docker 坑点:不写 tail -f /dev/null,容器 CMD 执行完就会退出。这是常见的"容器秒退"问题的标准解法。

Janitor atexit 兜底 code_env.py:348-389

正常流程: async with env: ─→ __aenter__:注册到 _ENVIRONMENT_JANITOR ─→ ... 使用 ... ─→ __aexit__:unregister,正常清理 异常流程(进程被 kill、Ctrl-C): __init__ 时已 atexit.register(_sync_cleanup) ─→ atexit 触发:遍历所有注册环境 ─→ 每个调 container.stop_and_remove()同步) ─→ 云资源被删除,不泄露

关键设计约束: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,从被截断的响应中抢救合法标签。

坑点:200k 阈值和 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 侧(线性代码) 环境侧(RL 循环) ───────────────── ───────────────── agent.run(): response = await llm_client(req) (1) │ ┌─── env.step(action): ↓ │ put((req, future)) 入 Queue (2) │ │ │ ↓ │ await future # 挂住! (3) │ │ (4) ←─── env.q.get() 拿 (req, future) │ ↓ (5) return TimeStep(obs=req) ← 训练器收到 │ ↓ (6) 训练器算出 action(下一个 LLMResponse) │ ↓ (7) env.step(action): future.set_result(action) future 返回! (8) │ response 被赋值 │ 继续 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

每个 LLMResponsecost 字段。价目表从 Martian API 拉取(LRU 缓存),按 prompt/completion token 累加。

注意不计入 cached_tokensreasoning_tokens(Martian 当前未区分)。对 GPT-o3 / Claude 3.7 thinking 场景要另行处理。

转换层:两份而非一份

OpenAI 的 Chat Completions APIResponses API 的消息/工具结构完全不同,单一转换器写起来会很乱,所以 ARES 拆了两份各司其职。

openai_chat_converter.py (395 行)openai_responses_converter.py (435 行)
目标 APIChat CompletionsResponses
system promptmessages[0] = system 角色instructions 参数
工具调用展平成 AssistantMessage.tool_calls多态 input 数组
损失检测top_kstop_sequences>4stop_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
  • 跨进程 / 跨容器 / 跨机器都能用

三端点数据流

容器内 Agent 宿主 Environment │ │ ├──POST /v1/chat/completions──→ │ │ (阻塞等响应) │ │ │ │ ←──GET /poll──┤ │ (拿请求) │ │ │ │ │ ┌──┘ │ │ ↓ │ │ Python 环境处理 │ │ return LLMResponse │ │ │ │ │ ──POST /respond──→ │ │ │◀──────────────响应回到 Agent────────────────┘

端点实现对照

端点文件:行行为
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-58tasks[5]
SliceSelector(0, 10):62-75tasks[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-msweasbv-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: ...

三种实现

实现位置机制
NullStatTrackerstat_tracker.py:23-30无操作,生产低开销路径
LoggingStatTracker:33-62后台任务每 60s 打分位数(p0/p25/p50/p75/p100),np.percentile()
TensorboardStatTrackertensorboard.py:14-4260s 周期 SummaryWriter.add_histogram()
约束 / 坑
· 无 MLflow / wandb 集成(仅 tensorboard)
· 60s 周期硬编码
· 无接口配置
· 想接 wandb 只能自己实现 Protocol

§15mech_interp · 机制可解释性附加

ARES 不仅能跑分,还支持可解释性研究的完整闭环。这是 withmartian 的亮点附加。

三个核心组件 contrib/mech_interp/

文件作用
hooked_transformer_client.py13-140实现 LLMClient,底层 transformer-lens.HookedTransformer.generate()
activation_capture.py13-89TrajectoryActivations:列表存每步 ActivationCachetorch.save/load 持久化
hook_utils.py20-100零融合钩子(ablate 位置/头)+ 路径补丁钩子(clean → corrupted 替换做因果分析)

和训练什么关系?

不是直接训练反馈,而是 离线可解释性研究

Phase 1 · 采集 在线 rollout → HookedTransformer.run_with_cache() → 抓残差流 / 注意力 / MLP 激活 → 存盘 Phase 2 · 离线分析 训练线性探针 → 识别"无效问题"方向(residual stream 某一层)→ 验证探针可迁移 Phase 3 · 干预 在线 rollout → CAA(Contrastive Activation Addition)在目标步骤 t* 加方向 → 测量问题有效性改善(因果验证) Phase 4 · 回写 把有用的方向 / 钩子作为新能力提供给 Agent

§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。覆盖所有部署形态。

成本内置

每个 LLMResponsecost。精细计费无痛。

线程局部 httpx

规避 async 事件循环跨线程死锁的最佳实践。

Janitor atexit

云资源兜底清理,防泄露。任何管外部资源的系统都该抄。

⚠️ 坑点 8 条

  1. Terminus2Agent tmux 初始化复杂:196-319):动态 apt-get 装 tmux,建议生产镜像预装
  2. 200k token 阈值硬编码:666):2 字符 = 1 token 估算粗糙,对 Unicode 不准
  3. ares-proxy 响应通道大小 = 1broker.go:41):agent 不及时取会延迟后续处理
  4. Chat 与 Responses 转换器有重复(tool_choice 部分)
  5. 增量输出定位失败兜底:453-456):rfind=-1 时输出整屏,可能重复
  6. Docker 不支持资源配置(CPU/Memory TODO 未完成)
  7. StatTracker 周期 60s 硬编码,无配置接口
  8. 无 wandb / mlflow,仅 tensorboard

💎 可抄片段 6 条(直接能用)

片段位置适用场景
Queue-Mediated 50 行queue_mediated_client.py:47-50任何"线性代码 + 外部控制"场景:模拟器、多租户推理、游戏 AI
三级降级 Parserjson_parser.py:75-99LLM 输出解析的最佳实践模板
Tenacity 重试装饰器chat_completions_compatible.py:44-53异步 API 客户端通用重试
线程局部 httpxchat_completions_compatible.py:22-41多线程 async 场景规避事件循环冲突
Janitor atexitcode_env.py:348-389任何管外部资源(容器、临时文件、远程 session)的系统
ValueAndFutureasync_utils.py8 行泛型 dataclass,"值 + 响应 future"原子单位

§18对标 / 启发

放到 Agent + RL 生态里看,ARES 占哪块地,能给其他项目什么。

vs 其他 Agent RL 框架

ARESVerlOpenPipe Mini-ARTGymnasium
定位环境层训练器 + 环境Fine-tune + eval 一体通用 RL 标准
Agent 支持SWE + terminalSWE多场景非 LLM
沙箱Daytona + Docker自研自研
拦截机制asyncio.Queue + Go proxyRPC直接调用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 AgentTerminus2Agent 的 tmux 增量输出追踪 + 主动概括策略可迁移到 GUI 长轨迹
Hermes PersonalChatCompletionCompatibleLLMClient 的线程局部客户端 + 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 工作环境"混用。拆清楚。

┌──────────────────────────────────────────────┐ │ Agent Runtime(整套工作环境) │ │ ┌────────────────────────────────┐ │ │ │ Sandbox(隔离执行环境) │ │ │ │ ├─ Claude Code 跑这里 │ │ │ │ ├─ 工具 / bash / 文件 / git │ │ │ │ └─ ~/.claude/(记忆 + 凭证)│ │ │ └────────────────────────────────┘ │ │ ─ Web UI(用户操作入口) │ │ ─ LLM Key(Claude API) │ │ ─ 持久化(项目 / 历史 / 文件) │ │ ─ 鉴权(谁能进) │ └──────────────────────────────────────────────┘

关键区分

  • 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 跑分必须有
类比健身房器械出租公寓
结论:你问的那个东西 ≈ Cloud Agent Runtime里面包含沙箱,但不只是沙箱。和 ARES 不是同一个层次的东西。

§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
永远干不了:碰你家局域网(除非 VPN)· 操作你本机显示器 · 绕过 API 限流 · 物理世界(除非接机器人 / IoT)

§19.3"长久记忆"的 4 层

关键事实:Claude(LLM 本身)每次 API 调用都是从 0 开始,没有任何内生记忆。 "长久记忆"完全靠文件系统 + prompt 注入

┌─────────────────────────────────────────────────────────┐ │ Claude Code 的记忆分层 │ │ │ │ 1. 用户级 CLAUDE.md ~/.claude/CLAUDE.md │ │ → 每次打开 Claude Code 自动加载(所有项目共用) │ │ │ │ 2. 项目级 CLAUDE.md <project>/CLAUDE.md │ │ → 进入该项目时加载 │ │ │ │ 3. Auto-memory ~/.claude/projects/<slug>/memory/ │ │ → 当前会话就在用!每个 memory 是独立 md 文件 │ │ 索引在 MEMORY.md,自动加载 │ │ │ │ 4. 会话历史 ~/.claude/projects/<slug>/*.jsonl │ │ → 本次对话完整 transcript,下次 /continue 能接 │ └─────────────────────────────────────────────────────────┘

💡 活例子:你正在读的这份报告

我(Claude Code)在这个会话里之所以"记得"你做过法考、AutoScan、LobeChat、Hermes、HiClaw、ARES 这几十个项目,知道你用 Coolify + Traefik,知道 Gitea 用户名是 kangwan 不是 kangwang —— 全部从 ~/.claude/projects/-Users-kangwan/memory/ 读出来的。这就是"长久记忆"的实体。

VPS 场景下的结论:只要沙箱内的 ~/.claude/ 目录持久化,记忆就一直在。用得越久,Claude 对你的工作习惯、项目、凭证、偏好、决策风格的了解越深。

⚠️ 三个实际的坑

  1. 登录凭证也在沙箱里~/.claude/.credentials.json —— 沙箱销毁 = 登录失效。沙箱必须私有 + 持久化,别放 ephemeral 卷。
  2. 多窗口并发抢 API 配额:VPS 一套 + 手机 + Mac 三个会话共享一个 Anthropic 账号的限流。
  3. 跟本机 Claude Code 相比:VPS 合盖不断 / 跨设备好,但 CPU 慢 / 网络 IO 慢 / VPS 供应商理论能看数据。

§19.4Claude Code vs Claude Desktop / Web

两个工具共享同一个 Claude 模型(API 层),但上层设计目标完全不同,决定了擅长什么、不擅长什么。

┌──────────────────────────────┐ │ Claude API │ │ (Anthropic 云端模型) │ └──────────────────────────────┘ ↑ ↑ ↑ ┌────────────┼───┼───┴────────────┐ │ │ │ │ Claude Code Claude Desktop Claude.ai 网页 (CLI+工具) (桌面聊天) (浏览器) │ │ │ 自带客户端 无头 Linux 浏览器打开 能力 VPS 装不了 即可 (要 GUI)

两种设计取向的对比

Claude Desktop / WebClaude 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

复杂任务先规划后执行,不盲目动手。

观察:Claude Desktop 功能广(MCP 集成 / Artifacts / Project)但 编写程序 + 长任务 差很多。Claude Code 正相反:前端简陋,但对工程任务和持久工作有专门优化。

§19.5合成路径 · Desktop 的体验 + Claude Code 的能力

这是自然会冒出来的念头: 能不能把 Claude Desktop 的漂亮 UI 和 Claude Code 的长任务 / 工具 / 文件 / 稳定合在一起? 可以,有 4 条路径,工程量不等。

┌─────────────────────────────────────────────┐ │ 理想态: │ │ ┌─────────────────────────────────────┐ │ │ │ 前台:漂亮聊天 UI(Desktop 体验) │ │ │ │ ↓ 下发任务 │ │ │ │ 后台:Claude Code 干活 │ │ │ │ (长任务 / 工具 / 文件 / 稳) │ │ │ └─────────────────────────────────────┘ │ └─────────────────────────────────────────────┘
路径做法成本优 / 劣
🟢 本机并用 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 的简化版

Web UI 聊天壳(已有) ↓ 发消息 后端网关 ↓ 路由判断 ├─ 简单聊天 → 直接调 Claude API(现做法) └─ 复杂任务 → spawn Claude Code 进程 → 返回结果

改动只在后端加一个"长任务分流到 Claude Code"的路由。UI 不动。

🧭 这就是 Manus / Devin / OpenHands / Cursor 在做的事

这些商业产品的核心逻辑都是:把 Claude Code 那种工程能力包装成 Chat UI。 你自建的好处 = 私有 + 数据留自己手里 + 不用每月付费 + 能接你特有的工具桥(公司内网 / 私有 API / 私有 MCP)。

§20阅读路径推荐

想理解 ARES,按以下顺序读最省脑。

  1. CLAUDE.md —— 13,139 字节,仓库自带,密度比 README 高 3 倍
  2. src/ares/__init__.py —— 公开 API 清单
  3. src/ares/environments/base.py —— Environment 协议 + TimeStep
  4. src/ares/llms/queue_mediated_client.py —— 50 行核心,看一眼就懂
  5. src/ares/async_utils.py —— ValueAndFuture 抽象
  6. src/ares/environments/code_env.py —— 250 行 RL 主循环
  7. src/ares/code_agents/mini_swe_agent.py —— 简单 Agent
  8. src/ares/containers/docker.py —— 熟悉容器抽象
  9. examples/03_parallel_eval_with_api.py —— 端到端用法
  10. ares-proxy/*.go —— 跨进程版队列中介
  11. src/ares/code_agents/terminus2/terminus2_agent.py —— 1,110 行生产级 Agent
  12. src/ares/contrib/mech_interp/* —— 可解释性加成
捷径:如果只有 30 分钟时间,直接读 4 + 5 + 6 + 10(Queue-Mediated + ValueAndFuture + CodeEnvironment + ares-proxy)。这四个文件涵盖了 ARES 最精华的 80%。