diff --git a/.memory/worklog.json b/.memory/worklog.json index 5f30bab..1018f6d 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -41,6 +41,13 @@ "message": "auto-save 2026-04-24 20:14 (~2)", "hash": "b4bcbea", "files_changed": 2 + }, + { + "ts": "2026-04-24T20:20:39+08:00", + "type": "commit", + "message": "auto-save 2026-04-24 20:20 (~1)", + "hash": "22e7eed", + "files_changed": 1 } ] } diff --git a/index.html b/index.html index 2297107..47a2e33 100644 --- a/index.html +++ b/index.html @@ -1,46 +1,1411 @@ - - - ARES 源码解析(withmartian RL Agent 训练框架) - + + +ARES 源码解析 · withmartian/ares RL Agent 训练框架 + -
-

ARES 源码解析(withmartian RL Agent 训练框架)

-

withmartian/ares RL-first LLM Agent 训练与评估框架全面源码解析:Agent runtime(沙箱/工具/观察/任务)+ RL 训练接口(reward/rollout/gymnasium 协议)两半都深入,带 file:line 证据

+
+ + + +
+ +
+
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. +
  3. 随机选任务 _reset_task():110
  4. +
  5. 启动容器 _start_container():111
  6. +
  7. 启动 agent 作为独立 asyncio Task(:112)——Agent 代码线性,环境在后台跑它
  8. +
  9. 等 agent 发出第一个 LLM 请求 _get_time_step():114
  10. +
  11. 包装成 FIRST TimeStep 返回
  12. +
+ +

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

+
    +
  1. 步数 +1(:138
  2. +
  3. action 喂回 agent:_llm_req_future.set_result(action):142)—— 唤醒 agent 的 await
  4. +
  5. 等 agent 下一个 LLM 请求或 agent 任务完成(:146
  6. +
  7. 步数超限(默认 250)强制 LAST,取消 agent task(:148-153
  8. +
  9. 否则返回 MID(reward=0.0),episode 终止才算分
  10. +
+ +

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/completionsmain.go:34-59
broker.go:36-73
生成 UUID,创建 responseChan,加入 map + 队列,阻塞等(默认 15min timeout)
GET /pollmain.go:64-80
broker.go:90-102
原子读整个 requestQueue立即清空:99),返回 JSON 数组
POST /respondmain.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. +
  3. 200k token 阈值硬编码:666):2 字符 = 1 token 估算粗糙,对 Unicode 不准
  4. +
  5. ares-proxy 响应通道大小 = 1broker.go:41):agent 不及时取会延迟后续处理
  6. +
  7. Chat 与 Responses 转换器有重复(tool_choice 部分)
  8. +
  9. 增量输出定位失败兜底:453-456):rfind=-1 时输出整屏,可能重复
  10. +
  11. Docker 不支持资源配置(CPU/Memory TODO 未完成)
  12. +
  13. StatTracker 周期 60s 硬编码,无配置接口
  14. +
  15. 无 wandb / mlflow,仅 tensorboard
  16. +
+ +

💎 可抄片段 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,按以下顺序读最省脑。

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

+ 解析元信息 · 基于 commit c804aa2 · 生成日期 2026-04-24 · + 共解析 72 个 Python 文件 + 5 个 Go 文件,总 LOC 约 11,900。 +

+

+ 上游仓库 → +  ·  + 本地源码:source/ +  ·  + 主报告:.memory/source-analysis.md +

+
+ +
+
+ + +