打造优秀的 Claude 智能体技能:来自跑步教练项目的经验教训
打造经得起考验的 Claude Agent 技能:精确的描述、先评估再编写正文、以 SKILL.md 作为路由机制、用脚本替代文本,同时在 CI 流程中设置凭证隔离机制。
来源: I Missed My Running Goal Three Years Running. So I Built an Agent Skill.
想象这样一位跑步者:他连续三年设定1,000公里的跑步目标,但每次都未能达成,三年累计完成的距离仅约2,500公里。问题不在于缺乏努力,伤病、重新开始以及忙碌的周际状况屡见不鲜。真正缺失的是一个计划,因为动力在某一天消失之前一直都有效。
一个解决办法是不再依赖意志力,而是建立一套系统。running-coach正是由此诞生——它是一种Claude智能体技能,能够根据个人的跑步历史来规划单次跑步及整周的训练安排,将每周的训练内容汇总成类似新闻简报风格的邮件,甚至还能根据即将进行的跑步距离来调整播放列表的长度。
跑步在这里只是背景而已。更有价值的话题是如何培养一项出色的技能,也就是那种能够稳定触发、适度利用上下文、在所有平台上表现一致且不会随时间退化的技能。下文将把Anthropic发布的各项开发规范与该项目的具体应用方式对应起来。
什么是智能体技能
技能实际上就是一个文件夹。其根目录下有一个SKILL.md文件,文件开头是包含name和description的YAML格式元数据,随后是用Markdown编写的说明。参考文档、脚本、模板以及其他资源可以放在该文件旁边。
有趣的是技能的加载方式,它采用了一种名为渐进式展示的三层模型:
- 第一层:元数据。在系统启动时即被加载,每个技能大约包含100个标记。其中仅包含名称和描述。
- 第二层:指令。仅在技能被触发时加载,长度控制在5千个标记以内。这部分内容即为
SKILL.md的正文。 - 第三层:资源。通过bash按需加载,未被使用则保持空白状态。参考文件、脚本及资源均存储于此。
Anthropic将技能比作为新同事编写的入职指南。所有设计的核心原则是上下文窗口是共享的公共资源。某个技能占用的空间会占用掉智能体试图记住的其他内容所需的空间。
1. 描述即API
在1级阶段,模型所能看到的关于某个技能的信息仅有其描述。该描述更像是路由表而非文档,因为模型会通过它来匹配传入的请求,从而决定是否启用该技能。描述必须同时说明该技能的功能及使用时机,需用第三人称撰写,长度不得超过1,024个字符;若希望在claude.ai的选择器中完整显示,则实际限制约为200个字符。以下是running-coach所使用的描述:
name: running-coach
description: >-
Plans a run or a whole week as Google Calendar blocks, builds a weekly
running digest email, and makes playlists — from Strava, Spotify, AccuWeather
and Calendar. Use for pacing or planning a week.
看看它的结构是如何设计的。开头说明该技能能实现什么功能(规划跑步路线、生成摘要、创建播放列表),接着说明何时使用它(控制节奏或规划一周行程),还会明确数据来源,以便在类似Strava的界面中能关联到该技能。其名称采用小写连写形式,且必须与目录名称完全一致——这是硬性规定而非风格偏好,项目构建过程也会强制执行这一规则,稍后会具体说明。
无法提前判断描述是否有效,必须通过实际测试才能确定,因此仓库中的第一项内容是一组评估结果,而非文档。
2. 先编写评估内容,再润色文字
Anthropic的建议是先构建评估机制,这样就能解决实际存在的问题,而非凭空想象的问题。该项目在evals/文件夹中包含了四个场景文件,每个能力对应一个。这些并非单元测试,而是用通俗语言写成的行为规范。以下是用于规划运行的那个文件:
{
"skills": ["running-coach"],
"query": "I'm heading out for 8k this evening, how long is that going to take me?",
"expected_behavior": [
"Triggers the running-coach skill without being named, and routes to the planning capability",
"Reads references/run-planning.md and does not read the digest or playlist references",
"Runs scripts/cache.py status before projecting…",
"Executes scripts/planning/plan_run.py 8 rather than reading its source or reimplementing Riegel inline",
"Reports the projected finish time, the pace, and the recommended effort band…",
"Never asks for or uses a Strava access token"
]
}
每条断言都对应一种最佳实践。在没有为测试指定名称的情况下触发测试,能够检验测试描述的质量。仅读取相关的参考测试,则体现了渐进式展示的理念。运行脚本而不重新实现它,可以检验人们更倾向于使用脚本而非文字描述。从不触碰任何令牌,则用于验证权限边界。这些要素共同将整个设计理念转化为可观察的行为,而构建过程的其余部分则致力于确保这些行为能够通过测试。
3. 将 SKILL.md 作为路由器
最容易耗尽技能上下文预算的方式就是把所有内容都放入 SKILL.md 中。更好的做法是将该文件视为目录表,在需要时指向更深入的内容。要保持文件简短,参考链接也只需一层深度,因为智能体往往只能部分阅读深度嵌套的文件,最终只能得到不完整的信息。
这里的四项能力每项都需要一页篇幅来解释,因此 SKILL.md 并未对它们进行任何说明。它只是进行跳转:
| Capability | Reference | Entry point |
| -------------- | -------------------------------- | --------------------------------- |
| Plan a run | references/run-planning.md | scripts/planning/plan_run.py |
| Plan the week | references/week-planning.md | scripts/planning/plan_week.py |
| Weekly digest | references/weekly-digest.md | scripts/digest/run_pipeline.py |
| Run playlist | references/run-playlist.md | scripts/playlist/build_playlist.py|
Work out which one is wanted, then read only that capability's reference.
要求生成播放列表时,代理只会打开run-playlist.md,除此之外不会处理其他任何内容。摘要的工作流规范、潜在问题以及模板说明都未被纳入考虑,因为未使用的捆绑内容无需额外成本。二级方案保持简洁,三级方案则仅贡献一个文件。评估过程中的“读取X,不读取Y”这一条款正是为了验证这一点。
4. 脚本优于文本:运行而非阅读
这或许是技能设计中最有价值的理念。当代理通过bash运行脚本时,只有输出结果会被纳入上下文,源代码本身则不会被考虑。预写好的脚本比即时生成的代码更可靠,且每次都能产生相同的结果。
因此running-coach的逻辑体现在Python代码中,而相关指令也明确要求代理不要去触碰源代码:
**Run the scripts, don't read them.** They print everything needed; every one
takes --help and most take --json. Reading their source wastes context.
Riegel的完赛时间预测、努力程度计算以及摘要分析均通过确定性Python代码实现,能够给出简洁的答案。代理负责协调这些计算过程而无需重复运算。相比那种需要根据速度变化进行复杂推理、效率较低且对计算资源消耗较大的方式,使用速度计算器并直接输出数值的方式则更为高效,且结果每次都完全一致。
5. 每个脚本都是遵循统一规范的完整命令行工具
为了实现“直接运行而非阅读代码”的使用方式,代理必须能够驱动那些不允许直接打开的脚本。这依赖于统一的、具有自描述功能的接口,因此所有脚本都遵循相同的规范:
- 一个位置参数,用于传递所有必需的信息(如规划与播放列表所需的距离);
--data DIR在所有命令中都存在,这样任何脚本都可以指向不同的缓存,从而实现离线测试和会话级沙箱功能;- 几乎所有命令都支持
--json,这样一个脚本的输出可以作为下一个脚本的输入; - 每个命令都提供
--help,这是代理在不确定某个参数时使用的备用方式。它会读取简短的帮助文本,因为这成本较低,而不会去读取成本高昂且不可访问的源代码。
以下是按功能分类的概览。
规划运行任务时会使用 plan_run.py:
python3 scripts/planning/plan_run.py 8 # 8 km, defaults
python3 scripts/planning/plan_run.py 8 --band steady # force an effort band
python3 scripts/planning/plan_run.py 8 --start-in 3 # start 3h from now (weather-aware)
python3 scripts/planning/plan_run.py 8 --json # machine-readable plan
唯一必需的参数是 distance_km。可选参数包括用于调整里格尔疲劳指数的 --exp,以及用于指定特定力量训练范围的 --band。若未进行特殊设置,该工具会根据跑者的近期训练负荷来确定训练范围。其启发式方法具有灵活性,而精确预测则保持确定性。
播放列表由 build_playlist.py负责生成,该脚本刻意模仿了规划器的结构:
python3 scripts/playlist/build_playlist.py 8 # size to the 8 km finish
python3 scripts/playlist/build_playlist.py 8 --genre "lo-fi"
python3 scripts/playlist/build_playlist.py 8 --minutes 55 # size by time instead
python3 scripts/playlist/build_playlist.py 8 --band hard
由于两者占据的位置距离相同,这两种功能便相互结合:plan_run计算出的完成时间即为build_playlist为播放列表设定的时长。脚本会输出提示信息而非播放列表,随后代理会将该提示传递给Spotify连接器。此处同样存在职责边界的问题,脚本负责进行算术运算,而代理则负责输入输出操作。
每周摘要只是一个小型处理流程,并非整体式系统。run_pipeline.py会先验证缓存,然后再执行两个阶段:
python3 scripts/digest/run_pipeline.py # validate → analyse → render
python3 scripts/digest/run_pipeline.py --plan-km 8 # lead the digest with a planned run
python3 scripts/digest/run_pipeline.py --seed 3 # pin the masthead title choice
python3 scripts/digest/run_pipeline.py --skip-validate # skip the pre-flight cache check
在其下方,analyze.py会将缓存的周数据汇总为一个digest_data.json文件,而build_email.py则将该JSON文件渲染为独立的HTML格式邮件。将分析与渲染分开意味着无需修改分析逻辑即可重新设计邮件样式,此外--seed参数还能让标题这一唯一的随机选择结果可重复,便于多次截取同一周的邮件截图。
Planning the week是功能最有趣的命令行工具,因为它通过一系列子命令实现了规划、验证和执行的流程,代理程序会按顺序依次执行这些子命令:
python3 scripts/planning/plan_week.py goal # show the weekly goal in force
python3 scripts/planning/plan_week.py propose # propose the split of runs (as JSON)
python3 scripts/planning/plan_week.py draft --split - # lay the split across the days
python3 scripts/planning/plan_week.py brief --draft - # hand the agent a per-run contract
python3 scripts/planning/plan_week.py events --draft - --content - # emit calendar blocks
每个阶段都会输出 JSON,下一个阶段会通过标准输入读取这些 JSON。在适用的情况下,这些阶段还会接受常见的覆盖参数(如 --km、--runs)以及调度选项,例如 --start-date、--time、--weekend-time、--location 和 --tz。相关指南建议对于那些容易出错的任务采用这种“可验证的中间输出”形式:代理程序不会直接随意生成一周的安排,而是先输出结构化的计划,脚本会对该计划进行校验和调整,之后才会将其转化为日历事件。
在中间阶段会有有意识的交接。在brief阶段,脚本会确定距离、速度范围和日期,然后为每次任务生成一份合同给代理。代理负责填写听起来像真人表述的文字,这样每次任务的描述就不会完全相同。脚本负责确保那些必须精确的内容,而模型则负责让内容具有真实的书写感。在单一指令中,对复杂的数学计算限制较多,而对散文创作则给予较大自由度,这便是“适当的自由度”原则。
6. 解决问题,而非推诿
低水平的技能只会给处理人员下达含糊的指令,比如“如果数据看起来过时了,就想办法解决”。Anthropic的要求是直接解决问题而非推诿:应在脚本中处理错误情况,不得将其转交他人。running-coach通过新鲜度模型来实现这一目标。它为每条缓存数据记录其年龄,并通过status指定能够填补每处缺失的具体MCP调用方式:
python3 scripts/cache.py status # what's cached, how stale, what to call
python3 scripts/cache.py put streams --file - # ingest an MCP response from stdin
python3 scripts/cache.py show week --json # a compact summary, not the raw blob
python3 scripts/cache.py validate --for digest # shape-check before a run
cache.py 包含四个函数(status、put、show、validate),它们分别代表刷新流程中的不同步骤。status 会列出所有键值及其有效期限:当前周的数据有效期限很短,因为每次运行都会更新;已完成周的数据及运动员数据的有效期限为每周;对于任何已过期或缺失的数据,它会提示应使用哪个具体的 MCP 工具来获取。代理程序会调用该工具并将响应直接传递给 put 函数。validate --for <capability> 则会在下游脚本执行之前确认结果是否可用,这样一旦数据获取失败,就会立即出现错误,而不会在后续处理中出现问题。
进一步的限制措施:put函数仅接受预设允许列表中的键值,show函数输出的是摘要而非原始JSON格式,因此查看缓存时不会将大量GPS数据一并显示在上下文中;同时系统还规定不得手动编辑这些JSON文件。这样一来,代理无需自行判断是否需要重新获取数据或获取何种数据,因为脚本已给出了明确指示。原本需要凭经验判断的“这些数据是否足够新鲜?”问题,现在变成了明确的操作指令,这正是执行cache.py status后再进行数据处理的意义所在。
7. 凭证边界
这是核心的架构设计选择,同时也是一种安全措施:需对异常的网络调用和文件访问模式进行审计。该机制从设计上就使得此类审计变得极为简单,因为其内部不存在任何存储凭证或打开套接字的操作。
agent --(Strava / Spotify / AccuWeather MCP)--> cache.py put --> the cache
the cache --> plan_run.py / run_pipeline.py / build_playlist.py --> output
仅有代理程序会与MCP连接器通信,而cache.py则负责写入缓存数据。其余所有脚本都仅在离线状态下处理缓存数据,该软件包中不存在任何HTTP客户端、API密钥或访问令牌。
# cache.py — the only gateway to the running-coach data cache.
# The agent is the only thing that talks to MCP; this script is the only thing
# that reads or writes the cache. Nothing here needs an access token.
由此带来三大优势。在安全性方面,没有数据可泄露,且无需审计的网络接口也不存在。在可移植性方面,该技能无需关心连接器是本地的、远程的还是缺失的,因为它仅依赖缓存。在可测试性方面,充满数据的缓存能让所有下游脚本完全离线运行,这正是实现持续集成冒烟测试的基础。
8. 运行时要求:Python 3.10
不同环境中的技能无法相互同步,且每个环境都有其自身的运行时限制。Claude API不提供网络访问功能,也不支持运行时包的安装,而Claude Code则具备这些功能。若要在所有环境中运行某项技能,需以限制最严格的环境为设计标准。
这就是为什么 running-coach 不需要任何外部依赖。它没有 requirements.txt,也没有虚拟环境或安装步骤。其说明指出只需 Python 3.10 或更高版本即可,而且单纯的 python3 也能直接使用,即便在无法安装软件包的托管沙箱环境中也是如此。
该技能所需的所有内容都位于 skills/running-coach/ 目录中。将此文件夹放入 ~/.claude/skills/ 即可供 Claude Code 使用,而通过压缩上传则可供 Cowork 或 claude.ai 使用,功能完全一致。路径中统一使用正斜杠,从而避免了跨平台可能出现的兼容问题。缓存文件和输出文件会被保存在技能文件夹之外的位置,使得已安装的副本为只读状态,并且永远不会被提交到 git 中,因为缓存中包含真实的 GPS 多边形数据,解码后会得到从某人家门口开始的路线。
9. 包装检查及支持声明的持续集成机制
包装步骤常常被人们忽视。如果技能的安装有误,其问题往往不会立即显现:包含松散文件的压缩包在上传时会被默默拒绝。因此该项目包含了package.py,这是一个无需依赖的验证工具,会重复执行Anthropic自身技能仓库中的检查项。它会验证允许的前置字段键、与目录名称一致的驼峰式name、符合规格的描述长度(当接近claude.ai设定的约200字符限制时会发出警告),以及一个仅包含一个顶层目录且该目录根目录下有SKILL.md文件的压缩包。
GitHub Actions会依据这些检查项来控制每次发布。当有v*标签被推送时,工作流会:
- 通过
python -m compileall对每个脚本进行字节级编译,以检测语法错误; - 根据规范检查文档的前置信息;
- 使用
--help运行每个入口点,至少可以确认每个脚本都是在干净的Python 3.10环境下进行导入和解析的; - 验证
running-coach/SKILL.md文件位于压缩包的根目录中; - 只有在完成所有这些步骤后,才会发布该压缩包。
# 3.10 is the version the skill claims to support, so building on it is
# also how that claim stays honest.
- uses: actions/setup-python@v5
with:
python-version: "3.10"
可推广的经验
在构建过程中有几点尤为突出:
一段约100个标记的描述就能完成90%的工作。由于这是唯一始终会被加载的部分,若无法触发则其他一切都将失去意义。因此需给予其足够的重视,并添加用于测试触发的评估逻辑,且无需直接提及相关技能的名称。
将逻辑从提示语转变为确定性脚本。智能体读取的文本内容多变且会消耗标记,而它运行的脚本成本较低且每次都能给出相同的结果。核心原则是执行而非读取。
相信那些能用一句话表述的边界条件。“仅智能体可与MCP交互,仅有一个脚本负责写入缓存,其余均为离线处理”这样的规则比大量防御性代码更有价值,因为这类规则从结构上就杜绝了各类错误和数据泄露的可能性。
让构建流程验证你的声明。那些承诺支持 Python 3.10 且无任何依赖的技能,应在每次发布时都在持续集成环境中以 exactly那种方式构建。否则,这些承诺就只是空谈而已。
跑步目标本身仍然是一个未解决的问题,但现在有了专门的规划工具,它会在每周一早上给出客观评估,并根据累计里程重新计算目标值。一旦将跑步视为一个可解决的工程问题,它就真的变得可处理了。
任何想要安装此类技能的人都需要开启代码执行功能,并拥有自己的 Strava 连接器,因为这些技能并不携带任何认证信息。同样,那些值得阅读的资料也是如此:Anthropic 的 Agent Skills 概述页面以及其技能开发最佳实践页面,均位于 Claude Platform 的文档中。
相关阅读
- Gemini Spark详解:任务、功能、调度、安全与定价 —— 谷歌这款全天候运行的Gemini Spark智能体在Gmail、日历和文档中的功能,其安全机制的运作方式,成本是多少,在哪些地方被限制使用,以及谁应该采用它。
- 生产环境中的高级RAG:架构、工具与权衡 —— 深入了解生产环境中的RAG系统:数据导入、解析、分块、嵌入向量、向量存储、混合检索、重排序、评估与安全措施,以及每种方法的相应权衡。