Chihao LiChihao Lichihao.li
EN中文
← 所有文章

被忽略的另一半:Claude Code Skills 运行时用户体验最佳实践

Claude CodeSkillsAI AgentBest PracticesDeveloper Experience

TL;DR — 官方文档介绍了怎么写 SKILL.md,但没覆盖 Skill 跑起来之后的体验。这篇文章的主线是怎么给 Agent 设计好用的技能(分层模型、脚本设计、生命周期、环境管理),附带一组面向用户的体验 tips。实践已打包为 better-skills,npx skills add psylch/better-skills -g -y 一键安装。

引言:从”怎么写 Skills”到”Skills 的用户体验优化”

Anthropic 的官方文档在 Skill 的编写层面做得相当好。无论是 Skill Creator 还是 Best Practices,都给出了清晰的指导:SKILL.md 要简洁、用 progressive disclosure 管理上下文、脚本要 “solve, don’t punt”、用 feedback loop 保证质量。这些建议是有效的。

但当我实际构建了十几个不同类型的 Skills——涵盖终端环境配置、多源技术调研、音乐下载、电子书搜索、网盘资源获取、DNS 管理、CORS 审计——之后,我发现官方文档有一个系统性的盲区:它几乎完全没有讨论 Skill 安装之后、实际使用时的runtime用户体验。

具体来说,以下这些问题在官方文档中找不到答案:

  1. 当一个 Skill 需要 Python 依赖时,应该用系统 pip、per-skill venv,还是其他方案?当多个 Skill 的依赖版本冲突时,怎么办?
  2. 当用户第一次触发 Skill 时,环境还没准备好,Agent 应该怎么引导?
  3. 当 Skill 依赖的外部服务不可用时,是报错退出还是降级运行?
  4. Skill 里的脚本是给 Agent 调用的 CLI 工具——它的输出格式、错误处理、子命令设计应该遵循什么原则?

本文前五部分讨论怎么给 Agent 设计工具接口:输出格式、生命周期、环境隔离。第六部分是一组面向最终用户的体验 tips。第七部分简单聊了聊安全——这块我还没系统研究,但真实攻击事件已经出现了,基本的代码卫生值得提一提。


第一部分:Skill 分层模型

不是所有 Skill 都需要脚本,也不是所有带脚本的 Skill 都需要复杂的环境管理。在讨论具体实践之前,先明确 Skill 的三个层级,因为不同层级面对的问题完全不同。

L0 纯 Prompt Skill 只有一个 SKILL.md,所有能力都靠 Agent 自身完成。Agent 调用内置工具(WebSearch、文件读写、bash 命令)或已配置的 MCP server 来执行任务,Skill 本身不包含任何可执行代码。这是最轻量的形态,也是大多数 Skill 作者最容易上手的方式——不需要会编程,只需要能把流程和领域知识写清楚。

典型例子是我的 ghostty-vibe-coding-stack skill:一个 12 步的终端环境配置向导,所有安装操作都靠 Agent 自己跑 brew install 和写配置文件,SKILL.md 里只有步骤指引和参考配置。

L0+ Prompt + 辅助脚本 的核心逻辑仍然在 SKILL.md 的 prompt 里,但附带了少量辅助脚本,用于环境检测、状态缓存等 Agent 自身不方便做或不适合每次重写的事情。脚本不承载业务功能,只做辅助。

典型例子是 tech-research-skill:多源技术调研的编排逻辑(选择 Light/Heavy 模式、分派 subagent、综合报告)全在 SKILL.md 里,而 grok_setup.sh 只负责检测浏览器后端和缓存登录状态这类辅助工作。

L1 Prompt + 业务脚本 的核心功能由脚本承载,SKILL.md 负责编排调用。脚本接收参数、执行操作、返回结构化结果,Claude 根据结果决定下一步。这类 Skill 的脚本就是给 AI 用的 CLI 工具,MCP 的工具设计原则在这里完全适用。

典型例子是 hifi-download、zlib-download、quark-download、cors-audit、cloudflare-dns。

分层的意义在于:L0 Skill 不需要考虑环境问题,L0+ 只需要极简的脚本运行环境,而 L1 才需要系统性地处理依赖、环境、输出格式等问题。后文的最佳实践主要针对 L0+ 和 L1 展开。

第二部分:脚本设计——给 AI 用的 CLI 工具

当你决定在 Skill 中包含脚本时,你实际上是在为 AI agent 设计一个 CLI 工具接口。这个接口的消费者不是人类用户,而是 Claude。它通过 bash 调用你的脚本,通过 stdout 读取结果,通过 stderr 和 exit code 判断是否出错。

换个角度看,带脚本的 Skill 和 MCP Tool 做的是同一件事——接收 AI agent 的指令,执行操作,把结果返回给 agent——只不过传输层不同。MCP Tool 走 JSON-RPC 协议,Skill 脚本走 bash 调用 + stdout/stderr。这意味着 MCP 生态中成熟的工具设计原则可以直接迁移过来。Anthropic 在 Writing effective tools for agents 中系统性地总结了这些原则,但在 Skill 文档里完全缺失——官方 Skill Best Practices 在脚本设计方面只给了一条建议:“solve, don’t punt”。这当然是对的,但远远不够。

下面是我把这些原则落地到 Skill 脚本中的具体实践。

输出格式:JSON + 自然语言 hint

脚本的标准输出应该是 JSON,因为 Agent 需要解析结构化数据来做决策。但不要只返回原始数据——在关键字段里包含自然语言 hint,帮助 Agent 更好地转述给用户。

{
  "status": "ok",
  "results": [
    {"title": "Deep Learning", "author": "Ian Goodfellow", "format": "PDF", "size": "22MB"}
  ],
  "hint": "找到 1 本匹配的书籍,PDF 格式,22MB"
}

避免使用 key=value 纯文本输出(比如 BACKEND=chrome\nLOGIN_STATUS=logged_in)。这种格式需要 Agent 自己做字符串解析,既不可靠也浪费 token。如果脚本足够简单只需要返回单个状态值,JSON 仍然是更好的选择——{"ready": true} 比 READY=true 更不容易被误解析。

错误信息走 stderr,同样使用 JSON 格式:

{"error": "zlib_login_failed", "hint": "Z-Library 登录失败,请检查 ~/.claude/zlib-download/.env 中的邮箱和密码是否正确"}

子命令设计:统一的动词表

一个 Skill 脚本通常包含多个子命令。从我多个 Skill 的实践来看,以下是反复出现的通用子命令:

preflight 检测当前环境是否满足运行条件,返回标准化的就绪状态。setup 执行首次安装和配置。status 查看当前运行状态(已配置的服务、登录态等)。然后是业务命令——search、download、validate、list 等,取决于 Skill 的具体功能。

关键是 preflight 和 setup 的命名应该在所有 Skill 中保持一致。当 Agent 遇到一个新 Skill 时,它应该能预期 python3 scripts/main.py preflight 会返回环境状态,而不需要每个 Skill 都去 SKILL.md 里查不同的命令名(有的叫 check,有的叫 status,有的叫 verify)。

错误处理:可操作的信息,有意义的 exit code

MCP 最佳实践中最有价值的一条可能就是”可操作的错误信息”。对于 Skill 脚本,这意味着:

不要只告诉 Agent “出错了”,要告诉它 “出了什么错、为什么、下一步该做什么”。

# 差
sys.exit("Error: authentication failed")

# 好
error = {
    "error": "spotify_auth_expired",
    "hint": "Spotify OAuth token 已过期,运行 'bash run.sh spotify_auth' 重新授权",
    "recoverable": True
}
print(json.dumps(error), file=sys.stderr)
sys.exit(1)

recoverable 字段特别重要——它告诉 Agent 这个错误是否可以通过某种操作自动恢复,还是必须告知用户手动介入。这直接影响了 Agent 在复杂任务之中的行动是否会劣化环境状态。

exit code 也应该有统一约定:0 表示成功,1 表示可恢复的错误(Agent 可以尝试修复),2 表示不可恢复的错误(需要用户介入)。

Token 感知:不要一次倾倒所有数据

这是 MCP 文档反复强调的一点。Skill 脚本返回大量数据时,应该支持 --limit 参数控制返回条目数,支持 --format concise 和 --format detailed 切换输出详略。

默认应该返回 concise 格式。Agent 可以在需要更多细节时带 --format detailed 重新调用。这比一次返回所有信息然后让 Agent 自己筛选要高效得多——每个多余的 token 都在消耗上下文窗口。

第三部分:运行时生命周期

每个带脚本的 Skill 在运行时都会经历三个阶段:preflight、setup、run。即使某些阶段是空操作(比如 stdlib-only 的 Skill 不需要 setup),这个生命周期模型应该在所有 Skill 中保持一致,让 Agent 形成稳定的交互预期。

Preflight:标准化的环境探测

Preflight 是 Agent 在执行 Skill 业务逻辑之前的第一个动作。它的目的是快速回答一个问题:这个 Skill 现在能用吗?

所有 Skill 的 preflight 应该返回统一结构的 JSON:

{
  "ready": true,
  "dependencies": {
    "python3": {"status": "ok", "version": "3.12.0"},
    "uv": {"status": "ok", "version": "0.6.0"}
  },
  "credentials": {
    "spotify": {"status": "configured"},
    "qobuz": {"status": "not_configured", "hint": "编辑 ~/.claude/hifi-download/.env 添加 QOBUZ_EMAIL 和 QOBUZ_PASSWORD", "required": false}
  },
  "services": {
    "quark_app": {"status": "running", "version": "6.12.0"}
  }
}

ready: true 意味着核心功能可用,Agent 可以直接进入业务流程。ready: false 时,Agent 读 hint 字段来引导用户完成缺失的配置。

注意 credentials 中的 required 字段——不是所有凭据都是必须的。hifi-download 的 Spotify 是核心功能需要的,但 Qobuz 和 TIDAL 是可选的增强。Preflight 应该区分”缺了就不能用”和”缺了只是少一个数据源”。

Setup:首次运行引导

Setup 只在 preflight 报告缺失项时执行。它应该分为 Claude 可以自动完成的部分和需要用户手动操作的部分。

Claude 可以自动完成的:安装依赖(uv sync、pip install)、创建目录结构、生成配置文件模板。

需要用户手动操作的:填写 API 凭据、登录外部服务、启动桌面应用。

SKILL.md 中应该明确标注哪些步骤是自动的、哪些需要用户介入。Claude 不应该在聊天中直接询问用户的密码或 API key——应该引导用户去编辑配置文件。这既是安全考虑(聊天记录可能被保存),也是更好的用户体验(用户可以用自己熟悉的编辑器操作)。

Run:业务执行

当 preflight 确认就绪后,Claude 按照 SKILL.md 中的编排逻辑调用业务命令。这个阶段的关键设计决策是降级策略——当部分能力不可用时怎么办。

我在实践中总结了四种降级模式:

跳过并报告:tech-research 在 Grok 不可用时跳过 Twitter 数据源,只用 DeepWiki + WebSearch 完成调研,在最终报告中注明 “Grok 数据源因登录状态过期被跳过”。这是最常用的模式。

自动回退:zlib-download 在 Z-Library 不可用时自动切换到 Anna’s Archive 后端。用户甚至不需要知道发生了什么。

禁用单个服务:hifi-download 允许显式禁用某个音乐平台(比如用户没有 TIDAL 会员),其他平台继续工作。

无法降级,终止并引导:quark-download 在夸克 APP 未运行时无法工作——这是硬依赖,没有替代方案。此时应该直接告知用户 “请启动夸克桌面 APP”,而不是反复重试。

SKILL.md 中应该为每个外部依赖标注它属于哪种降级模式,这样 Claude 在遇到问题时能做出合理的决策而不是卡住。

第四部分:环境管理策略

这是我认为现有 Skill 生态中最薄弱的环节。当多个 Skill 各自安装 Python 依赖时,版本冲突、磁盘占用、首次安装耗时都会成为问题。我的建议是一套分级策略——能用轻量方案解决的,绝不用重量方案。

第一优先级:不要有依赖

Python 标准库的能力远比大多数人想象的强大。urllib.request 可以发 HTTP 请求,json 可以解析和生成 JSON,argparse 可以处理命令行参数,pathlib 可以操作文件路径,subprocess 可以调用外部命令。

我的 cors-audit 和 quark-download 都证明了纯 stdlib 方案的可行性——前者实现了完整的 CORS 审计(包括 HTTP 请求、配置文件解析、报告生成),后者实现了网盘资源搜索和并发验证。零依赖意味着零环境问题。

当你觉得需要引入外部库时,先问自己:stdlib 真的做不到吗?很多时候,requests 能做的事 urllib.request 也能做,只是写法略微啰嗦。为了这点便利而引入整条依赖链,在 Skill 场景下通常不值得。

第二优先级:Vendoring

如果确实需要一个外部库,且这个库是纯 Python(没有 C 扩展),可以直接把它的源码文件放进 Skill 目录。我的 zlib-download 实际上就是这么做的——Zlibrary.py 是一个 vendored 的 Z-Library API 客户端。

Vendoring 的好处是彻底消除了运行时安装步骤。缺点是需要手动更新。对于 Skill 场景来说,依赖通常很少变化,手动更新是可以接受的。

第三优先级:uv run + 内联依赖声明

当依赖链较复杂或包含需要编译的库时,uv 提供了一个优雅的解决方案。uv 可以在不创建持久 venv 的情况下运行带依赖的 Python 脚本,依赖被缓存在全局 cache 中,多个 Skill 共享缓存但互不污染。

配合 PEP 723 内联元数据,可以在脚本头部直接声明依赖:

# /// script
# requires-python = ">=3.11"
# dependencies = ["requests>=2.28", "spotipy>=2.23"]
# ///

import requests
import spotipy

然后用 uv run scripts/search.py 执行。uv 会自动读取内联声明、解析依赖、在隔离环境中运行——不在 Skill 目录里留下任何 .venv 目录,不污染系统 Python。

这个方案的一个好处是依赖声明和代码在同一个文件里,不会出现 requirements.txt 和实际代码不一致的情况。另一个好处是 uv 的安装速度极快(比 pip 快 10-100 倍),首次运行的等待体验大幅改善。

uv 本身只需要一次安装(brew install uv 或 curl -LsSf https://astral.sh/uv/install.sh | sh),可以作为整个 Skill 生态的通用前置条件,就像 Node.js 生态里的 npm 一样。

第四优先级:Per-skill venv

只有在以下情况才建议使用传统的 per-skill venv:依赖链特别复杂,包含需要编译的 C 扩展(比如 numpy、Pillow),或者 Skill 需要运行长驻进程(比如 hifi-download 的下载进度 dashboard)。

venv 应该创建在 Skill 自己的目录内(scripts/.venv/ 或 .venv/),并且通过一个 run.sh 封装激活过程,让调用方只需要 bash run.sh <command> 而不需要知道 venv 的细节。

第五优先级:委托给 MCP

如果一个功能本身就由 MCP server 提供(比如 DeepWiki、Playwright),Skill 不需要在本地安装任何东西——直接在 SKILL.md 中编排 MCP 调用即可。这本质上是把环境管理完全委托给了 MCP server 的维护者。

这也是为什么我说 Skill 在一定程度上承接了 MCP 的职能:对于简单的工具,写一个 Skill 脚本比搭一个 MCP server 轻量得多,但当功能复杂到需要独立进程和完整的依赖管理时,MCP 仍然是更合适的载体。

环境决策流程图

这个功能需要外部 Python 库吗?
├─ 不需要 → stdlib-only(零环境成本,首选)
└─ 需要
   ├─ 是纯 Python 库,且只需要 1-2 个? → vendor 进 Skill 目录
   └─ 依赖较多或有 C 扩展?
      ├─ 需要长驻进程? → per-skill venv + run.sh 封装
      └─ 不需要 → uv run + PEP 723 内联声明

第五部分:凭据与状态管理

凭据的三种形态

在我七个 Skill 的实践中,凭据管理呈现出三种形态,各有适用场景。

环境变量适合一次性的 API token。cloudflare-dns 要求用户设置 CF_API_TOKEN 环境变量,脚本直接从环境中读取。好处是凭据不落盘(不写进文件),安全性高。缺点是用户每次新开 shell session 都要重新设置,除非写进 shell rc 文件。

配置文件适合需要持久化的多组凭据。hifi-download 需要 Spotify、Last.fm、Qobuz、TIDAL 四个平台的凭据,用环境变量管理会非常混乱。这种场景下,一个 .env 文件配合 .env.example 模板是更合理的方案。

浏览器登录态适合 OAuth 类的认证。tech-research 的 Grok 访问需要 X/Twitter 的登录态,这不是一个能写进配置文件的 API key,而是浏览器的 cookie session。Skill 能做的是检测登录状态、缓存检测结果、在过期后提示用户重新登录。

统一的状态目录

所有 Skill 的持久化状态(凭据文件、缓存、配置)应该统一放在 ~/.claude/<skill-name>/ 下。不要各自发明路径——我早期的 zlib-download 把配置放在 ~/.claude/book-tools/,这个命名既不一致也不直观。统一路径约定让用户在需要手动管理(清理、备份、迁移)时有可预期的位置。

~/.claude/
├── hifi-download/
│   ├── .env              # 凭据(Spotify、Qobuz 等)
│   └── .cache            # OAuth token 缓存
├── zlib-download/
│   ├── .env              # 凭据(Z-Library、Anna's Archive)
│   └── config.json       # 运行时配置(自动生成的 remix token 等)
├── tech-research/
│   └── .grok-status.json # 登录状态缓存
└── cloudflare-dns/       # (无持久化状态,凭据走环境变量)

登录状态缓存的设计模式

tech-research 的 Grok 登录状态缓存是一个值得推广的模式。它的核心思路是 乐观假设 + 被动失效:

默认假设用户已登录(乐观),只有当 agent 在实际访问时检测到 “Sign in” 页面才标记为 logged_out。logged_out 状态有 2 小时的过期时间——过了这个时间,系统重新回到乐观模式,因为用户可能已经重新登录了。

这个设计避免了两个糟糕的替代方案:每次使用前都要求用户确认登录状态(烦人),或者在登录失效后永远不再尝试(过于悲观)。

第六部分:面向用户的体验要点

前面五个部分系统性地讨论了怎么给 Agent 设计好用的工具接口。但 Skill 的最终用户是人,Agent 只是中间层。以下是我在实践中积累的、直接影响用户感受的几个要点:

别在聊天里问密码。 Agent 不应该在对话中索要 API key 或密码——聊天记录可能被保存,这既不安全也不方便。引导用户去编辑 ~/.claude/<skill-name>/.env,用自己熟悉的编辑器操作。

统一状态目录。 所有 Skill 的持久化状态放 ~/.claude/<skill-name>/ 下,不要各自发明路径。用户在清理、备份、迁移时应该有可预期的位置。

Setup 分清自动和手动。 SKILL.md 中明确标注哪些步骤 Agent 自动完成(装依赖、建目录),哪些需要用户介入(填凭据、登录服务、启动桌面应用)。别让用户猜。

服务挂了别卡死。 为每个外部依赖标注降级模式(跳过并报告 / 自动回退 / 禁用单个服务 / 终止并引导),让 Agent 遇到问题时能做合理决策而不是反复重试。

首次运行要快。 环境方案的选择直接影响用户第一次触发 Skill 时的等待体验。stdlib-only 零等待,uv 秒级安装,pip + venv 可能要等几十秒。优先选轻量方案。

登录态用乐观缓存。 对于浏览器登录态这类无法主动检测的凭据,采用”乐观假设 + 被动失效”:默认已登录,实际访问失败时才标记过期,过期后设冷却时间再重新尝试。别每次都让用户确认,也别一次失败就永久放弃。

长时间操作用后台模式。 下载进度面板、实时监控仪表盘这类长驻进程,SKILL.md 中应该明确标注用后台模式启动(Bash 工具的 run_in_background: true)。否则 Agent 会同步阻塞,用户既看不到”已开始”的反馈,Agent 也无法继续后续操作。

操作完成后告诉用户结果在哪。 下载了文件要报路径和大小,搜索了资源要报条目数和剩余配额,生成了配置要报文件位置。用户不应该自己去翻文件系统找结果。

第七部分:安全

这一部分我没有做过系统性的研究,坦白说也不是我的专业领域。但在调研社区讨论后,我认为有必要把这个话题摆出来——2025–2026 年间,AI agent 的 skill/plugin 生态已经出现了真实的安全事件(恶意 skill 投毒、MCP server 命令注入、IDE 插件窃取加密钱包),OWASP 甚至专门发布了 Agentic Applications Top 10。

先明确边界: 这里讨论的不是如何防范恶意开发者——投毒、后门、伪装成正常 skill 的恶意软件,这些是平台、registry 和供应链安全体系该管的事。我们关注的是另一面:一个善意的 skill 开发者,怎么避免因代码写法不当而留下安全隐患。 类比 Web 开发中的 OWASP Secure Coding Practices——不是教你防黑客,而是教你别在自己代码里留洞。

以下几个点是我从社区实践中看到的、与 skill 开发最相关的:

不要把用户输入直接拼进 shell 命令。 这是经典的命令注入。用 Python 的 subprocess.run(["cmd", arg]) 列表形式,不要用 shell=True 加字符串拼接。同理,路径参数要做校验,防止 ../../ 穿越到预期之外的目录。

错误信息里不要带凭据。 调试时在 stderr 里打印完整的请求(包括 Authorization header)很方便,但这些输出会进入 Agent 的上下文,可能被日志记录或回显给用户。脱敏后再输出。

.env 文件设合理的权限。 chmod 600 确保只有文件所有者可读写。如果 skill 的 setup 流程会创建 .env 文件,创建时就应该设好权限。

HTTPS 证书验证别关。 urllib.request 默认验证证书,不要为了”方便”传 context=ssl._create_unverified_context()。如果遇到证书问题,修证书,不要关验证。

不要在脚本里硬编码路径和密钥。 这一点在我们的 validate.py 中已经有自动检测(硬编码路径、OpenAI/GitHub/AWS/Slack API key 模式),但意识上也值得强调。

这个领域还在快速发展,以上只是基本的代码卫生。如果你想深入了解 AI agent 安全的完整图景,以下几篇值得一读:

第八部分:SKILL.md 中的运行时声明

前面讨论的所有实践,最终都需要体现在 SKILL.md 中,让 Claude 知道如何与这个 Skill 交互。我建议在 SKILL.md 中增加一个 ## Runtime 段落,结构化地声明运行时行为。

## Runtime

### Preflight
运行 `python3 scripts/main.py preflight` 获取环境状态。
输出标准 JSON:`ready` 为 true 时可直接使用,为 false 时根据 `hint` 字段引导用户。

### Setup(首次运行)
**自动步骤**(Claude 执行):
1. 检查 uv 是否安装,未安装则提示用户 `brew install uv`
2. 首次调用时 uv 自动安装依赖,无需额外操作

**手动步骤**(引导用户):
1. 将 .env.example 复制到 ~/.claude/<skill-name>/.env
2. 编辑 .env 填入 API 凭据

不要在聊天中询问用户的密码或 API key,引导他们编辑配置文件。

### Credentials
| 凭据 | 位置 | 必须 | 获取方式 |
|------|------|------|---------|
| Spotify | ~/.claude/hifi-download/.env | 是 | developer.spotify.com |
| Qobuz | ~/.claude/hifi-download/.env | 否 | qobuz.com 会员 |

### Degradation
| 依赖 | 不可用时的行为 |
|------|--------------|
| Spotify | 无法搜索和推荐,终止并提示配置 |
| Qobuz | 跳过 Qobuz 下载源,使用 TIDAL |
| TIDAL | 跳过 TIDAL 下载源,使用 Qobuz |
| Qobuz + TIDAL 均不可用 | 可以搜索和推荐,但无法下载 |

这种声明式的写法让 Claude 在触发 Skill 时有明确的决策依据,而不是靠在大段文字中搜索线索。


总结

回到开头的问题:官方文档介绍了怎么写一个 SKILL.md,但没告诉你 Skill 跑起来之后的体验该怎么设计。

这份最佳实践填补的就是这个空白——前五部分讲怎么给 Agent 设计工具接口(分层、脚本、生命周期、环境、凭据),第六部分是用户体验 tips,第七部分是安全方面的基本代码卫生(这块我还在学习中)。

这是我从十几个不同类型的 Skill项目 中反复迭代后提炼出来的。它们仍在演进中,但我相信,在 Skill 生态的当前阶段,这些方向是有价值的。