跟着跑一遍 Harbor:从 harbor run 到读懂 result.json

跟着跑一遍 Harbor:从 harbor run 到读懂 result.json

标签:#Harbor #Agent开发 #AI评测 #LLM #终端

前面四篇把概念、词汇、架构、命令都铺完了。这篇不再讲道理,我带你亲手跑一遍,再把跑出来的东西读明白。三个例子全部来自我自己机器上的真实 Job:一个过了(reward 1.0)、一个没过(reward 0.0)、还有一个直接 RuntimeError 报错。命令和每一行数字都来自 result.json,一个没编。

选这三个,不是要比谁强谁弱。terminal-bench 这类评测本身就是带随机性的采样,单次跑分说明不了模型好坏。我拿它们当「三种最常见的运行结果」来拆:你会过、你会挂、你会直接炸。学会读这三种结果,你才算真正会用 Harbor,而不是只会敲命令。


0. 跑之前准备什么

技术文章最怕不能复现。我这次的环境就两样东西:

  • 一个 Agent:terminus-2(Harbor 内置的 agent 框架),下面两趟都用它
  • 一个任务:本地 ./my-first-task 里一个 hello 冒烟任务(让 agent 在沙箱里生成一个文件)

想跟我跑一样的,把这两样备好就行。Agent 和任务锁死,剩下的变量才有意义。

flowchart TB GO["你:harbor run"] --> JOB["Harbor 在 ./jobs 下建一个时间戳目录"] JOB --> TRIAL["拉起沙箱 → agent 作答 → judge 判卷"] TRIAL --> JSON["写 result.json(分数/轨迹/异常)"] JSON --> READ["你读三层:分数 · 轨迹 · 成本"] style GO fill:#e8f3ff,stroke:#1e80ff,color:#1d2129 style JOB fill:#f0e6ff,stroke:#722ed1,color:#391085 style TRIAL fill:#fff7e8,stroke:#ff7d00,color:#d25f00 style JSON fill:#1e80ff,stroke:#1e80ff,color:#ffffff style READ fill:#e8ffea,stroke:#00b42a,color:#00875a

第一步:跑一个任务

命令就一行。我本地两趟的真实跑法(只换 --model):

bash 复制代码
# 跑通的那趟:terminus-2 agent + 本地 hello 冒烟任务
harbor run --dataset ./my-first-task \
  --agent terminus-2 \
  --model openai/doubao-seed-2-0-lite-260428 \
  --jobs-dir ./jobs

# 报错的那趟(同一个 agent,换了个模型):
harbor run --dataset ./my-first-task \
  --agent terminus-2 \
  --model openai/glm-5-2-260617 \
  --jobs-dir ./jobs

跑完 Harbor 不会在终端里甩给你一个「正确率」。它做的是在 ./jobs 下建一个以时间戳命名的目录,把这场 Trial、判卷结果、ATIF 轨迹全塞进去。想精确单跑某道题调试,用 harbor trial start -p <本地task目录>(注意是 trial 单数,-p 接路径不是 task-id)。具体 flag 以 harbor run --help 为准。

第二步:跑完去哪看

./jobs,你会看到类似这样的目录:

text 复制代码
jobs/
├── 2026-08-07__19-42-53/   # doubao run1,reward 0.0
├── 2026-08-07__20-03-58/   # doubao run2,reward 1.0
└── 2026-08-14__11-51-07/   # glm 那趟,RuntimeError

每个目录里都有 result.json。一个 Job 的全部真相都在这里。最省事的是直接 harbor view ./jobs 看 Web UI 汇总,但想写脚本、想 diff,还是得会读这个 json。

其中一个result.json文件内容如下:

json 复制代码
{
    "id": "e37a0d6d-7d47-4862-bc17-5ab81915f9ab",
    "task_name": "harbor/my-first-task",
    "trial_name": "hello__m2uKEis",
    "trial_uri": "file://XXX/my-first-task/jobs/2026-08-07__19-42-53/hello__m2uKEis",
    "task_id": {
        "path": "XXX/my-first-task/hello"
    },
    "source": "my-first-task",
    "task_checksum": "c88a7941b8aab4ae7ee250fda4b9be691179c84acf46119bd290db76e623189e",
    "config": {
        "task": {
            "path": "XXX/my-first-task/hello",
            "git_url": null,
            "git_commit_id": null,
            "name": null,
            "ref": null,
            "overwrite": false,
            "download_dir": null,
            "source": "my-first-task"
        },
        "trial_name": "hello__m2uKEis",
        "trials_dir": "jobs/2026-08-07__19-42-53",
        "install_only": false,
        "timeout_multiplier": 1.0,
        "agent_timeout_multiplier": null,
        "verifier_timeout_multiplier": null,
        "agent_setup_timeout_multiplier": null,
        "environment_build_timeout_multiplier": null,
        "agent": {
            "name": "terminus-2",
            "import_path": null,
            "model_name": "openai/doubao-seed-2-0-lite-260428",
            "n_concurrent": null,
            "concurrency_group": null,
            "skills": [],
            "override_timeout_sec": null,
            "override_setup_timeout_sec": null,
            "max_timeout_sec": null,
            "resume_trajectory": false,
            "load_trajectory": null,
            "extra_allowed_hosts": [],
            "kwargs": {},
            "mcp_servers": []
        },
        "environment": {
            "type": "docker",
            "import_path": null,
            "force_build": false,
            "delete": true,
            "cpu_enforcement_policy": "auto",
            "memory_enforcement_policy": "auto",
            "override_cpus": null,
            "override_memory_mb": null,
            "override_storage_mb": null,
            "override_gpus": null,
            "override_tpu": null,
            "mounts": null,
            "extra_docker_compose": [],
            "kwargs": {},
            "extra_allowed_hosts": []
        },
        "verifier": {
            "override_timeout_sec": null,
            "max_timeout_sec": null,
            "disable": false
        },
        "artifacts": [],
        "extra_instruction_paths": [],
        "job_id": "a18c5c53-1be1-4865-9340-b7d1051986d7"
    },
    "agent_info": {
        "name": "terminus-2",
        "version": "2.0.0",
        "model_info": {
            "name": "doubao-seed-2-0-lite-260428",
            "provider": "openai"
        }
    },
    "agent_result": {
        "n_input_tokens": 3846,
        "n_cache_tokens": 0,
        "n_output_tokens": 1961,
        "cost_usd": null,
        "rollout_details": [],
        "metadata": {
            "n_episodes": 3,
            "api_request_times_msec": [
                10138.22078704834,
                25633.883953094482,
                9491.698026657104
            ],
            "summarization_count": 0
        }
    },
    "verifier_result": {
        "rewards": {
            "reward": 0.0
        }
    },
    "exception_info": null,
    "started_at": "2026-08-07T11:43:02.409215Z",
    "finished_at": "2026-08-07T11:44:16.883833Z",
    "environment_setup": {
        "started_at": "2026-08-07T11:43:02.441041Z",
        "finished_at": "2026-08-07T11:43:05.140353Z"
    },
    "agent_setup": {
        "started_at": "2026-08-07T11:43:05.140594Z",
        "finished_at": "2026-08-07T11:43:26.123958Z"
    },
    "agent_execution": {
        "started_at": "2026-08-07T11:43:26.124529Z",
        "finished_at": "2026-08-07T11:44:13.335000Z"
    },
    "verifier": {
        "started_at": "2026-08-07T11:44:14.307660Z",
        "finished_at": "2026-08-07T11:44:14.631308Z"
    },
    "step_results": null
}

小提示:我本机这三次没接计费(后面成本那节解释),所以 result.json 里 cost_usd 全是 None。这不是 bug,是本地跑的常态。

第三步:读分数层

result.jsonstats 里,Job 级汇总直接给你 n_total_trials / n_completed_trials / n_errored_trials,每个 Trial 带一个 reward(0.0--1.0,judge 给的)。

我三个 Job 的真实数字(时间戳以目录名为准):

  • doubao run12026-08-07__19-42-53):任务 hello__m2uKEisreward = 0.0(没过),输入 3846 / 输出 1961 token,约 82 秒。
  • doubao run22026-08-07__20-03-58):任务 hello__dQHdvZbreward = 1.0(过了),输入 3655 / 输出 725 token,约 79 秒。
  • glm 那趟2026-08-14__11-51-07):任务 hello__zwnesATn_errored_trials = 1reward_stats 为空,cost_usd = None,约 74 秒。报错类型 RuntimeError,写在 exception_stats 里。

有个细节很多教程会写错:success_ratemean_reward 不是 harbor view 直接吐的字段,JobSummary 里压根没有这两个名字,得你自己从 result.json 把每个 trial 的 reward 抽出来算。派生方法:

  • success_rate = (reward == 1.0 的 trial 数) ÷ n_completed_trials
  • mean_reward = 所有 trial reward 的均值

一段 Python 就够(键名和路径都来自真实 result.json):

python 复制代码
import json
js = json.load(open("jobs/2026-08-07__20-03-58/result.json"))
evals = js["stats"]["evals"]
rk = next(iter(evals))                            # 本次 run 的 eval 键
reward_stats = evals[rk]["reward_stats"]["reward"] # {"1.0": [...], "0.0": [...]}
rewards = [float(r) for r in reward_stats]        # 每个键是一个 trial 的 reward
passed = sum(1 for r in rewards if r == 1.0)
success_rate = passed / len(rewards)             # doubao run2: 1.0 / 1 = 1.0
mean_reward = sum(rewards) / len(rewards)         # run1 则是 0.0

evals 里的 reward_stats 还直接列了每个 reward 值对应的 task-id(doubao 的 hello__m2uKEis 是 0.0、hello__dQHdvZb 是 1.0),exception_stats 则列了报错类型(glm 的 hello__zwnesAT 是 RuntimeError)。这比 success_rate 一个百分比信息量大得多.

第四步:结果是 0.0,怎么排查

reward = 0.0 不等于「agent 没动」。doubao run1 的 token 记到了(输入 3846 / 输出 1961),说明 agent 完整跑完了流程,只是没做对。这种最该拉轨迹看:

bash 复制代码
harbor view ./jobs            # Web UI 里点开某条 Trial 看 ATIF 回放
harbor traces export ./jobs --output traces/   # 导出 ATIF 做离线分析

ATIF(Agent Trajectory Interchange Format)是 Harbor 把各家 agent 的轨迹统一成同一套 schema 的标准,本质是一条 Agent → Tool → If → Feedback 的循环录像带。run1 和 run2 是同一个 hello、同一个模型,一个 0.0 一个 1.0。把两条轨迹并排,你看到的就是「姿势不同」:run1 走完了但路径不对,run2 路径对了一次过。这层结论,光看 reward 是看不出来的。

sequenceDiagram participant E as 沙箱环境 participant A as agent(terminus-2) participant J as judge 脚本 E->>A: 初始环境 + 题目(hello) A->>A: 调工具、读反馈、循环决策 A->>J: 交卷(生成 /app/hello.txt?) J->>J: test.sh 检查退出码 alt 退出码 0 J-->>A: reward 1.0 else 退出码非 0 J-->>A: reward 0.0 end

轨迹文件实在agent文件夹下面的trajectory文件中,其中一个内容如下:

json 复制代码
{
  "schema_version": "ATIF-v1.7",
  "session_id": "587a8169-961c-4179-b979-023c1df145fa",
  "agent": {
    "name": "terminus-2",
    "version": "2.0.0",
    "model_name": "openai/doubao-seed-2-0-lite-260428",
    "extra": {
      "parser": "json"
    }
  },
  "steps": [
    {
      "step_id": 1,
      "timestamp": "2026-08-07T11:43:26.406252+00:00",
      "source": "user",
      "message": 问题分析
    },
    {
      "step_id": 2,
      "timestamp": "2026-08-07T11:43:37.440717+00:00",
      "source": "agent",
      "model_name": "doubao-seed-2-0-lite-260428",
      "message": 问题分析,
      "reasoning_content": 操作理由,
      "tool_calls": [
        {
          "tool_call_id": "call_0_1",
          "function_name": "bash_command",
          "arguments": {
            "keystrokes": "ls -la\n",
            "duration": 0.1
          }
        },
        {
          "tool_call_id": "call_0_2",
          "function_name": "bash_command",
          "arguments": {
            "keystrokes": "cat numbers.txt\n",
            "duration": 0.1
          }
        },
        {
          "tool_call_id": "call_0_3",
          "function_name": "bash_command",
          "arguments": {
            "keystrokes": "sort -n numbers.txt > sorted.txt\n",
            "duration": 0.1
          }
        },
        {
          "tool_call_id": "call_0_4",
          "function_name": "bash_command",
          "arguments": {
            "keystrokes": "cat sorted.txt\n",
            "duration": 0.1
          }
        }
      ],
      "observation": {
        "results": [
          {
            "content":操作结果
          }
        ]
      },
      "metrics": {
        "prompt_tokens": 850,
        "completion_tokens": 609
      }
    },
    {
      "step_id": 3,
      "timestamp": "2026-08-07T11:44:03.547794+00:00",
      "source": "agent",
      "model_name": "doubao-seed-2-0-lite-260428",
      "message": 问题分析,
      "reasoning_content": 操作理由,
      "tool_calls": [
        {
          "tool_call_id": "call_1_task_complete",
          "function_name": "mark_task_complete",
          "arguments": {}
        }
      ],
      "observation": {
        "results": [
          {
            "content": 操作结果
          }
        ]
      },
      "metrics": {
        "prompt_tokens": 1380,
        "completion_tokens": 985
      }
    },
    {
      "step_id": 4,
      "timestamp": "2026-08-07T11:44:13.331744+00:00",
      "source": "agent",
      "model_name": "doubao-seed-2-0-lite-260428",
      "message": 问题分析,
      "reasoning_content": 操作理由,
      "tool_calls": [
        {
          "tool_call_id": "call_2_task_complete",
          "function_name": "mark_task_complete",
          "arguments": {}
        }
      ],
      "observation": {
        "results": [
          {
            "content": 操作结果
        ]
      },
      "metrics": {
        "prompt_tokens": 1616,
        "completion_tokens": 367
      }
    }
  ],
  "final_metrics": {
    "total_prompt_tokens": 3846,
    "total_completion_tokens": 1961,
    "total_cached_tokens": 0
  }
}

第五步:直接 RuntimeError,怎么读报错

glm 那趟是另一种常见问题:reward_stats 空、n_errored_trials = 1exception_stats = {'RuntimeError': ['hello__zwnesAT']}。关键信号在 token:n_input_tokens = None

token 是 None,说明 agent 在启动阶段就抛了错,连一次输入都没产生。这不代表它「不会做这道题」,是它压根没上场。这类 RuntimeError 通常是环境或初始化问题(比如 adhoc 模板跟 terminus-2 的握手没对上),能救:查 exception_stats 里的报错类型、去 ./jobs/2026-08-14__11-51-07 里看原始日志、把 agent 或任务初始化修一下再跑。

重点来了:同样是「没拿到 reward」,run1 是「动了没做对」(能力或提示问题,看轨迹),glm 这趟是「启动就炸」(环境或流程问题,看异常)。分数都不会告诉你区别,只有把分数、轨迹、异常三层合起来才分得清。这也是为什么我不拿单次跑分下结论,同一个 doubao 跑同一个 hello,run1 和 run2 就翻了盘。

第六步:把成本算上

回到账单。我本机这三次 cost_usd 全是 None(没接计费),所以美元数先空着;但 token 数给了真实口径:

text 复制代码
glm 那趟:n_input_tokens = None   → 启动报错,未产生消耗
doubao run1:3846 in / 1961 out    → 动了,没过
doubao run2:3655 in / 725  out    → 动了,过了,还比 run1 省了 ~2/3 输出

一个扎心的观察:run2 比 run1 省了快三分之二的输出 token,却从 0.0 翻到 1.0,便宜还更准。很多团队拿「平均 cost」当优化指标,结果换便宜模型后翻车姿势更野。所以别只看账单总数,要把 token 和 reward 放一起看:同样的钱,哪次真的换到了对题。

第七步:把这次跑固化成 baseline

最实用的收尾:把题和参数记进一个自定义 Dataset,下次模型升级或 Agent 改了 prompt,原样再跑一遍,两次 Job 的 result.json 放一起 diff,就知道这周是不是比上周强了。因为你知道单次有方差,diff 时也会多看几次、少被一次波动骗到。

bash 复制代码
harbor init --dataset my-baseline          # 生成 dataset.toml 骨架
harbor add ./my-first-task/hello \
  --to my-baseline/dataset.toml            # 把跑过的 hello 任务加进去
# 同题换模型,才是真正的「同一起跑线」
harbor run --dataset my-baseline --agent terminus-2 \
  --model openai/doubao-seed-2-0-lite-260428 --jobs-dir ./runs/modelA
harbor run --dataset my-baseline --agent terminus-2 \
  --model openai/glm-5-2-260617 --jobs-dir ./runs/modelB
# 想放大成真实大考(--dataset terminal-bench@2.0 --n-tasks 20),命令同构

这才是 Harbor 在团队里最该待的位置:发版前的守门员,而不是一次性的排行榜。

总结一下

跑评测不难,难的是跑完之后会读。我这篇用自己机器上三个真实 Job 带你过了一遍:一个过了(doubao run2,reward 1.0)、一个没过(doubao run1,0.0,但 agent 动了)、一个直接炸(glm 那趟 RuntimeError,token 是 None,agent 没上场)。三个结果教会你三件事:reward 0.0 要看轨迹才知道是「没做对」还是「没上场」;success_rate 得自己从 reward_stats 算;单次跑分带随机性,同题两次能翻盘,至少多跑几次看方差。把分数、轨迹、异常三层合起来,才是能拿去汇报的结论;只甩一个百分比,那是 demo,不是评测。下轮该做的,是把这套子集固化成 baseline,让每次发版都有把公平的尺。


来都来了,顺手三件事

  • 点赞:免费的奶茶,给这篇续命,也让更多被 demo 骗过的同学刷到。
  • 收藏:给你的 Agent 留条后路。哪天真翻车了,回来照着跑两遍、看三层就清醒了。
  • 关注:这个 Harbor 系列一共 8 篇,每篇都源码说话、不编数字。下一篇聊它为什么这么设计(Provider 抽象 / 沙箱隔离 / ATIF 标准,三个取舍各自放弃了什么),关注了不迷路。

评论区聊聊(任选)

  1. 你跑 Harbor / terminal-bench 时,踩过「启动就 RuntimeError、agent 没上场」的坑吗?怎么救的?聊聊你的排查故事。
  2. 你最想看我用 Harbor 把哪套任务跑成大考(terminal-bench 同题 20 道那种)?呼声高的,我下篇就照这套流程讲清楚怎么跑、怎么读 result.json。命令都在这篇里,你抄去自己跑也行。

系列连载 · 上一篇《Harbor 长什么样:一张图看懂分层架构》(架构)· 下一篇《Harbor的设计哲学:三个关键取舍》

相关推荐
AI产品测评官1 小时前
AI 招聘技术选型指南:四类垂直方案的技术路径与落地对比
人工智能·求职招聘
星火10241 小时前
【LangChain4j系列05】RAG 检索增强生成完整指南
人工智能·后端
xushichang123_1 小时前
如何观看 2026 亚马逊云科技中国峰会全场主题演讲与技术分论坛内容?
大数据·人工智能
科技新资讯1 小时前
视频出海精细化升级:帧级对口型技术与商用工具落地能力分析
人工智能·音视频
微硬创新1 小时前
大点数模拟量采集落地:耐达讯自动化32路0-20mA适配PROFINET总线实践解析
运维·人工智能·网络协议·自动化·信息与通信
招财小梗2 小时前
AI矩阵获客,品牌连锁落地有啥方案?
大数据·人工智能·矩阵
jianwuhuang822 小时前
告别手拷数据:怎么把平板电脑上Perplexity的表格导出来?详解“AI导出鸭”平板版底层逻辑
人工智能·电脑·ai导出鸭
Wang's Blog2 小时前
AI Agent白手起家57: 上下文记忆系统——基于 Redis 的长短期记忆实现
人工智能