1. 从一次终端任务说起:OpenHands skill 到底解决什么问题
如果你最近在折腾 Agent 编排,大概率会碰到一个尴尬:模型能选、工具能接,但真正跑起来的时候,每个 skill 的目录结构、启动方式、模型配置各写各的,换一个模型就得改一堆代码。OpenHands 这个 skill 之所以值得单独拆,是因为它把「模型无关」这件事做成了工程范本——背后接的是 LiteLLM,OpenAI、Anthropic、OpenRouter、DeepSeek、Ollama、vLLM 都能挂,而对外只暴露一个 headless terminal 调用入口。
先说清楚它是什么。OpenHands 本身是一个开源的软件工程 Agent 框架,而这个 skill 是把它包装成一个可被上层编排系统调用的标准单元。它能做的事很具体:接收一个任务描述,在隔离环境里自主读写文件、执行命令、跑测试,最后把结果以 JSON 形式吐回来。适合谁?适合那些已经在用 Claude Code 或 Codex 做原生开发、但需要「换模型对比效果」或者「多模型混跑」的团队。因为要 Claude 原生能力就走 claude-code,要 OpenAI 原生就走 codex,只有当你需要灵活切换 provider 时,OpenHands 这条链路才真正发挥价值。
我试过把同一个重构任务分别丢给三个 provider,OpenHands 的 headless 模式是唯一一个不需要改 skill 代码、只改环境变量就能切换的。这个特性决定了它的目录结构必须足够克制——配置文件、启动脚本、模型映射三者分离,谁都不越界。接下来我会把这份 skill 的骨架拆开,从目录结构到 headless 启动命令,再到 LiteLLM 的配置片段,最后用一次真实的终端任务验证整条链路。你照着抄,就能得到一个可复用的 skill 范本。
2. 目录骨架与 headless terminal 启动链路拆解
一个好 skill 的第一特征是「目录会说话」。OpenHands 这份的骨架大致长这样,我按职责分层列出来:
openhands-skill/ ├── SKILL.md # 能力声明与使用边界 ├── config/ │ ├── litellm.yaml # 模型路由配置 │ └── runtime.env # 运行时环境变量 ├── scripts/ │ ├── run_headless.sh # headless 启动入口 │ └── healthcheck.sh # 链路自检 └── workspace/ # 任务执行沙箱目录注意SKILL.md的位置。它放在根目录,开头就写清楚「什么时候不该用这个 skill」——比如需要 Claude 原生工具链时应该走 claude-code,需要 OpenAI 原生函数调用时走 codex。这个细节很关键,好 skill 会主动帮你做选择题,而不是让你在报错之后才反应过来选错了。
config/目录承担模型接入的全部职责。litellm.yaml定义 provider 和模型映射,runtime.env存放 API Key 和 Base URL 这类敏感信息。两者分离的好处是:你可以把litellm.yaml提交到版本库做团队共享,而runtime.env只留在本地或密钥管理服务里。
scripts/run_headless.sh是整条链路的触发点。它的核心就是一行 headless 调用,把任务描述、工作目录、模型配置通过参数和环境变量传进去。workspace/则是沙箱,Agent 的所有文件操作都被限制在这个目录内,避免污染宿主机。
这里要强调 headless terminal 的意义。所谓 headless,就是没有交互式界面,Agent 完全靠命令行参数和标准输入输出完成任务。这对编排系统极其友好——上层只需要拼接一条命令、读取一段 JSON 输出,不需要处理 TTY 或伪终端。OpenHands 的 headless 模式通过--json参数把执行过程结构化,每一步的工具调用、文件变更、命令输出都能被上层解析。
链路顺序是这样的:编排层触发run_headless.sh→ 脚本加载runtime.env注入密钥 → LiteLLM 根据litellm.yaml路由到具体 provider → OpenHands 在workspace/内执行任务 → 结果以 JSON 返回标准输出。整条链路没有隐藏状态,每一步都可观测、可复现。这也是为什么我说它适合当范本:你换任何模型,改的只是litellm.yaml里的一行,其余部分纹丝不动。
3. 可复制的 LiteLLM 配置与 headless 启动命令
这一节直接给可复制的片段。先看config/litellm.yaml,这是模型路由的核心:
model_list: - model_name: openhands-default litellm_params: model: anthropic/claude-sonnet-4-20250514 api_base: https://taotoken.net/api api_key: os.environ/TAOTOKEN_API_KEY - model_name: openhands-fast litellm_params: model: openai/gpt-4o-mini api_base: https://taotoken.net/api api_key: os.environ/TAOTOKEN_API_KEY - model_name: openhands-local litellm_params: model: ollama/qwen2.5-coder:7b api_base: http://127.0.0.1:11434 litellm_settings: drop_params: true set_verbose: false三个model_name对应三种场景:openhands-default走主力模型做复杂重构,openhands-fast走轻量模型做快速补全,openhands-local走本地 Ollama 做离线验证。api_base统一指向https://taotoken.net/api,密钥通过os.environ/TAOTOKEN_API_KEY从环境变量读取,不硬编码在文件里。
再看config/runtime.env:
export TAOTOKEN_API_KEY="sk-你的密钥" export OPENHANDS_MODEL="openhands-default" export OPENHANDS_WORKSPACE="./workspace" export LITELLM_CONFIG="./config/litellm.yaml"然后是scripts/run_headless.sh,这是 headless 启动的完整入口:
#!/usr/bin/env bash set -euo pipefail source ./config/runtime.env TASK_DESC="${1:?用法: run_headless.sh \"任务描述\"}" openhands --headless \ --json \ --override-with-envs \ --exit-without-confirmation \ --model "$OPENHANDS_MODEL" \ --workspace "$OPENHANDS_WORKSPACE" \ --config "$LITELLM_CONFIG" \ --task "$TASK_DESC"四个关键参数逐个说。--headless关闭交互界面,--json让输出结构化,--override-with-envs允许环境变量覆盖配置里的默认值,--exit-without-confirmation让 Agent 执行完自动退出而不是等待人工确认。这四个参数组合起来,才构成一个真正可被编排系统调用的无头单元。
如果你用的是 Cline MCP 或 Codex 的auth.json体系,三件套要写全:Base URL 填https://taotoken.net/api,Key 填你的密钥,Model ID 填openhands-default或你在litellm.yaml里定义的任意model_name。三者缺一,链路就会在鉴权或路由阶段断掉。
4. 验证一次终端任务:从触发到 JSON 返回
配置写完,必须验证。我拿一个最小任务来跑:让 Agent 在workspace/里创建一个 Python 文件并运行它。
触发命令:
chmod +x scripts/run_headless.sh ./scripts/run_headless.sh "在 workspace 下创建 hello.py,内容为打印当前目录下所有 .py 文件,然后运行它"预期返回的 JSON 结构大致如下(截取关键字段):
{ "status": "completed", "model": "openhands-default", "steps": [ { "action": "write_file", "path": "workspace/hello.py", "result": "success" }, { "action": "run_command", "command": "python workspace/hello.py", "stdout": "workspace/hello.py\n", "exit_code": 0 } ], "elapsed_seconds": 12.4 }看到status: completed和exit_code: 0,说明整条链路通了:编排层触发脚本 → LiteLLM 路由到openhands-default→ OpenHands 在沙箱内写文件、跑命令 → 结果结构化返回。
如果你想验证模型切换是否生效,把runtime.env里的OPENHANDS_MODEL改成openhands-fast再跑一次,返回 JSON 里的model字段会变成openhands-fast,而 skill 代码一行没动。这就是模型无关的价值。
再补一个自检脚本scripts/healthcheck.sh,用来快速确认 LiteLLM 配置是否可加载:
#!/usr/bin/env bash set -euo pipefail source ./config/runtime.env python -c " import yaml, os cfg = yaml.safe_load(open(os.environ['LITELLM_CONFIG'])) for m in cfg['model_list']: print(m['model_name'], '->', m['litellm_params']['model']) "跑通这个自检,再去触发真实任务,能省掉大量「配置写错但报错信息看不懂」的时间。
5. 常见报错排查:401、local proxy failed 与 reading choices
链路跑不通时,报错信息往往指向几个固定位置。我按实际踩过的坑逐个对照。
401 Unauthorized。最常见的原因是TAOTOKEN_API_KEY没被正确注入。检查顺序:先确认runtime.env里export了密钥,再确认run_headless.sh里source了该文件,最后确认litellm.yaml里写的是os.environ/TAOTOKEN_API_KEY而不是硬编码的空字符串。三者任一断掉都会 401。如果用的是 Codex 的auth.json,确认 Base URL 和 Key 字段没有多余空格。
local proxy failed。这个报错通常出现在api_base指向本地服务但服务没起来的时候。如果你配的是openhands-local走 Ollama,先确认ollama serve在跑、端口11434可访问。如果配的是远程api_base,检查网络连通性和 URL 是否漏了/api后缀。https://taotoken.net/api是完整路径,少写/api会路由失败。
reading choices 相关报错。这类错误一般出现在响应解析阶段,根因是 provider 返回的结构和 LiteLLM 预期不一致。排查方向:确认litellm.yaml里的model字段格式正确,比如anthropic/claude-sonnet-4-20250514这种provider/model的写法不能少 provider 前缀。另外drop_params: true建议保留,它能过滤掉某些 provider 不支持的参数,减少解析冲突。
OAuth 相关报错。如果你在 skill 里集成了需要 OAuth 的工具,确认 token 刷新逻辑没有和 headless 模式冲突。headless 环境下没有浏览器回调,OAuth 必须走 device code 或预置 token 的方式。把 token 放在runtime.env里注入,不要依赖交互式授权。
任务卡住不返回。检查--exit-without-confirmation是否生效。如果 Agent 在等待人工确认,headless 模式下会一直挂起。另外确认workspace/目录存在且有写权限,沙箱目录不可写会导致 Agent 反复重试。
排查的通用思路是:先跑healthcheck.sh确认配置可加载,再用最小任务触发一次,看 JSON 里status和steps停在哪一步。报错信息里的关键词——401、proxy、choices、OAuth——基本能定位到具体环节。
6. 把这条链路接进你的工作流
拆完这份范本,你会发现它的可复用性来自三个分离:配置与代码分离、模型与逻辑分离、执行与观测分离。你要做的不是照抄每一行,而是把这套结构迁移到自己的 skill 里。
具体动作:先把litellm.yaml的model_list换成你实际要用的 provider,Base URL 统一填https://taotoken.net/api,密钥走环境变量。然后确认run_headless.sh的四个核心参数齐全,尤其是--json和--exit-without-confirmation,这两个决定了它能不能被编排系统无头调用。最后用healthcheck.sh加一次最小任务验证,看到status: completed再接入生产流程。
如果你需要长期跑编码类 Agent 任务,建议把模型路由和密钥管理拆到独立配置里,方便团队共享和轮换。密钥申请和接入文档可以从 API Keys 页面入手,模型对话能力可以在模型对话页面试跑,长期编码和 Agent 编排则适合用 Coding Plan 来承载。链路通了之后,换模型就是改一行配置的事,这才是这份范本真正值钱的地方。