Prhub

#2806 examples: in-process Harbor agent function

原始 PR 作者 nblintao 合并时间 2026-09-09 06:14 文件变更 5 提交数 7 评论 8 代码增减 +864 / -0

执行摘要

新增进程内 Harbor 代理函数,让 rollout worker 直接在云端沙箱运行 Harbor 试验。

为实现更轻量、直接的 agentic 训练,需要让 rollout worker 能自身驱动 Harbor 试验,而不是依赖额外的代理服务器。PR body 明确指出:'Let a rollout worker run a Harbor trial itself when the task sandbox is on a cloud backend'。这消除了本地 Docker 依赖,利用云端沙箱的网络可达性,简化了架构并降低了运维复杂度。

该 PR 值得精读。它引入了重要的新架构路径,并展示了良好的设计模式:将配置构建、试验执行和结果映射职责清晰分离。测试策略(使用 fake 包进行快速迭代,同时用真实包进行契约验证)尤其值得借鉴。核心实现 harbor_agent_function.py 的注释详尽,解释了每个决策的权衡(如为什么 abort 策略优于 recover)。审阅者发现的 total_tool_time 缺失和 HARBOR_AGENT_MAX_ITERATIONS=0 问题是实际集成中可能出现的真实 bug,具有学习价值。

讨论亮点
  1. URL 后缀冲突:claude[bot] 发现 ANTHROPIC_BASE_URL 会因 session URL 已带 /v1 后缀而与 SDK 自身添加的 /v1/messages 冲突,导致 404。nblintao 后续修复此问题,使用 session_url.removesuffix("/v1")。
  2. 环境变量值为 0 的处理:claude[bot] 指出 if max_turns := _env_int("HARBOR_AGENT_MAX_ITERATIONS") 的写法会将值为 0 的情况视为 falsy,导致配置被忽略。这是一个潜在的语义错误。
  3. 缺失关键指标:claude[bot] 指出 agent_metrics 中未提供 total_tool_time 字段,会导致上游的吞吐量计算(non_generation_time)始终为 0,造成性能指标失真。
  4. Daytona 资源清理:Shi-Dong 询问 Daytona 后端是否需要自动清理参数,nblintao 确认需要,并提到有单独 PR (#2838) 处理此问题。
  5. 设计决策:配置错误(如未知环境类型、缺失任务目录)选择直接抛出异常,而不是返回零分。PR body 解释这比“silent all-zero rewards”更优,能快速失败,避免无意义的训练。

实现拆解

  1. 核心入口实现:新增 examples/experimental/harbor/harbor_agent_function.py,包含主函数 run() 和配置构建函数 build_trial_config()。run() 是 agentic_tool_call.generate 的入口点,负责构建配置、运行试验、处理超时并返回结果字典。
  2. 绑定与配置:定义了 HarnessBinding 数据类和 HARNESS_BINDINGS 字典,为每个支持的 agent(terminus-2, claude-code, opencode 等)提供特定的 kwargs 和 env 配置逻辑。例如,_terminus_kwargs 确保 API 密钥通过 llm_kwargs 传递,并设置 response_length_exceeded_policy 为 'abort'。
  3. 环境与凭证处理:通过 _environment_config() 和 _ensure_provider_key() 函数,将环境变量(如 HARBOR_ENV_TYPE)直接映射到 Harbor 的 EnvironmentType,并解析启动器通过文件路径传递的密钥。
  4. 结果映射:trial_result_to_metadata() 函数将 Harbor 的试验结果(包括奖励、耗时、异常)转换为训练管道期望的样本元数据格式,并定义了异常到 exit_status 的映射规则(如 AgentTimeoutError -> 'TimeLimitExceeded')。
  5. 测试配套:新增两个测试文件:test_harbor_agent_function.py 使用 fake harbor 包进行离线单元测试,覆盖配置构建、结果映射和入口行为;test_harbor_contract.py 是针对真实 harbor 包的契约测试,确保构建的配置能被 Harbor 的 Pydantic 模型验证通过。
文件 模块 状态 重要度
examples/experimental/harbor/harbor_agent_function.py 代理函数 added 9.18
tests/fast/examples/experimental/harbor/test_harbor_agent_function.py 代理函数测试 added 8.15
tests/fast/examples/experimental/harbor/test_harbor_contract.py 契约测试 added 7.58

关键符号

run build_trial_config trial_result_to_metadata _terminus_kwargs _claude_code_kwargs _opencode_kwargs _ensure_provider_key _environment_config

关键源码片段

examples/experimental/harbor/harbor_agent_function.py dependency-wiring

核心实现文件,定义了进程内 Harbor 试验的完整流程,包括配置构建、试验运行、凭证解析和结果映射。

# --- harness bindings -------------------------------------------------------
# 定义每个 harness 需要的连接配置:如何传递 session URL 和 API 密钥。
# 此表是与 harbor-framework/harbor 的同步锚点。
@dataclass(frozen=True)
class HarnessBinding:
    # (session_url, api_key, sampling_params, model, max_seq_len) -> AgentConfig.kwargs
    kwargs: Callable[[str, str, dict[str, Any], str, int | None], dict[str, Any]]
    # (session_url, api_key) -> AgentConfig.env
    env: Callable[[str, str], dict[str, str]]
    # the model name Harbor passes to the harness
    model_name: Callable[[str], str] = lambda model: model
​
​
# terminus-2 配置:必须设置 abort 策略以避免与 TITO session server 的 desync。
def _terminus_kwargs(
    session_url: str, api_key: str, sampling_params: dict[str, Any], model: str, max_seq_len: int | None
) -> dict[str, Any]:
    kwargs: dict[str, Any] = {
        "parser_name": "xml",
        "interleaved_thinking": True,
        "abort_on_response_length_exceeded": True,
        "llm_call_kwargs": dict(sampling_params),
        "api_base": session_url,
        # Terminus2 has no top-level api_key parameter: the key must ride
        # llm_kwargs (the LLM constructor extras, spread into every litellm
        # call) or litellm falls back to OPENAI_API_KEY in the process env.
        "llm_kwargs": {"api_key": api_key},
        "enable_summarize": _env_flag("HARBOR_TERMINUS_2_ENABLE_SUMMARIZE"),
        "trajectory_config": {"linear_history": _env_flag("HARBOR_TERMINUS_2_LINEAR_HISTORY")},
        # "abort" ends the trajectory when one reply is truncated at max_tokens;
        # upstream's "recover" re-sends the truncated reply as a fabricated
        # assistant turn, which desyncs the TITO session server.
        "response_length_exceeded_policy": os.getenv("HARBOR_RESPONSE_LENGTH_POLICY", "abort"),
    }
    if max_turns := _env_int("HARBOR_AGENT_MAX_ITERATIONS"):
        kwargs["max_turns"] = max_turns
        kwargs["suppress_max_turns_warning"] = True
    return kwargs
​
​
# OpenCode 需要重命名 provider id 以避免使用 Responses API。
_HARNESS_BINDINGS: dict[str, HarnessBinding] = {
    "terminus-2": HarnessBinding(kwargs=_terminus_kwargs, env=_openai_env),
    "claude-code": HarnessBinding(kwargs=_claude_code_kwargs, env=_claude_code_env),
    "opencode": HarnessBinding(
        kwargs=_opencode_kwargs,
        env=_opencode_env,
        model_name=lambda model: "/".join(_opencode_provider_model(model)),
    ),
    # ...
}
tests/fast/examples/experimental/harbor/test_harbor_agent_function.py test-coverage

核心单元测试,通过 fake harbor 包验证配置构建、结果映射和入口行为,无需真实依赖。

# --- result mapping --------------------------------------------------------
# 测试 Harbor 异常如何映射到训练管道的 exit_status 词汇。
@pytest.mark.parametrize(
    ("exc_type", "exit_status"),
    [
        ("AgentTimeoutError", "TimeLimitExceeded"),
        ("EnvironmentStartTimeoutError", "TimeLimitExceeded"),
        ("SingleTurnMaxSeqLenExceededError", "SequenceLengthLimitExceeded"),
        ("RuntimeError", "AgentError"),
    ],
)
def test_harbor_exceptions_map_to_the_exit_status_vocabulary(exc_type, exit_status):
    result = SimpleNamespace(
        exception_info=SimpleNamespace(exception_type=exc_type), verifier_result=None, agent_result=None
    )
    out = haf.trial_result_to_metadata(result)
    assert out["reward"] == 0.0
    assert out["exit_status"] == exit_status
​
​
# --- entry -----------------------------------------------------------------
# 测试主入口函数:配置错误应抛出异常,而不是返回零分。
def test_run_raises_on_a_missing_task_instead_of_scoring_zero(tasks_dir, fake_harbor):
    """A config error fails every sample; raising beats training on silent all-zero rewards."""
    with pytest.raises(FileNotFoundError):
        run_async(haf.run("http://s/sessions/s1", [], {}, {"instance_id": "missing"}))

评论区精华

ANTHROPIC_BASE_URL 的 /v1 后缀冲突 正确性

`claude[bot]` 指出 `ANTHROPIC_BASE_URL` 会因 session URL 已带 `/v1` 后缀而与 SDK 自身添加的 `/v1/messages` 冲突,导致 404。这是一个正确性 bug。

结论:作者回复“fixed”,在后续提交中使用 `session_url.removesuffix("/v1")` 修复。 · 已解决

HARBOR_AGENT_MAX_ITERATIONS=0 的 falsy 语义 正确性

`claude[bot]` 指出 `if max_turns := _env_int("HARBOR_AGENT_MAX_ITERATIONS")` 会将值为 0 的情况视为 falsy,导致配置被忽略,这是一个潜在的语义错误。

结论:未解决。该问题在 PR 评论中被标记为 nit,但未在本次合并中修复。 · 待处理

agent_metrics 中缺失 total_tool_time 指标 性能

`claude[bot]` 指出 `trial_result_to_metadata` 未提供 `total_tool_time` 字段,导致上游的吞吐量计算(`non_generation_time`)始终为 0,性能指标失真。

结论:未解决。该问题被标记为 optional,但会影响监控和资源评估的准确性。 · 待处理

Daytona 后端是否需要自动清理参数 question

`Shi-Dong` 询问在使用此 agent function 运行 Daytona 时,是否仍需设置自动清理参数(如 `auto_stop_interval_mins`)。

结论:作者确认需要,并提到有单独的 PR (#2838) 处理此问题。 · 已解决

风险与影响

  1. 集成风险:total_tool_time 指标的缺失会影响训练过程中的吞吐量监控和资源评估,可能掩盖真实的性能问题。
  2. 配置错误行为:未知 HARBOR_ENV_TYPE 直接抛异常(不同于 agent-server 的静默回退到 docker)是更安全的设计,但需要确保所有调用方已知悉此变更。
  3. 依赖风险:该实现严重依赖 Harbor 分支 (harbor-miles-*) 的特定 API(如 response_length_exceeded_policy、MaxSeqLenExceededError)。Harbor 上游变更可能导致不兼容。
  4. 状态管理:FakeTrial 类使用类级列表 created 来追踪试验创建,这在并行测试中可能导致状态污染,尽管目前是串行运行。

用户影响:为 Miles 用户提供了运行 agentic 试验的新选项,特别适用于云端沙箱环境,降低了本地部署门槛。
系统影响:在 rollout worker 中引入了新的依赖路径(harbor 包),并扩展了 agentic_tool_call.generate 的功能。这是 agentic 训练基础设施的重要扩展。
团队影响:该 PR 是多 PR 重构系列的一部分(#2805, #2804, #2836, #2837),为 Harbor 集成的完整实现奠定了基础。测试策略(fake 包 + 契约测试)为类似集成提供了参考。

核心路径变更 外部依赖漂移 监控指标缺失

关联 Issue

#2876 ci(e2e): Harbor trials on real e2b sandboxes through the full rollout path (manual for now)

完整报告

参与讨论