1. 从 S05 的痛点说起:skill 目录越写越多,Key 却越配越乱
如果你跟着 Claude Code 的学习记录一路走到 S05,大概率会撞上同一个问题:skill 系统本身不复杂,复杂的是它背后那套「模型通道」的配置。S05 这一章的核心是给 agent 加一个load_skill工具,让模型先看到一份轻量的 skill 目录,真正需要时再把完整正文注入上下文。这个设计很优雅,但前提是你的 agent 能稳定地连上模型。
我自己的项目里,skill 目录从最早的 2 个涨到十几个,每个 skill 对应不同的任务域:有的管命令行查询,有的管文件批处理,有的管代码审查。问题出在配置层——早期我图省事,把 Key 直接写死在每个脚本的.env里,结果就是:换一个 skill 测试,就得改一次环境变量;agent 和 skill 用的是两套 Key,报错时根本分不清是模型通道的问题还是 skill 加载的问题。
S05 的 skill 加载骨架本身是「两层心智模型」:第一层是系统提示词里的 skill 名称加描述,让模型知道有哪些可用;第二层是load_skill工具按需拉取正文。这个结构决定了 agent 会在一次对话里多次调用模型,如果 Key 或 base_url 配置不一致,load_skill返回的内容可能还没进上下文,请求就先失败了。
所以这篇记录的重点不是重写 skill 系统,而是把「统一 Key / API 通道」这件事做扎实。我用 TaoToken 作为统一的模型接入层,让 agent 主循环和 skill 加载走同一条通道,配置一次,后面所有 skill 复用。下面从环境准备开始,一步步把settings.json和config.toml配好,再跑通一次完整的load_skill调用链路。
2. 前置准备:用 TaoToken 统一 Key 与 API 通道
在动手改代码之前,先把「通道」这件事理清楚。S05 的 agent 主循环用的是 Anthropic 风格的客户端,通过base_url指向模型服务。如果你同时还在用别的编码工具(比如某些支持config.toml的 CLI),就会面临两套配置格式。统一 Key 的意义在于:不管从哪个入口发起请求,最终都走同一个 API 地址和同一个 Key,排障时只需要看一个地方。
TaoToken 在这里扮演的角色就是这层统一接入。它的 API 地址是https://taotoken.net/api,兼容 Anthropic 的调用方式,所以 S05 里Anthropic(base_url=...)那行代码几乎不用改,只要把base_url指过去、把 Key 放进环境变量即可。官网入口在https://taotoken.net/?utm_source=taotoken_aicg_blog_end,注册后到控制台创建 Key。
这里有个细节值得单独说:S05 的代码里有一段if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)。它的作用是避免同时存在两个认证变量导致冲突。用统一通道时,建议只保留一个 Key 变量,别让ANTHROPIC_AUTH_TOKEN和ANTHROPIC_API_KEY同时出现,否则客户端可能取到空值,表现为 401 但日志里看不出原因。
你需要准备的东西不多:一个可用的 Key、Python 3.10 以上(因为代码里用了int | None这种联合类型语法)、以及anthropic和python-dotenv两个包。安装命令如下:
pip install anthropic python-dotenvKey 的创建入口在控制台的 API Keys 页面,建议单独建一个给 S05 实验用的 Key,方便后面按项目隔离和吊销。拿到 Key 后不要写进代码,放进.env文件,由load_dotenv读取。
3. 可复制配置:settings.json 与 config.toml 双份片段
配置分两块:一块给 S05 的 Python agent 用,走.env加环境变量;另一块给支持config.toml的 CLI 工具用,方便你在不同入口之间切换时保持一致。先看.env,这是 S05 脚本直接读取的:
# .env ANTHROPIC_BASE_URL=https://taotoken.net/api ANTHROPIC_API_KEY=sk-你的Key MODEL_ID=claude-sonnet-4-20250514注意MODEL_ID这一项,S05 代码里是os.environ["MODEL_ID"]直接取的,缺了会 KeyError。模型名按你账号下可用的填,别照抄。
接着是给 CLI 工具用的config.toml。不同工具的字段名略有差异,但核心就三项:base_url、api_key、model。下面这份是通用骨架,放到工具要求的配置目录里:
# config.toml [model] provider = "anthropic" base_url = "https://taotoken.net/api" api_key = "sk-你的Key" model = "claude-sonnet-4-20250514" [agent] # skill 加载相关:允许 agent 在需要时调用 load_skill enable_skills = true skill_dir = "./skills"如果你更习惯用 JSON 管理配置,等价的settings.json长这样,适合放进项目根目录被脚本读取:
{ "model": { "provider": "anthropic", "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "claude-sonnet-4-20250514" }, "agent": { "enable_skills": true, "skill_dir": "./skills" } }两份配置的base_url和 Key 必须一致,这是「统一通道」的底线。我试过在.env里写一个地址、在config.toml里写另一个,结果 agent 主循环能跑,但 CLI 里触发 skill 时一直超时,排查了半天才发现是两套地址。所以配完之后,先做一次一致性检查:
grep -r "taotoken.net" .env config.toml settings.json三条输出里的域名应该完全相同。如果用了不同的 Key,也建议在这一步统一,避免后面load_skill报错时误判成 skill 本身的问题。
4. 跑通 load_skill:从 skill 目录到 agent 触发的完整链路
配置就绪后,来验证 skill 加载链路。S05 的SkillRegistry会扫描skills目录下的SKILL.md,解析 frontmatter 里的name和description,把目录塞进系统提示词。先建一个最小 skill 来测试:
mkdir -p skills/opencli-usage然后写skills/opencli-usage/SKILL.md,frontmatter 用三个短横线包起来:
--- name: opencli-usage description: 当需要查询命令行工具用法或执行批量命令时使用 --- # opencli 使用说明 执行查询类命令时,先确认目标平台,再拼接子命令。 例如查询热门内容:opencli bilibili hot 注意:命令输出可能较长,必要时用 limit 参数截断。这个文件的结构对应 S05 的两层模型:frontmatter 是轻量目录,正文是按需加载的部分。SkillRegistry._load_all()用rglob("SKILL.md")递归扫描,所以 skill 可以放在子目录里,name 默认取父目录名,但显式写 frontmatter 更稳妥。
接下来启动 agent。S05 的入口是交互式的,运行:
python s05_skill_loading.py看到s05 >>提示符后,输入一个会触发 skill 的请求,比如「用 opencli 帮我查一下 bilibili 热门」。预期行为是这样的:agent 先看到系统提示词里的 skill 目录,判断需要opencli-usage的详细说明,于是调用load_skill工具,参数name为opencli-usage;TOOL_HANDLERS里的load_skill分支执行SKILL_REGISTRY.load_full_text("opencli-usage"),返回被<skill>标签包裹的正文;模型拿到正文后,再决定是否调用bash执行具体命令。
终端里会打印类似> load_skill: <skill name="opencli-usage">...的行,这就是触发成功的标志。如果模型直接回答了、没调load_skill,说明系统提示词里的描述不够明确,把description写得更具体一点,比如加上「必须先加载本 skill 才能执行命令」。
想单独验证load_skill的返回值,可以写个小脚本直接调注册表,不用走完整对话:
from pathlib import Path from s05_skill_loading import SKILL_REGISTRY print(SKILL_REGISTRY.describe_available()) print(SKILL_REGISTRY.load_full_text("opencli-usage"))第一行输出目录,第二行输出完整正文。如果这里就报Unknown skill,那问题在 skill 文件本身,跟模型通道无关,可以快速定位。
5. 常见报错排查:401、Unknown skill 与工具未触发
跑这条链路时,报错基本集中在三类,按出现频率排一下。
第一类是认证失败,表现为AuthenticationError或 401。先确认.env里的ANTHROPIC_API_KEY和config.toml里的api_key是同一个值,再确认base_url结尾没有多余的斜杠。S05 代码里那行os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)是为了清掉冲突变量,如果你本地 shell 里还导出过ANTHROPIC_AUTH_TOKEN,它可能覆盖掉.env的值,用env | grep ANTHROPIC检查一下。
第二类是Error: Unknown skill 'xxx'。这是load_full_text里self.documents.get(name)返回 None 时的提示。原因通常是 skill 文件名不是SKILL.md(大小写敏感),或者 frontmatter 格式不对导致_parse_frontmatter没匹配上。正则要求开头就是---\n,结尾是\n---\n,中间每行用冒号分隔。如果 frontmatter 里name写的是 A、你调用时传的是 B,也会报这个错。用第 4 节那个小脚本先打印describe_available(),看注册表里到底有哪些 name。
第三类是模型不调用load_skill,直接凭已有知识回答。这不是报错,但链路没跑通。检查系统提示词里SKILL_REGISTRY.describe_available()的输出是否为空——如果skills目录不存在或没有SKILL.md,它会返回(no skills available),模型自然没得调。另外TOOLS列表里load_skill的input_schema要求name必填,如果模型传了别的字段名,handler 会 KeyError,被agent_loop里的 try 捕获成Error: 'name',这种也要留意。
还有一类比较隐蔽:load_skill返回了正文,但模型下一轮没有继续调用bash。这通常是 skill 正文里没写清楚「加载后该做什么」。正文里明确写出下一步动作,比如「加载本 skill 后,调用 bash 执行 opencli 命令」,模型更容易接上。
6. 把统一 Key 沉淀成可复用的 skill 骨架
走到这里,S05 的 skill 加载链路应该已经能在本地跑通了。回头看,真正让这套东西可复用的不是load_skill这个工具本身,而是「配置只写一处」的习惯。.env、config.toml、settings.json三份配置里的base_url和 Key 保持一致,后面再加新 skill 时,你只需要往skills目录里丢SKILL.md,不用碰任何通道配置。
如果你打算把这条链路用到长期编码或 agent 项目里,建议把 Key 的管理从单文件升级成按项目隔离,控制台里给每个项目建独立 Key,吊销和轮换都方便。模型对话类的快速验证可以直接在网页端做,省去本地起脚本的步骤;接入文档里有不同语言客户端的示例,改base_url就能迁移。至于长期跑的编码 agent,用 Coding Plan 这类按周期计费的方式比按次调用更可控,尤其适合 skill 目录还在持续增长的阶段。
最后留一个我踩过的坑:skill 的description别写得太泛,像「处理各种任务」这种描述会让模型在多个 skill 之间犹豫,甚至不调load_skill。把触发条件写具体,比如「当用户要求查询命令行工具用法时使用」,命中率会高很多。这个细节不涉及代码改动,但直接影响链路能不能稳定触发。