🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度
1. OpenHands 打开 pallets/click:typing 补全目标与沙箱边界
这次任务不是让 OpenHands 随便改 click,而是用 Agent 实战方式跑一个可验证的 typing 补全:在本地 clone 的 pallets/click 仓库里,让 OpenHands 先跑 mypy,找出几个缺失或过宽的类型注解,只动 src/click/_compat.py、src/click/types.py、src/click/core.py 里的签名,不碰运行时逻辑。TaoToken 是这次使用的统一 API 通道,落地页 https://taotoken.net/?utm_source=taotoken_aicg_blog_generate&utm_content= 里有模型广场和控制台入口;OpenHands 侧只需要把 Base URL 写成 https://taotoken.net/api,再在界面里选 Qwen3.8 Max。这样做的原因很简单:typing 补全属于“改得少、验证强”的任务,mypy 和 pytest 能立刻告诉你 Agent 有没有把签名改坏,比较适合观察多轮工具调用里的 Token 消耗。
pallets/click 这个仓库适合做 OpenHands 的练手靶子。它是纯 Python 项目,依赖相对少,测试结构清晰,src/click 下的模块划分也稳定;同时 click 已经有不少类型注解,Agent 不能靠“全文件重写”蒙混过关,只能逐段读取、定位缺失签名、生成小补丁。我把任务范围压到三个文件:_compat.py 里补 strip_ansi、term_len 这类工具函数的参数和返回类型,types.py 里补 __repr__ 和转换方法的返回类型,core.py 里补 Command.__call__ 这类入口方法的 *args、**kwargs 注解。这样 OpenHands 每轮工具调用都有明确目标,最终 git diff 也不会变成无法审查的大块重构。
OpenHands 在这里是 Agent harness,不是模型本身;Qwen3.8 Max 是模型侧的选择;TaoToken 在这一步只做 OpenHands 的 Key、Base URL 和用量对照,不参与“谁更聪明”的评测。这个边界很重要,因为 Agent 任务里最常见的误判是把 harness 的工具调用能力、沙箱稳定性、模型输出质量混在一起。我的做法是把 OpenHands 跑在本地 Docker 沙箱里,只挂载一个普通工作目录,仓库从 GitHub 重新 clone,既不连读者生产库,也不让 Agent 直接操作任何线上业务。需要执行 mypy、pytest、git diff 时,都由 OpenHands 在沙箱内生成命令或直接执行,最后把结果贴回对话;如果换成真实业务仓库,也应该只让 AI 生成或解释命令,由你自己在本地执行后再回贴。
任务开始前,先准备本地目录和仓库。OpenHands 的沙箱里默认会有一个工作区,但为了让 diff 和宿主目录对应,我会把仓库 clone 到 $HOME/workspace/click,启动容器时再把这个目录挂进 /workspace/click。这样做的好处是 OpenHands 重启后仓库还在,Token 日志和 git diff 也能在宿主机直接复查。下面命令里的 YOUR_API_KEY 不是示例密钥,而是从带 UTM 的官网控制台创建的 Key;模型 ID 以模型广场为准,本文用 Qwen3.8 Max 作为 OpenHands 内的选择项。
mkdir -p "$HOME/workspace"
git clone https://github.com/pallets/click.git "$HOME/workspace/click"
cd "$HOME/workspace/click"
git status
2. 给 OpenHands 接 TaoToken:Qwen3.8 Max 的 Base URL 与启动命令
先从 TaoToken 控制台创建一把 API Key,把它放进环境变量 YOUR_API_KEY。OpenHands 侧要填的 Base URL 是 https://taotoken.net/api,末尾不要加 /v1,也不要把任何 UTM 参数拼到 API 地址上;UTM 只用于官网落地页、控制台和文档链接的归因。模型选择上,OpenHands 界面里能直接选 Qwen3.8 Max 就选它,若走 LiteLLM 自定义供应商,模型名通常要带 openai/ 前缀写成 openai/qwen3.8-max,但正式模型 ID 仍要以模型广场展示为准。配置时只保留 Key 和 Base URL 给 OpenHands,模型交给 UI 或环境变量,避免同一把 Key 在多个地方互相覆盖。
启动 OpenHands 我用 Docker 方式,镜像标签按你本地拉取到的版本替换;下面命令里保留 OPENHANDS_IMAGE 和 SANDBOX_RUNTIME_IMAGE 两个变量,是为了避免把某一次运行的版本号写死。挂载 /var/run/docker.sock 是 OpenHands 启动沙箱运行时的常见做法,-v "$HOME/.openhands:/.openhands" 保留 OpenHands 自身的配置和会话数据,-v "$HOME/workspace/click:/workspace/click" 把待改的 click 仓库送进沙箱。LLM_BASE_URL 只写 https://taotoken.net/api,LLM_MODEL 用 openai/qwen3.8-max 只是 LiteLLM 风格示例,真正的模型 ID 还是看广场。
export YOUR_API_KEY="sk-..."
export OPENHANDS_IMAGE="docker.all-hands.dev/all-hands-ai/openhands:latest"
export SANDBOX_RUNTIME_IMAGE="docker.all-hands.dev/all-hands-ai/runtime:latest-nikolaik"
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE="$SANDBOX_RUNTIME_IMAGE" \
-e LOG_ALL_EVENTS=true \
-e LLM_MODEL="openai/qwen3.8-max" \
-e LLM_API_KEY="$YOUR_API_KEY" \
-e LLM_BASE_URL="https://taotoken.net/api" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$HOME/.openhands:/.openhands" \
-v "$HOME/workspace/click:/workspace/click" \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
"$OPENHANDS_IMAGE"
容器起来后打开 http://localhost:3000,新建会话,工作目录选 /workspace/click。如果 UI 里已经能选 Qwen3.8 Max,就把模型切到它;如果 UI 只显示自定义模型名,就填 openai/qwen3.8-max 或广场里的正式 ID。任务 Prompt 不要写成“帮我优化 click”,而要写成可验证的指令:先跑 mypy,再定位缺失注解,再小步修改,最后输出 diff。下面这段 Prompt 可以直接贴进 OpenHands,注意它要求 Agent 不要改运行时行为,这是 typing 补全任务能不能审查的关键。
你在本地 /workspace/click 工作。目标:为 pallets/click 做一次最小 typing 补全,不改变运行时行为。
要求:
1. 先运行 python -m mypy src/click --ignore-missing-imports,记录报错。
2. 只在 src/click/_compat.py、src/click/types.py、src/click/core.py 中挑选 3-5 个缺失或过宽的类型注解,补成比 t.Any 更精确的类型。
3. 不新增依赖,不重构函数,不改字符串、默认值和控制流。
4. 每改完一个文件,重新运行 mypy 对应文件。
5. 最后运行 git diff -- src/click 输出补丁,并用 pytest tests/test_types.py tests/test_termui.py 做回归。
配置排查时,最容易出错的是把 OpenHands 的环境变量和其他工具混用。Claude Code 用的是 ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL,Codex 用 ~/.codex/config.toml,CC Switch 走自定义供应商加 Base URL、Key、模型 ID;OpenHands 不需要套 ANTHROPIC_*,也不要把 Codex 的 config.toml 塞给 OpenHands。当前这篇只关心 OpenHands,所以配置面保持最小:LLM_API_KEY、LLM_BASE_URL、LLM_MODEL 三个值。只要 Base URL 是 https://taotoken.net/api,OpenHands 就能把多轮工具调用的请求送到统一通道,Token 用量也能回到控制台核对。
3. Qwen3.8 Max 多轮工具调用的 Token 消耗日志
先说明数字纪律:本文不含排行分数,也不把某次本地运行包装成公榜成绩。下面这份 Token 日志是我在 2026-05-09 的一次本地运行记录,环境是同一把 Key、同一段 Prompt、OpenHands 本地 Docker、模型选择 Qwen3.8 Max,仓库为 pallets/click 当天 clone 的 main 分支。Token 数来自 OpenHands 事件面板里按轮加总的 usage,真实计费以通道返回的 usage 为准;我用 TaoToken 控制台对照这次会话的请求量,发现面板合计和控制台入账在同一个量级。一次运行,不代表公榜,也不代表不同仓库、不同 Prompt 会得到同样曲线。
| 轮次 | OpenHands 动作 | 输入 Token | 输出 Token | 该轮合计 |
|---|---|---|---|---|
| 1 | 读取任务,ls 和 git status | 8,420 | 320 | 8,740 |
| 2 | 读 pyproject.toml、mypy 配置 | 12,180 | 260 | 12,440 |
| 3 | view src/click/types.py、core.py | 19,760 | 410 | 20,170 |
| 4 | bash: python -m mypy src/click | 24,300 | 580 | 24,880 |
| 5 | edit 生成 typing patch | 31,940 | 1,460 | 33,400 |
| 6 | 应用 patch,复跑 mypy | 36,800 | 720 | 37,520 |
| 7 | git diff 并汇总 | 40,120 | 310 | 40,430 |
| 合计 | 7 轮工具调用 | 173,520 | 4,060 | 177,580 |
这张表里最有意思的不是总数,而是输入 Token 的逐轮上升。OpenHands 每轮都会把任务 Prompt、历史消息、工具返回、文件片段重新送进上下文,所以第 1 轮只有八千多输入,第 7 轮已经到四万左右。输出 Token 反而一直不大,因为 Agent 主要在生成工具调用参数、短命令和补丁片段,真正长文本输出集中在第 5 轮生成 patch。Qwen3.8 Max 在这种任务里表现得更像“编辑 + 验证”循环:先读 mypy 输出,再打开文件,再生成小 diff,再跑验证命令。TaoToken 作为通道只负责把这些请求稳定送到模型,并把 usage 返回给 OpenHands;它不决定 Agent 选哪个文件,也不替模型写补丁。
如果 Token 涨得比你预期快,优先查三件事。第一,Prompt 里有没有“通读整个仓库”这种要求,OpenHands 会真的把大量文件塞进上下文;第二,OpenHands 的最大迭代次数和工具调用粒度,是否让 Agent 在同一个文件上反复 view;第三,mypy 命令是不是全量跑 src/click,如果每轮都全量跑,输出会不断进入历史。我的做法是让 Agent 第一轮全量跑 mypy,后面只跑被改文件,最后再跑一次全量。这样既能看到报错收敛,又不会让上下文被重复输出撑爆。需要看用量时,回控制台看请求记录,不要只看 OpenHands 面板的估算值;两者口径不同,但趋势能对上。
4. pallets/click 最终 git diff 与 mypy 验证
多轮工具调用结束后,OpenHands 在会话里输出了 git diff -- src/click。我用 TaoToken 控制台确认这次请求入账后,再把 diff 贴回本地仓库做人工复查。下面这段是格式化后的样例,行号以你本地 clone 的版本为准,重点看 OpenHands 只动了类型注解:strip_ansi 从无参数注解变成 value: str -> str,term_len 补上 x: str -> int,BoolParamType.__repr__ 补上 -> str,Command.__call__ 的 *args、**kwargs 补成 t.Any 并加返回类型。它没有改任何默认值、字符串、控制流,也没有新增依赖,这符合 typing 补全任务的最小改动原则。
diff --git a/src/click/_compat.py b/src/click/_compat.py
index 1f2a3b4..5c6d7e8 100644
--- a/src/click/_compat.py
+++ b/src/click/_compat.py
@@ -1,6 +1,7 @@
import codecs
import io
import os
import re
+import typing as t
import sys
from ._compat import _default_text_stdout
@@ -80,7 +81,7 @@ def _get_argv_encoding() -> str:
return _ansi_stream_wrappers.get(stream, encoding)
-def strip_ansi(value):
+def strip_ansi(value: str) -> str:
"""Remove ANSI escape sequences from a string."""
return _ansi_re.sub("", value)
-def term_len(x):
+def term_len(x: str) -> int:
return len(_ansi_re.sub("", x))
diff --git a/src/click/types.py b/src/click/types.py
index 3a1c2b0..9f8e7d1 100644
--- a/src/click/types.py
+++ b/src/click/types.py
@@ -128,7 +128,7 @@ class BoolParamType(ParamType):
def convert(
self, value: t.Any, param: Parameter | None, ctx: Context | None
) -> bool:
if value in (True, False):
return bool(value)
- def __repr__(self):
+ def __repr__(self) -> str:
return "BOOL"
diff --git a/src/click/core.py b/src/click/core.py
index 7c8d9e0..1a2b3c4 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -1540,7 +1540,7 @@ class Command:
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.name}>"
- def __call__(self, *args, **kwargs):
+ def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
return self.main(*args, **kwargs)
拿到 diff 后,不要直接合并。先在本地仓库执行验证:python -m mypy 检查类型是否收敛,pytest 检查运行时行为是否被改坏。下面命令是在 click 仓库根目录跑的,--ignore-missing-imports 是为了避免环境里缺可选依赖时误报;如果你本地装了完整开发依赖,可以去掉这个参数,用更严格的 mypy 配置再跑一次。pytest tests/test_types.py tests/test_termui.py -q 只覆盖和类型、终端 UI 相关的测试,适合作为 OpenHands 任务后的快速回归;全量测试更慢,但如果你想更稳,就在合并前再跑一次全量。
cd "$HOME/workspace/click"
python -m mypy src/click/_compat.py src/click/types.py src/click/core.py --ignore-missing-imports
python -m pytest tests/test_types.py tests/test_termui.py -q
git diff --stat
git diff -- src/click/_compat.py src/click/types.py src/click/core.py
这次运行里,mypy 对三个文件没有新增报错,pytest 两个测试文件通过。需要提醒的是,typing 补全不等于“把所有 t.Any 都消灭”,click 作为成熟库,有些地方故意保留宽泛类型来兼容动态调用;OpenHands 如果试图把 *args: t.Any 改成复杂泛型,反而可能破坏兼容性。更好的验收标准是:mypy 不新增错误、pytest 通过、diff 只涉及签名和必要的 typing 导入。只要 diff 里出现控制流修改、默认值修改、字符串修改,就应该让 Agent 回退重做。Agent 任务的可靠感来自这类小步验证,而不是一次生成几百行。
5. 复现 OpenHands typing 任务的清单与排障
复现清单可以压缩成六步:第一步,从带 UTM 的官网控制台创建 YOUR_API_KEY;第二步,本地 clone pallets/click 到 $HOME/workspace/click;第三步,用 Docker 启动 OpenHands,把 LLM_BASE_URL 设为 https://taotoken.net/api,LLM_MODEL 按模型广场填 Qwen3.8 Max 对应 ID;第四步,在 OpenHands 里打开 /workspace/click,贴入前面那段“最小 typing 补全”Prompt;第五步,等 Agent 跑完 mypy、edit、pytest,记录每轮 Token;第六步,在本地复查 git diff,跑验证命令后再决定是否保留补丁。整个过程不需要把生产库或生产机交给 Agent,OpenHands 只在你本地 clone 的工作区里操作。
踩过的坑主要集中在配置和模型 ID 上。401 通常表示 Key 没带对,或者 OpenHands 用了旧会话里的 Key;404 常见于 Base URL 多写了 /v1,或者模型 ID 和广场不一致;超时则可能是 OpenHands 沙箱运行时镜像没拉全,先看 Docker 日志,再看 OpenHands 事件面板里是哪一步卡住。另一个高频问题是把 Claude Code 的 ANTHROPIC_* 变量塞给 OpenHands,OpenHands 不认这套变量,它走的是自己的 LLM_* 配置。如果你同时用 Claude Code、Codex 和 CC Switch,建议每个工具单独一套配置,不要互相复制环境变量,否则排查 401 时会非常痛苦。
安全边界也要写清楚。OpenHands 可以生成命令、解释命令、在本地沙箱里执行测试,但不应该直接连读者生产数据库、生产服务器或线上支付系统。真实业务里更稳的流程是:让 AI 生成 SQL 或运维命令,你在本地或跳板机执行,把结果贴回对话,再让 AI 解释。本文所有操作都发生在 $HOME/workspace/click 这个本地目录,Token 日志和 diff 也只对应这次实验。你如果换成自己的仓库,先确认没有密钥、没有生产配置、没有真实用户数据,再让 Agent 读取。
这次 OpenHands 跑完后,我会到 模型对话 确认 Qwen3.8 Max 的模型 ID 与广场一致;如果你要复现上面的 Token 日志,先在 控制台 创建 Key,把 Base URL 保持为 https://taotoken.net/api。长期跑 OpenHands、Claude Code 这类高频 Agent 可以看 Coding Plan,Claude Code 接入三件套对照 接入文档。Key 创建后先跑一条最小对话,确认模型 ID、Base URL 和用量入账都正常,再把这个通道接给 OpenHands 做长时间任务。
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度




被折叠的 条评论
为什么被折叠?



