综合调研

Archipelago 单任务执行全流程深度解析:从 Docker 启动到 LLM Tool Calling 的每一行代码

基于 Mercor/Archipelago 源码,逐层拆解一个 Agent 任务从环境启动、MCP 工具注册、工具发现、LLM 调用循环到评分的完整执行链路。每个环节引用具体代码片段,让你既知道'做了什么',也知道'怎么做的'。

Agent MCP Archipelago Tool Calling Benchmark 源码解析

一、问题:Agent 到底是怎么跑起来的

Archipelago 是 Mercor 开源的 Agent 评测框架(对应论文 APEX-Agents, arXiv:2601.14242),用于在 Docker 沙箱环境中运行 AI Agent 并评分。

我在读 examples/simple_task 时有一个核心困惑:给到模型的输入到底长什么样?模型怎么知道自己能调哪些工具? 这篇文档就是围绕这个问题,从源码层面逐行拆解整个执行链路。


二、全局架构:三组件 × 五阶段

三个解耦组件

┌──────────────────┐    HTTP API     ┌──────────────────┐
│  Environment     │◄───────────────►│  Agent Runner    │
│  (Docker 容器)   │   /mcp/ /apps   │  (Python + uv)   │
│  /data/populate  │   /data/snapshot│                  │
└──────────────────┘                 └──────────────────┘
                                            │
                                     快照 Diff + 轨迹
                                            ▼
                                     ┌──────────────────┐
                                     │  Grading         │
                                     │  (LLM-as-Judge)  │
                                     └──────────────────┘

五个执行阶段

阶段做什么代码入口
① 环境启动Docker compose up + 健康检查examples/simple_task/main.py → start_environment()
② 快照加载zip → tar.gz → POST /data/populatemain.py → zip_to_tar_gz() + requests.post()
③ MCP 注册POST /apps 注入 MCP Server 配置main.py → requests.post(ENV_URL + "/apps")
④ Agent 运行LLM ↔ MCP 工具调用循环runner/main.py → loop_agent/main.py
⑤ 评分前后快照 Diff + LLM Judgegrading/runner/main.py

三、阶段 ①②③:环境准备(main.py 编排脚本)

examples/simple_task/main.py 是端到端编排入口,核心流程:

3.1 环境启动

def start_environment():
    # 强制清除旧容器,保证干净状态
    subprocess.run(["docker", "compose", "down", "-v"], cwd=ENVIRONMENT_DIR)
    # 重新构建并启动
    subprocess.run(["docker", "compose", "up", "-d", "--build"], cwd=ENVIRONMENT_DIR)
    # 等待健康检查通过(轮询 /health,最长 120 秒)
    wait_for_health(ENV_URL)

每次运行都 down -v 再 up --build,保证环境幂等。

3.2 快照加载

# 将 original_snapshot.zip 转换为 tar.gz(去除 "filesystem/" 前缀)
world_tar_gz = zip_to_tar_gz(EXAMPLE_DIR / "original_snapshot.zip")

# 上传到环境容器
requests.post(
    f"{ENV_URL}/data/populate",
    files={"archive": ("world.tar.gz", f, "application/gzip")},
    params={"subsystem": "filesystem"},
)

subsystem=filesystem 告诉环境容器把数据加载到 /filesystem 目录。对于 simple_task 示例,加载后容器内的文件系统如下:

/filesystem/animals/
  xk92m/qz7fw.png  (gorilla - 目标)
  ab47z/rt3ky.png  (cat)
  mn83p/jw9vb.png  (elephant)
  jh21q/pl4nc.png  (penguin)

3.3 MCP 服务器注册

with open(EXAMPLE_DIR / "mcp_config.json") as f:
    mcp_config = json.load(f)

resp = requests.post(f"{ENV_URL}/apps", json=mcp_config)

mcp_config.json 的内容:

{
  "mcpServers": {
    "filesystem-server": {
      "transport": "stdio",
      "command": "python",
      "args": ["main.py"],
      "cwd": "./mcp_servers/filesystem_server",
      "env": {"APP_FS_ROOT": "/filesystem"}
    }
  }
}

POST /apps 做了什么:环境容器内部启动 filesystem-server 作为子进程(stdio 传输),并将其注册到 MCP Gateway。此后所有通过 /mcp/ 端点的请求都会被路由到这个 server。

关键约束:transport 必须是 "stdio";cwd 必须是容器内有效路径。


四、阶段 ④:Agent 运行——核心链路

这是最关键的部分。从编排脚本发出的命令:

uv run python -m runner.main \
  --trajectory-id "task_001" \
  --initial-messages ./initial_messages.json \
  --mcp-gateway-url "http://localhost:8080/mcp/" \
  --agent-config ./agent_config.json \
  --orchestrator-model "google/gemini-2.5-flash" \
  --output ./trajectory.json

4.1 入口:runner/main.py

async def main(
    trajectory_id: str,
    initial_messages: list[dict[str, Any]],
    mcp_gateway_url: str | None,
    mcp_gateway_auth_token: str | None,
    agent_config: AgentConfig,
    orchestrator_model: str,
    orchestrator_extra_args: dict[str, Any] | None,
    ...
) -> AgentTrajectoryOutput:

    # 1. 从注册表获取 agent 实现
    agent_impl = get_agent_impl(agent_config.agent_config_id)

    # 2. 构造运行输入
    run_input = AgentRunInput(
        trajectory_id=trajectory_id,
        initial_messages=cast(list[LitellmInputMessage], initial_messages),
        mcp_gateway_url=mcp_gateway_url,
        mcp_gateway_auth_token=mcp_gateway_auth_token,
        orchestrator_model=orchestrator_model,
        orchestrator_extra_args=orchestrator_extra_args,
        agent_config_values=agent_config.agent_config_values,
    )

    # 3. 带超时保护地执行 agent
    async with asyncio.timeout(settings.AGENT_TIMEOUT_SECONDS):
        output = await agent_impl(run_input)

get_agent_impl 如何工作(registry.py):

from runner.agents.loop_agent.main import run as loop_agent_run

AGENT_REGISTRY: dict[AgentConfigIds, AgentDefn] = {
    AgentConfigIds.LOOP_AGENT: AgentDefn(
        agent_config_id=AgentConfigIds.LOOP_AGENT,
        agent_impl=loop_agent_run,
        agent_config_fields=[
            TaskFieldSchema(field_id="timeout", default_value=10800),
            TaskFieldSchema(field_id="max_steps", default_value=100),
            TaskFieldSchema(field_id="tool_call_timeout", default_value=60),
            TaskFieldSchema(field_id="llm_response_timeout", default_value=600),
        ],
    ),
}

注册表模式——通过 agent_config_id 字符串查找对应的异步函数。loop_agent 的 run() 就是整个 Agent 循环的入口。

4.2 LoopAgent 初始化

class LoopAgent:
    def __init__(self, run_input: AgentRunInput):
        self.model = run_input.orchestrator_model
        self.messages = list(run_input.initial_messages)

        # 构建 MCP 客户端
        self.mcp_client = FastMCPClient(
            build_mcp_gateway_schema(
                run_input.mcp_gateway_url,
                run_input.mcp_gateway_auth_token,
            )
        )

build_mcp_gateway_schema(utils/mcp.py)做了什么:

def build_mcp_gateway_schema(
    mcp_gateway_url: str,
    mcp_gateway_auth_token: str | None,
) -> dict:
    gateway_config = {
        "transport": "streamable-http",
        "url": mcp_gateway_url,
    }
    if mcp_gateway_auth_token:
        gateway_config["headers"] = {
            "Authorization": f"Bearer {mcp_gateway_auth_token}"
        }
    return {"mcpServers": {"gateway": gateway_config}}

注意这里 transport 是 "streamable-http"——Agent Runner 通过 HTTP 连接 MCP Gateway,而 Gateway 内部通过 stdio 连接各个 MCP Server。两层传输协议不同。

4.3 工具发现:从 MCP 到 OpenAI Tool 格式

这就是”模型怎么知道有哪些工具”的答案。

async def _initialize_tools(self) -> None:
    async with self.mcp_client as client:
        tools: list[ChatCompletionToolParam] = await load_mcp_tools(
            client.session, format="openai"
        )
    logger.info(f"Loaded {len(tools)} MCP tools")
    self.tools = tools

load_mcp_tools 是 LiteLLM 的实验性 API,它:

  1. 通过 MCP 协议调用 tools/list 方法
  2. 获取 MCP Server 暴露的所有工具定义
  3. 自动转换为 OpenAI function calling 格式(format="openai")

转换前(MCP 原生格式):

{
  "name": "read_file",
  "description": "Read contents of a file",
  "inputSchema": {
    "type": "object",
    "properties": {"path": {"type": "string"}},
    "required": ["path"]
  }
}

转换后(OpenAI Tool 格式,即传给 LLM 的):

{
  "type": "function",
  "function": {
    "name": "read_file",
    "description": "Read contents of a file",
    "parameters": {
      "type": "object",
      "properties": {"path": {"type": "string"}},
      "required": ["path"]
    }
  }
}

差异很小——inputSchema 变成了 parameters,外面包了一层 type: "function" + function: ...。但这个转换是关键的:MCP 协议和 LLM function calling 协议是两个不同的标准,LiteLLM 在中间做了桥接。

4.4 核心循环:LLM 调用 → 工具执行 → 结果追加

async def run(self) -> AgentTrajectoryOutput:
    async with asyncio.timeout(self.timeout):  # 总超时 3 小时
        await self._initialize_tools()          # 加载工具

        for i in range(self.max_steps):         # 最多 100 步
            if self._finalized:
                break
            await self.step()

        if not self._finalized:
            self.status = AgentStatus.FAILED     # 100 步没完成 = 失败
        else:
            self.status = AgentStatus.COMPLETED

每一步 step() 的核心逻辑:

Step 4.4.1:调用 LLM

async def step(self):
    response = await generate_response(
        self.model,       # "google/gemini-2.5-flash"
        self.messages,    # 完整对话历史
        self.tools,       # OpenAI 格式的工具列表
        self.llm_response_timeout,
        self.extra_args,
    )

generate_response(utils/llm.py)的实现:

@with_retry(max_retries=10, base_backoff=5, jitter=5, retry_on=(
    RateLimitError, Timeout, BadRequestError, ServiceUnavailableError, ...
))
async def generate_response(model, messages, tools, llm_response_timeout, extra_args, ...):
    kwargs = {
        "model": model,
        "messages": messages,
        "timeout": llm_response_timeout,
        **extra_args,
    }
    if tools:
        kwargs["tools"] = tools    # 有工具时才传

    response = await acompletion(**kwargs)  # LiteLLM 的异步调用
    return ModelResponse.model_validate(response)

这就是传给模型的实际输入——messages(对话历史)+ tools(工具列表),通过 LiteLLM 的 acompletion 发给任意 LLM Provider。LiteLLM 会自动把 OpenAI 格式的 tools 转成各 Provider 的原生格式(Anthropic 的 tool_use、Gemini 的 function_declarations 等)。

重试策略:最多 10 次,指数退避 5 秒 + 5 秒抖动。上下文窗口溢出(ContextWindowExceededError)不重试。

Step 4.4.2:解析 LLM 响应

    response_message = LitellmOutputMessage.model_validate(choices[0].message)
    tool_calls = getattr(response_message, "tool_calls", None)

    # 兼容多种模型的思考过程
    if getattr(response_message, "reasoning_content", None):  # o1/o3
        logger.bind(message_type="reasoning").info(...)
    if getattr(response_message, "thinking_blocks", None):    # Claude
        for block in response_message.thinking_blocks:
            logger.bind(message_type="thinking").debug(...)

Step 4.4.3:执行工具调用

    if tool_calls:
        deferred_image_messages = []

        async with self.mcp_client as client:  # 每轮新建 MCP 会话
            for tool_call in tool_calls:
                try:
                    call_result = await asyncio.wait_for(
                        call_openai_tool(client.session, tool_call),
                        timeout=self.tool_call_timeout,  # 60 秒
                    )
                except TimeoutError:
                    self.messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": "Tool call timed out",
                    })
                    continue

                # MCP 结果 → LiteLLM 消息格式
                messages = content_blocks_to_messages(
                    call_result.content,
                    tool_call.id,
                    tool_call.function.name,
                    self.model,
                    deferred_image_messages=deferred_image_messages,
                )
                self.messages.extend(messages)

        self.messages.extend(deferred_image_messages)  # 图片延迟追加

call_openai_tool 做了什么:把 OpenAI 格式的 tool_call(name + arguments)转成 MCP 的 tools/call 请求,发给 MCP Gateway,Gateway 路由到对应的 MCP Server 执行。

content_blocks_to_messages(utils/mcp.py)处理了一个重要的兼容性问题:

def content_blocks_to_messages(content_blocks, tool_call_id, name, model, ...):
    supports_image_tool_results = model.startswith("anthropic/")

    for content_block in content_blocks:
        match content_block:
            case TextContent():
                text_contents.append(block.text)
            case ImageContent():
                data_uri = f"data:{block.mimeType};base64,{block.data}"
                image_data_uris.append(data_uri)

    if supports_image_tool_results:
        # Anthropic: 图片直接嵌入 tool result
        content.append({"type": "image_url", "image_url": {"url": data_uri}})
    else:
        # 其他模型: 图片不能放在 tool result 里
        # 延迟追加为 user 角色的消息
        deferred_image_messages.append({
            "role": "user",
            "content": [{"type": "image_url", ...}],
        })

Anthropic 支持在 tool result 中嵌入图片,OpenAI 和 Gemini 不支持。所以非 Anthropic 模型的图片被单独提出来作为 user 消息追加。

Step 4.4.4:任务完成判定

    else:  # 没有 tool_calls
        self._finalized = True
        finalize_answer(response_message.content or "No content")

极简判定:LLM 不再请求任何工具 → 认为任务完成。

4.5 一个完整的 messages 演进示例

以 simple_task(找大猩猩图片)为例,messages 数组在循环过程中大致如此演进:

第 0 步(初始):

[
  {"role": "user", "content": "Find the path to the gorilla image in the filesystem."}
]

第 1 步(LLM 决定列目录):

[
  {"role": "user", "content": "Find the path to the gorilla image..."},
  {"role": "assistant", "tool_calls": [
    {"id": "tc_1", "function": {"name": "list_directory", "arguments": "{\"path\": \"/animals\"}"}}
  ]},
  {"role": "tool", "tool_call_id": "tc_1", "content": "[\"xk92m\", \"ab47z\", \"mn83p\", \"jh21q\"]"}
]

第 2-5 步(LLM 逐个子目录探索):

[
  ... (前面的消息),
  {"role": "assistant", "tool_calls": [
    {"id": "tc_2", "function": {"name": "read_file", "arguments": "{\"path\": \"/animals/xk92m/qz7fw.png\"}"}}
  ]},
  {"role": "tool", "tool_call_id": "tc_2", "content": "(图片内容/描述)"}
]

最终步(LLM 给出答案,不再调用工具):

[
  ... (所有历史消息),
  {"role": "assistant", "content": "The gorilla image is at /animals/xk92m/qz7fw.png"}
]

此时 tool_calls 为 None → self._finalized = True → 循环结束。


五、阶段 ⑤:评分

Agent 完成后,编排脚本:

  1. 从环境容器获取最终快照 POST /data/snapshot
  2. 调用 grading runner,输入:
    • original_snapshot.zip(初始状态)
    • final_snapshot.zip(最终状态)
    • trajectory.json(Agent 行为轨迹)
    • verifiers.json(评分标准)

评分系统使用 LLM-as-Judge 模式,根据 verifiers 定义的 criteria 评估 Agent 是否达成目标。

verifiers.json 示例:

[
  {
    "verifier_id": "ver_001",
    "verifier_values": {
      "criteria": "The agent found the gorilla image path",
      "is_primary_objective": true
    }
  }
]

最终输出 grades.json,包含分数和评判理由。


六、关键设计决策总结

6.1 三层超时保护

层级位置默认值作用
L1 工具调用级asyncio.wait_for(call_openai_tool(...))60s单个 MCP 工具卡死不影响整体
L2 LLM 响应级generate_response 的 timeout 参数600s单次 LLM 调用超时
L3 总运行级asyncio.timeout(self.timeout)10800s (3h)整个 Agent 生命周期

6.2 错误处理策略

错误类型处理方式
LLM 空响应自动追加 "continue" 消息让 LLM 继续
工具调用超时记录 “Tool call timed out”,跳过继续
非致命 MCP 错误记录错误信息,跳过该工具继续
致命 MCP 错误直接 raise,终止 Agent
LLM API 限流/5xx指数退避重试,最多 10 次
上下文窗口溢出不重试,直接抛出

6.3 多模型兼容

差异点处理方式
Tool 格式LiteLLM 统一转换(OpenAI ↔ Anthropic ↔ Gemini)
图片 in tool resultAnthropic 直接嵌入;其他模型延迟追加为 user 消息
推理过程reasoning_content(o1/o3)和 thinking_blocks(Claude)都记录

七、一张图看完全链路

examples/simple_task/main.py
│
├─① docker compose up → Environment 容器 (8080)
│
├─② POST /data/populate ← original_snapshot.zip → tar.gz
│
├─③ POST /apps ← mcp_config.json
│     └→ 容器内启动 filesystem-server (stdio)
│     └→ 注册到 MCP Gateway (/mcp/)
│
├─④ uv run python -m runner.main
│     │
│     ├─ runner/main.py
│     │   └─ get_agent_impl("loop_agent") → LoopAgent
│     │
│     ├─ LoopAgent.__init__()
│     │   └─ FastMCPClient(build_mcp_gateway_schema(url))
│     │       └→ {"mcpServers": {"gateway": {"transport": "streamable-http", "url": ".../mcp/"}}}
│     │
│     ├─ _initialize_tools()
│     │   └─ load_mcp_tools(session, format="openai")
│     │       └→ MCP tools/list → OpenAI ChatCompletionToolParam[]
│     │
│     ├─ for step in range(100):
│     │   │
│     │   ├─ generate_response(model, messages, tools, ...)
│     │   │   └─ litellm.acompletion(model=..., messages=..., tools=...)
│     │   │       └→ LLM 看到 messages + tools,决定调用哪个工具
│     │   │
│     │   ├─ if tool_calls:
│     │   │   └─ call_openai_tool(session, tool_call)
│     │   │       └→ MCP tools/call → filesystem-server 执行
│     │   │       └→ content_blocks_to_messages() → 追加到 messages
│     │   │
│     │   └─ else: _finalized = True (任务完成)
│     │
│     └─ 输出 trajectory.json
│
└─⑤ POST /data/snapshot → final_snapshot.zip
     └─ uv run python -m grading/runner/main.py
         └→ 对比快照 + LLM Judge → grades.json

附录:核心源码文件索引

文件职责关键函数/类
examples/simple_task/main.py端到端编排start_environment(), main()
agents/runner/main.pyAgent 运行入口main()
agents/runner/agents/registry.pyAgent 注册表get_agent_impl(), AGENT_REGISTRY
agents/runner/agents/loop_agent/main.py循环 Agent 实现LoopAgent, step(), _initialize_tools()
agents/runner/agents/models.py数据模型AgentRunInput, AgentTrajectoryOutput
agents/runner/utils/llm.pyLLM 调用封装generate_response()
agents/runner/utils/mcp.pyMCP 工具函数build_mcp_gateway_schema(), content_blocks_to_messages()

仓库地址:github.com/Mercor-Intelligence/archipelago 关联论文:APEX-Agents (arXiv:2601.14242)