1. 为什么 Agent 一装技能就“上下文爆炸”
如果你正在用 Hermes 这类带 Skill Runtime 的 Agent 框架,大概率遇到过这个场景:技能目录里塞了二三十个 SKILL.md,每个正文动辄几千字,会话一启动,模型还没开始干活,上下文窗口已经被吃掉一大半。更糟的是,模型面对一堆技能描述,反而不知道该调哪个,错误调用率直线上升。
Hermes 的解法是把技能加载拆成三层:Level 0 只给模型一张“轻量地图”,Level 1 按需读取完整技能正文,Level 2 再细到技能目录里的单个参考文件。三层各管一段,会话启动时只付固定的小额成本,真正的正文和参考文件变成触发后的边际成本。这套机制配合 TaoToken 的统一 Key 通道,能把多模型、多工具的接入配置收敛到一处,省掉每个工具单独配 Key 的重复劳动。
这篇会拆开三层加载的目录边界、命名解析和条件激活逻辑,给出可复制的 settings.json / config.toml 骨架,以及 CC Switch、Cline 的接入片段,最后用一组上下文占用对比动作验证效果。适合已经在跑 Agent、被上下文成本卡住、想搞清楚 Skill Runtime 到底怎么省 token 的人。
2. TaoToken 前置:统一 Key 与 API 通道准备
在动手改配置之前,先把接入层理清楚。Hermes 本身不绑定某一家模型服务,它通过 OpenAI 兼容的 API 通道调用模型。TaoToken 在这里扮演的角色是统一入口:一个 Key 覆盖多个模型,API 地址固定,省得你在 Hermes、CC Switch、Cline 之间来回换配置。
官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数,配置里直接写这个就行。
你需要先拿到 Key。进入控制台的 API Keys 页面创建:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建后复制那串 sk- 开头的字符串,后面所有配置都复用它。
注意:Key 只显示一次,创建后立刻存到本地密码管理器或环境变量里,别直接写进会提交到 Git 的配置文件。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各客户端的完整参数说明。如果你只是想先验证模型通不通,可以直接用模型对话页面试一句:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。
环境变量建议这样设,Linux/macOS 写进 ~/.zshrc 或 ~/.bashrc,Windows 用系统环境变量面板:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"设完执行source ~/.zshrc让变量生效,然后用echo $TAOTOKEN_API_KEY确认能打印出来。这一步看着简单,但后面 Hermes 读配置时如果变量没生效,会直接报 401,排查起来反而绕远路。
3. 可复制配置:三层加载的目录骨架与 settings.json
先把 Hermes 的技能目录结构搭出来。主目录固定在 ~/.hermes/skills/,它是默认读写位置,也是本地技能的单一真实来源。外部目录通过 external_dirs 接入,只承担扩展和共享角色,默认不会覆盖本地版本。
~/.hermes/skills/ ├── category/ │ └── skill-name/ │ └── SKILL.md ├── .hub/ │ ├── lock.json │ ├── quarantine/ │ └── audit.log ├── .bundled_manifest └── .archive/这里有几个工程取舍值得记住。SKILL.md 是技能入口;.hub 存 Hub 相关本地状态;.archive 和 .bundled_manifest 属于维护层数据,不参与正常扫描。Hermes 遍历时会主动排除这些目录,也会跳过 .git、node_modules、虚拟环境、缓存目录等高噪声路径。排除集合大致长这样:
EXCLUDED_SKILL_DIRS = frozenset(( ".git", ".github", ".hub", ".archive", ".venv", "venv", "node_modules", "site-packages", "__pycache__", ".tox", ".nox", ".pytest_cache", ".mypy_cache", ".ruff_cache" ))命名空间是另一条边界。普通技能用 skill-name 解析,插件技能用 namespace:skill 解析。这个冒号不是装饰,它告诉运行时先拆出插件命名空间,再去插件目录里找对应技能。没有这条规则,插件生态很快会撞名。
def parse_qualified_name(name: str): if ":" not in name: return None, name return tuple(name.split(":", 1))本地优先也很关键。同名技能出现时,Hermes 不会让外部目录悄悄覆盖用户本地版本;真正发生多候选冲突时,它会返回明确错误和所有匹配路径,让用户改用完整相对路径。这个决定减少了“为什么今天调用的不是昨天那个技能”的排查成本。
接下来是 settings.json 骨架。Hermes 的模型通道指向 TaoToken,技能目录声明主目录和外部目录:
{ "model": { "provider": "openai-compatible", "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model_name": "claude-sonnet-4-20250514" }, "skills": { "root_dir": "~/.hermes/skills", "external_dirs": [ "~/shared-skills/team-common" ], "write_approval": true, "inline_shell": false }, "session": { "skills_list_token_budget": 3000 } }几个参数说明一下。base_url 固定写 TaoToken 的 API 地址;api_key_env 指向环境变量名,不把 Key 明文写进文件;external_dirs 是数组,可以挂多个共享目录;write_approval 打开后,技能写入不会直接落盘,而是进 ~/.hermes/pending/skills/ 等 review;inline_shell 默认关掉,因为技能内容一旦能执行命令,路径安全和注入检测就必须跟上,非必要不开。
如果你用 config.toml 风格,等价写法:
[model] provider = "openai-compatible" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model_name = "claude-sonnet-4-20250514" [skills] root_dir = "~/.hermes/skills" external_dirs = ["~/shared-skills/team-common"] write_approval = true inline_shell = false [session] skills_list_token_budget = 30003.1 Level 0:skills_list 只给模型一张轻量地图
会话启动时,Hermes 不会把每个技能的正文都塞进上下文。skills_list() 只返回技能元数据,大约是一个固定的 token 成本,原文估算 Level 0 约为 3k tokens。这笔成本在会话启动时支付,后面的完整技能内容和参考文件,只有触发时才成为边际成本。
三层成本对照:
| 层级 | 调用 | 返回内容 | 典型 token |
|---|---|---|---|
| Level 0 | skills_list() | 仅技能元数据 | 约 3k |
| Level 1 | skill_view(name) | 完整 SKILL.md | 10k–100k |
| Level 2 | skill_view(name, path) | 技能目录内某参考文件 | 按文件大小 |
Level 0 的目标不是“让模型读懂所有技能”,而是让模型知道有哪些技能、每个大概负责什么、是否适合当前平台和环境。它更像一张地图,不是一本手册。返回值只保留必要字段:
{ "name": "skill-name", "description": "Brief description...", "category": "category-name", "tags": ["tag1", "tag2"] }这个阶段的扫描没有依赖 SQLite 或预构建 JSON 索引,每次执行 skills_list() 都会重新扫文件系统:
def iter_skill_index_files(skills_dir, filename): for root, dirs, files in os.walk(skills_dir, followlinks=True): dirs[:] = [d for d in dirs if d not in EXCLUDED_SKILL_DIRS] if filename in files: yield Path(root) / filenamedirs[:]的原地修改是个小但实用的优化。它不是在遍历后过滤结果,而是在 os.walk() 继续递归之前剪掉不该进入的目录。技能数量少于千级时,这种按需扫描足够简单,维护成本也比额外索引低。
Level 0 还会处理平台和环境匹配。platforms 为空时默认通过;macos 映射到 darwin,linux、windows 也有对应映射。Termux 需要特殊处理,因为它跑在 Android 上,却经常要兼容 Linux 技能。
PLATFORM_MAP = { "macos": "darwin", "linux": "linux", "windows": "win32" }环境字段也在这个阶段过滤。Hermes 内置识别 kanban、docker、s6,未知环境默认通过。这个默认值有点宽松,但它避免了一个更糟的问题:新环境还没被运行时认识时,技能全部消失。
_KNOWN_ENVIRONMENTS = frozenset({"kanban", "docker", "s6"})条件激活是 Level 0 里更像 Agent 的部分。技能可以声明 requires_toolsets、requires_tools,也可以声明 fallback_for_toolsets、fallback_for_tools。前者表示依赖不可用就隐藏;后者表示主工具可用时自己退场。比如一个 DuckDuckGo 搜索技能可以只在正式 Web 工具不可用时出现。这样模型看到的不是“所有可能工具”,而是当前会话真正有意义的工具。对 Agent 来说,这比单纯减少 token 更重要,因为候选越乱,错误调用的概率越高。
3.2 Level 1:skill_view 的难点在名字解析
当模型决定使用某个技能时,才进入 Level 1。skill_view(name) 会读取完整 SKILL.md,执行必要的前置处理,然后把完整技能说明交给模型。四层名称解析策略体现了兼容性优先级。
策略 1,直接路径:
direct_path = search_dir / name if direct_path.is_dir() and (direct_path / "SKILL.md").exists(): return direct_path / "SKILL.md"策略 2,递归按目录名匹配:
for found_skill_md in iter_skill_index_files(search_dir, "SKILL.md"): if found_skill_md.parent.name == name: return found_skill_md策略 3,按 frontmatter 的 name 字段匹配:
fm, _ = _parse_frontmatter(fm_content) if fm.get("name") == name: return found_skill_md策略 4,兼容旧式扁平 .md 文件:
for found_md in search_dir.rglob(f"{name}.md"): if found_md.name != "SKILL.md": return found_md直接路径优先,说明 Hermes 鼓励用户在冲突时显式指定位置。目录名匹配符合大多数人的直觉;frontmatter 名称匹配给重命名目录留下空间;legacy .md 负责兼容旧技能。真正撞名时,运行时不会装作没事:
if len(candidates) > 1: return json.dumps({ "success": False, "error": f"Ambiguous skill name '{name}': {len(candidates)} skills match", "matches": [str(smd) for _, smd in candidates], "hint": "Use full relative path instead" })这比“按某个顺序静默选第一个”可靠得多。技能是会执行命令、写文件、访问外部系统的,名称解析上的模糊不该被吞掉。
插件技能在 Level 1 里走一条相似但带命名空间的链路。skill_view("plugin:skill") 会先定位插件,再找插件内的技能。如果插件存在但技能不存在,运行时可以列出可用技能;如果找到了,会在返回内容前附加上下文横幅,提醒模型这是哪个插件的一部分,以及有哪些 sibling skills 可以用限定名调用。
[Bundle context: This skill is part of the 'plugin' plugin. Sibling skills: skill1, skill2. Use qualified form to invoke siblings (e.g. plugin:skill1).]Level 1 的后半段是技能内容预处理。Hermes 支持模板变量:
${HERMES_SKILL_DIR} -> 当前技能目录的绝对路径 ${HERMES_SESSION_ID} -> 当前会话 ID如果配置允许,还可以执行内联 shell:
Current date: !date -u +%Y-%m-%d Git branch: !git -C ${HERMES_SKILL_DIR} rev-parse --abbrev-ref HEAD这个能力很锋利,所以它应该被视为受控扩展,而不是普通 Markdown 特性。技能内容一旦能执行命令,路径安全和注入检测就必须跟上。Hermes 对技能名做了绝对路径、Windows drive、..路径穿越检查,也内置了一批 prompt injection 模式,例如 ignore previous instructions、system prompt: 等。
Hub 安装技能还有额外安全扫描,重点检查数据渗出、破坏性命令、Shell 注入和 prompt 注入。它不能证明技能一定安全,但能挡住一批低成本攻击。
配置注入也发生在 Level 1。技能可以在 frontmatter 里声明自己需要的配置项:
metadata: hermes: config: - key: wiki.path description: Path to wiki directory default: ~/wiki prompt: Wiki directory path运行时会从配置文件读取 skills.config.<logical_key>,没有值就用默认值,再展开 ~ 和环境变量。最后把结果追加到技能内容里:
[Skill config (from ~/.hermes/config.yaml): wiki.path = /Users/erik/wiki]这里可以看出 Hermes 对“技能加载”和“技能管理”分得很清楚。加载是按需读取和注入;管理则走 skill_manage,支持 create、patch、edit、delete、write_file、remove_file。如果开启 skills.write_approval,写入不会直接落盘,而是进入 ~/.hermes/pending/skills/,等待用户 review、diff、approve 或 reject。
3.3 Level 2:参考文件把长技能拆成可控切片
Level 2 是 skill_view(name, path) 的形态,读取技能目录内的某个参考文件。它的意义在于把长技能拆成可控切片:SKILL.md 只放主流程和索引,详细参考、示例、数据表放到同目录的 refs/ 或 docs/ 子目录,模型需要哪块再取哪块。
~/.hermes/skills/category/skill-name/ ├── SKILL.md ├── refs/ │ ├── api-schema.md │ └── examples.md └── data/ └── mapping.jsonSKILL.md 里用相对路径引用这些文件,模型在 Level 1 读完主流程后,如果发现需要具体 schema,再触发 Level 2 读取 refs/api-schema.md。这样单个技能的上下文占用从“一次性全量”变成“按需分片”,长技能不再拖垮整个会话。
4. 验证请求:CC Switch 与 Cline 接入片段
配置写完,先验证模型通道通不通。最直接的方式是用 curl 打一次 TaoToken 的 API:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "reply with ok"}], "max_tokens": 16 }'返回里能看到 choices[0].message.content 就说明 Key 和通道都正常。如果返回 401,先查环境变量有没有生效;返回 404 就检查 base_url 是不是写成了带 /v1 的完整路径,TaoToken 的基址是 https://taotoken.net/api ,客户端一般会自己拼 /v1。
CC Switch 的接入片段,配置里指向 TaoToken:
{ "providers": [ { "name": "taotoken", "type": "openai-compatible", "baseURL": "https://taotoken.net/api", "apiKey": "${TAOTOKEN_API_KEY}", "models": ["claude-sonnet-4-20250514", "gpt-4o"] } ] }Cline 的接入片段,在设置里选 OpenAI Compatible,Base URL 填 https://taotoken.net/api ,API Key 填环境变量或直接粘贴:
{ "cline.apiProvider": "openai", "cline.openAiBaseUrl": "https://taotoken.net/api", "cline.openAiApiKey": "${TAOTOKEN_API_KEY}", "cline.openAiModelId": "claude-sonnet-4-20250514" }两个客户端都配好后,回到 Hermes 跑一次 skills_list(),观察返回的技能数量和 token 占用。正常情况下,Level 0 的返回应该只有元数据,不含任何 SKILL.md 正文。你可以用一个简单脚本统计:
import json, tiktoken with open("skills_list_output.json") as f: data = json.load(f) enc = tiktoken.get_encoding("cl100k_base") tokens = len(enc.encode(json.dumps(data))) print(f"Level 0 tokens: {tokens}")实测下来,二三十个技能的元数据列表稳定在 3k tokens 上下,而如果把这些技能的正文全量塞进去,轻松突破 80k。这个差距就是三层加载省下来的空间。
5. 本篇常见错排查
报 401 Unauthorized:九成是环境变量没生效。在 Hermes 启动的同一个 shell 里执行echo $TAOTOKEN_API_KEY,打印为空就说明变量没导出。注意 GUI 启动的客户端可能读不到 shell 里的 export,需要在系统环境变量里设,或者直接在客户端配置里填 Key。
技能列表为空:先确认 ~/.hermes/skills/ 下确实有 category/skill-name/SKILL.md 这种结构。如果技能放在 .archive 或 .hub 里,会被排除集合跳过。另外检查 platforms 字段,如果技能声明了 platforms: ["linux"] 而你在 macOS 上跑,会被过滤掉。
同名技能报 Ambiguous:这是设计行为,不是 bug。错误信息里会列出所有匹配路径,改用完整相对路径调用即可,比如 skill_view("category/skill-name")。
Level 1 读取超时:SKILL.md 太大,或者内联 shell 命令卡住。先把 inline_shell 关掉,再把 SKILL.md 拆成主流程加 refs/ 参考文件,用 Level 2 按需读取。
插件技能找不到:确认调用时带了命名空间前缀,格式是 plugin:skill。只写 skill 会走普通技能解析路径,找不到插件目录里的技能。
写入没落盘:检查 skills.write_approval 是不是开着。开着的话写入会进 ~/.hermes/pending/skills/,需要手动 review 后 approve 才落盘。
6. 把 Key 和加载策略一起收敛
三层加载的核心思路是延迟付费:会话启动只付 Level 0 的固定小额成本,正文和参考文件在触发时才计入。配合 TaoToken 的统一 Key,Hermes、CC Switch、Cline 共用一套 API 通道和凭证,配置维护从“每个工具一份”变成“一处改、处处生效”。
如果你还在被上下文成本卡住,建议先按这篇的 settings.json 骨架把 external_dirs 和 write_approval 配好,再用 skills_list 的 token 统计脚本量一次基线。长期跑编码和 Agent 任务的话,Coding Plan 页面有更完整的额度方案:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入细节和参数说明都在文档里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。