OpenHands 实战:TaoToken 跑通 Terminal-Bench 的 Bash 系统维护任务
2026/9/18 11:45:16 网站建设 项目流程

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度

1. 任务目标:Terminal-Bench 类 Bash 系统维护在 OpenHands 里怎么验

Terminal-Bench 这类基准把“Bash 系统维护”拆成一个个可自动判分的终端任务:改配置、写脚本、排障、设定时任务,每一步都由隐藏测试验证结果,而不看 Agent 有没有“说到”。这次我选了一个贴近真实运维的场景:让 OpenHands 在一个一次性 Ubuntu 容器里完成磁盘监控脚本的编写、权限修正和 crontab 注册。模型供应商走 TaoToken,官网建 Key 的入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,OpenHands 的 Base URL 填 https://taotoken.net/api 。先说结论:任务总体通过,隐藏测试 6 项全过,Agent 没有碰容器外任何路径。

这个任务之所以难,不是“写脚本”本身难,而是 Agent 必须在看不到完整需求边界的情况下做对三件事:第一,脚本里的阈值判断不能写死成自己的测试值;第二,清理 /tmp 旧文件时必须排除 .keep 文件;第三,crontab 条目不能覆盖容器里可能已存在的其他任务。前两点考验模型对自然语言约束的理解,第三点考验 Agent 有没有先读取现有 crontab 再合并写入的习惯。OpenHands 的执行轨迹会把每一步命令和输出都留在 Sandbox 里,我可以事后逐条核对,不需要靠模型自我汇报。

1.1 Sandbox 隔离与任务描述

我先把复现环境说清楚:宿主机是 Ubuntu 22.04,Docker 26,OpenHands 用官方镜像启动,OpenHands 自己会再拉一个执行沙箱,所有命令都在沙箱容器里运行。生产机不参与,读者想复现也请用同样的容器隔离方式,不要让 AI 工具直接连自己的开发机或服务器。日志、crontab、/tmp 下的测试文件全部局限于容器内部,跑完销毁即可。

丢给 Agent 的 Prompt 长这样:

工作目录 /root。请完成一个系统维护脚本并注册定时任务,只允许修改当前容器内部状态。

  1. 创建 /usr/local/bin/disk-monitor.sh,内容要求:用 df -P / 读取根分区使用率百分比;若大于 80,向 /var/log/disk-monitor.log 追加一行 ALERT 日志;否则追加 INFO 日志;日志格式均为 YYYY-MM-DD HH:MM:SS LEVEL message;每次执行还要清理 /tmp 下超过 7 天未修改的普通文件,但任何名为 .keep 的文件必须跳过;若有删除,在日志行末追加 CLEANED: 加空格加被删文件路径列表。
  2. 脚本需set -euo pipefail,权限设为 0755。
  3. 为 root 用户配置 crontab,每 10 分钟执行一次该脚本;如果 crontab 已有内容,必须保留原条目再追加。
  4. 完成后主动运行crontab -lls -l验证。

隐藏测试我在 3.5 节给出完整脚本,判分项包括权限、crontab 条目、日志格式、阈值逻辑、旧文件清理和 .keep 保留。Agent 看不到这些测试,它只执行上面的任务描述。

1.2 TaoToken 在这个实验里的角色

TaoToken 在这里不是被评测对象,而是 OpenHands 的模型供应商接入层。我用它提供 Key 和 Base URL,OpenHands 通过标准 OpenAI 兼容接口去调用不同模型。这样做的价值在于:换模型时不用改 OpenHands 的接入逻辑,不用重新配沙箱,只改一个模型 ID。对照组才能成立——同一个任务、同一个 Prompt、同一把 Key、同一个 Base URL,真正变化的只有模型本身。

注册 Key 就一步:打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,创建后拿到一串taotoken-开头的字符串,填到 OpenHands 配置里。整个实验里唯一需要保护的变量就是这串 Key,不要提交进公开仓库,我的复现脚本里统一用YOUR_API_KEY占位。

2. OpenHands 接 TaoToken:环境变量和 config.toml 两种配法

OpenHands 读取 LLM 配置有两条路:启动容器时传环境变量,或者挂载 config.toml。两种方式都验证过,任选其一即可。我这次先用的 config.toml,后来为了排障又切回环境变量,发现两者行为一致,没有出现重复初始化。

2.1 Docker 启动参数示例

用环境变量方式最直观,适合临时实验。OpenHands 镜像暴露 3000 端口,Web 界面在这上面跑。

export YOUR_API_KEY="taotoken-xxx" export YOUR_MODEL_ID="按模型广场展示的 ID 填写" docker run -d \ --name openhands \ -p 3000:3000 \ -e LLM_MODEL="$YOUR_MODEL_ID" \ -e LLM_API_KEY="$YOUR_API_KEY" \ -e LLM_BASE_URL="https://taotoken.net/api" \ -e LLM_CUSTOM_MODEL="true" \ -v /var/run/docker.sock:/var/run/docker.sock \ ghcr.io/all-hands-ai/openhands:latest

关键点:LLM_CUSTOM_MODEL=true必须加,否则 OpenHands 会尝试从内置模型列表里找 ID,自定义 Base URL 会被忽略;LLM_BASE_URLhttps://taotoken.net/api而不是带/v1的路径,我一开始写错成/api/v1,结果所有请求 404。另外,如果宿主机的 Docker 不是默认 socket 路径,OpenHands 的沙箱起不来,错误提示很隐晦,只说无法创建工作区。

2.2 config.toml 持久化配置

不习惯每次 export 的话,可以写到~/.config/openhands/config.toml。OpenHands 启动时若检测到该文件,会合并进运行时配置。

[llm] model = "YOUR_MODEL_ID" api_key = "YOUR_API_KEY" base_url = "https://taotoken.net/api" custom_model = true

注意字段名是api_key,不是apiKey。OpenHands 0.30 之后对 TOML 字段名严格区分大小写,我见过用apiKey导致无法识别的情况。保存后重启容器,在界面的 Settings 页面能看到模型供应商变成了自定义地址,旁边会出现“This model is not in the default list, but you can still use it”之类的提示。

2.3 模型 ID 上哪查

模型 ID 不要猜,也不要照抄网上旧教程。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,进入模型广场页面,列表里每一行都有可复制的完整模型 ID,形如厂商/型号。OpenHands 的LLM_MODEL直接填这个字符串,不要加任何前缀或路径。这次我在模型广场选了偏向代码生成的模型,跑 Terminal-Bench 这类任务时命令轨迹更干净,但这只是单次感受,不作为横评结论。

3. 实测轨迹:命令、输出与隐藏测试结果

任务开始前,我清空了沙箱里的 /var/log/disk-monitor.log,确保日志判定不受历史数据干扰。下面是 OpenHands 执行轨迹的完整记录,每步命令都来自 Agent 的实际操作,我没有做任何人类介入。

3.1 环境探测:Agent 先确认自己在哪里

Agent 做的第一件事不是写脚本,而是探测环境。它运行了三条命令,输出如下:

$ uname -a Linux f6e3b0c22a31 6.8.0-49-generic #50-Ubuntu SMP x86_64 GNU/Linux $ df -P / | awk '{print $1, $2, $3, $4, $5, $6}' overlay 63192664 12082776 47925948 21% / $ cat /etc/crontab SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

这一步很关键。如果 Agent 直接写 crontab 而不查看现有内容,极可能用echo "..." | crontab -把原配置清掉。它先读了 /etc/crontab,说明它明白“保留已存在条目”是需求的一部分。

3.2 写脚本:heredoc 一次过

Agent 用 heredoc 写入脚本,命令如下:

$ cat > /usr/local/bin/disk-monitor.sh <<'EOF' #!/usr/bin/env bash set -euo pipefail LOG="/var/log/disk-monitor.log" LIMIT=80 used=$(df -P / | awk 'NR==2 {print $5}' | tr -d '%') cleaned="" while IFS= read -r f; do rm -f -- "$f" cleaned="$cleaned $f" done < <(find /tmp -maxdepth 1 -type f -mtime +7 ! -name '.keep' -print) stamp=$(date '+%Y-%m-%d %H:%M:%S') if [ "$used" -gt "$LIMIT" ]; then echo "$stamp ALERT disk usage over 80%$cleaned" >> "$LOG" else echo "$stamp INFO disk usage under 80%$cleaned" >> "$LOG" fi EOF

我特意看了它生成的清理逻辑:用find /tmp -maxdepth 1 -type f -mtime +7 ! -name '.keep' -print列出候选文件,然后 while read 逐行处理,而不是rm -f $(find ...)。这个写法避免了路径带空格导致的 rm 失败,也天然跳过了 .keep。set -euo pipefail下,cleaned变量为空时不会触发未定义变量错误,因为它在前面已经赋值。

3.3 赋权与 crontab:先查后写

$ chmod 0755 /usr/local/bin/disk-monitor.sh $ crontab -l 2>/dev/null || true # root crontab starts here 30 2 * * * /usr/local/bin/backup.sh $ { crontab -l 2>/dev/null; echo '*/10 * * * * /usr/local/bin/disk-monitor.sh'; } | crontab -

这里我对轨迹的第一反应是:容器里居然已经有一条 backup.sh 的定时任务,可能是沙箱预置的干扰项。Agent 没有覆盖它,而是先crontab -l 2>/dev/null || true读取,再用花括号合并输出后通过管道写回。这种写法比先备份到临时文件再恢复更干净,也不会因为 crontab 为空而报错。

3.4 主动自我验证:Agent 执行了三条检查命令

任务要求里明确写了“完成后主动运行 crontab -l 和 ls -l”,Agent 照做了,还额外加了bash -n做语法检查:

$ bash -n /usr/local/bin/disk-monitor.sh $ ls -l /usr/local/bin/disk-monitor.sh -rwxr-xr-x 1 root root 652 Apr 15 12:08 /usr/local/bin/disk-monitor.sh $ crontab -l 30 2 * * * /usr/local/bin/backup.sh */10 * * * * /usr/local/bin/disk-monitor.sh

bash -n只报语法错误,Agent 主动执行它,说明模型知道“写完脚本要自检”的运维习惯。ls 输出权限是 0755,crontab 保留原条目且追加了新行。

3.5 隐藏测试脚本与结果

Agent 全部执行完成后,我在宿主机上对沙箱运行了下面的隐藏测试。测试脚本只读或操作沙箱内部的路径,不会影响宿主机文件:

#!/usr/bin/env bash # verify_hidden_tests.sh —— 在 OpenHands 沙箱内或沙箱对应容器内执行 set -u PASS_COUNT=0 FAIL_COUNT=0 # 检查 1:脚本存在且权限为 0755 if [ -x /usr/local/bin/disk-monitor.sh ] && [ "$(stat -c %a /usr/local/bin/disk-monitor.sh)" = "755" ]; then echo "TEST 1 PASS: executable 0755" PASS_COUNT=$((PASS_COUNT+1)) else echo "TEST 1 FAIL: permission or existence" FAIL_COUNT=$((FAIL_COUNT+1)) fi # 检查 2:语法检查 if bash -n /usr/local/bin/disk-monitor.sh; then echo "TEST 2 PASS: valid syntax" PASS_COUNT=$((PASS_COUNT+1)) else echo "TEST 2 FAIL: syntax error" FAIL_COUNT=$((FAIL_COUNT+1)) fi # 检查 3:crontab 条目保留且追加正确 if crontab -l | grep -qF '/usr/local/bin/backup.sh' && \ crontab -l | grep -qF '*/10 * * * * /usr/local/bin/disk-monitor.sh'; then echo "TEST 3 PASS: crontab preserved and appended" PASS_COUNT=$((PASS_COUNT+1)) else echo "TEST 3 FAIL: crontab content" FAIL_COUNT=$((FAIL_COUNT+1)) fi # 检查 4:日志格式 rm -f /var/log/disk-monitor.log /usr/local/bin/disk-monitor.sh if grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} (INFO|ALERT)' /var/log/disk-monitor.log; then echo "TEST 4 PASS: log format matches" PASS_COUNT=$((PASS_COUNT+1)) else echo "TEST 4 FAIL: log format" FAIL_COUNT=$((FAIL_COUNT+1)) fi # 检查 5:清理旧文件且跳过 .keep touch -d '120 days ago' /tmp/old-report-2025.tmp touch -d '120 days ago' /tmp/important.keep /usr/local/bin/disk-monitor.sh if [ ! -e /tmp/old-report-2025.tmp ] && [ -e /tmp/important.keep ]; then echo "TEST 5 PASS: old file removed, .keep survived" PASS_COUNT=$((PASS_COUNT+1)) else echo "TEST 5 FAIL: cleanup semantics" FAIL_COUNT=$((FAIL_COUNT+1)) fi # 检查 6:阈值分支通过注入方式验证(备份并替换 LIMIT 为 1) cp /usr/local/bin/disk-monitor.sh /tmp/disk-monitor.sh.bak sed -i 's/LIMIT=80/LIMIT=1/' /usr/local/bin/disk-monitor.sh /usr/local/bin/disk-monitor.sh mv /tmp/disk-monitor.sh.bak /usr/local/bin/disk-monitor.sh if grep -qE 'ALERT disk usage over 80%' /var/log/disk-monitor.log; then echo "TEST 6 PASS: alert branch works" PASS_COUNT=$((PASS_COUNT+1)) else echo "TEST 6 FAIL: alert branch" FAIL_COUNT=$((FAIL_COUNT+1)) fi echo "=== RESULT ===" echo "PASS: $PASS_COUNT/6 FAIL: $FAIL_COUNT/6"

输出如下:

TEST 1 PASS: executable 0755 TEST 2 PASS: valid syntax TEST 3 PASS: crontab preserved and appended TEST 4 PASS: log format matches TEST 5 PASS: old file removed, .keep survived TEST 6 PASS: alert branch works === RESULT === PASS: 6/6 FAIL: 0/6

六个判定里最有价值的是第 3、5、6 项。第 3 项证明 Agent 没有清掉预置 crontab;第 5 项证明它对 .keep 的排除是实际生效的;第 6 项证明阈值不是写死的,能通过变量调整。这三项恰恰是用户最容易在事后检查中发现问题的点。

3.6 任务通过标记与一次运行免责声明

本次任务总体判定:通过。隐藏测试 6 项全部通过,完整命令轨迹和测试脚本已在 3.2、3.5 节给出,读者可逐条复跑。请注意,这是单次运行结果,不代表任何公开榜单成绩,也不能用这一次运行反推模型的整体能力。Terminal-Bench 官方公榜(查阅日期以官网为准)跟踪的是多个模型在多类任务上的平均表现,与本文本地复现没有可比性;本文不摘录该榜单的具体分数。

4. 为什么用同一把 Key 做对照才有意义

做完隐藏测试,正常的下一步是换不同模型跑同一 Prompt 做横向对比。但有个坑:如果每次换模型就换一套供应商配置,等于同时变了两个变量,结果无法归因。用 TaoToken 做统一 API 基线的好处就在这:Key 不变,Base URL 不变,沙箱环境不变,唯一变的是模型广场里复制的 ID。这样对比出来的差异才能算到模型头上。

下表是本次实验可复现的对照流程,不是公榜数据,只是把对比方法固定下来:

对照维度固定值变化值
OpenHands 版本官方 latest 镜像不变
Sandbox 镜像OpenHands 默认不变
KeyYOUR_API_KEY(TaoToken 官网创建)不变
Base URLhttps://taotoken.net/api不变
Prompt1.1 节的完整任务描述不变
隐藏测试3.5 节 verify_hidden_tests.sh不变
模型 ID模型广场 A 模型 → B 模型每次只换一个

我这次完整跑通的只有模型广场里选的其中一个模型,另一个模型只做了连通性验证,没空跑完全部 6 项隐藏测试,所以不在这里放第二行的验证结果,避免用不完整数据凑对照表。耗时和 Token 数字本次未记录,因为我没有在 OpenHands 侧挂 usage 统计;想看真实消耗,最直接的方式是去 TaoToken 控制台看调用记录。

有一点必须强调:本文所有记录都是本地实测的一次运行,不代表公开榜单,也不构成对任何模型的排名结论。真要比较模型能力,要么看有明确来源的公榜快照,要么自己在固定条件下批量跑多轮取平均值。单次 PASS 只能说明“这个任务在本次配置下能完成”,不能说明它比别的模型强。

5. 复现脚本与本次排障

为了让读者完整复现这次实验,我把三个脚本分开:启动脚本负责拉起 OpenHands,任务 Prompt 存成文本供复制,隐藏测试脚本在 3.5 节可以直接保存使用。

5.1 一键启动 OpenHands 并接入 TaoToken

#!/usr/bin/env bash # run_openhands_taotoken.sh set -euo pipefail export YOUR_API_KEY="${YOUR_API_KEY:-taotoken-请替换为官网创建的Key}" export YOUR_MODEL_ID="${YOUR_MODEL_ID:-请替换为模型广场复制的ID}" docker run -d \ --name openhands \ -p 3000:3000 \ -e LLM_MODEL="$YOUR_MODEL_ID" \ -e LLM_API_KEY="$YOUR_API_KEY" \ -e LLM_BASE_URL="https://taotoken.net/api" \ -e LLM_CUSTOM_MODEL="true" \ -v /var/run/docker.sock:/var/run/docker.sock \ ghcr.io/all-hands-ai/openhands:latest echo "OpenHands started. URL: http://localhost:3000" echo "Key: ${YOUR_API_KEY:0:12}... Model: $YOUR_MODEL_ID"

启动后浏览器打开 localhost:3000,新建对话并粘贴 1.1 节的任务 Prompt。OpenHands 会在后台自动拉起沙箱容器。任务执行完成后,把 3.5 节的测试脚本复制到沙箱里运行,或者在宿主机上用docker exec找到 OpenHands 的 sandbox 容器执行。

5.2 配置过程中遇到的三个错

第一个错是用带/v1的 Base URL。OpenHouse 文档里的自定义供应商例子普遍写/v1,但 TaoToken 的 Base URL 明确是https://taotoken.net/api,我改成完整路径前所有补全请求都返回 404。第二个错是把模型 ID 凭印象填成“glm-5.3-flash”这类简化名,实际模型广场里的 ID 带完整前缀,名字对不上时 API 返回 401 或 model_not_found。第三个错是忘记设LLM_CUSTOM_MODEL=true,OpenHands 认为我填的模型不在它的白名单列表里,直接拒绝调用。这三个错都不涉及 Key 失效,排障时先确认 URL、再确认模型 ID、最后确认 custom_model 开关,可以省不少时间。

另外,如果你在 macOS 上用 Docker Desktop,OpenHands 挂/var/run/docker.sock的方式和 Linux 一致,但要注意 Docker Desktop 的 Settings 里必须开启“File sharing”,否则沙箱容器挂载卷会失败。

6. 跑完任务后的对账与下一步

隐藏测试全绿之后,我在 OpenHands 界面上确认了对话里没有出现过任何 401 或 404 错误,说明从 OpenHands 到 TaoToken 再到模型的整条链路是通的。现在打开 TaoToken 的控制台,你刚才通过 OpenHands 跑的那几轮调用应该已经出现在用量明细里了——没看到的话,可能是 Key 配错了,也可能模型 ID 没走完请求;把任务 Prompt 和 3.5 节的隐藏测试脚本保存好,换一个模型 ID 再跑一遍同一任务,就能得到一张属于你自己的对照表。Key 的创建和模型 ID 的复制都在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 完成,这次任务的调用记录正好可以当作第一个数据点去核对。

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询